본문 바로가기

IT와 과학/API

국토교통부 API (교통, 부동산 정보) 완전 정복

728x90
반응형

국토교통부 API (교통, 부동산 정보) 완전 정복

개요

국토교통부에서 제공하는 공공데이터 API는 교통, 부동산, 건축물 등 국토 전반에 관한 다양한 정보를 제공합니다. 부동산 앱, 교통 정보 서비스, 건축물 관리 시스템 등에서 핵심적으로 활용되는 데이터입니다.

주요 API 서비스 분류

🏠 부동산 관련 API

1. 아파트매매 실거래가 조회

  • 용도: 아파트 실제 거래 가격 정보
  • 제공 정보: 거래금액, 거래년월, 동, 아파트명, 전용면적 등
  • 갱신 주기: 월 1회 (매월 말일 기준)

2. 연립다세대 매매 실거래가 조회

  • 용도: 연립주택, 다세대주택 거래 정보
  • 제공 정보: 거래금액, 건축년도, 대지면적, 연면적 등
  • 갱신 주기: 월 1회

3. 단독주택 매매 실거래가 조회

  • 용도: 단독주택 거래 정보
  • 제공 정보: 거래금액, 대지면적, 건물면적, 건축년도 등
  • 갱신 주기: 월 1회

4. 오피스텔 매매 실거래가 조회

  • 용도: 오피스텔 거래 정보
  • 제공 정보: 거래금액, 전용면적, 건축년도, 층 등
  • 갱신 주기: 월 1회

5. 상업업무용 부동산 매매 실거래가 조회

  • 용도: 상가, 사무실 등 상업용 부동산 정보
  • 제공 정보: 거래금액, 건물용도, 대지면적, 건물면적 등
  • 갱신 주기: 월 1회

🚗 교통 관련 API

1. 전국 고속도로 교통량 정보

  • 용도: 고속도로 구간별 교통량
  • 제공 정보: 교통량, 통행속도, 교통상황 등
  • 갱신 주기: 5분마다

2. 국도 교통량 정보

  • 용도: 일반국도 교통량 데이터
  • 제공 정보: 차종별 교통량, 속도 정보
  • 갱신 주기: 5분마다

3. 고속도로 휴게소 정보

  • 용도: 휴게소 위치, 시설 정보
  • 제공 정보: 휴게소명, 위치, 편의시설, 연락처 등
  • 갱신 주기: 월 1회

4. 버스정류장 위치정보

  • 용도: 전국 버스정류장 좌표 및 정보
  • 제공 정보: 정류장명, 위경도 좌표, 관리기관 등
  • 갱신 주기: 분기 1회

🏢 건축물 관련 API

1. 건축물대장 정보

  • 용도: 건축물 기본 정보
  • 제공 정보: 건축면적, 연면적, 용도, 구조, 층수 등
  • 갱신 주기: 일 1회

2. 건축인허가 정보

  • 용도: 건축 허가 및 신고 정보
  • 제공 정보: 허가일자, 건축면적, 용도, 구조 등
  • 갱신 주기: 일 1회

API 사용 준비하기

1단계: 서비스 키 발급

  1. 공공데이터포털 접속
  2. 원하는 서비스 검색 (예: "아파트매매 실거래가")
  3. 활용신청 후 승인 대기
  4. 승인 후 서비스키 확인

2단계: 지역 코드 이해

국토교통부 API는 법정동 코드를 기반으로 합니다.

  • 시도코드: 2자리 (11: 서울, 26: 부산 등)
  • 시군구코드: 5자리 (11110: 서울 종로구 등)
  • 읍면동코드: 8자리 또는 10자리

핵심 API 상세 가이드

아파트 실거래가 조회 (가장 인기)

API 엔드포인트:

http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDev

필수 파라미터:

  • serviceKey: 발급받은 API 인증키
  • LAWD_CD: 지역코드 (5자리 법정동코드)
  • DEAL_YMD: 계약월 (YYYYMM 형식)

선택 파라미터:

  • numOfRows: 한 페이지 결과 수 (기본값: 10)
  • pageNo: 페이지 번호 (기본값: 1)

호출 예시:

const API_KEY = 'your_service_key_here';
const REGION_CODE = '11110'; // 서울 종로구
const DEAL_MONTH = '202312'; // 2023년 12월

const url = `http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDev?serviceKey=${API_KEY}&LAWD_CD=${REGION_CODE}&DEAL_YMD=${DEAL_MONTH}&numOfRows=1000`;

fetch(url)
  .then(response => response.text())
  .then(xmlText => {
    // XML 파싱 필요
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
    console.log('아파트 거래 데이터:', xmlDoc);
  })
  .catch(error => {
    console.error('API 호출 오류:', error);
  });

XML 응답 데이터 구조

국토교통부 API는 XML 형태로 데이터를 제공합니다:

<response>
  <header>
    <resultCode>00</resultCode>
    <resultMsg>NORMAL SERVICE.</resultMsg>
  </header>
  <body>
    <items>
      <item>
        <거래금액>82,000</거래금액>
        <건축년도>2008</건축년도>
        <년>2023</년>
        <동>1204</동>
        <등기일자>2023-12-15</등기일자>
        <매매일>15</매매일>
        <매매월>12</매매월>
        <매매년>2023</매매년>
        <아파트>래미안 첼리투스</아파트>
        <전용면적>84.93</전용면적>
        <지번>189</지번>
        <지역코드>11110</지역코드>
        <층>12</층>
      </item>
    </items>
    <numOfRows>1000</numOfRows>
    <pageNo>1</pageNo>
    <totalCount>158</totalCount>
  </body>
</response>

실용적인 XML 파싱 함수

class RealEstateAPI {
  constructor(serviceKey) {
    this.serviceKey = serviceKey;
    this.baseUrl = 'http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc';
  }

  // XML을 JavaScript 객체로 변환
  parseXMLResponse(xmlText) {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
    
    // 에러 체크
    const resultCode = xmlDoc.querySelector('resultCode')?.textContent;
    if (resultCode !== '00') {
      const resultMsg = xmlDoc.querySelector('resultMsg')?.textContent;
      throw new Error(`API 오류: ${resultMsg}`);
    }
    
    // 데이터 추출
    const items = xmlDoc.querySelectorAll('item');
    const result = [];
    
    items.forEach(item => {
      const data = {};
      const children = item.children;
      
      for (let i = 0; i < children.length; i++) {
        const child = children[i];
        data[child.nodeName] = child.textContent;
      }
      
      result.push(data);
    });
    
    return result;
  }

  // 아파트 실거래가 조회
  async getApartmentDeals(regionCode, dealMonth) {
    try {
      const url = `${this.baseUrl}/getRTMSDataSvcAptTradeDev?` +
                  `serviceKey=${this.serviceKey}&` +
                  `LAWD_CD=${regionCode}&` +
                  `DEAL_YMD=${dealMonth}&` +
                  `numOfRows=1000`;
      
      const response = await fetch(url);
      const xmlText = await response.text();
      const data = this.parseXMLResponse(xmlText);
      
      return this.formatApartmentData(data);
      
    } catch (error) {
      console.error('아파트 실거래가 조회 실패:', error);
      throw error;
    }
  }

  // 데이터를 사용자 친화적 형태로 변환
  formatApartmentData(rawData) {
    return rawData.map(item => ({
      apartmentName: item['아파트'],
      dealPrice: this.parsePrice(item['거래금액']),
      dealDate: `${item['년']}-${item['월'].padStart(2, '0')}-${item['일'].padStart(2, '0')}`,
      exclusiveArea: parseFloat(item['전용면적']),
      floor: parseInt(item['층']),
      buildYear: parseInt(item['건축년도']),
      dong: item['법정동'],
      jibun: item['지번'],
      regionCode: item['지역코드']
    }));
  }

  // 거래금액 파싱 (쉼표 제거, 숫자 변환)
  parsePrice(priceString) {
    const cleaned = priceString.replace(/,/g, '').trim();
    return parseInt(cleaned) * 10000; // 만원 단위를 원 단위로 변환
  }
}

// 사용법
const realEstateAPI = new RealEstateAPI('your_service_key');

realEstateAPI.getApartmentDeals('11110', '202312')
  .then(deals => {
    console.log('아파트 거래 정보:', deals);
    
    // 평균 거래가격 계산
    const avgPrice = deals.reduce((sum, deal) => sum + deal.dealPrice, 0) / deals.length;
    console.log('평균 거래가격:', avgPrice.toLocaleString() + '원');
  })
  .catch(error => {
    console.error('오류:', error);
  });

교통량 정보 조회

API 엔드포인트:

http://data.ex.co.kr/openapi/business/trafficAmountBySection

호출 예시:

class TrafficAPI {
  constructor(serviceKey) {
    this.serviceKey = serviceKey;
  }

  async getHighwayTraffic(routeCode, startDate, endDate) {
    try {
      const url = `http://data.ex.co.kr/openapi/business/trafficAmountBySection?` +
                  `key=${this.serviceKey}&` +
                  `type=json&` +
                  `routeCode=${routeCode}&` +
                  `startDate=${startDate}&` +
                  `endDate=${endDate}`;
      
      const response = await fetch(url);
      const data = await response.json();
      
      return this.formatTrafficData(data.list);
      
    } catch (error) {
      console.error('교통량 정보 조회 실패:', error);
      throw error;
    }
  }

  formatTrafficData(rawData) {
    return rawData.map(item => ({
      routeName: item.routeName,
      sectionName: item.sectionName,
      totalTraffic: parseInt(item.trafficAmountSum),
      avgTraffic: Math.round(parseInt(item.trafficAmountSum) / 24), // 시간당 평균
      date: item.dateCode
    }));
  }
}

실제 프로젝트 구현 예제

부동산 가격 분석 대시보드

class RealEstateDashboard {
  constructor(serviceKey) {
    this.api = new RealEstateAPI(serviceKey);
    this.regionCodes = {
      '서울 강남구': '11680',
      '서울 서초구': '11650',
      '서울 송파구': '11710',
      '부산 해운대구': '26440',
      '대구 수성구': '27200'
    };
  }

  // 여러 지역의 최근 3개월 거래 데이터 수집
  async collectMultiRegionData() {
    const results = {};
    const recentMonths = this.getRecentMonths(3);
    
    for (const [regionName, regionCode] of Object.entries(this.regionCodes)) {
      results[regionName] = [];
      
      for (const month of recentMonths) {
        try {
          const deals = await this.api.getApartmentDeals(regionCode, month);
          results[regionName] = results[regionName].concat(deals);
          
          // API 호출 제한 고려 (1초 대기)
          await this.delay(1000);
          
        } catch (error) {
          console.error(`${regionName} ${month} 데이터 수집 실패:`, error);
        }
      }
    }
    
    return results;
  }

  // 지역별 평균 가격 분석
  analyzeRegionalPrices(data) {
    const analysis = {};
    
    for (const [region, deals] of Object.entries(data)) {
      if (deals.length === 0) continue;
      
      const prices = deals.map(deal => deal.dealPrice);
      const areas = deals.map(deal => deal.exclusiveArea);
      
      analysis[region] = {
        totalDeals: deals.length,
        avgPrice: Math.round(prices.reduce((a, b) => a + b, 0) / prices.length),
        medianPrice: this.getMedian(prices),
        avgPricePerSquare: Math.round(
          prices.reduce((sum, price, idx) => sum + (price / areas[idx]), 0) / prices.length
        ),
        priceRange: {
          min: Math.min(...prices),
          max: Math.max(...prices)
        },
        mostExpensiveApt: deals.find(deal => deal.dealPrice === Math.max(...prices))?.apartmentName
      };
    }
    
    return analysis;
  }

  // 시계열 분석 (월별 가격 추이)
  analyzePriceTrends(data) {
    const trends = {};
    
    for (const [region, deals] of Object.entries(data)) {
      const monthlyData = {};
      
      deals.forEach(deal => {
        const month = deal.dealDate.substring(0, 7); // YYYY-MM
        if (!monthlyData[month]) {
          monthlyData[month] = [];
        }
        monthlyData[month].push(deal.dealPrice);
      });
      
      trends[region] = Object.entries(monthlyData).map(([month, prices]) => ({
        month,
        avgPrice: Math.round(prices.reduce((a, b) => a + b, 0) / prices.length),
        dealCount: prices.length
      })).sort((a, b) => a.month.localeCompare(b.month));
    }
    
    return trends;
  }

  // 유틸리티 함수들
  getRecentMonths(count) {
    const months = [];
    const now = new Date();
    
    for (let i = 0; i < count; i++) {
      const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
      const yearMonth = date.getFullYear().toString() + 
                       (date.getMonth() + 1).toString().padStart(2, '0');
      months.push(yearMonth);
    }
    
    return months;
  }

  getMedian(numbers) {
    const sorted = numbers.slice().sort((a, b) => a - b);
    const mid = Math.floor(sorted.length / 2);
    return sorted.length % 2 === 0 ? 
           (sorted[mid - 1] + sorted[mid]) / 2 : 
           sorted[mid];
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 대시보드 실행
  async generateReport() {
    console.log('🏠 부동산 시장 분석 리포트 생성 중...\n');
    
    const data = await this.collectMultiRegionData();
    const priceAnalysis = this.analyzeRegionalPrices(data);
    const trends = this.analyzePriceTrends(data);
    
    // 리포트 출력
    console.log('📊 지역별 평균 가격 분석');
    console.log('==========================');
    
    for (const [region, analysis] of Object.entries(priceAnalysis)) {
      console.log(`\n🏢 ${region}`);
      console.log(`   총 거래 건수: ${analysis.totalDeals}건`);
      console.log(`   평균 가격: ${analysis.avgPrice.toLocaleString()}원`);
      console.log(`   중간값 가격: ${analysis.medianPrice.toLocaleString()}원`);
      console.log(`   평당 평균 가격: ${analysis.avgPricePerSquare.toLocaleString()}원/㎡`);
      console.log(`   가격 범위: ${analysis.priceRange.min.toLocaleString()}원 ~ ${analysis.priceRange.max.toLocaleString()}원`);
      console.log(`   최고가 아파트: ${analysis.mostExpensiveApt}`);
    }
    
    console.log('\n📈 월별 가격 추이');
    console.log('================');
    
    for (const [region, trend] of Object.entries(trends)) {
      console.log(`\n📍 ${region}`);
      trend.forEach(monthData => {
        console.log(`   ${monthData.month}: ${monthData.avgPrice.toLocaleString()}원 (${monthData.dealCount}건)`);
      });
    }
    
    return { priceAnalysis, trends };
  }
}

// 사용법
const dashboard = new RealEstateDashboard('your_service_key');
dashboard.generateReport()
  .then(report => {
    console.log('\n✅ 리포트 생성 완료!');
  })
  .catch(error => {
    console.error('리포트 생성 실패:', error);
  });

지역 코드 조회 및 관리

법정동 코드 검색 함수

class RegionCodeManager {
  constructor() {
    // 주요 지역 코드 (실제로는 외부 파일에서 로드)
    this.codes = {
      '서울특별시': {
        code: '11',
        districts: {
          '종로구': '11110',
          '중구': '11140',
          '용산구': '11170',
          '성동구': '11200',
          '광진구': '11215',
          '동대문구': '11230',
          '중랑구': '11260',
          '성북구': '11290',
          '강북구': '11305',
          '도봉구': '11320',
          '노원구': '11350',
          '은평구': '11380',
          '서대문구': '11410',
          '마포구': '11440',
          '양천구': '11470',
          '강서구': '11500',
          '구로구': '11530',
          '금천구': '11545',
          '영등포구': '11560',
          '동작구': '11590',
          '관악구': '11620',
          '서초구': '11650',
          '강남구': '11680',
          '송파구': '11710',
          '강동구': '11740'
        }
      },
      '부산광역시': {
        code: '26',
        districts: {
          '중구': '26110',
          '서구': '26140',
          '동구': '26170',
          '영도구': '26200',
          '부산진구': '26230',
          '동래구': '26260',
          '남구': '26290',
          '북구': '26320',
          '해운대구': '26440',
          '사하구': '26470',
          '금정구': '26500',
          '강서구': '26530',
          '연제구': '26560',
          '수영구': '26590',
          '사상구': '26620',
          '기장군': '26710'
        }
      }
      // 다른 시도도 추가...
    };
  }

  // 지역명으로 코드 검색
  findCode(city, district = null) {
    const cityData = this.codes[city];
    if (!cityData) {
      throw new Error(`${city}를 찾을 수 없습니다.`);
    }
    
    if (district) {
      const districtCode = cityData.districts[district];
      if (!districtCode) {
        throw new Error(`${city} ${district}를 찾을 수 없습니다.`);
      }
      return districtCode;
    }
    
    return cityData.code;
  }

  // 자동완성용 지역 목록
  getRegionSuggestions(query) {
    const suggestions = [];
    
    for (const [city, cityData] of Object.entries(this.codes)) {
      if (city.includes(query)) {
        suggestions.push(city);
      }
      
      for (const district of Object.keys(cityData.districts)) {
        if (district.includes(query)) {
          suggestions.push(`${city} ${district}`);
        }
      }
    }
    
    return suggestions.slice(0, 10); // 최대 10개만 반환
  }
}

자주 발생하는 오류와 해결책

1. SERVICE_ACCESS_DENIED_ERROR

원인: 잘못된 서비스키 또는 미승인 해결:

  • 공공데이터포털에서 해당 서비스 승인 상태 확인
  • 서비스키 인코딩 문제 체크 (encodeURIComponent() 사용)

2. NO_DATA_ERROR

원인: 해당 조건에 맞는 데이터가 없음 해결:

  • 지역코드가 정확한지 확인
  • 조회 월이 유효한지 확인 (너무 최근 월은 데이터 없을 수 있음)

3. XML_PARSE_ERROR

원인: XML 응답 파싱 실패 해결:

  • 응답 텍스트가 유효한 XML인지 확인
  • 브라우저에서는 CORS 문제로 프록시 서버 필요할 수 있음

4. CORS 오류 (브라우저에서)

원인: 브라우저의 동일 출처 정책 해결:

// 프록시 서버 사용 또는 서버사이드에서 호출
const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
const apiUrl = 'http://openapi.molit.go.kr/...';
fetch(proxyUrl + apiUrl)

고급 활용 팁

1. 데이터 캐싱과 배치 처리

class DataCache {
  constructor() {
    this.cache = new Map();
    this.cacheExpiry = new Map();
  }
  
  set(key, data, ttl = 3600000) { // 1시간 기본 TTL
    this.cache.set(key, data);
    this.cacheExpiry.set(key, Date.now() + ttl);
  }
  
  get(key) {
    if (this.cacheExpiry.get(key) > Date.now()) {
      return this.cache.get(key);
    }
    
    this.cache.delete(key);
    this.cacheExpiry.delete(key);
    return null;
  }
}

class BatchProcessor {
  constructor(api) {
    this.api = api;
    this.queue = [];
    this.isProcessing = false;
    this.delay = 1000; // API 호출 간 1초 대기
  }
  
  addTask(task) {
    this.queue.push(task);
    this.processQueue();
  }
  
  async processQueue() {
    if (this.isProcessing || this.queue.length === 0) return;
    
    this.isProcessing = true;
    
    while (this.queue.length > 0) {
      const task = this.queue.shift();
      try {
        await task();
        await new Promise(resolve => setTimeout(resolve, this.delay));
      } catch (error) {
        console.error('배치 처리 오류:', error);
      }
    }
    
    this.isProcessing = false;
  }
}

2. 데이터 시각화 준비

class ChartDataProcessor {
  // 가격 추이 차트용 데이터 변환
  static formatPriceTrendChart(trendsData) {
    return Object.entries(trendsData).map(([region, trend]) => ({
      id: region,
      data: trend.map(item => ({
        x: item.month,
        y: item.avgPrice
      }))
    }));
  }
  
  // 지역별 평균가 비교 차트용 데이터 변환
  static formatRegionComparisonChart(analysisData) {
    return Object.entries(analysisData).map(([region, data]) => ({
      region,
      avgPrice: data.avgPrice,
      dealCount: data.totalDeals,
      pricePerSquare: data.avgPricePerSquare
    }));
  }
  
  // 가격대별 분포 히스토그램용 데이터
  static formatPriceDistribution(dealsData, binCount = 10) {
    const prices = dealsData.map(deal => deal.dealPrice);
    const min = Math.min(...prices);
    const max = Math.max(...prices);
    const binSize = (max - min) / binCount;
    
    const bins = Array.from({ length: binCount }, (_, i) => ({
      range: `${Math.round((min + i * binSize) / 10000)}만원~${Math.round((min + (i + 1) * binSize) / 10000)}만원`,
      count: 0
    }));
    
    prices.forEach(price => {
      const binIndex = Math.min(Math.floor((price - min) / binSize), binCount - 1);
      bins[binIndex].count++;
    });
    
    return bins;
  }
}

3. 알림 시스템

class PriceAlert {
  constructor(api) {
    this.api = api;
    this.alerts = [];
  }
  
  addPriceAlert(regionCode, threshold, callback) {
    this.alerts.push({
      regionCode,
      threshold,
      callback,
      lastCheck: null
    });
  }
  
  async checkAlerts() {
    const currentMonth = new Date().toISOString().substring(0, 7).replace('-', '');
    
    for (const alert of this.alerts) {
      try {
        const deals = await this.api.getApartmentDeals(alert.regionCode, currentMonth);
        const avgPrice = deals.reduce((sum, deal) => sum + deal.dealPrice, 0) / deals.length;
        
        if (avgPrice > alert.threshold && alert.lastCheck !== currentMonth) {
          alert.callback(avgPrice, deals);
          alert.lastCheck = currentMonth;
        }
        
      } catch (error) {
        console.error('알림 체크 오류:', error);
      }
    }
  }
}

결론

국토교통부 API는 부동산과 교통 분야에서 핵심적인 데이터를 제공하는 중요한 자원입니다. XML 파싱과 지역코드 관리만 잘 이해하면 다양한 부동산 서비스와 교통 정보 시스템을 구축할 수 있습니다.

특히 아파트 실거래가 데이터는 부동산 시장 분석, 투자 의사결정 지원, 지역별 시세 비교 등에 매우 유용하며, 교통량 데이터는 물류 최적화, 교통 정보 서비스, 도시 계획 등에 활용할 수 있습니다.

 

참고 자료

728x90
반응형