MaskWork::maskPanos | script img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
RCA: MaskWork::maskPanos | cv2.cvtColor assertion failure
Overview#
What Happened#
2026-05-08 20:20~20:55 UTC 사이에 cupixworks-pix-genie-preprocessor-instance 서비스에서 pano 이미지 마스킹 처리 중 OpenCV cvtColor 함수가 빈 이미지를 받아 assertion failure가 발생했다. 총 9건의 에러가 기록되었으며, 다수의 capture가 병렬 처리 중이었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | cv2.error |
| exception.message | OpenCV(4.10.0) error: (-215:Assertion failed) !_src.empty() in function 'cvtColor' |
| top_frame | /tmp/lib/face-body-detector/cupix_inference.py:334 |
| runtime | Python 3.11, OpenCV 4.10.0, Node.js (agent) |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| smartcoms / pano-postprocessor | 9 | 마스킹 실패한 pano는 face/body blur 처리가 적용되지 않음 |
Timeline#
- 2026-05-08T20:20:29Z — 첫 번째 cvtColor assertion failure 발생 (error code:1)
- 2026-05-08T20:55:36Z — 두 번째 batch에서 동일 에러 재발생
- 2026-05-09 — Error-sweeper 감지 및 RCA 수행
Error Log#
MaskWork::maskPanos | script img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgproc/src/color.cpp:196: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'
Impact#
- Service:
cupixworks-pix-genie-preprocessor-instance - Team: smartcoms
- 발생 횟수: 9
- 최초 발생: 2026-05-08T20:55:36.081Z
- 최근 발생: 2026-05-08T20:55:36.081Z
Root Cause Summary#
AwsS3Manager::downloadParallel에서 S3 NoSuchKey 에러("The specified key does not exist.")로 개별 pano 이미지 다운로드가 실패했다. 에러를 catch하고 로그만 남긴 후 계속 진행하기 때문에, 다운로드 실패한 pano 파일이 디스크에 존재하지 않는 상태에서 maskPanos가 Python face-body-detector 스크립트를 실행했다. cv2.imread()가 존재하지 않는 파일에 대해 None을 반환하고 이것이 cv2.cvtColor()에 전달되어 !_src.empty() assertion failure가 발생했다.
Kibana(Watch) debug 로그 분석 결과, 두 capture 모두 v0 버전 키를 가진 pano가 각 1건씩 존재했으며, 해당 S3 key가 존재하지 않아 다운로드가 실패한 것이 확인되었다:
capture_694181—resources/hyypt8/uswe1/v0→pano_83949969.jpgcapture_694180—resources/obszdo/uswe1/v0→pano_83954507.jpg
v0 키의 의미: Tesla API의 Resource 모델에서 v0은 리소스가 생성만 되고 아직 이미지가 업로드되지 않은 초기 상태(revision=0)를 의미한다. 업로드는 v1(revision+1) 경로로 수행되며, 업로드 완료 확인(check_uploading) 후 revision이 1로 증가한다. 두 pano 모두 Kibana에서 resource_state: "created", state: "abandoned", filesize: null로 확인되어, 이미지가 S3에 업로드된 적이 없다.
abandoned 상태의 의미: Tesla state machine(statable/pano.rb:83-85)에서 abandoned 이벤트는 created 또는 resource_missing 상태에서만 전이 가능하다. CaptureInvoker#create_capture(capture_invoker.rb:29-31)가 capture 처리 시작 시점에 not_processible pano(state: created | resource_missing)를 일괄 abandoned로 마킹한다. 즉 abandoned는 업로드 미완료로 인해 처리 불가능한 pano를 정리하는 상태이다. default_scope(statable/pano.rb:8)가 abandoned pano를 API 쿼리에서 자동 제외하지만, pano postprocessor가 실행되는 시점에 아직 created 상태인 pano가 API 응답에 포함될 수 있다(abandoned 마킹 전 race condition).
근본 원인은 업로드 미완료 pano가 후처리 파이프라인에 포함된 것이며, pix-genie에서 이러한 pano를 다운로드 전에 필터링하여 제외하고 나머지만 처리하는 것이 올바른 접근이다.
Technical Analysis#
Code Path#
- Entry point:
pano-postprocessor-service.ts:79—run()메서드 - Download step:
pano-postprocessor-service.ts:117—awsS3Manager.downloadParallel(cpPanos)호출 - Download failure handling:
aws-s3.manager.ts:75-77— 에러 catch 후 로그만 남기고 진행 (re-throw 없음) - Mask step:
pano-postprocessor-service.ts:126—maskWork.maskPanos(downloadDir, maskDir!, optionPath)호출 - Spawn:
mask-work.ts:29-33— Python 스크립트를 child process로 실행 - Failure point:
/tmp/lib/face-body-detector/cupix_inference.py:334—cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
1단계: 다운로드 실패 무시
const tasks = cpPano.map(cpPano => {
return limit(async () => {
try {
// ... download logic
if (cpPano.panoBucketEndpoint) {
await this.downloadByPanoApi(cpPano);
} else {
await this.download(
cpPano.panoBucketRegion!,
cpPano.panoBucketName!,
cpPano.panoBucketKey!,
cpPano.downloadImagePath!
);
}
} catch (err) {
logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s',
cpPano.downloadImagePath, (err as Error).message);
// 에러를 삼키고 계속 진행 — 파일이 디스크에 없는 상태로 남음
}
});
});
await Promise.all(tasks);
기대 동작: 다운로드 실패 시 해당 pano를 마스킹 대상에서 제외하거나 전체 프로세스를 중단해야 한다. 실제 동작: 에러를 삼키고 모든 다운로드가 성공한 것처럼 다음 단계(maskPanos)로 진행한다.
2단계: 마스킹 스크립트가 디렉토리 내 모든 파일을 순회
maskPanos(inputDir: string, outputDir: string, optionPath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const process: ChildProcessWithoutNullStreams = spawn('stdbuf', ['-oL', 'bash', BlurScriptPath], {
cwd: path.dirname(BlurScriptPath),
stdio: 'pipe',
env: { INPUT_DIRPATH: inputDir, OUTPUT_DIRPATH: outputDir, OPTION_FILE_PATH: optionPath }
});
maskPanos는 inputDir (download 디렉토리)을 Python 스크립트에 전달한다. Python 스크립트(cupix_inference.py)는 해당 디렉토리의 모든 이미지 파일을 순회하며 cv2.imread()로 로드한다.
부가 이슈: env 옵션에 ...process.env가 없어 PATH, HOME, LD_LIBRARY_PATH 등 부모 환경변수가 전달되지 않는다. 그러나 stdbuf와 bash가 절대 경로에 있고 Python 의존성이 시스템 전역으로 설치되어 있으므로 실행 자체는 가능하다. 이 문제는 향후 다른 환경변수 의존성 추가 시 잠재적 위험이다.
3단계: Python 스크립트에서 assertion failure
File "/tmp/lib/face-body-detector/cupix_inference.py", line 453, in <module>
detect(args)
File "/tmp/lib/face-body-detector/cupix_inference.py", line 334, in detect
img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
detect() 함수의 line 334에서 cv2.imread()로 읽은 결과를 cv2.cvtColor()에 바로 전달한다. cv2.imread()는 파일이 없거나 읽기 실패 시 예외를 발생시키지 않고 None을 반환하는 것이 OpenCV의 알려진 동작이다. None이 cvtColor에 전달되면 C++ 레벨에서 !_src.empty() assertion이 실패한다.
4단계: Tesla Resource 버전 모델 — v0은 업로드 미완료 상태
def upload_revision
self.revision + 1
end
리소스 생성 시 default_initial_revision은 0이다 (revisionable/pano.rb:15-17). 업로드 URL은 항상 revision + 1 (즉 v1) 경로로 발급된다.
def check_uploading
_revision = self.revision
_object = self.object(_revision + 1)
if _object.exists?
# ... etag, size 확인
self.increase_revision # revision 0 → 1
self.done
true
else
self.missing
false
end
end
업로드 확인 시 v1 경로에 S3 객체가 존재하면 revision을 1로 증가시킨다. v0 키가 API에서 반환된다는 것은 check_uploading이 한 번도 성공하지 못했음을 의미한다 — 즉 S3에 v0에도 v1에도 실제 이미지 데이터가 없다.
def download_url(opts = {})
ver = opts[:ver] || self.revision
raise Cupix::Errors::Resource.new(code: 'ENT10011', reason: "Resource does not uploaded: #{ver}") if ver.zero?
download_url 메서드는 ver.zero?일 때 명시적으로 에러를 발생시킨다 — v0 리소스는 다운로드가 허용되지 않는 설계이다.
Kibana DB 확인 결과:
| Pano ID | resource_state | state | filesize | 결론 |
|---|---|---|---|---|
| 83949969 | created |
abandoned |
null |
업로드 미완료 → abandoned 처리됨 |
| 83954507 | created |
abandoned |
null |
업로드 미완료 → abandoned 처리됨 |
두 pano 모두 resource_state: "created" (업로드 시작조차 안 됨), filesize: null이다. v1 경로로 fallback 다운로드를 시도해도 S3에 객체가 없으므로 동일한 NoSuchKey 에러가 발생한다.
5단계: abandoned 상태 전이 경로
event :abandoned do
transition %i[created resource_missing] => :abandoned
end
abandoned 이벤트는 created 또는 resource_missing 상태에서만 발생 가능하다. 호출 지점은 두 곳:
if @model.panos.not_processible.exists?
@model.panos.not_processible.find_each(&:abandoned_state!)
end
if self.panos.not_processible.exists?
not_processible_panos = self.panos.not_processible
_panos_count = not_processible_panos.count
not_processible_panos.each(&:abandoned_state!)
end
not_processible 스코프는 where(state: %i[created resource_missing])(statable/pano.rb:15)로 정의된다. 즉 업로드가 완료되지 않은 pano를 capture 처리/재개 시점에 abandoned로 마킹하는 것이다. resource.rb:78-82의 check_uploading 클래스 메서드가 15분 이상 created/uploading 상태인 리소스를 missing으로 전환한다.
6단계: pix-genie의 pano 필터링 부재
async getCPPanosByCaptureId(captureId: number): Promise<CPPano[]> {
const srvPanos = await this.cupixApi.pano.getAll(captureId);
return srvPanos.filter((srvPano) => srvPano.state !== 'done').map((srvPano) => new CPPano(srvPano));
};
현재 필터링 조건은 state !== 'done'뿐이다. API 응답에 resource_state 필드가 포함되어 있으나(cupix-fields.ts:153), 이를 기반으로 업로드 미완료 pano를 제외하는 로직이 없다. CPPano 모델(cppano.ts:23-40)도 resource_state를 저장하지 않고 S3 bucket 정보만 추출한다.
Log Evidence#
Datadog 검색 쿼리:
service:cupixworks-pix-genie-preprocessor-instance status:error @environment:production "MaskWork::maskPanos"
에러 로그 (20:20:29Z):
MaskWork::maskPanos | script Traceback (most recent call last):
File "/tmp/lib/face-body-detector/cupix_inference.py", line 453, in <module>
detect(args)
File "/tmp/lib/face-body-detector/cupix_inference.py", line 334, in detect
img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgproc/src/color.cpp:196: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'
에러 로그 (동일 타임스탬프):
MaskWork::maskPanos | error code:1
활성 capture 정보 (info 로그):
PreprocessorService::run | loaded capture - id: 694020, name: eval_denison_37775 (20:13:44Z)
PixGenieManager::executePixGenieProcessing | begin - capture: 694033 (20:12:01Z)
PreprocessorService::run | loaded capture - id: 41024, name: eval_denison_37793 (20:37:06Z)
PreprocessorService::run | loaded capture - id: 694032, name: eval_melonba_hs_43013 (20:40:55Z)
다운로드 실패 관련 에러 로그 (Kibana/Watch — logstash-processing* 인덱스):
[2026-05-08T20:13:42.983Z] [error] capture:694181, job:1059761
AwsS3Manager::downloadParallel | begin - "The specified key does not exist."
[2026-05-08T20:42:40.407Z] [error] capture:694180, job:1059860
AwsS3Manager::downloadParallel | begin - "The specified key does not exist."
v0 키를 가진 pano의 debug 로그 (다운로드 시도):
[2026-05-08T20:13:42.893Z] [debug] capture_694181
AwsS3Manager::downloadParallel | buket region, name, key, endpoint, download path us-west-1 cupixworks-source-888a512bf858-uswe1 resources/hyypt8/uswe1/v0 null /tmp/workspace/capture_694181/download/pano_83949969.jpg
[2026-05-08T20:42:40.274Z] [debug] capture_694180
AwsS3Manager::downloadParallel | buket region, name, key, endpoint, download path us-west-1 cupixworks-source-888a512bf858-uswe1 resources/obszdo/uswe1/v0 null /tmp/workspace/capture_694180/download/pano_83954507.jpg
참고: Kibana의 error 로그 메시지 포맷(downloadParallel | begin - "The specified key does not exist.")이 코드의 catch 블록 포맷(download failed - pano: %s, error: %s)과 다르다. 이는 로깅 프레임워크의 메시지 interpolation 이슈이거나 프로덕션 배포 코드의 차이로 추정된다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 다운로드 실패한 pano 파일이 디스크에 없는 상태에서 Python 스크립트가 해당 파일을 imread하여 None 반환 | aws-s3.manager.ts:75-77에서 다운로드 에러를 삼키는 코드 확인. cv2.imread()는 파일 미존재 시 None 반환하는 것이 OpenCV 문서화된 동작. 스크립트가 exit code 1로 종료. Kibana에서 S3 NoSuchKey 에러 로그 2건 확인 — capture_694181(20:13:42Z), capture_694180(20:42:40Z). 각 capture에 v0 버전 키 pano가 1건씩 존재하여 S3에서 key not found 발생. |
download failed 에러 로그는 Datadog에서 미확인 |
Confirmed |
| H2 | 이미지 파일이 다운로드되었으나 손상(corrupt)되어 imread가 None 반환 | cv2.imread()는 디코딩 실패 시에도 None 반환 가능 |
9건 모두 동일 에러로 corrupt 파일이 여러 건 동시에 발생하기 어려움. 네트워크 이슈로 partial download 시 파일이 0 byte이거나 미존재하는 것이 더 일반적 | Inconclusive |
| H3 | env 옵션에서 process.env 누락으로 Python 라이브러리 로딩 실패 |
mask-work.ts:32에서 process.env 미포함 확인. pixgenie.process.ts:66과 비교하면 올바른 패턴은 ...process.env spread |
SAM3 모델이 정상 로드되어 UserWarning을 출력한 것이 로그에서 확인됨 — Python 및 라이브러리 자체는 정상 동작 | Rejected |
| H4 | 디스크 공간 부족으로 다운로드된 파일이 0 byte | 다수의 capture가 병렬 처리 중이어서 workspace가 찰 수 있음 | workspace는 capture별로 분리되고, 다른 capture는 정상 완료됨 | Rejected |
Fix Recommendation#
즉시 조치 (Critical) — pix-genie에서 업로드 미완료 pano 제외#
pano-postprocessor-manager.ts:29-32의 getCPPanosByCaptureId()에서 resource_state가 uploaded가 아닌 pano를 다운로드 대상에서 제외한다. API 응답에 이미 resource_state 필드가 포함되어 있으므로(cupix-fields.ts:153) 추가 API 호출 없이 클라이언트 사이드 필터링으로 구현 가능하다.
// 현재: state !== 'done' 만 필터링
return srvPanos.filter((srvPano) => srvPano.state !== 'done').map((srvPano) => new CPPano(srvPano));
// 변경: resource_state가 uploaded가 아닌 pano도 제외
return srvPanos
.filter((srvPano) => srvPano.state !== 'done' && srvPano.resource_state === 'uploaded')
.map((srvPano) => new CPPano(srvPano));
이렇게 하면 resource_state: "created" (업로드 미완료), resource_state: "missing" (업로드 확인 실패), resource_state: "uploading" (업로드 진행 중) pano가 모두 다운로드 및 마스킹 대상에서 제외되어, S3 NoSuchKey 에러가 원천 차단된다. 나머지 정상 pano는 그대로 프로세싱을 진행한다.
- v0→v1 fallback은 해결책이 아님: v0은 업로드 미완료 상태를 의미하며 v1에도 S3 객체가 없다.
abandoned상태 기반 필터링은 불충분: Tesladefault_scope(statable/pano.rb:8)가abandonedpano를 API에서 이미 제외하지만, postprocessor 실행 시점에 아직created상태인 pano가 포함될 수 있다.resource_state기반 필터링이 더 신뢰성 있다.
단기 개선 (1주 이내)#
aws-s3.manager.ts:75-77— 다운로드 실패 시 해당 pano를 실패 목록에 추가하고, 후속 단계(mask, infer, resize)에서 제외하는 방어 로직 추가. 현재는 에러를 삼키고 진행하기 때문에resource_state필터링을 우회하는 다른 다운로드 실패 케이스에도 대응 가능.mask-work.ts:32—env옵션에...process.env를 spread하여 부모 환경변수를 자식 프로세스에 전달.pixgenie.process.ts:66의 패턴을 참조.- Python 스크립트(
cupix_inference.py:334)에cv2.imread()결과 None 체크를 추가하여, 개별 파일 로드 실패 시 skip하고 나머지 파일은 정상 처리하도록 방어 코드 추가.
장기 개선 (재발 방지)#
- 개별 pano 실패 시 해당 pano만 에러 처리하고 나머지는 계속 진행하는 partial failure 패턴 적용 (현재
maskPanos는 전체 directory를 일괄 처리하므로 한 건 실패 시 전체 실패)
Monitoring#
AwsS3Manager::downloadParallel | download failed로그에 대한 알림 추가:
service:cupixworks-pix-genie-preprocessor-instance "download failed"
MaskWork::maskPanos | error code발생 빈도 모니터링:
service:cupixworks-pix-genie-preprocessor-instance "MaskWork::maskPanos | error code"
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — 다운로드 실패 추적 로직 추가 및 Python 스크립트 방어 코드 필요
Revision History#
Revision 1#
Feedback: 동일 시간대 debug 레벨 로그를 Kibana(Watch)에서 검색하여 AwsS3Manager::downloadParallel | download failed 로그의 실제 에러 메시지를 확인 요청
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| Kibana debug 로그에서 download failed 실제 에러 메시지 확인 | 수용 | Kibana logstash-processing* 인덱스에서 error 레벨 로그 2건 확인: `AwsS3Manager::downloadParallel |
변경 사항:
## Root Cause Summary: 일반적 "다운로드 실패"에서 구체적 S3NoSuchKey에러로 업데이트. 실패한 v0 키 pano 2건(pano_83949969, pano_83954507) 명시.### Log Evidence: Kibana 에러 로그 2건 + debug 로그(v0 키 pano 다운로드 시도) 추가. 기존 "uncertain" 문구를 실제 증거로 대체.## Hypotheses Considered: H1 판정 근거를 Kibana 증거로 보강. "Datadog에서 미확인" → "Kibana error 레벨에서 확인됨"으로 수정.
추가 조사 내용:
- Kibana
logstash-processing*인덱스에서cupixworks-pix-genie-preprocessor-instance서비스의 2026-05-08 20:00~21:00 UTC 구간 로그 검색 downloadParallel키워드로 debug/error 레벨 로그 총 1,401건 중 error 2건 식별"specified key does not exist"정확한 문구 검색으로 S3 NoSuchKey 에러 2건 확인v0버전 키 pano 검색으로 실패 원인 pano 특정 (capture_694181:resources/hyypt8/uswe1/v0, capture_694180:resources/obszdo/uswe1/v0)aws-s3.manager.ts소스 코드의downloadParallel메서드 catch 블록 확인 — 코드상 로그 포맷(download failed - pano: %s, error: %s)과 Kibana 실제 로그 포맷(downloadParallel | begin - "...")이 불일치하나 에러 내용은 동일
Revision 2#
Feedback: v0 인걸 v1 으로 판단해서 다운로드를 시도하면 되는지 확인
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| v0 키를 v1로 변환하여 다운로드 시도하면 해결되는지 | 거부 | Tesla API의 Resource 버전 모델에서 v0은 업로드 미완료 초기 상태를 의미. upload_revision은 self.revision + 1로 항상 v1 경로에 업로드됨 (storagable/resource.rb:21-23). check_uploading (resource.rb:163-183)이 v1에서 S3 객체를 확인 후 revision을 1로 증가시키는데, API가 v0 키를 반환한다는 것은 check_uploading이 성공한 적이 없음을 의미 — v1에도 S3 객체가 없다. Kibana DB 확인: 두 pano 모두 resource_state: "created", filesize: null — 이미지 업로드 자체가 수행되지 않음. 또한 download_url 메서드(resource.rb:188)가 ver.zero?일 때 명시적으로 에러를 raise하는 것도 v0이 다운로드 불가 상태임을 입증. |
변경 사항:
## Root Cause Summary: v0 키의 의미 설명 추가 — v0은 업로드 미완료 상태이며, v0→v1 fallback도 불가함을 명시.### Code Path: "4단계: Tesla Resource 버전 모델" 섹션 추가 —upload_revision,check_uploading,download_url의 코드 경로와 Kibana DB 확인 결과를 포함.## Fix Recommendation: v0→v1 fallback이 해결책이 아닌 이유 명시,resource_state필터링을 올바른 접근법으로 제시.
추가 조사 내용:
- Tesla API 코드 탐색:
storagable/resource.rb(upload_revision, check_uploading, download_url),decorators/resource.rb(object_key),revisionable/pano.rb(default_initial_revision),revisionable.rb(increase_revision) pano_serializer.rb:100-102— API 응답에서storage_bucket_key가resource.object_key(현재 revision 기반)로 동적 생성됨을 확인cppano.ts:34— agent가pano.resources[0]['storage_bucket_key']를 그대로 사용하여 버전 선택 로직 없음을 확인- Kibana
panos인덱스에서 pano_83949969, pano_83954507 조회 — 둘 다resource_state: "created",state: "abandoned",filesize: null
Revision 3#
Feedback: state: "abandoned" 이 언제 발생하는지 확인. 파일 업로드 실패 케이스라면 pix-genie에서 다운로드 받지 않고, 해당 pano를 빼고 나머지 프로세싱을 진행하도록 변경.
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
state: "abandoned" 발생 시점 확인 |
수용 | Tesla state machine(statable/pano.rb:83-85)에서 abandoned 이벤트는 created 또는 resource_missing 상태에서만 전이 가능. 호출 지점 2곳 확인: (1) capture_invoker.rb:29-31 — capture 처리 시작 시 not_processible pano를 일괄 abandoned 마킹, (2) resumable.rb:71-77 — capture resume 시 cleanup_materials에서 동일 처리. not_processible 스코프는 where(state: %i[created resource_missing])(statable/pano.rb:15). 리소스 레벨에서는 resource.rb:78-82의 check_uploading 클래스 메서드가 15분 이상 created/uploading 상태인 리소스를 missing으로 전환. 즉 abandoned는 업로드 미완료(파일 업로드 실패/타임아웃) pano를 처리 불가로 정리하는 상태임이 확인됨. |
| pix-genie에서 해당 pano를 빼고 나머지 프로세싱 진행 | 수용 | pano-postprocessor-manager.ts:29-32의 getCPPanosByCaptureId()에서 현재 state !== 'done'만 필터링. API 응답에 resource_state 필드가 이미 포함(cupix-fields.ts:153). resource_state === 'uploaded' 조건 추가로 업로드 미완료 pano를 다운로드 전에 제외 가능. Tesla default_scope(statable/pano.rb:8)가 abandoned pano를 API에서 제외하지만, postprocessor 실행 시점에 아직 created 상태인 pano가 포함될 수 있으므로 resource_state 기반 필터링이 더 신뢰성 있음. |
변경 사항:
## Root Cause Summary:abandoned상태의 의미와 발생 경로 설명 추가.resource_state기반 필터링으로 pix-genie에서 미업로드 pano를 제외하는 접근법 명시.### Code Path: "5단계:abandoned상태 전이 경로" 추가 —statable/pano.rb,capture_invoker.rb,resumable.rb의 코드 경로. "6단계: pix-genie의 pano 필터링 부재" 추가 —getCPPanosByCaptureId()의 현재 필터링 로직과resource_state미활용 문제.## Fix Recommendation: 즉시 조치를getCPPanosByCaptureId()에서resource_state === 'uploaded'필터링 추가로 변경. 구체적 코드 변경안 포함. 기존 다운로드 실패 추적 로직은 단기 개선으로 이동.
추가 조사 내용:
- Tesla state machine 탐색:
statable/pano.rb—abandoned이벤트 정의(line 83-85),not_processible스코프(line 15),default_scope(line 8) capture_invoker.rb:29-31— capture 처리 시작 시not_processiblepanoabandoned마킹resumable.rb:67-96— capture resume 시cleanup_materials에서not_processiblepano/videoabandoned마킹storagable/resource.rb:78-82—check_uploading클래스 메서드: 15분 타임아웃 후missing전환pano-postprocessor-manager.ts:29-32—getCPPanosByCaptureId()필터링 로직 확인cupix-fields.ts:151-158— API 요청 시resource_state필드 포함 확인cppano.ts:23-40—CPPano모델이resource_state를 저장하지 않음 확인pano-postprocessor-service.ts:96-117— pano 목록 fetch → download → mask 전체 흐름 확인