원격 엔진 제어 API 코드랩
개요
이 코드랩은 사용자가 모바일 애플리케이션을 통해 차량 엔진을 원격으로 시작하거나 중지할 수 있게 하는 원격 엔진 제어 API를 구현하는 방법을 안내합니다. 엔진 제어 기능을 통합하고, 실시간으로 상태를 모니터링하고, 안전 요구 사항을 처리하는 방법을 배우게 됩니다.
사전 요구 사항
- REST API에 대한 기본 이해
- JavaScript/TypeScript 친숙도
- Node.js와 npm 설치
- 텍스트 에디터 또는 IDE
학습 목표
- 원격 엔진 시작/중지 기능 구현
- SSE를 통한 실시간 상태 업데이트 처리
- 엔진 제어 기록 및 설정 관리
- 안전 및 보안 조치 구현
- 오류 시나리오 및 엣지 케이스 처리
설정
1. 프로젝트 초기화
mkdir remote-engine-control-demo
cd remote-engine-control-demo
npm init -y
npm install axios node-fetch2. 구성
config.js 파일을 생성하세요:
export const API_CONFIG = {
baseURL: 'https://api.ecarus.run/api/v1/remotecontrol',
authToken: 'sk_4f9c7b8e2d1a6c0f3e7a9b5d8c1e4f2a7c6d9e0b3f5a8c1d4e7f9b2c6a1e3d',
sampleVIN: 'KMHSH81C7LU123456',
timeout: 30000 // 30초
};단계 1: 기본 엔진 제어
엔진 시작 구현
engineControl.js를 생성하세요:
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를 생성하세요:
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();테스트 실행:
node test-basic.js단계 2: 상태 모니터링
상태 확인 구현
engineControl.js에 추가:
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를 생성하세요:
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를 생성하세요:
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를 생성하세요:
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에 추가:
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를 생성하세요:
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를 생성하세요:
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를 수정하여 오류 처리 포함:
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를 생성하세요:
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();구현 테스트
완전한 애플리케이션 실행:
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
✅ 앱 종료 완료챌린지 연습
향상된 안전 기능: 주차 브레이크 상태 및 후드 위치와 같은 추가 안전 확인 구현.
명령 큐 관리: 여러 엔진 제어 요청을 안전하게 처리하는 큐 시스템 생성.
분석 대시보드: 엔진 사용 통계 및 패턴을 보여주는 간단한 대시보드 구축.
모바일 통합: 오프라인 지원을 통해 모바일 앱 환경에 코드를 적응.
다중 차량 지원: 여러 차량을 동시에 처리하도록 애플리케이션 확장.
요약
이 코드랩에서 다음을 배웠습니다:
- ✅ 원격 엔진 시작/중지 기능 구현
- ✅ SSE를 통해 실시간 엔진 상태 모니터링
- ✅ 안전 요구 사항 및 오류 시나리오 처리
- ✅ 엔진 설정 및 진단 관리
- ✅ 완전한 프로덕션 준비 애플리케이션 구축
원격 엔진 제어 API는 엄격한 안전 요구 사항을 유지하면서 차량 엔진을 원격으로 제어하는 안전하고 신뢰할 수 있는 방법을 제공합니다. 여기서 배운 구현 패턴은 다른 차량 제어 API에도 적용할 수 있습니다.