Claude Code Plugins

Community-maintained marketplace

Feedback

api-integration

@k002bill2/LiveMetro
0
0

Seoul Subway API integration and data management for LiveMetro. Use when working with external APIs or data fetching.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name api-integration
description Seoul Open Data API integration for real-time subway arrival data and timetables. Use when working with external Seoul Metro APIs.

Seoul API Integration Guidelines

When to Use This Skill

  • Integrating Seoul Open Data APIs
  • Fetching real-time train arrival data
  • Working with subway timetables
  • Handling API errors and retries
  • Implementing data fallback strategies

API Endpoints

1. Real-Time Arrival API

Base URL: http://swopenapi.seoul.go.kr/api/subway/{API_KEY}/json/realtimeStationArrival/{START}/{END}/{STATION_NAME}

Response Fields:
- arvlMsg2, arvlMsg3: Arrival messages ("2분후[1번째전]", "곧 도착")
- btrainNo: Train number
- updnLine: Direction ("상행" = up, "하행" = down)
- trainLineNm: Line name
- bstatnNm: Destination station

2. Timetable API

Base URL: http://openAPI.seoul.go.kr:8088/{API_KEY}/json/SearchSTNTimeTableByIDService/{START}/{END}/{STATION_CODE}/{WEEK_TAG}/{INOUT_TAG}/

Parameters:
- WEEK_TAG: '1' (Weekday), '2' (Saturday), '3' (Sunday/Holiday)
- INOUT_TAG: '1' (Up/Inner), '2' (Down/Outer)

Service Implementation Pattern

seoulSubwayApi.ts Structure

import axios, { AxiosInstance } from 'axios';

class SeoulSubwayApi {
  private client: AxiosInstance;
  private readonly API_KEY: string;
  private readonly BASE_URL: string;
  private readonly TIMEOUT = 5000; // 5 seconds

  constructor() {
    this.API_KEY = process.env.SEOUL_SUBWAY_API_KEY || '';
    this.BASE_URL = process.env.SEOUL_SUBWAY_API_BASE_URL || '';

    this.client = axios.create({
      baseURL: this.BASE_URL,
      timeout: this.TIMEOUT,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors(): void {
    // Request interceptor
    this.client.interceptors.request.use(
      (config) => {
        console.log(`[API] Request: ${config.url}`);
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor
    this.client.interceptors.response.use(
      (response) => {
        console.log(`[API] Response: ${response.status}`);
        return response;
      },
      (error) => {
        console.error(`[API] Error: ${error.message}`);
        return Promise.reject(error);
      }
    );
  }

  async getRealtimeArrival(stationName: string): Promise<ArrivalData[]> {
    try {
      const url = `/${this.API_KEY}/json/realtimeStationArrival/1/10/${stationName}`;
      const response = await this.client.get(url);

      // Handle Seoul API error responses
      if (response.data.RESULT?.CODE !== 'INFO-000') {
        throw new Error(response.data.RESULT?.MESSAGE || 'API Error');
      }

      return this.parseArrivalData(response.data.realtimeArrivalList);
    } catch (error) {
      console.error('Failed to fetch arrival data:', error);
      throw error;
    }
  }

  private parseArrivalData(rawData: any[]): ArrivalData[] {
    if (!Array.isArray(rawData)) {
      return [];
    }

    return rawData.map(item => ({
      trainNo: item.btrainNo,
      direction: item.updnLine,
      arrivalMessage: item.arvlMsg2 || item.arvlMsg3,
      destination: item.bstatnNm,
      lineName: item.trainLineNm,
      updatedAt: new Date(item.recptnDt),
    }));
  }
}

export const seoulSubwayApi = new SeoulSubwayApi();

Error Handling Strategy

1. Retry Logic

async function fetchWithRetry<T>(
  fetchFn: () => Promise<T>,
  maxRetries = 3,
  delay = 1000
): Promise<T> {
  let lastError: Error;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetchFn();
    } catch (error) {
      lastError = error as Error;
      console.log(`Retry ${i + 1}/${maxRetries}`);

      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
      }
    }
  }

  throw lastError!;
}

// Usage
const data = await fetchWithRetry(() =>
  seoulSubwayApi.getRealtimeArrival('강남')
);

2. Timeout Handling

const fetchWithTimeout = async <T>(
  promise: Promise<T>,
  timeoutMs: number
): Promise<T> => {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('Request timeout')), timeoutMs);
  });

  return Promise.race([promise, timeout]);
};

3. Service Disruption Detection

const detectServiceDisruptions = (messages: string[]): boolean => {
  const keywords = [
    '운행중단',
    '전면중단',
    '운행불가',
    '장애',
    '고장',
    '사고',
    '탈선',
    '화재'
  ];

  return messages.some(msg =>
    keywords.some(keyword => msg.includes(keyword))
  );
};

Data Manager Integration

Multi-tier Fallback Pattern

class DataManager {
  private subscribers = new Map<string, Set<Function>>();

  async fetchTrainData(stationId: string): Promise<Train[]> {
    // Priority: Seoul API → Firebase → Cache
    try {
      // 1. Primary: Seoul API
      const apiData = await seoulSubwayApi.getRealtimeArrival(stationId);
      if (apiData.length > 0) {
        await this.updateCache(stationId, apiData);
        this.notifySubscribers(stationId, apiData);
        return apiData;
      }
    } catch (error) {
      console.log('Seoul API failed, trying Firebase');
    }

    try {
      // 2. Fallback: Firebase
      const fbData = await trainService.getTrainsByStation(stationId);
      if (fbData.length > 0) {
        return fbData;
      }
    } catch (error) {
      console.log('Firebase failed, using cache');
    }

    // 3. Last resort: Cache
    return await this.getCachedData(stationId);
  }

  subscribe(
    stationId: string,
    callback: (data: Train[]) => void
  ): () => void {
    if (!this.subscribers.has(stationId)) {
      this.subscribers.set(stationId, new Set());
    }

    this.subscribers.get(stationId)!.add(callback);

    // Return unsubscribe function
    return () => {
      this.subscribers.get(stationId)?.delete(callback);
    };
  }

  private notifySubscribers(stationId: string, data: Train[]): void {
    this.subscribers.get(stationId)?.forEach(callback => {
      callback(data);
    });
  }
}

export const dataManager = new DataManager();

Rate Limiting

Polling Strategy

class PollingManager {
  private intervals = new Map<string, NodeJS.Timeout>();
  private readonly POLL_INTERVAL = 30000; // 30 seconds

  startPolling(
    stationId: string,
    callback: (data: Train[]) => void
  ): void {
    // Clear existing interval
    this.stopPolling(stationId);

    // Initial fetch
    this.fetchAndNotify(stationId, callback);

    // Set up interval
    const interval = setInterval(() => {
      this.fetchAndNotify(stationId, callback);
    }, this.POLL_INTERVAL);

    this.intervals.set(stationId, interval);
  }

  stopPolling(stationId: string): void {
    const interval = this.intervals.get(stationId);
    if (interval) {
      clearInterval(interval);
      this.intervals.delete(stationId);
    }
  }

  private async fetchAndNotify(
    stationId: string,
    callback: (data: Train[]) => void
  ): Promise<void> {
    try {
      const data = await dataManager.fetchTrainData(stationId);
      callback(data);
    } catch (error) {
      console.error('Polling error:', error);
    }
  }
}

Best Practices

1. Environment Variables

// Always use env variables for API keys
const API_KEY = process.env.SEOUL_SUBWAY_API_KEY;

if (!API_KEY) {
  throw new Error('SEOUL_SUBWAY_API_KEY is not set');
}

2. Response Validation

const validateResponse = (data: unknown): boolean => {
  if (!data || typeof data !== 'object') {
    return false;
  }

  // Add specific validation logic
  return true;
};

3. Logging

const logApiCall = (endpoint: string, params: Record<string, any>) => {
  console.log('[API Call]', {
    endpoint,
    params,
    timestamp: new Date().toISOString(),
  });
};

Testing

Mock API Responses

// __tests__/seoulSubwayApi.test.ts
jest.mock('axios');

const mockResponse = {
  data: {
    RESULT: { CODE: 'INFO-000' },
    realtimeArrivalList: [
      {
        btrainNo: '1234',
        updnLine: '상행',
        arvlMsg2: '2분후[1번째전]',
        // ...
      }
    ]
  }
};

test('should fetch arrival data', async () => {
  (axios.get as jest.Mock).mockResolvedValue(mockResponse);

  const data = await seoulSubwayApi.getRealtimeArrival('강남');

  expect(data).toHaveLength(1);
  expect(data[0].trainNo).toBe('1234');
});

Important Notes

  • Seoul API has no official rate limits but use conservative polling (30s+)
  • Always implement fallback to Firebase/cache
  • Handle Korean encoding properly
  • Monitor API health and switch sources if needed
  • Log all API errors for debugging