2025.05 — 2025.09회사 프로젝트
부산 도시철도 통합 관제
3D 역사 관제 + WebRTC CCTV 실시간 스트리밍
- 맡은 일
- Frontend 단독 개발 (3D 관제 · CCTV 스트리밍 · 외부 플랫폼 연동)
- 기간
- 2025.05 — 2025.09
- 소속
- 플럭시티DX 기술팀
부산교통공사 도시철도(사상하단선) 역사의 시설물을 3D 도면 위에서 관제하는 시스템입니다. 자체 3D 엔진으로 역사 모델을 띄우고 층별 전환·POI를 붙였고, 협업처 관제 플랫폼이 제공한 API 스펙에 맞춰 조명·셔터·CCTV·화재감지 등 시설물 9종과 제어 인터페이스를 연결했습니다.
가장 중요한 축은 CCTV였습니다 — RTSP 카메라를 브라우저에서 보려면 WebRTC 게이트웨이와 SDP를 직접 교환해야 했고, 이 연결을 붙여 현장에서 실제로 영상이 도는 상태까지 만들었습니다. 디자인 협업처 시안을 반영하면서 두 개의 서로 다른 백엔드(협업처 플랫폼 · 자사 API)를 한 화면에서 다뤘고, 현장에 배포한 뒤 즉시 수정·재배포하는 사이클도 직접 돌렸습니다.
i.Problem
협업처가 CCTV 연동 코드도 문서도 주지 않은 채 현장 일정이 먼저 잡혔습니다. 역사 안 폐쇄망이라 바깥 서비스를 붙일 수도, 인터넷에서 답을 받아 올 수도 없었습니다.
ii.Action
WebRTC를 먼저 파고들어 RTSP 카메라를 브라우저로 가져오는 경로를 정리했습니다. 상대가 구현했을 법한 방식을 모두 나열하고 각각에 맞는 연결 코드를 미리 준비해 현장에 들어갔습니다. 3D는 현장 PC 사양이 정해져 있어, 한 프레임을 무엇이 잡아먹는지 재 가며 덜어 냈습니다.
iii.Result
현장에서 하나씩 맞춰 본 끝에 영상을 띄웠고, 시설물 9종과 함께 실제 운영에 올렸습니다. 배포한 뒤에도 그 자리에서 고쳐 다시 올리는 사이클을 직접 돌렸습니다.
핵심
- 01
협업처가 어떻게 구현했는지 알 수 없는 상태에서, 가능한 연결 방식을 미리 다 준비해 가서 현장에서 하나씩 붙여 보며 맞는 것을 찾아냄
- 02
WebRTC CCTV 연결
RTCPeerConnection recvonly 트랜시버로 게이트웨이와 SDP를 교환해 RTSP 카메라를 브라우저에서 재생, 현장 운영 적용
- 03
ICE 상태 감시 + 스트림 정리
failed/disconnected 시 연결을 닫고, 트랙 stop·srcObject 해제 후 재연결하는 경로를 분리
- 04
협업처 플랫폼 API 스펙 기반 연결
조명·셔터·CCTV·화재감지·엘리베이터·에스컬레이터·물탱크·집수정·공기청정기 9종 + 역사 환경정보·이벤트 현황·제어 요청을 타입으로 고정
나머지 5개 더 보기접기
- 05
서로 다른 두 백엔드를 한 화면에서
협업처 관제 플랫폼과 자사 API를 클라이언트 레벨에서 분리해 호출
- 06
3D 역사 관제
자체 엔진으로 GLTF 모델 로드, 전체층/층별 전환 셀렉터, POI·장치 상태 표시
- 07
SSE 실시간 수신
열차 도착(ttc) · 이벤트 · 셔터 3개 채널을 이름 있는 이벤트로 구독하고 도착역 코드로 필터링
- 08
관리자 공간 관리
GLB 장비 모델을 3D 도면 위에 배치·이동·회전·삭제하는 편집기 구현
- 09
현장 배포·즉시 수정·재배포
사무실에서 재현되지 않는 실시간 버그를 현장에서 직접 잡음
화면
Pl. 2of8
실제 운영 화면입니다. 손님·직원의 이름과 CCTV에 잡힌 이용객은 알아볼 수 없게 처리했고, 내부 접속 주소는 잘라냈습니다.
자세히
궁금한 항목을 펼치면 그때 무엇이 문제였고 어떻게 판단했는지 나옵니다.
협업처는 CCTV 연동 코드도 문서도 주지 않았습니다. 카메라 주소만 넘어왔고, 저쪽이 서버를 어떤 방식으로 세웠는지는 붙여 보기 전까지 알 수 없었습니다.
답을 기다리기만 하기보다 먼저 WebRTC 자체를 파고들었습니다
RTSP 카메라를 브라우저까지 가져오는 경로에 어떤 것들이 있는지, P2P·SFU·게이트웨이가 각각 무엇을 요구하는지, 폐쇄망에서 무엇이 살아남는지.
그래서 '이렇게 만들었을 수도 있다'는 경우를 미리 나열하고, 각 경우에 맞는 연결 코드를 준비한 상태로 들어갔습니다. 현장에서 처음부터 알아보는 게 아니라, 준비한 것을 하나씩 대 보는 방식이었습니다.
그렇게 맞춰 가며 알아낸 것들
SDP를 base64로 감싸 주고받는 규약을 쓴다는 것, 수신 전용이라 recvonly 트랜시버만 두면 된다는 것, 스트림 타입과 구간 파라미터를 함께 보내야 한다는 것.
연결이 확인된 조합을 샘플로 먼저 세운 뒤, 재연결·스트림 정리 로직을 얹어 제품 코드로 옮겼습니다. 이후 카메라가 늘어나도 백엔드가 주는 주소만 갈아 끼우면 되게 만들었습니다.
정해진 방법을 받아 붙인 것이 아니라, 붙는 방법을 직접 찾아 정했습니다.
RTCPeerConnection에 video recvonly 트랜시버만 추가해 수신 전용 연결을 구성, ontrack으로 받은 트랙을 MediaStream에 붙여 video 엘리먼트에 연결
onnegotiationneeded에서 offer 생성 → base64로 감싼 SDP(sdp64)를 게이트웨이 스트림 엔드포인트에 POST → 응답 SDP를 디코드해 setRemoteDescription
oniceconnectionstatechange로 failed·disconnected를 감지해 연결을 닫고, 사용자가 다시 시도할 수 있는 재연결 핸들러를 분리
언마운트·재연결 시 getTracks().stop()과 srcObject 해제를 한 곳(cleanupStream)에 모아 스트림이 남지 않게 처리
카메라 주소는 백엔드가 내려주는 streamAddress를 그대로 사용
현장 카메라가 늘어나도 코드 변경 없이 붙음
다중 CCTV 동시 스트리밍
관제 화면에서 복수 영상을 함께 모니터링
배운 것
사업부와 1대1
사업부와 1대1로 매칭되어 들어간 프로젝트였습니다. 요청이 오면 그날 화면으로 옮기고 다시 확인받는 일이 매일이었습니다. 말로 들은 것과 실제로 필요한 것이 다를 때가 많아, 먼저 그려서 보여 드리고 고치는 쪽이 빠르다는 걸 알게 됐습니다. 협업하고 대응하는 힘은 그 사이에서 늘었습니다.
쓰는 사람의 눈높이
좋은 화면은 만드는 사람 눈이 아니라 쓰는 사람 눈높이에 맞춘 화면이라는 걸 배웠습니다. 쓰실 분들은 중년의 부산 공무원분들이었고, 보시는 곳은 제 책상이 아니라 조명이 밝은 관제실의 큰 모니터였습니다. 빛이 반사되는 자리라 옅은 글씨는 지워진 것처럼 보였고, 화면이 커진 만큼 멀리서도 읽혀야 했습니다. 글자와 대비를 올리고, 상태를 색 하나로만 알리지 않고 글자를 같이 붙이고, 누르는 단계를 줄였습니다. 제 취향은 접어 두고 그 기준에 맞춰 만들었습니다.
폐쇄망의 보안
역사에 들어가는 시스템이라 보안을 계속 신경 써야 했습니다. 밖과 끊긴 망 안에서만 도는 서비스라 평소 쓰던 방식이 그대로 통하지 않았습니다. 만들기 편한 길과 현장에서 쓸 수 있는 길이 다르다는 걸 이때 알았습니다.
남의 일정에 기대지 않기
협업처가 제때 움직여 주지 않는 상황도 있었습니다. 연동 규격을 받지 못한 채로 현장 일정이 먼저 잡히기도 했습니다. 기다리는 동안 무엇을 먼저 해 둘지, 끝까지 막히면 어디까지 대신 보여 드릴지를 미리 준비해 두어야 한다는 걸 배웠습니다. 그 뒤로는 남의 일정에 기대야 하는 일이 보이면 대비책부터 적어 둡니다.
주어진 하드웨어 안에서
3D가 무거워 화면이 버벅였습니다. 현장 PC는 이미 정해져 있어 더 좋은 기계를 쓸 수 없었고, 주어진 하드웨어 안에서 가장 부드럽게 도는 지점을 찾아야 했습니다. 3D 엔진 수석님과 1대1로 붙어 한참을 같이 들여다봤습니다. 무엇이 한 프레임을 잡아먹는지 재 보고 덜어 낼 것을 하나씩 덜어 내는 일의 반복이었고, 그 과정에서 브라우저가 화면을 그리는 방식을 처음으로 제대로 배웠습니다. 부드럽게 움직이는 상태까지 가는 데 시행착오가 많았고, 쉽지 않았습니다.
대신 들어간 회의
백엔드와 서버를 배워 둔 덕에 팀장님을 대신해 회의에 들어가는 일이 잦았습니다. 제가 그 자리에서 답한 말이 그대로 일정이 되었습니다. 모르는 것은 모른다고 말한 뒤 언제까지 확인해 드리겠다고 하는 법도 그때 배웠습니다. 그때부터 이 프로젝트를 제 것으로 여기게 됐습니다.
쓴 기술
링크
이어서 볼 것
플럭시티에서 한 다른 프로젝트