Company Project@ 플럭시티2025.05 — 2025.09

부산 도시철도 통합 관제

3D 역사 관제 + WebRTC CCTV 실시간 스트리밍

Role: Frontend 단독 개발 (3D 관제 · CCTV 스트리밍 · 외부 플랫폼 연동)

Overview

부산교통공사 도시철도(사상하단선) 역사의 시설물을 3D 도면 위에서 관제하는 시스템입니다. 자체 3D 엔진으로 역사 모델을 띄우고 층별 전환·POI를 붙였고, 협업처 관제 플랫폼이 제공한 API 스펙에 맞춰 조명·셔터·CCTV·화재감지 등 시설물 9종과 제어 인터페이스를 연결했습니다. 가장 중요한 축은 CCTV였습니다 — RTSP 카메라를 브라우저에서 보려면 WebRTC 게이트웨이와 SDP를 직접 교환해야 했고, 이 연결을 붙여 현장에서 실제로 영상이 도는 상태까지 만들었습니다. 디자인 협업처 시안을 반영하면서 두 개의 서로 다른 백엔드(협업처 플랫폼 · 자사 API)를 한 화면에서 다뤘고, 현장에 배포한 뒤 즉시 수정·재배포하는 사이클도 직접 돌렸습니다.

이 프로젝트의 핵심

협업처가 어떻게 구현했는지 알 수 없는 상태에서, 가능한 연결 방식을 미리 다 준비해 가서 현장에서 하나씩 붙여 보며 맞는 것을 찾아냄

WebRTC CCTV 연결

RTCPeerConnection recvonly 트랜시버로 게이트웨이와 SDP를 교환해 RTSP 카메라를 브라우저에서 재생, 현장 운영 적용

ICE 상태 감시 + 스트림 정리

failed/disconnected 시 연결을 닫고, 트랙 stop·srcObject 해제 후 재연결하는 경로를 분리

협업처 플랫폼 API 스펙 기반 연결

조명·셔터·CCTV·화재감지·엘리베이터·에스컬레이터·물탱크·집수정·공기청정기 9종 + 역사 환경정보·이벤트 현황·제어 요청을 타입으로 고정

나머지 5개 더 보기

서로 다른 두 백엔드를 한 화면에서

협업처 관제 플랫폼과 자사 API를 클라이언트 레벨에서 분리해 호출

3D 역사 관제

자체 엔진으로 GLTF 모델 로드, 전체층/층별 전환 셀렉터, POI·장치 상태 표시

SSE 실시간 수신

열차 도착(ttc) · 이벤트 · 셔터 3개 채널을 이름 있는 이벤트로 구독하고 도착역 코드로 필터링

관리자 공간 관리

GLB 장비 모델을 3D 도면 위에 배치·이동·회전·삭제하는 편집기 구현

현장 배포·즉시 수정·재배포

사무실에서 재현되지 않는 실시간 버그를 현장에서 직접 잡음

Screens

3D 역사 도면 위에서 CCTV를 고르면 WebRTC 스트림이 모달로 열린다. 좌측은 역사 내 CCTV 68대 목록, 상단은 대합실·승강장·외부 환경값, 우측은 층 전환.
3D 역사 도면 위에서 CCTV를 고르면 WebRTC 스트림이 모달로 열린다. 좌측은 역사 내 CCTV 68대 목록, 상단은 대합실·승강장·외부 환경값, 우측은 층 전환.
관리자 공간 관리 — 좌측 GLB 장비 라이브러리(화재센서·공기청정기·소방펌프·FAN·물탱크 등)에서 모델을 골라 역사 도면 위에 배치하고, 이동·회전·삭제로 POI를 편집한다.
관리자 공간 관리 — 좌측 GLB 장비 라이브러리(화재센서·공기청정기·소방펌프·FAN·물탱크 등)에서 모델을 골라 역사 도면 위에 배치하고, 이동·회전·삭제로 POI를 편집한다.
에스컬레이터 상세 — 중간·대합 두 대의 카메라를 동시에 스트리밍하면서 운행 상태와 장애 상태를 함께 표시.
에스컬레이터 상세 — 중간·대합 두 대의 카메라를 동시에 스트리밍하면서 운행 상태와 장애 상태를 함께 표시.
엘리베이터 상세 — 대합·내부 카메라 2개와 동작·장애 상태를 한 모달에서.
엘리베이터 상세 — 대합·내부 카메라 2개와 동작·장애 상태를 한 모달에서.
셔터 상세 — 출구 CCTV와 화재수신기 감지 상태를 함께 확인.
셔터 상세 — 출구 CCTV와 화재수신기 감지 상태를 함께 확인.
화재감지기 39개를 목록과 3D POI로 관리하고 감지 상태를 조회.
화재감지기 39개를 목록과 3D POI로 관리하고 감지 상태를 조회.
환기시설 18개 — 원격 여부, 운전 상태, 제어 모드, 회전 방향까지 제어 정보를 표출.
환기시설 18개 — 원격 여부, 운전 상태, 제어 모드, 회전 방향까지 제어 정보를 표출.

실제 운영 화면입니다. 손님·직원의 이름과 CCTV에 잡힌 이용객은 알아볼 수 없게 처리했고, 내부 접속 주소는 잘라냈습니다.

자세히

코드도 문서도 없이 붙이기 — 경우의 수를 준비해 갔다6

협업처는 CCTV 연동 코드도 문서도 주지 않았습니다. 카메라 주소만 넘어왔고, 저쪽이 서버를 어떤 방식으로 세웠는지는 붙여 보기 전까지 알 수 없었습니다.

물어봐도 안 나올 답을 기다리는 대신, 먼저 WebRTC 자체를 파고들었습니다

RTSP 카메라를 브라우저까지 가져오는 경로에 어떤 것들이 있는지, P2P·SFU·게이트웨이가 각각 무엇을 요구하는지, 폐쇄망에서 무엇이 살아남는지.

그래서 '이렇게 만들었을 수도 있다'는 경우를 미리 나열하고, 각 경우에 맞는 연결 코드를 준비한 상태로 들어갔습니다. 현장에서 처음부터 알아보는 게 아니라, 준비한 것을 하나씩 대 보는 방식이었습니다.

그렇게 맞춰 가며 알아낸 것들

SDP를 base64로 감싸 주고받는 규약을 쓴다는 것, 수신 전용이라 recvonly 트랜시버만 두면 된다는 것, 스트림 타입과 구간 파라미터를 함께 보내야 한다는 것.

연결이 확인된 조합을 샘플로 먼저 세운 뒤, 재연결·스트림 정리 로직을 얹어 제품 코드로 옮겼습니다. 이후 카메라가 늘어나도 백엔드가 주는 주소만 갈아 끼우면 되게 만들었습니다.

결과적으로 알려준 대로 붙인 연동이 아니라, 붙는 방법을 스스로 찾아 정한 연동이 됐습니다.

WebRTC CCTV — 현장에서 도는 상태까지6

RTCPeerConnection에 video recvonly 트랜시버만 추가해 수신 전용 연결을 구성, ontrack으로 받은 트랙을 MediaStream에 붙여 video 엘리먼트에 연결

onnegotiationneeded에서 offer 생성 → base64로 감싼 SDP(sdp64)를 게이트웨이 스트림 엔드포인트에 POST → 응답 SDP를 디코드해 setRemoteDescription

oniceconnectionstatechange로 failed·disconnected를 감지해 연결을 닫고, 사용자가 다시 시도할 수 있는 재연결 핸들러를 분리

언마운트·재연결 시 getTracks().stop()과 srcObject 해제를 한 곳(cleanupStream)에 모아 스트림이 남지 않게 처리

카메라 주소는 백엔드가 내려주는 streamAddress를 그대로 사용

현장 카메라가 늘어나도 코드 변경 없이 붙음

다중 CCTV 동시 스트리밍

관제 화면에서 복수 영상을 함께 모니터링

협업처 백엔드 스펙 연결4

관제 플랫폼이 제공한 스펙에 맞춰 역사 단위 API 클라이언트를 만들고, 시설물 9종을 각각 타입 정의와 함께 서비스로 분리

역사 환경정보·이벤트 현황 조회와 제어 요청/응답(ControlRequest·ControlResponse)까지 계약을 타입으로 고정해 화면이 응답 형태를 추측하지 않게 함

협업처 플랫폼과 자사 API를 별도 클라이언트로 나눠, 한쪽 스펙이 바뀌어도 다른 쪽 화면이 영향받지 않도록 경계를 둠

위젯·역사 단위로 서비스 파일을 쪼개고 index에서 재노출

기존 호출부 호환을 유지한 채 구조만 정리

3D 관제 화면 & 실시간5

자체 3D 엔진을 컨테이너에 마운트하고 역사 GLTF 모델을 로드, 로드 완료·실패 콜백으로 화면 상태를 분기

층 목록에 '전체층'을 자동으로 끼워 넣고 정렬 순서대로 노출하는 층 셀렉터

층 데이터가 비어 있어도 화면이 깨지지 않음

3D 맵 위 POI·조명·장치 관리 UI로 시설물 상태를 직관적으로 시각화

SSE로 열차 도착·이벤트·셔터 3개 채널을 구독하고 스토어에 누적, 도착역 코드가 맞는 것만 반영

장비 상세·CCTV 모달을 스토어 기반으로 열고 닫아 3D 뷰와 패널 상태를 분리

관리자 페이지 — 3D 공간을 편집하는 화면8

GLB 장비 모델 라이브러리

화재센서·공기청정기·소방펌프·FAN·물탱크·에어컨 등 3D 에셋을 썸네일로 고르고 도면 위에 배치

배치한 POI를 이동·회전·스케일·삭제하는 편집 도구

관제 화면에 뜰 장비 위치를 운영자가 직접 잡을 수 있게 함

역사·층(B1/B2/B3) 단위로 도면을 전환하며 편집

서면역 B2 승강장처럼 층마다 다른 배치를 따로 관리

배치 결과가 그대로 관제 뷰의 POI로 이어지도록 좌표계를 맞춤

관리자가 옮기면 관제 화면에서도 옮겨짐

자산(Asset)·장비(Device)·시설(Facility)·노선(Line)·사용자(User)·뷰어(Viewer)·대시보드 관리 화면 구성

토스트·에러 바운더리를 공통 컴포넌트로 두고 화면마다 개별 처리하지 않도록 정리

디자인 협업처 시안을 반영하며 헤더·서비스 페이지 UI/UX를 반복 개선

현장 폐쇄망 환경에 배포한 뒤 발생하는 실시간 버그·데이터 오류를 그 자리에서 디버깅·수정·재배포

Tech Stack

ReactTypeScriptViteThree.jsWebGLWebRTCSSEZustandkyTailwind CSSpnpm

Links