쓰리아이
경력근거: 로그·모니터링·장애 분석 등 반복적인 운영 업무의 자동화 및 개선
쓰리아이는 360° 사진으로 시설의 디지털 트윈을 만드는 beamo 를 29개국 출신 120여 명이 개발·서비스하고, 매출의 90% 이상이 해외에서 납니다. 고객사가 일본·독일·미국·브라질에 흩어져 있어 리전마다 처리 대기 허용 시간이 다르고, 고객지원 문의는 대개 상대 리전의 업무 시간에 들어옵니다. 사진 업로드와 처리 대기열, 트윈 생성 작업이 촬영 번호 하나로 이어지는 beamo 의 비동기 처리 흐름이 있어야 이 판정이 서고, 이 공고가 우대하는 중복 처리·장애 복구·미디어 처리 파이프라인 경험이 그 흐름 위에서 쓰입니다. 이 포트폴리오는 그 기록으로 촬영의 처리 상태를 판정하는 백엔드 규칙과 고객지원 화면, 메시지 재전달을 재현해 두 번 처리를 막는 워커를 보여줍니다. 3D 재구성이나 컴퓨터 비전 알고리즘에 대한 전문성은 주장하지 않습니다.
쓰리아이는 pivo.ai 와 beamo.ai 를 개발·서비스하고 매출의 90% 이상이 해외에서 나는 회사이며, 이 자리는 글로벌 사용자를 위한 백엔드 시스템을 만들고 운영합니다. beamo 고객사는 360° 카메라로 시설을 찍어 올리면 디지털 트윈을 받는데, 그 사이 업로드와 처리 대기열이 어디서 멈췄는지가 해외 고객지원 문의의 첫 질문입니다.
우대사항은 분산·비동기 환경의 중복 처리·순서 보장·장애 복구 설계와 파일 업로드·미디어 처리 파이프라인 운영 경험이고, 사용 스택에 Kafka 와 SQS 가 있습니다. 사진 업로드 → 대기열 → 트윈 생성 작업으로 이어지는 흐름에서 메시지 재전달로 같은 촬영이 두 번 처리되는 경우와 실패 뒤 재시도가 끊긴 경우를 판정하는 이 포트폴리오가 그 경험을 그대로 다룹니다.
주요업무에 로그·모니터링·장애 분석 같은 반복 운영 업무의 자동화와 Claude Code·Codex 를 쓴 개발·배포·운영 흐름 설계가 있습니다. 고객지원 담당자가 매번 백엔드 엔지니어에게 묻던 '이 촬영 왜 트윈이 안 나왔나요' 를 판정 화면으로 바꾸고, 판정 규칙을 고칠 때는 Claude Code 의 변경이 정답 대조를 통과해야 합쳐지게 합니다.
beamo 는 쓰리아이가 운영하는 B2B SaaS 로, 고객사가 공장·물류센터·변전소를 360° 카메라로 찍어 올리면 쓰리아이 서버가 사진이 다 모였을 때 디지털 트윈(시설을 화면 속에서 걸어 다니며 볼 수 있는 3D 사본) 생성 작업을 대기열에 넣어 처리합니다. 이 관리자 페이지는 촬영 목록(현장·리전·찍은 사진 수), 사진별 수신 기록, 트윈 생성 작업 기록, 리전별 대기 허용 분과 재시도 한도를 적은 리전 기준표를 받아 촬영마다 다섯 판정 중 하나를 붙입니다. 같은 사진 번호가 두 번 들어온 수신 기록을 한 장으로 세어 빠진 사진을 찾고, 끝나지 않은 작업이 그 리전 기준보다 오래 대기열에 있는지 재며, 실패한 작업 뒤에 새 작업이 생겼는지와 완료된 작업이 둘 이상인지를 봅니다. 고객지원 담당자와 백엔드 엔지니어는 같은 화면에서 이 촬영을 재처리할지, 고객에게 재촬영을 안내할지 정합니다.
전 세계 beamo 고객사의 '트윈이 안 나왔다' 문의를 받는 쓰리아이 고객지원 담당자와, 그 건을 넘겨받는 백엔드 엔지니어 — 같은 화면을 보고 재처리할지 재촬영을 안내할지 정한다
지금 고객지원 담당자는 해외 고객사가 '트윈이 안 나왔다' 고 문의하면 촬영 번호를 백엔드 엔지니어에게 넘기고, 엔지니어는 업로드 로그와 대기열, 작업 기록을 따로 열어 원인을 찾습니다. 세 기록은 촬영 번호 하나로 이어지고, 빠진 사진 번호·대기 분·재시도 횟수·완료 작업 수를 리전 기준과 견주면 원인이 다섯 갈래 중 하나로 정해집니다. 담당자는 그 판정을 보고 문의가 오기 전에 재처리나 재촬영 안내를 시작합니다.
고객지원 담당자가 촬영 목록·사진 수신 기록·트윈 생성 작업 기록·리전 기준표 네 파일을 올리면 촬영마다 다섯 판정 중 하나와 받은 사진 수, 마지막 수신 뒤 지난 분, 대기 분, 재시도 횟수, 완료 작업 수가 서는 관리자 페이지입니다. 같은 사진 번호를 한 장으로 세는 집계, UTC 시각 차 계산, 촬영별 마지막 작업 고르기, 판정 차례가 이 주에 들어갑니다. 실제 대기열 대신 내려받은 작업 기록 파일로 시작합니다.
더미를 촬영 200건으로 늘리고, 엔지니어라면 붙였을 판정을 미리 적어 둔 표와 자동 판정을 맞대 판정별로 몇 건이 갈리는지 냅니다. LocalStack 의 SQS 대기열에서 가시성 타임아웃보다 오래 걸리는 작업을 돌려 같은 메시지가 다시 전달되는 상황을 만들고, 그때 생긴 두 완료 작업이 판정에 잡히는지 봅니다. 2주차부터 AWS 에 배포하고 판정 작업은 Kubernetes CronJob 으로 10분마다 돕니다.
Go 로 만든 트윈 생성 워커가 메시지 키를 Redis 에 한 번만 기록하고 처리해, 2주차 재전달 재현에서 두 번 처리 판정이 0건이 되는지 봅니다. 고객지원 담당자가 판정이 붙은 촬영을 고르면 Claude 가 고객사 리전 언어(일본어·독일어·영어)로 재처리 또는 재촬영 안내 답장 초안을 쓰고, 초안의 촬영 번호와 사진 수가 판정 값과 다르면 초안 없이 판정 요약만 보냅니다.
고객지원 담당자가 10월 1일 촬영 기록을 올리면 뮌헨 공장 3라인 C-77830 이 161분째 대기열에 있어 독일 리전 기준 45분을 넘긴 촬영으로, 평택 설비동 B C-77866 이 같은 메시지로 완료 작업이 둘 생긴 촬영으로 섭니다.
백엔드 엔지니어가 촬영 하나를 펼쳐 받은 사진 번호와 마지막 수신 시각, 작업마다 상태·시도 횟수·대기열에 들어간 시각을 읽고 재처리 버튼을 누를지 재촬영을 안내할지 고객지원 담당자와 정합니다.
같은 사진 번호가 두 번 들어와 수신 줄 수로는 다 온 것처럼 보이는 촬영, 상태가 processing 인 채 멈춘 작업, 한 번 실패한 뒤 재시도로 끝난 촬영, 대기는 길었지만 결국 끝난 촬영, 마지막 수신이 30분 전이라 아직 판정하지 않는 촬영을 한 번에 올리면 촬영마다 묶음 하나가 정해지고, 상태 값이 낯설거나 시각이 빠진 작업 기록 파일 10종은 읽지 못한 줄 수를 화면 위에 알리고 남은 촬영만 판정합니다.
2주차의 촬영 200건 대조는 백엔드 엔지니어 눈으로 제가 매긴 판정과 자동 판정이 갈린 촬영 수를 판정별로 냅니다. 업로드 기록과 작업 기록, 정답을 모두 제가 지었으니 0 이 나와도 개수 세기와 판정 차례가 적힌 대로 돈다는 뜻이고, beamo 처리 장애의 실제 원인을 맞혔다는 뜻은 아닙니다.
위 수치는 더미데이터 자가채점이며, 판정이 고객 문의보다 먼저 문제 촬영을 잡는지는 쓰리아이의 실제 업로드·작업 기록과 고객지원 문의 기록이 있어야 측정됩니다. MongoDB 의 촬영·작업 문서에서 네 표의 컬럼만 뽑아 내면 판정 코드는 손대지 않고 그 기록으로 다시 돕니다.
beamo 처리 장애의 실제 원인은 쓰리아이 백엔드 엔지니어가 워커 로그까지 열어 봐야 가려지는데, 더미 촬영 열 건의 정답은 제가 매겼습니다. 이 화면이 증명하는 것은 같은 사진 번호를 한 장으로 세고 대기 분을 리전 기준과 견주는 계산이 적은 대로 돈다는 데까지이고, 판정이 실제 장애 원인과 맞는다는 주장은 하지 않습니다.
사진이 번호 순서대로 한 번씩 오고 작업이 한 번에 끝나는 촬영만 있으면 판정 규칙은 시험받지 않습니다. 사진 2번이 두 번 들어온 싱가포르 촬영은 수신 줄 수만 세는 구현을, processing 상태로 110분 머문 오사카 촬영은 queued 만 정체로 보는 구현을, 실패 뒤 재시도로 끝난 시드니 촬영은 첫 실패만 보는 구현을, 같은 메시지 키로 완료 작업이 둘 생긴 평택 촬영은 완료 여부만 보는 구현을 각각 틀리게 만듭니다.
재시도 한도를 다 쓰고 실패로 끝난 촬영, 사진은 다 왔는데 트윈 생성 작업이 아직 만들어지지 않은 촬영, 대기 중이지만 리전 기준 안인 촬영은 다섯 판정에 자리가 없어 화면 위 집계(재시도 소진·작업 미생성·처리 중)로 따로 셉니다. 작업 미생성은 대기열 메시지가 사라진 장애일 수 있어, 실제 운영에서 얼마나 나오는지 본 뒤 판정 값으로 올릴지 정하는 일을 도입 단계 과제로 둡니다.
실제 beamo 촬영은 사진이 수백 장이라 인라인 더미의 3~4장과 규모가 다릅니다. 수신 기록이 수십만 줄이 되면 파일 업로드 방식은 버티지 못하므로, 2주차부터 MongoDB 집계로 옮기는 것을 전제로 둡니다.
이 포트폴리오가 다루는 범위는 사진 수신과 대기열, 작업 기록의 개수와 시각까지입니다. 3D 재구성이나 컴퓨터 비전 알고리즘에 대한 전문성은 주장하지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
beamo 앱이 올린 현장 촬영 목록입니다. photos_expected 는 앱이 촬영을 마칠 때 서버에 알린 찍은 사진 수이고, 시각은 모두 UTC 입니다. 실제 촬영은 수백 장이지만 판정 흐름을 보이려고 3~4장짜리 촬영으로 줄였습니다.
| capture_id | site_id | site_name | region | photos_expected | uploaded_at |
|---|---|---|---|---|---|
| C-77801 | S-311 | 평택 설비동 A | KR | 3 | 2026-10-01T02:10:00Z |
| C-77812 | S-221 | 도쿄 물류센터 B동 | JP | 4 | 2026-10-01T17:58:00Z |
| C-77830 | S-145 | 뮌헨 공장 3라인 | DE | 3 | 2026-10-01T21:12:00Z |
| C-77851 | S-402 | 텍사스 변전소 | US-E | 3 | 2026-10-01T19:40:00Z |
| C-77866 | S-310 | 평택 설비동 B | KR | 3 | 2026-10-01T04:55:00Z |
아래 JSON Schema 에 맞는 beamo 현장 촬영 목록 행을 더 만들어 주세요. {"type":"object","properties":{"capture_id":{"type":"string"},"site_id":{"type":"string"},"site_name":{"type":"string"},"region":{"type":"string","description":"고객사 데이터가 처리되는 리전 코드입니다."},"photos_expected":{"type":"integer","minimum":1,"description":"앱이 알린 찍은 사진 수입니다."},"uploaded_at":{"type":"string","description":"업로드를 시작한 UTC 시각(ISO 8601, Z)입니다."}},"required":["capture_id","site_id","site_name","region","photos_expected","uploaded_at"]} 공장·물류센터·변전소·데이터센터·건설 현장을 일본·독일·미국·브라질·한국·싱가포르·호주·영국에 고루 두고, region 은 region_sla 에 있는 코드만 씁니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 현장을 하루에 두 번 찍은 촬영, (2) 기준 시각 직전에 업로드를 시작해 아직 사진이 오는 중인 촬영, (3) photos_expected 가 실제 받은 사진 수보다 큰 촬영. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"capture_id":{"type":"string"},"site_id":{"type":"string"},"site_name":{"type":"string"},"region":{"type":"string","description":"고객사 데이터가 처리되는 리전 코드입니다."},"photos_expected":{"type":"integer","minimum":1,"description":"앱이 알린 찍은 사진 수입니다."},"uploaded_at":{"type":"string","description":"업로드를 시작한 UTC 시각(ISO 8601, Z)입니다."}},"required":["capture_id","site_id","site_name","region","photos_expected","uploaded_at"]}촬영마다 서버가 사진을 받을 때 남기는 수신 기록입니다. 앱이 응답을 못 받아 같은 사진을 다시 보내면 같은 photo_seq 가 두 줄 남고, 중간에 끊긴 촬영은 photos_expected 보다 적은 번호만 남습니다.
| capture_id | photo_seq | received_at |
|---|---|---|
| C-77801 | 1 | 2026-10-01T02:10:20Z |
| C-77801 | 2 | 2026-10-01T02:10:55Z |
| C-77801 | 3 | 2026-10-01T02:11:40Z |
| C-77812 | 1 | 2026-10-01T17:58:10Z |
| C-77812 | 2 | 2026-10-01T18:00:02Z |
아래 JSON Schema 에 맞는 사진 수신 기록 행을 더 만들어 주세요. {"type":"object","properties":{"capture_id":{"type":"string"},"photo_seq":{"type":"integer","minimum":1,"description":"촬영 안에서 사진의 번호입니다."},"received_at":{"type":"string","description":"서버가 받은 UTC 시각(ISO 8601, Z)입니다."}},"required":["capture_id","photo_seq","received_at"]} 사진은 대개 번호 순서대로 수십 초 간격으로 들어옵니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 photo_seq 가 몇 분 간격으로 두 번 들어와 줄 수는 찍은 사진 수와 같지만 번호가 하나 빈 촬영, (2) 번호가 뒤섞인 순서로 들어온 촬영, (3) 중간 번호 하나만 빠진 촬영, (4) 마지막 수신이 기준 시각 1시간 안이라 아직 올라오는 중인 촬영. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"capture_id":{"type":"string"},"photo_seq":{"type":"integer","minimum":1,"description":"촬영 안에서 사진의 번호입니다."},"received_at":{"type":"string","description":"서버가 받은 UTC 시각(ISO 8601, Z)입니다."}},"required":["capture_id","photo_seq","received_at"]}사진이 다 모인 촬영마다 대기열에 들어간 디지털 트윈 생성 작업 기록입니다. 끝나지 않은 작업은 finished_at 이 비어 있고, 실패 뒤 재시도하면 attempt 가 하나 큰 새 작업이 생깁니다. trigger_message_key 는 작업을 만든 대기열 메시지의 키라서, 메시지가 다시 전달되면 같은 키를 가진 작업이 하나 더 생깁니다.
| job_id | capture_id | status | enqueued_at | finished_at | attempt | trigger_message_key |
|---|---|---|---|---|---|---|
| J-40801 | C-77801 | done | 2026-10-01T02:12:00Z | 2026-10-01T02:31:00Z | 1 | msg-7f3a01 |
| J-40830 | C-77830 | queued | 2026-10-01T21:19:00Z | — | 1 | msg-7f3a30 |
| J-40851 | C-77851 | failed | 2026-10-01T19:45:00Z | 2026-10-01T20:10:00Z | 1 | msg-7f3a51 |
| J-40866 | C-77866 | done | 2026-10-01T04:59:00Z | 2026-10-01T05:21:40Z | 1 | msg-7f3a66 |
| J-40867 | C-77866 | done | 2026-10-01T05:04:00Z | 2026-10-01T05:24:50Z | 1 | msg-7f3a66 |
아래 JSON Schema 에 맞는 트윈 생성 작업 기록 행을 더 만들어 주세요. {"type":"object","properties":{"job_id":{"type":"string"},"capture_id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"enqueued_at":{"type":"string","description":"대기열에 들어간 UTC 시각(ISO 8601, Z)입니다."},"finished_at":{"type":["string","null"],"description":"done 또는 failed 로 끝난 시각입니다. 끝나지 않았으면 null 입니다."},"attempt":{"type":"integer","minimum":1,"description":"같은 촬영에서 몇 번째 시도인지입니다."},"trigger_message_key":{"type":"string","description":"이 작업을 만든 대기열 메시지의 키입니다."}},"required":["job_id","capture_id","status","enqueued_at","finished_at","attempt","trigger_message_key"]} 작업은 마지막 사진 수신 1~5분 뒤 대기열에 들어가고 보통 15~25분 걸립니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 가시성 타임아웃이 지나 같은 trigger_message_key 로 작업이 하나 더 생겨 둘 다 done 인 촬영, (2) status 가 processing 인 채 finished_at 이 null 로 오래 남은 작업, (3) failed 뒤 attempt 2 로 다시 돌아 done 이 된 촬영, (4) failed 로 끝났는데 attempt 가 재시도 한도보다 작고 새 작업이 없는 촬영, (5) 대기열에 오래 있었지만 결국 done 이 된 작업. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"job_id":{"type":"string"},"capture_id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"enqueued_at":{"type":"string","description":"대기열에 들어간 UTC 시각(ISO 8601, Z)입니다."},"finished_at":{"type":["string","null"],"description":"done 또는 failed 로 끝난 시각입니다. 끝나지 않았으면 null 입니다."},"attempt":{"type":"integer","minimum":1,"description":"같은 촬영에서 몇 번째 시도인지입니다."},"trigger_message_key":{"type":"string","description":"이 작업을 만든 대기열 메시지의 키입니다."}},"required":["job_id","capture_id","status","enqueued_at","finished_at","attempt","trigger_message_key"]}리전마다 트윈 생성 작업이 끝나지 않은 채 대기해도 되는 분과 실패 뒤 자동 재시도 한도를 적은 리전 기준표입니다. 판정이 도는지 보이기 위한 더미 기준이며 쓰리아이의 실제 운영 기준이 아닙니다.
| region | max_queue_min | max_retry |
|---|---|---|
| KR | 30 | 3 |
| JP | 30 | 3 |
| SG | 40 | 3 |
| AU | 45 | 2 |
| DE | 45 | 3 |
아래 JSON Schema 에 맞는 리전 기준표 행을 더 만들어 주세요. {"type":"object","properties":{"region":{"type":"string"},"max_queue_min":{"type":"integer","minimum":1,"description":"enqueued_at 부터 끝나지 않은 채 기다려도 되는 분입니다."},"max_retry":{"type":"integer","minimum":1,"description":"attempt 가 이 값에 닿을 때까지 실패하면 자동으로 다시 시도합니다."}},"required":["region","max_queue_min","max_retry"]} 처리 서버와 가까운 리전은 대기 허용 분을 짧게, 먼 리전은 길게 씁니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 재시도 한도가 1 이라 한 번 실패하면 끝나는 리전, (2) 같은 대륙인데 대기 허용 분이 다른 두 리전. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"region":{"type":"string"},"max_queue_min":{"type":"integer","minimum":1,"description":"enqueued_at 부터 끝나지 않은 채 기다려도 되는 분입니다."},"max_retry":{"type":"integer","minimum":1,"description":"attempt 가 이 값에 닿을 때까지 실패하면 자동으로 다시 시도합니다."}},"required":["region","max_queue_min","max_retry"]}| TypeScript | 사진 번호 집계, 대기 분 계산, 마지막 작업 고르기, 판정 차례를 담은 판정 서비스와 화면 코드의 언어입니다. |
| Node.js | 네 파일을 받아 판정하는 API 서버가 돌고, 2주차부터 MongoDB 에서 직접 읽습니다. |
| MongoDB | 촬영과 트윈 생성 작업 기록을 촬영 번호로 묶은 문서로 두고 리전별로 끝나지 않은 작업을 찾습니다. |
| Amazon SQS | 2주차에 LocalStack 의 SQS 로 가시성 타임아웃을 넘긴 작업의 메시지 재전달을 재현합니다. |
| Kafka | 3주차에 사진 수신 이벤트를 토픽으로 받아 촬영별 수신 수를 갱신하고 판정을 다시 돌립니다. |
| Redis | 3주차 워커가 처리할 메시지 키를 SET NX 로 한 번만 기록해 같은 메시지를 두 번 처리하지 않게 합니다. |
| Go | 3주차에 두 번 처리를 막는 트윈 생성 워커 예제를 만듭니다. |
| Kubernetes | 2주차에 판정 작업을 CronJob 매니페스트로 써서 10분마다 돌립니다. |
| AWS | 2주차부터 판정 API 와 화면을 AWS 에 올리고 CloudWatch 로 판정 작업 실패를 봅니다. |
| Claude Code·Codex | CLAUDE.md 에 판정 규칙과 테스트 명령을 적고, 규칙을 고치는 변경은 정답 대조가 CI 에서 통과해야 합칩니다. |
✗ fail → 시각 계산이 어긋나면 수신 기록과 작업 기록의 시간대를 UTC 로 맞췄는지부터 보고, 판정 차례가 엔지니어 판단과 어긋나면 두 번 처리와 사진 미수신의 순서를 다시 잡습니다.
✓ pass → 고객지원 담당자가 이 목록으로 엔지니어에게 넘길 촬영을 고를 수 있으면 1주차로 완결입니다. 이어서 200건 정답 대조와 SQS 재전달 재현, AWS 배포를 붙입니다.
✗ fail → 재전달 재현이 들쭉날쭉하면 가시성 타임아웃과 작업 시간을 고정값으로 두고 다시 돌린 뒤, 워커 작업은 미룹니다.
✓ pass → Redis 멱등 워커와 고객 답장 초안으로 3주차를 엽니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · capture_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
실패 뒤 재시도 없음재시도 끊김대기열에서 기준 시간 넘게 멈춤대기열 정체사진이 덜 올라옴사진 미수신같은 촬영이 두 번 처리됨두 번 처리트윈 생성 완료생성 완료beamo 촬영 하나가 사진 번호 집계, 마지막 수신 뒤 경과 시간, 마지막 작업의 대기 분과 재시도, 완료 작업 수 계산을 거쳐 어떤 판정을 받는지 남기는 형식입니다. 판정은 규칙이 정하고, 3주차부터 고객 답장 초안만 모델이 씁니다.
{"type":"object","properties":{"capture_id":{"type":"string"},"site_name":{"type":"string"},"region":{"type":"string"},"verdict":{"type":"string","enum":["트윈 생성 완료","사진이 덜 올라옴","대기열에서 기준 시간 넘게 멈춤","실패 뒤 재시도 없음","같은 촬영이 두 번 처리됨"]},"as_of":{"type":"string","description":"판정 기준 UTC 시각입니다."},"photos_expected":{"type":"integer","minimum":1},"photos_received":{"type":"integer","minimum":0,"description":"photo_uploads 에서 이 capture_id 의 서로 다른 photo_seq 개수입니다."},"photo_rows":{"type":"integer","minimum":0,"description":"같은 번호를 두 번 받은 줄도 따로 센 수신 줄 수입니다."},"last_received_at":{"type":["string","null"]},"latest_job_id":{"type":["string","null"]},"latest_status":{"type":["string","null"]},"latest_attempt":{"type":["integer","null"],"minimum":1},"queue_wait_min":{"type":["integer","null"],"minimum":0,"description":"마지막 작업이 끝나지 않았을 때 as_of 에서 enqueued_at 을 뺀 분입니다."},"max_queue_min":{"type":"integer","minimum":1},"max_retry":{"type":"integer","minimum":1},"done_job_count":{"type":"integer","minimum":0},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{}},"required":["field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["capture_id","site_name","region","verdict","as_of","photos_expected","photos_received","max_queue_min","max_retry","done_job_count","evidence","summary"]}[
{
"capture_id": "C-77866",
"site_name": "평택 설비동 B",
"region": "KR",
"verdict": "같은 촬영이 두 번 처리됨",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 3,
"photo_rows": 3,
"last_received_at": "2026-10-01T04:58:30Z",
"latest_job_id": "J-40867",
"latest_status": "done",
"latest_attempt": 1,
"queue_wait_min": null,
"max_queue_min": 30,
"max_retry": 3,
"done_job_count": 2,
"evidence": [
{
"field": "job_id",
"value": "J-40866"
},
{
"field": "job_id",
"value": "J-40867"
},
{
"field": "trigger_message_key",
"value": "msg-7f3a66"
},
{
"field": "status",
"agg": "count",
"value": 2
}
],
"summary": "같은 메시지 키 msg-7f3a66 으로 작업이 둘 생겨 두 작업 모두 트윈 생성을 마쳤습니다.",
"hedge": "두 번째 작업이 첫 작업보다 5분 늦게 대기열에서 꺼내져, 첫 작업이 가시성 타임아웃 안에 끝나지 않아 메시지가 다시 전달된 것으로 보입니다. 고객 화면에 어느 트윈이 걸렸는지는 엔지니어가 확인합니다."
},
{
"capture_id": "C-77812",
"site_name": "도쿄 물류센터 B동",
"region": "JP",
"verdict": "사진이 덜 올라옴",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 4,
"photos_received": 2,
"photo_rows": 2,
"last_received_at": "2026-10-01T18:00:02Z",
"latest_job_id": null,
"latest_status": null,
"latest_attempt": null,
"queue_wait_min": null,
"max_queue_min": 30,
"max_retry": 3,
"done_job_count": 0,
"evidence": [
{
"field": "photos_expected",
"value": 4
},
{
"field": "photo_seq",
"agg": "count",
"value": 2
},
{
"field": "received_at",
"value": "2026-10-01T18:00:02Z"
}
],
"summary": "찍은 사진 4장 중 2장만 받았고 마지막 수신 뒤 6시간 가까이 지나 트윈 생성 작업이 만들어지지 않았습니다.",
"hedge": "앱에 3·4번 사진이 남아 있으면 다시 올려 달라고 안내하고, 없으면 재촬영을 안내합니다."
},
{
"capture_id": "C-77872",
"site_name": "싱가포르 데이터센터 2층",
"region": "SG",
"verdict": "사진이 덜 올라옴",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 2,
"photo_rows": 3,
"last_received_at": "2026-10-01T09:40:12Z",
"latest_job_id": null,
"latest_status": null,
"latest_attempt": null,
"queue_wait_min": null,
"max_queue_min": 40,
"max_retry": 3,
"done_job_count": 0,
"evidence": [
{
"field": "photos_expected",
"value": 3
},
{
"field": "photo_seq",
"agg": "count",
"value": 3
},
{
"field": "received_at",
"value": "2026-10-01T09:38:00Z"
},
{
"field": "received_at",
"value": "2026-10-01T09:40:12Z"
}
],
"summary": "수신 줄은 3줄이지만 2번 사진이 두 번 들어온 것이라 서로 다른 사진은 2장뿐이고 3번 사진이 오지 않았습니다.",
"hedge": "수신 줄 수로만 세면 다 온 것으로 보여 작업이 잘못 시작될 수 있는 촬영입니다."
},
{
"capture_id": "C-77830",
"site_name": "뮌헨 공장 3라인",
"region": "DE",
"verdict": "대기열에서 기준 시간 넘게 멈춤",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 3,
"photo_rows": 3,
"last_received_at": "2026-10-01T21:18:40Z",
"latest_job_id": "J-40830",
"latest_status": "queued",
"latest_attempt": 1,
"queue_wait_min": 161,
"max_queue_min": 45,
"max_retry": 3,
"done_job_count": 0,
"evidence": [
{
"field": "enqueued_at",
"value": "2026-10-01T21:19:00Z"
},
{
"field": "finished_at",
"value": null
},
{
"field": "max_queue_min",
"value": 45
}
],
"summary": "21시 19분에 대기열에 들어간 작업이 161분째 시작하지 못해 독일 리전 기준 45분을 넘었습니다.",
"hedge": null
},
{
"capture_id": "C-77903",
"site_name": "오사카 물류센터",
"region": "JP",
"verdict": "대기열에서 기준 시간 넘게 멈춤",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 4,
"photos_received": 4,
"photo_rows": 4,
"last_received_at": "2026-10-01T21:46:00Z",
"latest_job_id": "J-40903",
"latest_status": "processing",
"latest_attempt": 2,
"queue_wait_min": 110,
"max_queue_min": 30,
"max_retry": 3,
"done_job_count": 0,
"evidence": [
{
"field": "status",
"value": "processing"
},
{
"field": "enqueued_at",
"value": "2026-10-01T22:10:00Z"
},
{
"field": "max_queue_min",
"value": 30
},
{
"field": "attempt",
"value": 2
}
],
"summary": "첫 시도가 실패한 뒤 22시 10분에 다시 들어간 작업이 processing 상태로 110분째 끝나지 않아 일본 리전 기준 30분을 넘었습니다.",
"hedge": "상태가 queued 가 아니라 processing 이라도 끝난 시각이 없으면 같은 판정입니다. 워커가 작업을 잡은 채 멈췄을 수 있어 엔지니어가 워커 로그를 봅니다."
},
{
"capture_id": "C-77851",
"site_name": "텍사스 변전소",
"region": "US-E",
"verdict": "실패 뒤 재시도 없음",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 3,
"photo_rows": 3,
"last_received_at": "2026-10-01T19:44:00Z",
"latest_job_id": "J-40851",
"latest_status": "failed",
"latest_attempt": 1,
"queue_wait_min": null,
"max_queue_min": 60,
"max_retry": 3,
"done_job_count": 0,
"evidence": [
{
"field": "status",
"value": "failed"
},
{
"field": "attempt",
"value": 1
},
{
"field": "max_retry",
"value": 3
},
{
"field": "finished_at",
"value": "2026-10-01T20:10:00Z"
}
],
"summary": "20시 10분에 첫 시도가 실패했는데 재시도 한도 3번 중 1번만 썼고 그 뒤 새 작업이 없습니다.",
"hedge": "자동 재시도가 걸리지 않은 것이라 엔지니어가 재처리를 누르면 됩니다. 재촬영은 필요하지 않습니다."
},
{
"capture_id": "C-77880",
"site_name": "시드니 창고",
"region": "AU",
"verdict": "트윈 생성 완료",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 3,
"photo_rows": 3,
"last_received_at": "2026-10-01T05:54:10Z",
"latest_job_id": "J-40881",
"latest_status": "done",
"latest_attempt": 2,
"queue_wait_min": null,
"max_queue_min": 45,
"max_retry": 2,
"done_job_count": 1,
"evidence": [
{
"field": "job_id",
"value": "J-40880"
},
{
"field": "job_id",
"value": "J-40881"
},
{
"field": "attempt",
"value": 2
},
{
"field": "max_retry",
"value": 2
}
],
"summary": "첫 시도가 실패한 뒤 두 번째 시도에서 트윈 생성을 마쳤습니다.",
"hedge": "호주 리전 재시도 한도 2번을 다 썼으므로, 같은 현장에서 실패가 반복되면 엔지니어가 원인을 봅니다."
},
{
"capture_id": "C-77915",
"site_name": "상파울루 공장",
"region": "BR",
"verdict": "트윈 생성 완료",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 3,
"photo_rows": 3,
"last_received_at": "2026-10-01T09:34:00Z",
"latest_job_id": "J-40915",
"latest_status": "done",
"latest_attempt": 1,
"queue_wait_min": null,
"max_queue_min": 90,
"max_retry": 2,
"done_job_count": 1,
"evidence": [
{
"field": "enqueued_at",
"value": "2026-10-01T09:36:00Z"
},
{
"field": "finished_at",
"value": "2026-10-01T11:46:00Z"
},
{
"field": "max_queue_min",
"value": 90
}
],
"summary": "대기열에 들어간 지 130분 만에 트윈 생성을 마쳤습니다.",
"hedge": "브라질 리전 기준 90분보다 오래 걸렸지만 끝난 작업은 대기열 멈춤으로 보지 않습니다. 늦게 끝난 작업이 잦은지는 2주차 화면에서 따로 봅니다."
},
{
"capture_id": "C-77801",
"site_name": "평택 설비동 A",
"region": "KR",
"verdict": "트윈 생성 완료",
"as_of": "2026-10-02T00:00:00Z",
"photos_expected": 3,
"photos_received": 3,
"photo_rows": 3,
"last_received_at": "2026-10-01T02:11:40Z",
"latest_job_id": "J-40801",
"latest_status": "done",
"latest_attempt": 1,
"queue_wait_min": null,
"max_queue_min": 30,
"max_retry": 3,
"done_job_count": 1,
"evidence": [
{
"field": "photo_seq",
"agg": "count",
"value": 3
},
{
"field": "status",
"value": "done"
},
{
"field": "finished_at",
"value": "2026-10-01T02:31:00Z"
}
],
"summary": "사진 3장을 모두 받았고 대기열에 들어간 지 19분 만에 트윈 생성을 마쳤습니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 트윈 생성 완료·사진이 덜 올라옴·대기열에서 기준 시간 넘게 멈춤·실패 뒤 재시도 없음·같은 촬영이 두 번 처리됨 다섯 값 중 하나인지 확인합니다. 2. capture_id·site_name·region·verdict·as_of·photos_expected·photos_received·max_queue_min·max_retry·done_job_count·evidence·summary 필드가 빠짐없이 존재하는지 확인합니다. 3. photos_received 가 0 이상 photos_expected 이하이고 photo_rows 가 photos_received 이상인지 확인합니다. 4. photos_received 를 서로 다른 photo_seq 개수로, queue_wait_min 을 as_of − enqueued_at(분)으로, done_job_count 를 status 가 done 인 twin_jobs 수로 다시 계산합니다. 5. done_job_count 가 2 이상이면 verdict 는 같은 촬영이 두 번 처리됨입니다. 6. 앞 조건에 걸리지 않고 photos_received 가 photos_expected 미만이며 last_received_at 뒤 60분이 지났으면 verdict 는 사진이 덜 올라옴입니다. 7. 앞 조건에 걸리지 않고 마지막 작업의 finished_at 이 null 이며 queue_wait_min 이 max_queue_min 을 넘으면 verdict 는 대기열에서 기준 시간 넘게 멈춤입니다. 8. 앞 조건에 걸리지 않고 마지막 작업의 status 가 failed 이며 attempt 가 max_retry 미만이면 verdict 는 실패 뒤 재시도 없음입니다. 마지막 작업이 done 이면 verdict 는 트윈 생성 완료입니다. 9. evidence 의 field 이름이 네 데이터셋 중 하나에 있는 컬럼인지 대조합니다. 10. evidence 의 value 가 수록된 행의 원문 값과 같은지 확인합니다. 여러 줄을 센 수는 agg 를 적습니다. 11. summary 가 존댓말로 끝나는지 확인합니다. [판정 대상 · 무엇을 목록에 올리는가] 기준 시각(as_of)은 판정을 돌린 UTC 시각이고, 더미는 2026-10-02T00:00:00Z 로 고정합니다. 사진이 모자라지만 마지막 수신이 60분 안인 촬영은 아직 올라오는 중이라 판정하지 않고 화면 위 집계의 업로드 중으로 셉니다. 더미의 C-77891 맨체스터 변전소가 그렇습니다(마지막 수신 23:30:00Z, 기준 시각 30분 전). 재시도 한도를 다 쓰고 failed 로 끝난 촬영도 다섯 판정에 자리가 없어 화면 위 집계의 재시도 소진으로 셉니다. 사진은 다 왔는데 작업이 아직 없는 촬영은 작업 미생성으로, 끝나지 않은 작업이 리전 기준 안에 있는 촬영은 처리 중으로 같은 집계에 셉니다. [판정 차례 · 위 규칙이 둘 이상 걸릴 때] 촬영 하나에는 판정을 하나만 남깁니다. ① 같은 촬영이 두 번 처리됨 → ② 사진이 덜 올라옴 → ③ 대기열에서 기준 시간 넘게 멈춤 → ④ 실패 뒤 재시도 없음 → ⑤ 트윈 생성 완료. [하드 케이스 · 판정이 갈리는 이유] C-77872 는 2번 사진을 두 번 받아 photo_rows 는 3 이지만 photos_received 는 2 라 덜 올라옴입니다. C-77903 은 status 가 processing 이어도 finished_at 이 null 이라 대기 분을 잽니다. C-77880 은 J-40880 이 failed 지만 마지막 작업 J-40881 이 done 이라 완료입니다. C-77915 는 130분 걸렸어도 끝났으므로 대기열 멈춤이 아닙니다. C-77866 의 J-40866·J-40867 은 trigger_message_key 가 msg-7f3a66 으로 같아 메시지 재전달로 생긴 두 작업입니다. 모든 시각은 UTC 로 비교합니다. [평가 러너 · 2주차] 더미 촬영 200건에 손으로 매긴 정답 판정과 자동 판정을 대조해 판정별 일치율을 내고, 어긋난 촬영은 따로 모아 60분 수신 창과 판정 차례를 다시 봅니다. [수동 검증 · 사람이 봅니다] 1. 사진이 덜 올라온 촬영을 재업로드로 풀지 재촬영으로 풀지는 고객지원 담당자가 고객과 정합니다. 2. 두 번 처리된 촬영에서 어느 트윈을 남길지는 백엔드 엔지니어가 정합니다. 두 완료 작업의 trigger_message_key 가 서로 다르면 사람이 누른 재처리일 수 있어 엔지니어가 먼저 가립니다.