Frontend

[Frontend] 디지털트윈 방역공조 모니터링 시스템(feat. R&D 과제)

teddy bear 2026. 7. 4. 20:59
반응형

TypeScript 로 구현한 디지털트윈 방역공조 모니터링 시스템

프로젝트명: 디지털트윈 감염병 위험 알람시스템 (Preventative)
기술 스택: TypeScript · React 18 · Umi Max 4 · Cesium · Ant Design · WebSocket
작성일: 2026-07-04


목차

  1. 프로젝트 소개
  2. 모노레포 아키텍처
  3. TypeScript 기반 프론트엔드 구조
  4. 핵심 기능 구현
  5. API · WebSocket 연동
  6. Cesium 3D 지도 통합
  7. 개발 환경 설정
  8. 이슈 및 트러블슈팅
  9. 회고 및 개선 방향

1. 프로젝트 소개

이 프로젝트는 3D 디지털트윈 지도 위에서 감염병·공기질·공조기(살균기)를 실시간 모니터링하고 원격 제어하는 웹 애플리케이션입니다.

경남도청, 마산병원, 한마음병원, 예산군 노인요양원 등 여러 시설의 센서 데이터를 Cesium 기반 3D 지도에 시각화하고, 공기질(PM10, PM2.5, CO₂, VOC), 바이러스(부유균) 센서, 공조기(전원·모드·풍량·플라즈마)를 통합 제어합니다.

주요 목표

  • IoT 센서 데이터의 실시간 수신 및 시각화
  • 공조기 자동/수동 모드 전환 및 풍량 임계치 기반 자동 제어
  • 3D 건물 모델(B3DM Tileset) 위 센서·공조기 아이콘 배치
  • 공기질 상태에 따른 바람길(Line Flow) 시각 효과 표현

2. 모노레포 아키텍처

프로젝트는 Yarn Workspaces + Turborepo 기반 모노레포로 구성되어 있습니다.

preventative/
├── packages/
│   ├── servicecomponents/   # Cesium Map 래퍼 컴포넌트 (@component/service)
│   ├── uicomponents/        # 공통 UI 컴포넌트 (@component/ui)
│   └── utils/               # Cesium 유틸리티 (@packages/utils)
└── projects/
    ├── master/              # 메인 앱 (방역공조 디지털트윈)
    ├── cultureheritage/     # 문화유산 프로젝트
    ├── terrain/             # 지형 편집
    └── ...

패키지역할

@component/service Cesium Viewer 초기화, MapProps 타입 정의
@packages/utils 지도 드로잉, 측정, 시각 분석 등 Cesium 유틸
master 방역공조 메인 애플리케이션

빌드 · 실행

# 의존성 설치
yarn

# master 프로젝트 개발 서버 실행
cd projects/master
yarn start

# Turborepo로 특정 프로젝트 빌드
turbo run build --filter=master

3. TypeScript 기반 프론트엔드 구조

3.1 프레임워크 스택

레이어기술

프레임워크 Umi Max 4 (@umijs/max)
UI React 18 + Ant Design 4
상태 관리 DVA (Redux 기반, connect HOC)
3D 지도 @cesiumgs/cesium-analytics 1.96.0
HTTP umi-request (axios 래퍼)
차트 @ant-design/charts (Line Chart)

3.2 핵심 컴포넌트 구조

방역공조 기능은 초기에 cesium-view.tsx에 모두 포함되어 있었으나, 리팩터링을 통해 Preventative.tsx로 분리되었습니다.

projects/master/src/components/cesium/
├── cesium-view.tsx      # Cesium 지도 컨테이너 (Map + 패널 조합)
├── Preventative.tsx     # 방역공조 메인 로직 (~3,700줄)
├── Preventative.less    # 방역공조 전용 스타일
├── sensorlist.ts        # REST API 호출 레이어
└── sensorchart.tsx      # 부유균 차트 + 자동 모드 제어

3.3 TypeScript 타입 활용 예시

Cesium Map 컴포넌트 Props (@component/service)

// packages/servicecomponents/src/lib/MapComponent/Map.tsx
export interface MapProps extends Viewer.ConstructorOptions {
  getViewInstance?: (v: Viewer) => void;
  accessToken?: string;
  cesium_base_url?: string;
  widgets_css?: string;
}

Viewer.ConstructorOptions를 확장하여 Cesium Viewer의 모든 옵션을 타입 안전하게 전달할 수 있습니다.

Redux Connect 타입 (cesium-view.tsx)

import type { ConnectState } from '@/states/connect';
import type { CesiumMapProps } from '@/states/MapState';

const CesiumView: React.FC<CesiumMapProps> = (props) => {
  // ...
};

export default connect(
  ({
    cesiumview,
    comPanel,
    newHistory,
    // ...
  }: ConnectState) => ({
    cesiumview,
    comPanel,
    newHistory,
    // ...
  }),
)(CesiumView);

DVA connect와 ConnectState를 통해 Redux 스토어 상태를 타입 안전하게 매핑합니다.

센서 차트 데이터 파싱 (sensorchart.tsx)

const extractHour = (time: string): string => {
  return time.substring(11, 13);
};

const extractValue = (avg_vol: string): number => {
  const match = avg_vol.match(/-?(\d+)/);
  return match ? parseInt(match[1], 10) : 0;
};

API 응답 문자열에서 시간·수치를 추출하는 헬퍼 함수에 명시적 반환 타입을 지정했습니다.


4. 핵심 기능 구현

4.1 실시간 센서 모니터링 (WebSocket)

Preventative.tsx 마운트 시 3개의 WebSocket 채널을 연결합니다.

채널URL데이터

혼잡도 ws://.../wsapp/prev-motion 건물 혼잡도
공기질 ws://.../wsapp/prev-air-quality PM10, PM2.5, CO₂, VOC
바이러스 ws://.../wsapp/prev-virus 부유균 센서
ws.current = new WebSocket("ws://112.217.210.138:11080/wsapp/prev-air-quality");
ws.current.onmessage = (message) => {
  const airQualityDataSet = JSON.parse(message.data);
  if (airQualityDataSet.place2 === 'suyoo_room_hall') {
    setAirQualityData3(airQualityDataSet);
    localStorage.setItem('airQualityData3', JSON.stringify(airQualityDataSet));
    CheckAirQuality(airQualityDataSet, 3);
  }
  // ...
};

공기질 데이터는 place2 필드로 구역(left / right / hall)을 구분하고, localStorage에 캐싱하여 새로고침 후에도 마지막 값을 유지합니다.

4.2 공조기 원격 제어 (REST API)

sensorlist.ts에서 공조기 제어 API를 모듈화했습니다.

// 공조기 전원 제어
export const controlPower = async (param) => {
  const url = '/kims-api/v1/air_fan';
  const headers = {
    'Content-Type': 'application/json',
    'x-onpoomkey': 'SH25B3K-668488C-PHFEGHN-131S6NA',
  };
  return request.post(url, { data: param, headers });
};

// 자동/수동 모드 변경
export const modeChange = async (param) => {
  return request.put('/i1/settings/mode/change', { data: param });
};

// 풍량 임계치 설정
export const airvolChange = async (param) => {
  return request.put('/i1/settings/standard/change', { data: param });
};

제어 항목:

  • 전원 (power): ON/OFF
  • 모드 (status): 자동 / 수동
  • 풍량 (wind): quiet · weak · middle · strong · turbo
  • 플라즈마 (plazma): ON/OFF
  • 풍량 임계치 (settings): 부유균 농도 구간별 풍량 매핑

4.3 자동 모드 — 부유균 기반 풍량 제어

sensorchart.tsx는 10분(600,000ms) 주기로 부유균 평균값을 조회하고, 자동 모드인 공조기의 풍량을 자동 조절합니다.

const autoModeContorl = async (acData, bacteria) => {
  if (acData && acData.status === '자동') {
    const lastValue = bacteria[bacteria.length - 1].value;
    let fanSpeed: string;

    if (lastValue < 100)       fanSpeed = 'quiet';
    else if (lastValue < 200)  fanSpeed = 'weak';
    else if (lastValue < 300)  fanSpeed = 'middle';
    else if (lastValue < 400)  fanSpeed = 'strong';
    else                       fanSpeed = 'turbo';

    await controlSterilization({ dev_id: acData.device, wind: fanSpeed });
  }
};

4.4 3D 지도 위 센서·건물 시각화

Cesium Entity API로 각 시설의 센서·공조기 위치에 아이콘을 배치합니다.

cesiumview.entities.add({
  id: '한마음_공조기1',
  position: Cesium.Cartesian3.fromDegrees(lon, lat, height),
  billboard: {
    image: new Cesium.ConstantProperty('images/preventative/mapIcon03-nor.png'),
    scale: 0.5,
    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  },
});

3D 건물 모델은 Cesium3DTileset으로 로드합니다.

cesiumview.scene.primitives.add(
  new Cesium.Cesium3DTileset({ url: '/data/nursinghome/tileset.json' })
);

4.5 바람길(Line Flow) 시각 효과

공기질 구역 선택 시 Cesium PolylineGeometry + 커스텀 Material로 공기 흐름(바람길) 을 3D 지도 위에 애니메이션으로 표현합니다. lineFlow 상태 토글로 on/off를 제어합니다.


5. API · WebSocket 연동

5.1 개발 프록시 설정 (.umirc.ts)

로컬 개발 시 CORS를 우회하기 위해 Umi proxy를 설정합니다.

proxy: {
  '/kims-api': {
    target: 'http://kims-api.askstoryboard.com:3003',
    changeOrigin: true,
    pathRewrite: { '^/kims-api/': '/' },
  },
  '/i1': {
    target: 'http://i1.askstoryboard.com:8080',
    changeOrigin: true,
    pathRewrite: { '^/i1/': '/' },
  },
}

5.2 API 엔드포인트 요약

메서드경로설명

GET /i1/status?dev_id={id} 공조기 상태 조회
POST /i1/control 공조기 제어
PUT /i1/settings/mode/change 자동/수동 모드 변경
PUT /i1/settings/standard/change 풍량 임계치 변경
GET /i1/data?place={place} 2시간 부유균 평균 조회
GET /kims-api/v1/place/kims 건물·장소 정보
POST /kims-api/v1/air_fan 공조기 전원·풍량·플라즈마 제어

6. Cesium 3D 지도 통합

6.1 지도 초기화 (cesium-view.tsx)

<Map
  terrainProvider={new CesiumTerrainProvider({ url: AppCfg.koreaTerrain })}
  imageryProvider={new WebMapTileServiceImageryProvider({
    url: AppCfg.worldUrl,
    layer: '', style: '', tileMatrixSetID: '',
  })}
  sceneMode={SceneMode.SCENE3D}
  shadows={true}
  cesium_base_url={"./ionsdk"}
  widgets_css={"./ionsdk/Widgets/widgets.css"}
  contextOptions={{
    webgl: { preserveDrawingBuffer: true }  // 스크린샷 캡처 지원
  }}
  getViewInstance={(v) => {
    setTimeout(() => {
      dispatch({ type: 'cesiumview/initViewV2', payload: { viewer: v } });
    }, 100);
  }}
/>
<Preventative />

6.2 Webpack Externals

Cesium, React, Ant Design 등 대용량 라이브러리는 CDN 스크립트로 로드하고 Webpack externals로 번들에서 제외합니다.

config.externals({
  '@cesiumgs/cesium-analytics': 'Cesium',
  react: 'React',
  'react-dom': 'ReactDOM',
  antd: 'antd',
  echarts: 'echarts',
});

이 방식으로 초기 번들 크기를 줄이고, Cesium Worker 파일은 public/ionsdk/에 정적 배포합니다.


7. 개발 환경 설정

7.1 사전 요구사항

  • Node.js 16+
  • Yarn (Workspaces)
  • Turbo (글로벌): npm install turbo -g

7.2 master 프로젝트 실행

cd projects/master
yarn          # 의존성 설치 (postinstall: max setup)
yarn start    # theme 생성 + max dev

7.3 프로덕션 빌드

yarn build    # publicPath: /map/

빌드 결과물은 /map/ 경로 기준으로 배포됩니다 (hash 라우팅).


8. 이슈 및 트러블슈팅

개발 과정에서 발생한 주요 이슈와 해결 방법을 커밋 히스토리 기반으로 정리했습니다.


Issue #1 — 방역공조 API 변경 대응

증상: 백엔드 API 스펙이 변경되면서 기존 공조기 제어·상태 조회가 동작하지 않음.

커밋:

  • 방역공조 수정된 API 반영 (2024-05-21)
  • 공조기/공기질 신규 API 적용 (2024-05-30)

원인: 기존 단일 API에서 /kims-api, /i1 두 개의 서로 다른 API 서버로 분리됨.

해결:

  1. .umirc.ts에 /kims-api, /i1 프록시 추가
  2. sensorlist.ts에 API 함수별 엔드포인트·헤더(x-onpoomkey) 분리
  3. sensorchart.tsx에 자동 모드 제어 로직 추가
// Before: 단일 API
request.get('/server/api/sensor/list');

// After: 역할별 분리
request.get('/i1/status?dev_id=all');          // 상태 조회
request.post('/kims-api/v1/air_fan', { ... }); // 제어
request.put('/i1/settings/mode/change', { ... }); // 모드 변경

Issue #2 — cesium-view.tsx 비대화 (3,000줄+)

증상: cesium-view.tsx에 방역공조 로직·스타일·지도 초기화가 모두 혼재하여 유지보수가 어려움.

커밋: 방역공조 소스 정리 (2024-07-04)

해결: 방역공조 관련 로직을 Preventative.tsx + Preventative.less로 분리.

파일BeforeAfter

cesium-view.tsx ~3,500줄 ~150줄
Preventative.tsx 없음 ~3,000줄
cesium-view.less ~400줄 최소화
// cesium-view.tsx — 분리 후
<Map {...mapProps} />
<Preventative />

교훈: 단일 책임 원칙(SRP)을 적용해 Cesium 지도 컨테이너와 비즈니스 로직을 분리하면, TypeScript 타입 추론과 코드 탐색이 훨씬 수월해집니다.


Issue #3 — 3D Tileset CORS 오류

증상: 노인요양원 3D 건물 모델을 외부 URL로 로드할 때 브라우저 CORS 정책에 의해 차단됨.

커밋: model 외부 경로 지정 시 CORS 오류로 인해 프로젝트 내부에 저장 (2024-07-16)

원인: 외부 서버(http://...)의 B3DM 타일셋에 Access-Control-Allow-Origin 헤더가 없음.

해결: 157개의 .b3dm 타일 파일과 tileset.json을 projects/master/public/data/nursinghome/에 직접 배치.

// Before
new Cesium.Cesium3DTileset({ url: 'http://external-server/nursinghome/tileset.json' })

// After
new Cesium.Cesium3DTileset({ url: '/data/nursinghome/tileset.json' })

교훈: Cesium 3D Tiles는 same-origin 또는 CORS 허용 서버에서만 안정적으로 로드됩니다. 프로덕션 배포 시 정적 파일 내장 또는 CDN CORS 설정이 필수입니다.


Issue #4 — 풍량 임계치 입력값 타입 불일치

증상: 풍량 임계치 설정 UI에서 숫자 입력 후 API 전송 시 문자열로 전달되어 서버에서 거부되거나 비교 연산이 오동작.

커밋: value parseInt (2024-10-28)

원인: <input type="number">의 onChange 이벤트에서 event.target.value는 TypeScript/JavaScript 모두 string 타입.

해결:

// Before
const newValue = event.target.value;  // string

// After
const newValue = parseInt(event.target.value);  // number

// JSX 속성도 string → number로 수정
min={0}   // Before: min="0"
max={9999}
step={1}

교훈: React controlled input에서 숫자 상태를 다룰 때는 parseInt/parseFloat로 명시적 변환이 필요합니다. TypeScript strict mode에서는 value: string과 value: number 불일치를 컴파일 타임에 잡을 수 있습니다.


Issue #5 — WebSocket 다중 연결 덮어쓰기

증상: 혼잡도·공기질·바이러스 3개 채널 중 마지막 WebSocket만 유효하고, 이전 연결이 끊김.

원인: 단일 useRef에 WebSocket을 3번 할당.

// 문제 코드
ws.current = new WebSocket(".../prev-motion");     // 1번째 → 덮어씀
ws.current = new WebSocket(".../prev-air-quality"); // 2번째 → 덮어씀
ws.current = new WebSocket(".../prev-virus");       // 3번째만 살아남음

해결 방향 (개선 제안):

const wsMotion = useRef<WebSocket | null>(null);
const wsAirQuality = useRef<WebSocket | null>(null);
const wsVirus = useRef<WebSocket | null>(null);

// 또는 Map/Record로 관리
const wsMap = useRef<Record<string, WebSocket>>({});

useEffect(() => {
  wsMap.current.motion = new WebSocket(".../prev-motion");
  wsMap.current.airQuality = new WebSocket(".../prev-air-quality");
  wsMap.current.virus = new WebSocket(".../prev-virus");

  return () => {
    Object.values(wsMap.current).forEach(ws => ws?.close());
  };
}, []);

교훈: TypeScript의 useRef<WebSocket | null> 타입을 활용해 ref 구조를 명확히 설계하면, 다중 WebSocket 관리 실수를 줄일 수 있습니다.


Issue #6 — 예산군 노인요양원 신규 시설 추가

증상: 기존 3개 시설(경남도청, 마산병원, 한마음병원) 외 예산군 노인요양원 시설 추가 요구.

커밋:

  • 예산군 노인요양원 작업 (2024-07-16) — UI·Entity 1,500줄 추가
  • 공조기, 공기질 센서 추가 (2024-07-22) — 센서 3대 추가

작업 내용:

  • 노인요양원 전용 Entity(공조기 3대, 공기질 센서) Cesium 지도 배치
  • checkNursingHome 상태 및 전용 UI 패널 추가
  • 3D Tileset 로컬 배치 (Issue #3 연계)
  • sensorchart.tsx에 place 매핑 추가 (left / right / corridor)

Issue #7 — 바람길(Line Flow) 효과 적용

증상: 공기질 구역 선택 시 공기 흐름을 시각적으로 표현하는 바람길 효과가 동작하지 않음.

커밋: 바람길 효과 적용 (2024-07-05)

해결: Cesium PolylineGeometry + 커스텀 Material Property로 라인 애니메이션 구현. lineFlow 상태와 useEffect 연동.

useEffect(() => {
  if (lineFlow) {
    lineFlowFunction();
  } else {
    // Primitive 제거
  }
}, [lineFlow]);

9. 회고 및 개선 방향

잘한 점

  • 모노레포 + 공통 패키지 구조로 Cesium Map 컴포넌트를 재사용
  • API 레이어 분리 (sensorlist.ts)로 HTTP 호출과 UI 로직 분리
  • Preventative 컴포넌트 분리로 cesium-view.tsx 가독성 대폭 개선
  • localStorage 캐싱으로 WebSocket 끊김 시에도 마지막 센서값 유지

개선이 필요한 부분

항목현재개선 방향

Preventative.tsx 크기 ~3,700줄 단일 파일 시설별·기능별 Hook/컴포넌트 분리
API 타입 param: any 암시적 Request/Response interface 정의
WebSocket 단일 ref 덮어쓰기 ref Map 또는 커스텀 Hook
상태 관리 useState 50+ useReducer 또는 DVA model로 이전
테스트 없음 sensorlist API mock + Vitest 도입

TypeScript strict 모드 적용 시 기대 효과

// sensorlist.ts — 타입 정의 예시 (개선안)
interface AirFanControlParam {
  dev_id: string;
  power?: 'ON' | 'OFF';
  wind?: 'quiet' | 'weak' | 'middle' | 'strong' | 'turbo';
  plazma?: 'ON' | 'OFF';
}

interface SterilizationStatus {
  device: string;
  power: string;
  status: '자동' | '수동';
  wind: string;
  plazma: string;
  settings: StandardVolumeSettings;
}

interface StandardVolumeSettings {
  weak: number;
  middle: number;
  strong: number;
  quiet: number;
}

export const controlSterilization = async (
  param: AirFanControlParam
): Promise<{ status: 'SUCCESS' | 'FAIL' }> => {
  // ...
};

부록: 커밋 히스토리 요약

날짜커밋내용

2024-05-21 방역공조 수정된 API 반영 API 엔드포인트 변경 대응
2024-05-30 공조기/공기질 신규 API 적용 kims-api, i1 프록시 추가
2024-07-04 방역공조 소스 정리 Preventative.tsx 분리
2024-07-05 바람길 효과 적용 Line Flow 시각화
2024-07-16 예산군 노인요양원 작업 신규 시설 UI·Entity
2024-07-16 CORS → 로컬 Tileset 3D 모델 내장 배치
2024-07-22 공조기, 공기질 센서 추가 센서 3대 확장
2024-10-28 value parseInt 임계치 입력 타입 수정

이 문서는 preventative 모노레포의 projects/master 방역공조 모듈을 기준으로 작성되었습니다.