Skip to content

원격 엔진 제어 API 코드랩

개요

이 코드랩은 사용자가 모바일 애플리케이션을 통해 차량 엔진을 원격으로 시작하거나 중지할 수 있게 하는 원격 엔진 제어 API를 구현하는 방법을 안내합니다. 엔진 제어 기능을 통합하고, 실시간으로 상태를 모니터링하고, 안전 요구 사항을 처리하는 방법을 배우게 됩니다.

사전 요구 사항

  • REST API에 대한 기본 이해
  • JavaScript/TypeScript 친숙도
  • Node.js와 npm 설치
  • 텍스트 에디터 또는 IDE

학습 목표

  • 원격 엔진 시작/중지 기능 구현
  • SSE를 통한 실시간 상태 업데이트 처리
  • 엔진 제어 기록 및 설정 관리
  • 안전 및 보안 조치 구현
  • 오류 시나리오 및 엣지 케이스 처리

설정

1. 프로젝트 초기화

bash
mkdir remote-engine-control-demo
cd remote-engine-control-demo
npm init -y
npm install axios node-fetch

2. 구성

config.js 파일을 생성하세요:

javascript
export const API_CONFIG = {
  baseURL: 'https://api.ecarus.run/api/v1/remotecontrol',
  authToken: 'sk_4f9c7b8e2d1a6c0f3e7a9b5d8c1e4f2a7c6d9e0b3f5a8c1d4e7f9b2c6a1e3d',
  sampleVIN: 'KMHSH81C7LU123456',
  timeout: 30000 // 30초
};

단계 1: 기본 엔진 제어

엔진 시작 구현

engineControl.js를 생성하세요:

javascript
import axios from 'axios';
import { API_CONFIG } from './config.js';

class EngineController {
  constructor() {
    this.client = axios.create({
      baseURL: API_CONFIG.baseURL,
      headers: {
        'Authorization': `Bearer ${API_CONFIG.authToken}`,
        'Content-Type': 'application/json'
      },
      timeout: API_CONFIG.timeout
    });
  }

  async startEngine(vin, options = {}) {
    try {
      const payload = {
        targetTemperature: options.targetTemperature || 22.0,
        climateControl: options.climateControl !== false,
        duration: options.duration || 1800
      };

      const response = await this.client.post(
        `/vehicles/${vin}/engine/start`,
        payload
      );

      console.log('엔진 시작 시작됨:', response.data);
      return response.data;
    } catch (error) {
      console.error('엔진 시작 실패:', error.response?.data || error.message);
      throw error;
    }
  }

  async stopEngine(vin, options = {}) {
    try {
      const payload = {
        force: options.force || false,
        reason: options.reason || '사용자 중지',
        preserveClimate: options.preserveClimate || false
      };

      const response = await this.client.post(
        `/vehicles/${vin}/engine/stop`,
        payload
      );

      console.log('엔진 중지 시작됨:', response.data);
      return response.data;
    } catch (error) {
      console.error('엔진 중지 실패:', error.response?.data || error.message);
      throw error;
    }
  }

  async cancelCommand(vin, correlationId, options = {}) {
    try {
      const payload = {
        correlationId,
        reason: options.reason || '사용자 취소',
        force: options.force || false
      };

      const response = await this.client.post(
        `/vehicles/${vin}/engine/cancel`,
        payload
      );

      console.log('명령 취소됨:', response.data);
      return response.data;
    } catch (error) {
      console.error('명령 취소 실패:', error.response?.data || error.message);
      throw error;
    }
  }
}

export default EngineController;

기본 엔진 제어 테스트

test-basic.js를 생성하세요:

javascript
import EngineController from './engineControl.js';

const controller = new EngineController();

async function testBasicControl() {
  const vin = API_CONFIG.sampleVIN;

  try {
    // 엔진 시작 테스트
    console.log('엔진 시작 테스트 중...');
    const startResult = await controller.startEngine(vin, {
      targetTemperature: 24.0,
      climateControl: true,
      duration: 1800
    });

    // 잠시 기다린 후 엔진 중지 테스트
    setTimeout(async () => {
      console.log('엔진 중지 테스트 중...');
      await controller.stopEngine(vin, {
        reason: '테스트 완료'
      });
    }, 5000);

  } catch (error) {
    console.error('테스트 실패:', error.message);
  }
}

testBasicControl();

테스트 실행:

bash
node test-basic.js

단계 2: 상태 모니터링

상태 확인 구현

engineControl.js에 추가:

javascript
async getEngineStatus(vin, includeDetails = true) {
  try {
    const response = await this.client.get(
      `/vehicles/${vin}/engine/status`,
      { params: { includeDetails } }
    );

    console.log('엔진 상태:', response.data);
    return response.data;
  } catch (error) {
    console.error('엔진 상태 가져오기 실패:', error.response?.data || error.message);
    throw error;
  }
}

async getEngineHistory(vin, options = {}) {
  try {
    const params = {
      period: options.period || '24h',
      limit: options.limit || 50,
      offset: options.offset || 0,
      action: options.action || undefined
    };

    const response = await this.client.get(
      `/vehicles/${vin}/engine/history`,
      { params }
    );

    console.log('엔진 기록:', response.data);
    return response.data;
  } catch (error) {
    console.error('엔진 기록 가져오기 실패:', error.response?.data || error.message);
    throw error;
  }
}

상태 모니터링 테스트

test-status.js를 생성하세요:

javascript
import EngineController from './engineControl.js';

const controller = new EngineController();

async function testStatusMonitoring() {
  const vin = API_CONFIG.sampleVIN;

  try {
    // 현재 상태 가져오기
    console.log('현재 엔진 상태 가져오는 중...');
    const status = await controller.getEngineStatus(vin);
    
    // 기록 가져오기
    console.log('엔진 기록 가져오는 중...');
    const history = await controller.getEngineHistory(vin, {
      period: '7d',
      limit: 10
    });

    console.log('상태 모니터링 테스트가 성공적으로 완료됨');
  } catch (error) {
    console.error('상태 모니터링 테스트 실패:', error.message);
  }
}

testStatusMonitoring();

단계 3: SSE를 통한 실시간 업데이트

SSE 클라이언트 구현

sseClient.js를 생성하세요:

javascript
export class SSEClient {
  constructor(url, authToken) {
    this.url = url;
    this.authToken = authToken;
    this.eventSource = null;
    this.listeners = new Map();
  }

  connect() {
    return new Promise((resolve, reject) => {
      try {
        this.eventSource = new EventSource(this.url, {
          headers: {
            'Authorization': `Bearer ${this.authToken}`
          }
        });

        this.eventSource.onopen = () => {
          console.log('SSE 연결 열림');
          resolve();
        };

        this.eventSource.onerror = (error) => {
          console.error('SSE 연결 오류:', error);
          if (this.eventSource.readyState === EventSource.CLOSED) {
            reject(new Error('SSE 연결 닫힘'));
          }
        };

        this.eventSource.onmessage = (event) => {
          try {
            const data = JSON.parse(event.data);
            this.handleEvent(data);
          } catch (error) {
            console.error('SSE 데이터 파싱 실패:', error);
          }
        };

      } catch (error) {
        reject(error);
      }
    });
  }

  on(eventType, callback) {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    this.listeners.get(eventType).push(callback);
  }

  handleEvent(data) {
    const eventType = data.type || 'default';
    const callbacks = this.listeners.get(eventType) || [];
    
    callbacks.forEach(callback => {
      try {
        callback(data);
      } catch (error) {
        console.error('이벤트 콜백에서 오류:', error);
      }
    });
  }

  disconnect() {
    if (this.eventSource) {
      this.eventSource.close();
      this.eventSource = null;
      console.log('SSE 연결 닫힘');
    }
  }
}

실시간 업데이트 테스트

test-sse.js를 생성하세요:

javascript
import { SSEClient } from './sseClient.js';
import { API_CONFIG } from './config.js';

async function testRealTimeUpdates() {
  const vin = API_CONFIG.sampleVIN;
  const sseUrl = `${API_CONFIG.baseURL}/vehicles/${vin}/engine/updates/stream`;
  
  const sseClient = new SSEClient(sseUrl, API_CONFIG.authToken);

  try {
    await sseClient.connect();

    // 엔진 상태 업데이트 수신
    sseClient.on('ENGINE_STATUS_UPDATE', (data) => {
      console.log('엔진 상태 업데이트:', data);
      
      if (data.data.changeType === 'STARTED') {
        console.log('🚗 엔진이 성공적으로 시작됨!');
      } else if (data.data.changeType === 'STOPPED') {
        console.log('🛑 엔진 중지됨');
      }
    });

    // 명령 상태 업데이트 수신
    sseClient.on('COMMAND_STATUS_UPDATE', (data) => {
      console.log('명령 상태 업데이트:', data);
    });

    // 시스템 경고 수신
    sseClient.on('SYSTEM_ALERT', (data) => {
      console.log('시스템 경고:', data);
    });

    console.log('실시간 업데이트 수신 중... 중지하려면 Ctrl+C를 누르세요');

    // 연결 유지
    process.on('SIGINT', () => {
      console.log('\n연결 해제 중...');
      sseClient.disconnect();
      process.exit(0);
    });

  } catch (error) {
    console.error('SSE 연결 실패:', error.message);
  }
}

testRealTimeUpdates();

단계 4: 설정 관리

설정 제어 구현

engineControl.js에 추가:

javascript
async getEngineConfig(vin) {
  try {
    const response = await this.client.get(`/vehicles/${vin}/engine/config`);
    console.log('엔진 구성:', response.data);
    return response.data;
  } catch (error) {
    console.error('엔진 구성 가져오기 실패:', error.response?.data || error.message);
    throw error;
  }
}

async updateEngineConfig(vin, config) {
  try {
    const payload = {
      remoteStartEnabled: config.remoteStartEnabled !== false,
      maxRunTime: config.maxRunTime || 1800,
      autoStopEnabled: config.autoStopEnabled !== false,
      securityRequired: config.securityRequired !== false,
      climateControl: config.climateControl !== false,
      defaultTemperature: config.defaultTemperature || 22.0,
      lowFuelThreshold: config.lowFuelThreshold || 15.0,
      highTemperatureThreshold: config.highTemperatureThreshold || 105.0
    };

    const response = await this.client.put(
      `/vehicles/${vin}/engine/config`,
      payload
    );

    console.log('엔진 구성 업데이트됨:', response.data);
    return response.data;
  } catch (error) {
    console.error('엔진 구성 업데이트 실패:', error.response?.data || error.message);
    throw error;
  }
}

async getEngineDiagnostics(vin) {
  try {
    const response = await this.client.get(`/vehicles/${vin}/engine/diagnostics`);
    console.log('엔진 진단:', response.data);
    return response.data;
  } catch (error) {
    console.error('엔진 진단 가져오기 실패:', error.response?.data || error.message);
    throw error;
  }
}

설정 관리 테스트

test-settings.js를 생성하세요:

javascript
import EngineController from './engineControl.js';

const controller = new EngineController();

async function testSettingsManagement() {
  const vin = API_CONFIG.sampleVIN;

  try {
    // 현재 설정 가져오기
    console.log('현재 엔진 설정 가져오는 중...');
    const currentConfig = await controller.getEngineConfig(vin);

    // 설정 업데이트
    console.log('엔진 설정 업데이트 중...');
    await controller.updateEngineConfig(vin, {
      maxRunTime: 2400, // 40분
      defaultTemperature: 23.0,
      lowFuelThreshold: 20.0
    });

    // 진단 가져오기
    console.log('엔진 진단 가져오는 중...');
    const diagnostics = await controller.getEngineDiagnostics(vin);

    console.log('설정 관리 테스트가 성공적으로 완료됨');
  } catch (error) {
    console.error('설정 관리 테스트 실패:', error.message);
  }
}

testSettingsManagement();

단계 5: 오류 처리 및 안전

강력한 오류 처리 구현

errorHandler.js를 생성하세요:

javascript
export class EngineControlError extends Error {
  constructor(message, code, details = null) {
    super(message);
    this.name = 'EngineControlError';
    this.code = code;
    this.details = details;
  }
}

export class SafetyErrorHandler {
  static handleEngineError(error) {
    if (error.response) {
      const status = error.response.status;
      const data = error.response.data;

      switch (status) {
        case 400:
          throw new EngineControlError(
            '잘못된 요청 파라미터',
            'INVALID_REQUEST',
            data
          );
        
        case 401:
          throw new EngineControlError(
            '인증 실패',
            'AUTHENTICATION_ERROR',
            data
          );
        
        case 403:
          throw new EngineControlError(
            '차량 제어 권한 거부',
            'PERMISSION_DENIED',
            data
          );
        
        case 409:
          throw new EngineControlError(
            '엔진 제어 충돌 - 다른 명령이 진행 중',
            'COMMAND_CONFLICT',
            data
          );
        
        case 422:
          throw new EngineControlError(
            '엔진 제어에 대한 안전 조건이 충족되지 않음',
            'SAFETY_VIOLATION',
            data
          );
        
        case 503:
          throw new EngineControlError(
            '차량 오프라인 또는 사용할 수 없음',
            'VEHICLE_OFFLINE',
            data
          );
        
        default:
          throw new EngineControlError(
            '엔진 제어 작업 실패',
            'UNKNOWN_ERROR',
            data
          );
      }
    } else if (error.code === 'ECONNABORTED') {
      throw new EngineControlError(
        '요청 시간 초과 - 작업이 아직 처리 중일 수 있음',
        'TIMEOUT',
        null
      );
    } else {
      throw new EngineControlError(
        '네트워크 오류 발생',
        'NETWORK_ERROR',
        error.message
      );
    }
  }

  static getSafetyRecommendations(errorCode) {
    const recommendations = {
      'SAFETY_VIOLATION': [
        '차량이 변속기가 Park에 있는 상태로 주차되어 있는지 확인',
        '모든 도어가 닫혀 있는지 확인',
        '차량 보안 시스템 상태 확인',
        '엔진이 현재 실행되고 있지 않은지 확인'
      ],
      'VEHICLE_OFFLINE': [
        '차량 네트워크 연결 확인',
        '차량 점화 상태 확인',
        '차량 신호가 더 좋을 때 다시 시도',
        '문제가 지속되면 지원팀에 문의'
      ],
      'COMMAND_CONFLICT': [
        '현재 명령이 완료될 때까지 대기',
        '새 명령을 발행하기 전에 기존 명령 취소',
        '다시 시도하기 전에 현재 엔진 상태 확인'
      ],
      'TIMEOUT': [
        '명령이 아직 처리 중일 수 있음',
        '몇 분 후 엔진 상태 확인',
        '충돌을 피하기 위해 즉시 재시도하지 않음'
      ]
    };

    return recommendations[errorCode] || ['지원팀에 문의하세요'];
  }
}

오류 처리로 엔진 제어기 업데이트

engineControl.js를 수정하여 오류 처리 포함:

javascript
import { SafetyErrorHandler, EngineControlError } from './errorHandler.js';

// 기존 메서드에 오류 처리 추가
async startEngine(vin, options = {}) {
  try {
    // ... 기존 코드 ...
  } catch (error) {
    throw SafetyErrorHandler.handleEngineError(error);
  }
}

// 안전 확인 메서드 추가
async performSafetyCheck(vin) {
  try {
    const status = await this.getEngineStatus(vin);
    
    const safetyChecks = {
      isVehicleSafe: !status.isRunning || status.isRemoteStarted,
      isTransmissionInPark: status.transmissionGear === 'PARK',
      areDoorsClosed: status.doorsLocked || !status.doorsOpen,
      hasFuel: status.fuelLevel > 10.0,
      isTemperatureSafe: status.engineTemperature < 105.0
    };

    const allSafe = Object.values(safetyChecks).every(check => check);
    
    if (!allSafe) {
      const failedChecks = Object.entries(safetyChecks)
        .filter(([key, value]) => !value)
        .map(([key]) => key);
      
      throw new EngineControlError(
        '안전 조건이 충족되지 않음',
        'SAFETY_VIOLATION',
        { failedChecks, recommendations: SafetyErrorHandler.getSafetyRecommendations('SAFETY_VIOLATION') }
      );
    }

    return safetyChecks;
  } catch (error) {
    throw SafetyErrorHandler.handleEngineError(error);
  }
}

단계 6: 완전한 통합 예제

완전한 애플리케이션 생성

app.js를 생성하세요:

javascript
import EngineController from './engineControl.js';
import { SSEClient } from './sseClient.js';
import { SafetyErrorHandler } from './errorHandler.js';
import { API_CONFIG } from './config.js';

class RemoteEngineApp {
  constructor() {
    this.controller = new EngineController();
    this.sseClient = null;
    this.vin = API_CONFIG.sampleVIN;
  }

  async initialize() {
    try {
      console.log('🚗 원격 엔진 제어 앱 초기화 중...');
      
      // 실시간 업데이트를 위한 SSE 연결
      await this.connectSSE();
      
      // 초기 상태 가져오기
      await this.getStatus();
      
      console.log('✅ 앱이 성공적으로 초기화됨');
    } catch (error) {
      console.error('❌ 앱 초기화 실패:', error.message);
    }
  }

  async connectSSE() {
    const sseUrl = `${API_CONFIG.baseURL}/vehicles/${this.vin}/engine/updates/stream`;
    this.sseClient = new SSEClient(sseUrl, API_CONFIG.authToken);
    
    await this.sseClient.connect();
    
    this.sseClient.on('ENGINE_STATUS_UPDATE', (data) => {
      this.handleStatusUpdate(data);
    });
    
    this.sseClient.on('SYSTEM_ALERT', (data) => {
      this.handleSystemAlert(data);
    });
  }

  async startEngine(options = {}) {
    try {
      console.log('🔑 엔진 시작 중...');
      
      // 먼저 안전 확인 수행
      await this.controller.performSafetyCheck(this.vin);
      
      // 엔진 시작
      const result = await this.controller.startEngine(this.vin, options);
      
      console.log('⏳ 엔진 시작 명령 전송됨:', result.commandId);
      return result;
      
    } catch (error) {
      console.error('❌ 엔진 시작 실패:', error.message);
      
      if (error.details && error.details.recommendations) {
        console.log('💡 권장 사항:');
        error.details.recommendations.forEach(rec => console.log(`   - ${rec}`));
      }
      
      throw error;
    }
  }

  async stopEngine(options = {}) {
    try {
      console.log('🛑 엔진 중지 중...');
      
      const result = await this.controller.stopEngine(this.vin, options);
      
      console.log('⏳ 엔진 중지 명령 전송됨:', result.commandId);
      return result;
      
    } catch (error) {
      console.error('❌ 엔진 중지 실패:', error.message);
      throw error;
    }
  }

  async getStatus() {
    try {
      const status = await this.controller.getEngineStatus(this.vin);
      
      console.log('📊 현재 엔진 상태:');
      console.log(`   실행 중: ${status.isRunning ? '✅' : '❌'}`);
      console.log(`   원격 시작됨: ${status.isRemoteStarted ? '✅' : '❌'}`);
      console.log(`   남은 시간: ${status.remainingTimeSec}s`);
      console.log(`   기후 활성: ${status.climateActive ? '✅' : '❌'}`);
      console.log(`   연료 레벨: ${status.fuelLevel}%`);
      console.log(`   엔진 온도: ${status.engineTemperature}°C`);
      
      return status;
    } catch (error) {
      console.error('❌ 상태 가져오기 실패:', error.message);
      throw error;
    }
  }

  handleStatusUpdate(data) {
    const changeType = data.data.changeType;
    
    switch (changeType) {
      case 'STARTED':
        console.log('🚀 엔진이 성공적으로 시작됨!');
        break;
      case 'STOPPED':
        console.log('🛑 엔진 중지됨');
        break;
      case 'TIMEOUT_WARNING':
        console.log('⚠️ 엔진이 곧 자동 중지됨');
        break;
      case 'LOW_FUEL_WARNING':
        console.log('⛽ 낮은 연료 경고');
        break;
    }
    
    // 업데이트된 상태 표시
    console.log('📊 업데이트된 상태:', {
      running: data.data.isRunning,
      remainingTime: data.data.remainingTimeSec,
      temperature: data.data.currentTemperature
    });
  }

  handleSystemAlert(data) {
    console.log('🚨 시스템 경고:', data);
  }

  async shutdown() {
    console.log('🔄 앱 종료 중...');
    
    if (this.sseClient) {
      this.sseClient.disconnect();
    }
    
    console.log('✅ 앱 종료 완료');
  }
}

// 사용 예제
async function main() {
  const app = new RemoteEngineApp();
  
  try {
    await app.initialize();
    
    // 예제: 기후 제어로 엔진 시작
    await app.startEngine({
      targetTemperature: 24.0,
      climateControl: true,
      duration: 1800
    });
    
    // 기다린 후 중지
    setTimeout(async () => {
      await app.stopEngine({ reason: '데모 완료' });
      
      // 짧은 지연 후 종료
      setTimeout(() => {
        app.shutdown();
        process.exit(0);
      }, 2000);
    }, 10000);
    
  } catch (error) {
    console.error('애플리케이션 오류:', error.message);
    process.exit(1);
  }
}

// 정상 종료 처리
process.on('SIGINT', async () => {
  console.log('\n🔄 SIGINT 수신, 정상 종료 중...');
  if (app) {
    await app.shutdown();
  }
  process.exit(0);
});

main();

구현 테스트

완전한 애플리케이션 실행:

bash
node app.js

예상 출력:

🚗 원격 엔진 제어 앱 초기화 중...
SSE connection opened
📊 현재 엔진 상태:
   실행 중: ❌
   원격 시작됨: ❌
   남은 시간: 0s
   기후 활성: ❌
   연료 레벨: 65.5%
   엔진 온도: 45.2°C
✅ 앱이 성공적으로 초기화됨
🔑 엔진 시작 중...
⏳ 엔진 시작 명령 전송됨: cmd-uuid-12345
🚀 엔진이 성공적으로 시작됨!
📊 업데이트된 상태: { running: true, remainingTime: 1800, temperature: 18.5 }
🛑 엔진 중지 중...
⏳ 엔진 중지 명령 전송됨: cmd-uuid-67890
🛑 엔진 중지됨
🔄 앱 종료 중...
SSE connection closed
✅ 앱 종료 완료

챌린지 연습

  1. 향상된 안전 기능: 주차 브레이크 상태 및 후드 위치와 같은 추가 안전 확인 구현.

  2. 명령 큐 관리: 여러 엔진 제어 요청을 안전하게 처리하는 큐 시스템 생성.

  3. 분석 대시보드: 엔진 사용 통계 및 패턴을 보여주는 간단한 대시보드 구축.

  4. 모바일 통합: 오프라인 지원을 통해 모바일 앱 환경에 코드를 적응.

  5. 다중 차량 지원: 여러 차량을 동시에 처리하도록 애플리케이션 확장.

요약

이 코드랩에서 다음을 배웠습니다:

  • ✅ 원격 엔진 시작/중지 기능 구현
  • ✅ SSE를 통해 실시간 엔진 상태 모니터링
  • ✅ 안전 요구 사항 및 오류 시나리오 처리
  • ✅ 엔진 설정 및 진단 관리
  • ✅ 완전한 프로덕션 준비 애플리케이션 구축

원격 엔진 제어 API는 엄격한 안전 요구 사항을 유지하면서 차량 엔진을 원격으로 제어하는 안전하고 신뢰할 수 있는 방법을 제공합니다. 여기서 배운 구현 패턴은 다른 차량 제어 API에도 적용할 수 있습니다.

추가 리소스

Released under the MIT License.