왜
prod 30일 이미지 등록 18건 중 16건이 실패했다(89%). 같은 기간 링크 경로는 85/87 성공이라 대비가 뚜렷하다.
원인은 버그가 아니라 계약 충돌이다. READY 불변식(ItemSnapshot.requireReadyInvariant)이 name·price·imageUrl·extractedAt 을 모두 요구하는데, 사용자가 직접 찍은 사진이나 가격이 박혀 있지 않은 제품컷에는 가격이 존재하지 않는다. extractor 의 응답 경계(ExtractionResponse.from)가 missingReadyField 로 같은 조건을 검사해 422 UNTRUSTWORTHY_VALUE 로 닫는다.
실측 사례:
- item 1904 - 몬치치 인형을 직접 촬영한 사진(
.heic). 가격도 브랜드 표기도 없음
- item 1902 - 화장품 콤팩트 제품컷. 브랜드 각인이 곡면에 눌려 판독 불가
- 반면 성공한 2건(item 1900, 1901)은 모두 쇼핑몰 화면 스크린샷이었다
즉 "쇼핑몰 화면을 캡처한 것"만 통과하고, 사용자가 물건을 찍어 올리면 구조적으로 실패한다. 하나라도 비면 등록 자체를 거부하는 것은 불친절하다. 채울 수 있는 만큼 채워주고 나머지를 사용자가 채우게 한다.
무엇을
ItemStatus 에 INCOMPLETE 를 신설한다 (PROCESSING 다음, READY 전)
- 추출이 일부 필드만 채우면
INCOMPLETE 로 안착시킨다
INCOMPLETE 는 READY 취급을 받지 못한다 - 토너먼트 출전 자격(TournamentService 의 isReady() 검사) 등에서 걸러진다
- 사용자가 빈 필드를 채워 세 필드가 다 차면 자동으로
READY 로 승격한다. 기존 ItemSnapshot.manual 이 "base 값에 입력을 덮어 병합 후 필수 필드가 차면 READY 행 생성" 으로 이미 동작하므로 그대로 재사용한다
- extractor 의
missingReadyField 게이트를 풀어 부분값을 200 으로 반환한다 (이미지·링크 둘 다). 링크 경로는 헤드리스 에스컬레이션 판정(FallbackProductLinkExtractor)이 같은 조건을 공유하므로 그 얽힘을 먼저 푼다
함께 고치는 이미지 경로 버그:
- HEIC/WebP/HEIF 위장 업로드 -
ImageCropper 가 ImageIO 로 디코딩하는데 png·jpeg 만 지원해 나머지는 크롭을 건너뛴다(의도된 fallback). 그런데 업로드는 항상 items/{uuid}.png / image/png 로 하드코딩돼 있어, HEIC 바이트가 png 로 위장돼 저장된다. 브라우저 대부분이 렌더링하지 못한다. presign 이 허용하는 5개 포맷 중 3개가 해당된다
- 실패 필드 미로깅 -
UNTRUSTWORTHY_VALUE 만 남고 어느 필드가 비었는지 없어, name 이 없었는지 price 가 없었는지 사후 판별이 불가능하다
범위 밖
- 파싱 재시도 정책은 현행(단번) 유지. 이미지 raw 는 확정 실패 시 즉시 회수되고 S3 lifecycle 이 1일 만료하므로 재파싱 입력 자체가 없다
extraction_models 에 IMAGE 대상 모델이 미지정이라 extractor 기본값(GeminiProperties.DEFAULT_MODEL)이 쓰이는 것은 별건(백오피스 데이터)
- 링크 경로의
INCOMPLETE 는 수기 입력뿐 아니라 새로고침(재파싱)도 열린다. URL 이 남아 있고 WishPersistenceService 가 isFailed() 일 때만 refresh 를 막기 때문
왜
prod 30일 이미지 등록 18건 중 16건이 실패했다(89%). 같은 기간 링크 경로는 85/87 성공이라 대비가 뚜렷하다.
원인은 버그가 아니라 계약 충돌이다. READY 불변식(
ItemSnapshot.requireReadyInvariant)이name·price·imageUrl·extractedAt을 모두 요구하는데, 사용자가 직접 찍은 사진이나 가격이 박혀 있지 않은 제품컷에는 가격이 존재하지 않는다. extractor 의 응답 경계(ExtractionResponse.from)가missingReadyField로 같은 조건을 검사해 422UNTRUSTWORTHY_VALUE로 닫는다.실측 사례:
.heic). 가격도 브랜드 표기도 없음즉 "쇼핑몰 화면을 캡처한 것"만 통과하고, 사용자가 물건을 찍어 올리면 구조적으로 실패한다. 하나라도 비면 등록 자체를 거부하는 것은 불친절하다. 채울 수 있는 만큼 채워주고 나머지를 사용자가 채우게 한다.
무엇을
ItemStatus에INCOMPLETE를 신설한다 (PROCESSING다음,READY전)INCOMPLETE로 안착시킨다INCOMPLETE는 READY 취급을 받지 못한다 - 토너먼트 출전 자격(TournamentService의isReady()검사) 등에서 걸러진다READY로 승격한다. 기존ItemSnapshot.manual이 "base 값에 입력을 덮어 병합 후 필수 필드가 차면 READY 행 생성" 으로 이미 동작하므로 그대로 재사용한다missingReadyField게이트를 풀어 부분값을 200 으로 반환한다 (이미지·링크 둘 다). 링크 경로는 헤드리스 에스컬레이션 판정(FallbackProductLinkExtractor)이 같은 조건을 공유하므로 그 얽힘을 먼저 푼다함께 고치는 이미지 경로 버그:
ImageCropper가ImageIO로 디코딩하는데 png·jpeg 만 지원해 나머지는 크롭을 건너뛴다(의도된 fallback). 그런데 업로드는 항상items/{uuid}.png/image/png로 하드코딩돼 있어, HEIC 바이트가 png 로 위장돼 저장된다. 브라우저 대부분이 렌더링하지 못한다. presign 이 허용하는 5개 포맷 중 3개가 해당된다UNTRUSTWORTHY_VALUE만 남고 어느 필드가 비었는지 없어,name이 없었는지price가 없었는지 사후 판별이 불가능하다범위 밖
extraction_models에 IMAGE 대상 모델이 미지정이라 extractor 기본값(GeminiProperties.DEFAULT_MODEL)이 쓰이는 것은 별건(백오피스 데이터)INCOMPLETE는 수기 입력뿐 아니라 새로고침(재파싱)도 열린다. URL 이 남아 있고WishPersistenceService가isFailed()일 때만 refresh 를 막기 때문