제노소프트
경력근거: AI 영상관제 솔루션 기획 및 개발
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고가 뽑는 자리는 '영상관제 솔루션 프로덕트 엔지니어' 이고 주요업무를 'AI 영상관제 솔루션 기획 및 개발' 한 줄로 적었습니다. 관제 제품에서 무엇을 만들지 정하는 사람이 그대로 만든다는 뜻이라, 관제 요원이 화면 앞에서 못 보는 것부터 정하고 그 화면을 직접 세우는 일이 이 자리의 첫 일입니다. 두 번째 업무인 'AI 관련 기술지원' 도 현장에서 채널이 왜 안 뜨는지 답해야 하는 자리라, 이 판정 화면이 그 답의 근거가 됩니다. 이 포트폴리오는 송출 상태 값을 채널 기준과 맞춰 사유로 나누는 기술을 보여주며, 영상 코덱이나 스트리밍 프로토콜 최적화 전문성은 주장하지 않습니다. 영상 안에서 무엇이 움직이는지 묻는 자리에서는 제가 답할 것이 없다고 먼저 말하겠습니다.
모집 분야가 'AI 활용 프로덕트 엔지니어 (기획 + 개발 통합)' 이고 주요업무가 'AI 영상관제 솔루션 기획 및 개발' 한 줄입니다. 무엇을 만들지 정하는 사람이 그대로 만드는 자리라, 관제 요원이 화면 앞에서 무엇을 못 보는지부터 잡아야 합니다.
모집분야를 '영상관제 솔루션 프로덕트 엔지니어' 로 적었습니다. 다루는 대상이 일반 웹 서비스가 아니라 현장 영상이 실시간으로 올라오는 관제 화면이라, 화면 한 칸이 지금 살아 있는지가 제품의 첫 질문입니다.
주요업무 두 번째가 'AI 관련 기술지원' 입니다. 현장에서 채널이 왜 안 뜨는지 묻는 전화를 받는 자리라, 상태와 그 근거 수치를 화면에서 바로 읽는 도구가 그 답의 바탕이 됩니다.
현장 장비가 보내는 수신 기록을 채널 등록표·근무 편성표와 맞춰 관제 멀티뷰의 칸마다 지금 살아 있는지 판정합니다. 수신이 끊긴 채널, 프레임 번호가 그대로 멈춘 채널, 대역폭이 채널 기준에 못 미치는 채널, 근무 편성에는 있는데 송출이 없는 채널, 등록표에 없는 채널을 사유별로 나눠 보여주고 판정에 쓴 수신 시각과 대역폭을 카드 안에 같이 세웁니다.
여러 현장 화면을 한 번에 지켜보는 관제실 요원
멈춘 화면은 눈으로 봐서 잘 모릅니다. 정지 화상과 흐르는 화면이 관제 멀티뷰에서 똑같이 보이기 때문입니다. 장비가 보내는 수신 시각과 프레임 번호, 대역폭을 채널마다 정해 둔 기준과 맞추면 눈에 안 잡히던 멈춤과 미달이 사유로 갈라집니다.
수신 기록과 채널 등록표, 근무 편성표를 올리면 채널마다 살아 있음·신호가 끊김·화면이 멈춤·화질이 기준 미달·근무시간인데 안 켜짐·등록에 없는 채널 중 하나를 판정하고, 판정에 쓴 수신 시각과 대역폭을 카드 안에 세워 사유별로 보여주는 관리자 페이지가 도는 완결품입니다.
손으로 정답 상태를 붙인 채널 판정 60건과 대조해 일치율을 내고, 기준 대역폭과 기대 수신 간격을 흔들었을 때 판정이 뒤집히는 채널이 몇 개인지 세어 채널별 기준을 다시 잡습니다.
밤사이 이상이 이어진 채널을 현장 담당별로 나눠 교대 인수인계 리포트를 조립하고, 등록표에 없던 채널은 등록 요청 목록으로 함께 냅니다.
관제 요원이 수신 기록과 채널 등록표를 올리면 채널마다 상태와 그 근거 수치가 사유별로 나오는 과정을 시연합니다.
손으로 정답 상태를 붙인 채널 판정 60건과 대조해 일치율을 냅니다. 단, 수신 기록도 정답 상태도 제가 초 단위로 지어낸 것이라 이 값은 이상 탐지 정확도가 아니라 여섯 사유가 서로 안 섞이고 갈리는지만 말합니다.
대역폭이 안 실리거나 프레임 번호가 되돌아가거나 등록표에 없는 채널이 섞인 입력 30종을 넣어도 화면이 깨지지 않고 사유별 채널 수가 그대로 나옵니다.
기준 대역폭과 기대 수신 간격을 바꿨을 때 어느 채널의 판정이 뒤집히는지 회귀로 확인합니다.
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 작성자가 만든 수신 기록의 자가채점이며, 실제 관제 채널의 이상 탐지 정확도는 제노소프트가 공급한 관제 서버의 실 수신 로그가 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다.
위 수치는 제가 만든 수신 기록을 제가 채점한 값이라 성능 증명이 못 됩니다. 면접에서는 화면이 도는 범위와, 실 수신 로그가 들어오면 그날 바로 같은 수치를 낼 준비가 돼 있다는 것까지만 말합니다.
정답을 작성자가 붙였으니 잘 맞는 게 당연하므로, 대역폭 결측과 채널 기준에 딱 맞는 값, 등록표에 없는 채널을 더미에 일부러 섞어 쉬운 판정만 도는 함정을 피합니다.
이 포트폴리오는 송출 상태 값을 채널 기준과 맞춰 사유로 나누는 기술을 보여주며, 영상 코덱이나 스트리밍 프로토콜 최적화 전문성은 주장하지 않습니다. 상태는 수신 시각과 프레임 번호, 대역폭을 견주는 산술이 정합니다. 대역폭이 0 인 채널을 화질 미달이 아니라 신호 끊김으로 보내는 것처럼 사유의 자리를 정하는 판단도 규칙에 적어 두었고, 클로드는 걸린 채널의 사유 문장만 씁니다.
살아 있음이라는 판정이 영상이 쓸 만하다는 뜻은 아닙니다. 초점이 나갔거나 렌즈가 더러워진 화면은 이 도구가 보지 못하므로, 화상 판독은 관제 요원이 계속 맡습니다.
판정 기준 시각을 지금으로 두면 화면을 여는 순간마다 결과가 달라지므로, 실제 도입에서는 관제 서버 시각과 현장 장비 시각의 차이를 먼저 맞추는 일이 앞섭니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
현장 장비가 관제 서버로 올려 보내는 송출 상태 기록입니다. 기록 번호와 채널 번호, 장비 번호, 수신 시각, 프레임 번호, 대역폭을 한 행에 둡니다. 같은 채널이 여러 행으로 들어와 마지막 두 건을 견주면 프레임 번호가 그대로인지 알 수 있습니다. 대역폭이 안 실려 비어 있는 행, 대역폭이 0 으로 올라온 행(화질이 나쁜 것이 아니라 영상이 안 실려 오는 것이라 신호가 끊김으로 갑니다), 등록표에 없는 채널 번호로 올라온 행을 일부러 섞어 두어 사유가 갈리게 둡니다. 판정 기준 시각은 2026-09-10T14:22:07 로 고정합니다.
| beat_no | channel_id | device_id | received_at | frame_seq | bitrate_kbps |
|---|---|---|---|---|---|
| HB-001 | CH-01 | DEV-0101 | 2026-09-10T14:21:57 | 88,098 | 1,440 |
| HB-002 | CH-01 | DEV-0101 | 2026-09-10T14:22:02 | 88,120 | 1,450 |
| HB-003 | CH-03 | DEV-0307 | 2026-09-10T14:21:05 | 41,208 | 980 |
| HB-004 | CH-03 | DEV-0307 | 2026-09-10T14:22:05 | 41,208 | 970 |
| HB-005 | CH-07 | DEV-0712 | 2026-09-10T14:19:41 | 20,510 | 720 |
아래 JSON Schema 를 그대로 만족하는 송출 상태 기록 행을 600개 더 만들어 주세요. 채널 번호는 CH-01 부터 CH-24 사이로 두고 채널마다 3~8행씩 초 단위로 이어지게 씁니다. 프레임 번호는 대체로 올라가되 몇 채널은 두 행 이상 같은 값으로 두고, 두 행에 한 번꼴로 대역폭을 비우거나 0 으로 둡니다. 등록표에 없는 채널 번호도 몇 행 넣으세요. 수신 시각은 2026-09-10T14:00:00 과 2026-09-10T14:22:07 사이로 둡니다. 판정 결과나 사유는 넣지 마세요.
{"type": "object", "properties": {"beat_no": {"type": "string"}, "channel_id": {"type": "string"}, "device_id": {"type": "string"}, "received_at": {"type": "string"}, "frame_seq": {"type": "integer", "minimum": 0}, "bitrate_kbps": {"type": ["integer", "null"], "minimum": 0}}, "required": ["beat_no", "channel_id", "device_id", "received_at", "frame_seq", "bitrate_kbps"]}{"type": "object", "properties": {"beat_no": {"type": "string"}, "channel_id": {"type": "string"}, "device_id": {"type": "string"}, "received_at": {"type": "string"}, "frame_seq": {"type": "integer", "minimum": 0}, "bitrate_kbps": {"type": ["integer", "null"], "minimum": 0}}, "required": ["beat_no", "channel_id", "device_id", "received_at", "frame_seq", "bitrate_kbps"]}관제 서버에 등록된 채널의 기준표입니다. 채널 번호와 장비 번호, 채널 이름, 현장 코드, 기대 수신 간격, 기준 대역폭을 한 행에 둡니다. 같은 현장이라도 드론과 바디캠은 기대 수신 간격이 다르고, 저대역 출력 채널은 기준 대역폭을 낮게 잡아 한 채널의 정상이 다른 채널에서는 미달이 되게 둡니다. 수신 기록에 올라오는 채널 중 일부는 일부러 넣지 않아 등록에 없는 채널이 도출되게 둡니다.
| channel_id | device_id | channel_name | site_code | expected_interval_sec | min_bitrate_kbps |
|---|---|---|---|---|---|
| CH-01 | DEV-0101 | 성수 지하차도 IP캠 | S-SEONGSU | 5 | 800 |
| CH-02 | DEV-0102 | 성수 지하차도 진입로 IP캠 | S-SEONGSU | 5 | 800 |
| CH-03 | DEV-0307 | 성수 지하차도 바디캠 | S-SEONGSU | 5 | 800 |
| CH-07 | DEV-0712 | 광진 드론 1호 | S-GWANGJIN | 10 | 600 |
| CH-08 | DEV-0815 | 광진 드론 2호 | S-GWANGJIN | 10 | 600 |
아래 JSON Schema 를 그대로 만족하는 채널 등록 행을 40개 더 만들어 주세요. 현장 코드를 네댓 개 두고 현장마다 IP캠·바디캠·드론·스마트폰 채널을 섞습니다. 기대 수신 간격은 5 또는 10 으로, 기준 대역폭은 400·600·800·1200 중에서 채널 성격에 맞게 다르게 두세요. 수신 기록에 나오는 채널 중 일부는 일부러 넣지 마세요. 판정 결과는 넣지 마세요.
{"type": "object", "properties": {"channel_id": {"type": "string"}, "device_id": {"type": "string"}, "channel_name": {"type": "string"}, "site_code": {"type": "string"}, "expected_interval_sec": {"type": "integer", "minimum": 1}, "min_bitrate_kbps": {"type": "integer", "minimum": 0}}, "required": ["channel_id", "device_id", "channel_name", "site_code", "expected_interval_sec", "min_bitrate_kbps"]}{"type": "object", "properties": {"channel_id": {"type": "string"}, "device_id": {"type": "string"}, "channel_name": {"type": "string"}, "site_code": {"type": "string"}, "expected_interval_sec": {"type": "integer", "minimum": 1}, "min_bitrate_kbps": {"type": "integer", "minimum": 0}}, "required": ["channel_id", "device_id", "channel_name", "site_code", "expected_interval_sec", "min_bitrate_kbps"]}현장별 교대 편성표입니다. 편성 번호와 현장 코드, 교대 이름, 시작 시각, 종료 시각, 그 시간대에 켜져 있어야 하는 채널 목록을 한 행에 둡니다. 야간 교대는 자정을 넘겨 종료 시각이 시작 시각보다 앞서고, 편성 채널이 없는 대기 시간대도 두어 그 시간대에는 송출이 없어도 이상이 아니게 둡니다.
| shift_no | site_code | shift_name | shift_start | shift_end | required_channels |
|---|---|---|---|---|---|
| SH-001 | S-SEONGSU | 주간 | 08:00 | 20:00 | ["CH-01","CH-02","CH-03"] |
| SH-002 | S-SEONGSU | 야간 | 20:00 | 08:00 | ["CH-01","CH-03"] |
| SH-003 | S-GWANGJIN | 주간 | 09:00 | 18:00 | ["CH-07","CH-08"] |
| SH-004 | S-GWANGJIN | 야간 | 18:00 | 09:00 | [] |
| SH-005 | S-GURO | 주간 | 07:00 | 19:00 | ["CH-11","CH-12"] |
아래 JSON Schema 를 그대로 만족하는 교대 편성 행을 30개 더 만들어 주세요. 현장마다 주간·야간 두 행 이상을 두고, 야간은 종료 시각이 시작 시각보다 앞서게 씁니다. 편성 채널 목록은 그 현장의 등록 채널 중 일부만 담고, 대기 시간대는 빈 목록으로 두세요. 판정 결과는 넣지 마세요.
{"type": "object", "properties": {"shift_no": {"type": "string"}, "site_code": {"type": "string"}, "shift_name": {"type": "string"}, "shift_start": {"type": "string"}, "shift_end": {"type": "string"}, "required_channels": {"type": "array", "items": {"type": "string"}}}, "required": ["shift_no", "site_code", "shift_name", "shift_start", "shift_end", "required_channels"]}{"type": "object", "properties": {"shift_no": {"type": "string"}, "site_code": {"type": "string"}, "shift_name": {"type": "string"}, "shift_start": {"type": "string"}, "shift_end": {"type": "string"}, "required_channels": {"type": "array", "items": {"type": "string"}}}, "required": ["shift_no", "site_code", "shift_name", "shift_start", "shift_end", "required_channels"]}| TypeScript | 채널 카드와 사유별 묶음을 그리는 관제 상태판 화면을 맡습니다. |
| Next.js | 판정을 서버에서 계산해 화면으로 내리는 라우트와 자동 새로고침을 맡습니다. |
| Python | 수신 기록을 채널 등록표·근무 편성표와 맞춰 여섯 사유로 판정하는 규칙을 맡습니다. |
| PostgreSQL | 수신 기록을 채널별로 모아 마지막 두 건을 꺼내는 조회를 맡습니다. |
| Docker | 관제 서버가 놓인 현장에서도 같은 결과가 나오도록 판정기를 컨테이너로 싸 둡니다. |
✗ fail → 사유가 서로 겹쳐 읽히면 판정 순서를 먼저 손봅니다.
✓ pass → 2주차 기준 조정으로 넘어갑니다.
✗ fail → 채널마다 판정이 흔들리면 기준을 채널별로 나눠 다시 잡습니다.
✓ pass → 3주차 인수인계 리포트로 넘어갑니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · channel_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
살아 있음정상신호가 끊김신호 끊김화면이 멈춤화면 멈춤화질이 기준 미달화질 미달근무시간인데 안 켜짐근무시간 미송출등록에 없는 채널미등록 채널관제 멀티뷰의 채널 하나가 판정 기준 시각에 어떤 상태인지 정합니다. 여섯 사유 중 하나를 싣고 판정에 쓴 침묵 초와 정지 초, 모자란 대역폭을 함께 담아 화면 카드가 그대로 읽습니다.
{"type": "object", "properties": {"verdict_no": {"type": "string"}, "channel_id": {"type": "string"}, "device_id": {"type": "string"}, "channel_name": {"type": ["string", "null"]}, "site_code": {"type": ["string", "null"]}, "verdict": {"type": "string", "enum": ["살아 있음", "신호가 끊김", "화면이 멈춤", "화질이 기준 미달", "근무시간인데 안 켜짐", "등록에 없는 채널"]}, "last_beat_no": {"type": ["string", "null"], "description": "판정에 쓴 마지막 수신 기록의 번호입니다. 수신이 한 건도 없으면 null 입니다"}, "silent_sec": {"type": ["integer", "null"], "description": "판정 기준 시각 2026-09-10T14:22:07 에서 마지막 received_at 을 뺀 초입니다. 수신이 한 건도 없으면 null 입니다"}, "frozen_sec": {"type": ["integer", "null"], "description": "같은 frame_seq 가 처음 들어온 received_at 에서 판정 기준 시각까지의 초입니다. 직전 수신이 없거나 frame_seq 가 올라갔으면 null 입니다"}, "bitrate_shortfall_kbps": {"type": ["integer", "null"], "description": "channel_registry.min_bitrate_kbps 에서 마지막 수신의 bitrate_kbps 를 뺀 값입니다. 대역폭이 기준 이상이거나 bitrate_kbps 가 비어 있으면 null 이고, 0 이면 신호가 끊김으로 가므로 null 입니다"}, "evidence": {"type": "array", "items": {"type": "object", "properties": {"table": {"type": "string", "enum": ["stream_heartbeats", "channel_registry", "site_shifts"]}, "field": {"type": "string"}, "agg": {"type": ["string", "null"]}, "value": {}}, "required": ["table", "field", "value"]}}, "summary": {"type": "string"}, "hedge": {"type": ["string", "null"]}}, "required": ["verdict_no", "channel_id", "device_id", "verdict", "evidence", "summary"]}[
{
"verdict_no": "CV-001",
"channel_id": "CH-01",
"device_id": "DEV-0101",
"channel_name": "성수 지하차도 IP캠",
"site_code": "S-SEONGSU",
"verdict": "살아 있음",
"last_beat_no": "HB-002",
"silent_sec": 5,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "received_at",
"agg": null,
"value": "2026-09-10T14:22:02"
},
{
"table": "channel_registry",
"field": "expected_interval_sec",
"agg": null,
"value": 5
},
{
"table": "stream_heartbeats",
"field": "frame_seq",
"agg": null,
"value": 88120
},
{
"table": "stream_heartbeats",
"field": "bitrate_kbps",
"agg": null,
"value": 1450
}
],
"summary": "수신이 5초 전에 들어왔고 프레임 번호도 올라가 화면이 흐르고 있습니다.",
"hedge": null
},
{
"verdict_no": "CV-002",
"channel_id": "CH-12",
"device_id": "DEV-1205",
"channel_name": "구로 공장 B동 저대역 출력",
"site_code": "S-GURO",
"verdict": "살아 있음",
"last_beat_no": "HB-009",
"silent_sec": 1,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "bitrate_kbps",
"agg": null,
"value": 400
},
{
"table": "channel_registry",
"field": "min_bitrate_kbps",
"agg": null,
"value": 400
},
{
"table": "stream_heartbeats",
"field": "received_at",
"agg": null,
"value": "2026-09-10T14:22:06"
}
],
"summary": "대역폭이 이 채널 기준과 똑같은 400kbps 라 미달이 아닙니다.",
"hedge": null
},
{
"verdict_no": "CV-003",
"channel_id": "CH-08",
"device_id": "DEV-0815",
"channel_name": "광진 드론 2호",
"site_code": "S-GWANGJIN",
"verdict": "살아 있음",
"last_beat_no": "HB-008",
"silent_sec": 7,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "received_at",
"agg": null,
"value": "2026-09-10T14:22:00"
},
{
"table": "channel_registry",
"field": "expected_interval_sec",
"agg": null,
"value": 10
},
{
"table": "stream_heartbeats",
"field": "bitrate_kbps",
"agg": null,
"value": null
}
],
"summary": "수신은 7초 전에 들어왔고 대역폭 값이 안 실려 화질은 판정하지 않았습니다.",
"hedge": "드론 링크가 대역폭을 안 올려 보내는 구간이라, 화질 미달 여부는 이 판정으로 말하지 않습니다."
},
{
"verdict_no": "CV-004",
"channel_id": "CH-03",
"device_id": "DEV-0307",
"channel_name": "성수 지하차도 바디캠",
"site_code": "S-SEONGSU",
"verdict": "화면이 멈춤",
"last_beat_no": "HB-004",
"silent_sec": 2,
"frozen_sec": 62,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "frame_seq",
"agg": null,
"value": 41208
},
{
"table": "stream_heartbeats",
"field": "received_at",
"agg": null,
"value": "2026-09-10T14:21:05"
},
{
"table": "stream_heartbeats",
"field": "bitrate_kbps",
"agg": null,
"value": 970
}
],
"summary": "수신은 계속 들어오는데 프레임 번호가 41208 에서 62초째 그대로라 정지 화상이 떠 있습니다.",
"hedge": null
},
{
"verdict_no": "CV-005",
"channel_id": "CH-07",
"device_id": "DEV-0712",
"channel_name": "광진 드론 1호",
"site_code": "S-GWANGJIN",
"verdict": "신호가 끊김",
"last_beat_no": "HB-005",
"silent_sec": 146,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "received_at",
"agg": null,
"value": "2026-09-10T14:19:41"
},
{
"table": "channel_registry",
"field": "expected_interval_sec",
"agg": null,
"value": 10
}
],
"summary": "마지막 수신이 14시 19분 41초라 기대 간격 10초의 세 배를 한참 넘겼습니다.",
"hedge": null
},
{
"verdict_no": "CV-006",
"channel_id": "CH-11",
"device_id": "DEV-1104",
"channel_name": "구로 공장 A동 IP캠",
"site_code": "S-GURO",
"verdict": "화질이 기준 미달",
"last_beat_no": "HB-006",
"silent_sec": 4,
"frozen_sec": null,
"bitrate_shortfall_kbps": 420,
"evidence": [
{
"table": "stream_heartbeats",
"field": "bitrate_kbps",
"agg": null,
"value": 380
},
{
"table": "channel_registry",
"field": "min_bitrate_kbps",
"agg": null,
"value": 800
}
],
"summary": "대역폭이 380kbps 로 이 채널 기준 800kbps 에 420kbps 모자랍니다.",
"hedge": null
},
{
"verdict_no": "CV-007",
"channel_id": "CH-14",
"device_id": "DEV-1409",
"channel_name": "잠실 순찰 스마트폰",
"site_code": "S-JAMSIL",
"verdict": "근무시간인데 안 켜짐",
"last_beat_no": null,
"silent_sec": null,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "site_shifts",
"field": "required_channels",
"agg": null,
"value": [
"CH-14",
"CH-15"
]
},
{
"table": "site_shifts",
"field": "shift_start",
"agg": null,
"value": "09:00"
},
{
"table": "stream_heartbeats",
"field": "beat_no",
"agg": "count",
"value": 0
}
],
"summary": "잠실 주간 편성에 들어 있는 채널인데 수신 기록이 한 건도 없어 송출이 시작되지 않았습니다.",
"hedge": null
},
{
"verdict_no": "CV-008",
"channel_id": "CH-33",
"device_id": "DEV-3301",
"channel_name": null,
"site_code": null,
"verdict": "등록에 없는 채널",
"last_beat_no": "HB-012",
"silent_sec": 2,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "channel_id",
"agg": null,
"value": "CH-33"
},
{
"table": "channel_registry",
"field": "channel_id",
"agg": "count",
"value": 10
},
{
"table": "stream_heartbeats",
"field": "device_id",
"agg": null,
"value": "DEV-3301"
}
],
"summary": "등록표 열 채널 어디에도 없는 번호로 영상이 올라와 어느 현장 것인지 정해지지 않았습니다.",
"hedge": "현장에서 장비를 새로 붙이고 등록을 안 한 경우가 흔하므로, 사용 중지가 아니라 등록 요청으로 넘깁니다."
},
{
"verdict_no": "CV-009",
"channel_id": "CH-21",
"device_id": "DEV-2101",
"channel_name": "여의도 중계차 송출",
"site_code": "S-BANGSONG",
"verdict": "신호가 끊김",
"last_beat_no": "HB-011",
"silent_sec": 9,
"frozen_sec": null,
"bitrate_shortfall_kbps": null,
"evidence": [
{
"table": "stream_heartbeats",
"field": "bitrate_kbps",
"agg": null,
"value": 0
},
{
"table": "stream_heartbeats",
"field": "received_at",
"agg": null,
"value": "2026-09-10T14:21:58"
},
{
"table": "channel_registry",
"field": "min_bitrate_kbps",
"agg": null,
"value": 800
}
],
"summary": "수신 신호는 9초 전까지 붙어 있었는데 대역폭이 0 이라 영상이 한 조각도 실려 오지 않았습니다.",
"hedge": "중계차가 송출을 멈춘 것인지 링크만 끊긴 것인지는 이 화면에서 갈리지 않으므로 현장에 확인해야 합니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 여섯 값 중 하나인지 확인합니다. 2. verdict_no·channel_id·device_id·verdict·evidence·summary 가 모두 채워졌는지 확인합니다. 3. silent_sec 이 판정 기준 시각 2026-09-10T14:22:07 에서 마지막 received_at 을 뺀 초와 같은지 다시 계산합니다. 수신이 한 건도 없으면 null 인지 봅니다. 4. frozen_sec 이 같은 frame_seq 가 처음 들어온 received_at 에서 기준 시각까지의 초와 같은지 다시 계산합니다. 5. bitrate_shortfall_kbps 가 min_bitrate_kbps 에서 마지막 bitrate_kbps 를 뺀 값과 같은지 다시 계산합니다. bitrate_kbps 가 비어 있으면 null 인지 봅니다. 6. silent_sec 이 0 이상이고 bitrate_shortfall_kbps 가 1 이상인지 범위를 봅니다. 7. channel_id 가 channel_registry 에 없으면 verdict 가 등록에 없는 채널인지 확인합니다. 8. 등록된 채널인데 stream_heartbeats 에 그 channel_id 가 한 건도 없고 기준 시각이 그 site_code 의 shift_start 와 shift_end 사이이며 required_channels 에 그 채널이 들어 있으면, verdict 가 근무시간인데 안 켜짐인지 확인합니다. 9. silent_sec 이 expected_interval_sec 의 세 배보다 크거나 마지막 수신의 bitrate_kbps 가 0 이면 verdict 가 신호가 끊김인지 확인합니다. 대역폭 0 은 화질이 나빠진 것이 아니라 수신은 붙어 있는데 영상이 한 조각도 안 실려 오는 것이라, 화질이 기준 미달로 보내면 관제 요원이 화질 조정을 하러 갑니다. 갈 곳은 송출이 멎은 채널 쪽이라 신호가 끊김으로 보냅니다. CH-21 이 조건이 성립한 경우이고, 대역폭이 380 인 CH-11 은 영상이 실려 오므로 성립하지 않습니다. 10. 마지막 두 수신의 frame_seq 가 같으면 verdict 가 화면이 멈춤인지 확인합니다. 수신이 한 건뿐이면 이 검사를 건너뜁니다. 11. bitrate_kbps 가 0 보다 크고 min_bitrate_kbps 보다 작으면 verdict 가 화질이 기준 미달인지 확인합니다. bitrate_kbps 가 0 이면 9번이 먼저 가져가므로 이 검사를 돌리지 않고, 비어 있으면 건너뛴 뒤 hedge 에 그 사정을 적었는지 봅니다. 12. verdict 가 살아 있음이면 위 다섯 검사에 하나도 걸리지 않는지 다시 봅니다. 13. 판정 순서는 등록에 없는 채널, 근무시간인데 안 켜짐, 신호가 끊김, 화면이 멈춤, 화질이 기준 미달 순으로 먼저 걸리는 갈래를 씁니다. 다만 등록표에 없는 채널은 기준값이 없으므로 뒤 검사를 돌리지 않습니다. 14. evidence 의 field 이름이 세 데이터셋에 실재하는 컬럼인지 대조합니다. 15. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 행 값과 같은지 확인합니다. 16. summary 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 1. 손으로 정답 상태를 붙인 채널 판정 60건과 대조해 일치율을 냅니다. 2. 기준 대역폭과 기대 수신 간격을 흔들어 판정이 뒤집히는 채널을 셉니다. [수동 검증 · 사람이 봅니다] 1. 살아 있음으로 나온 채널의 화면이 실제로 볼 만한지 관제 요원이 표본으로 확인합니다. 2. 등록에 없는 채널을 등록할지 현장 담당이 정합니다.