소개
14기 활동 내역
지피터스 14시에서 GPTs 스터디를 참여하면서 KB 부동산 지수 데이터로 GPTs를 만들었습니다.
예를 들어 “2006년 1월부터 2025년 2월까지 서울특별시, 부산광역시, 대구광역시, 경기도, 세종특별자치시 분석하고 그래프 그려줘.”라고 요청하면, 분석해서 동적시각화 차트를 그려주고, 변곡점이 있었던 시점에 뉴스를 검색하여 변곡점의 원인을 알려주는 GPTs로, 시장 사이클 속에서 변동 원인을 빠르고 효율적으로 탐색해볼 수 있도록 하기 위해 만들어보았습니다.
GPTs에서 아래와 같이 질문/답변을 받을 수 있습니다.
Q) 2009년부터 2012년 사이에 부산광역시에서 매매가격이 급격하게 상승한 이유는 뭐야?
A) 물론입니다. 2009년부터 2012년 사이 부산광역시의 매매가격 급등 현상은 다음과 같은 구체적인 지역 특성과 시기별 시장 환경에서 비롯되었습니다:
🏙️ 1. 해운대·수영구 중심의 고급 주거지 개발
센텀시티, 마린시티, 해운대 두산위브더제니스, 엘시티 계획 발표 등 해운대 일대 고급 주거단지 개발 붐이 일었습니다.
이로 인해 해운대를 중심으로 "부산의 강남화" 현상이 발생, 투자자와 실수요자가 몰렸습니다.
고급 아파트 분양권과 기존 재건축 단지에 대한 프리미엄이 빠르게 붙으며 가격 급등을 주도했습니다.
📉 2. 2008년 글로벌 금융위기 후유증 → 반사적 반등
2008년 글로벌 금융위기 여파로 전국 부동산 시장이 침체됐지만, 2009년 하반기부터 부산은 빠르게 회복세를 탔습니다.
정부의 경기 부양책, 금리 인하, 부동산 세제 완화 등이 맞물려 침체되었던 매물이 거래로 전환, 가격 상승으로 이어졌습니다.
이 시기 서울 대비 상대적으로 저평가된 부산이 주목을 받으며 투자 수요가 급증했습니다.
🚧 3. 재개발·재건축 사업 본격화
남구, 동래구, 동구 등 구도심 지역에서 재개발 사업이 활발히 추진되었습니다.
정비사업이 본격화되면서 개발 기대감이 높아졌고, 투자 수요 증가로 집값 상승 압력이 커졌습니다.
특히 ‘국제업무지구’, 북항 재개발, 송도 해양레포츠벨트 등 개발 호재들이 맞물려 기대 심리가 상승했습니다.
📉 4. 신규 공급 부족으 로 인한 수급 불균형
2009년~2011년 사이 신규 아파트 입주 물량이 역대 최저 수준에 머물렀습니다.
⤷ 당시 한 달 평균 공급세대는 약 3,700여 세대에 불과반면, 거래량은 급증했고, 수요 > 공급 구조가 장기간 지속되며 매매가격이 상승했습니다.
💸 5. 저금리·완화된 대출 정책
한국은행 기준금리는 2009년 초 2.0%까지 떨어졌으며, 대출 규제도 상대적으로 느슨했습니다.
주택담보대출 증가 + 유동성 풍부 → 부동산 자산 선호 강화
이로 인해 실수요와 투자수요가 동시에 증가하며 시장 과열로 이어졌습니다.
📰 관련 기사 링크:
필요하시면 해운대나 센텀시티 등 특정 지역의 상세 시세 흐름도 추가 분석해드릴 수 있어요.
15기 스터디에 앞서..
부동산 시장을 분석하기 위해 실거래가 데이터 와 KB 데이터 등을 태블로를 이용하여 분석하고 대시보드를 만들어서 활용하고 있습니다.
아실/호갱노노 같은 프롭테크 앱이 있으나 단순히 원하는 조건을 선택해서 가격을 보는 용도일 뿐 직접 분석 해볼 수는 없는데, 데이터 가지고 노는게 약간의 취미(?)라 직접 데이터를 분석해보려고 하다보니 대시보드까지 만들어버렸네요.
기본적인 통계 분석이나 동적 시각화 분석 같은 것들도 현재의 프롭테크 앱에서는 해보기는 어렵습니다. 그렇다보니 여러 사례들을 보면 아직도 여전히 직접 한땀한땀 손으로 작업하는 분들도 많은게 현실입니다.
일부 유료 분석 프로그램이 있긴 하나, 분석에 대한 자유도는 제한되고 디지털 월세도 발생할 수 밖에 없는 아쉬움도 있습니다.
Tableau vs Looker Studio
많은 분들이 Looker Studio를 쓰고 계시는데, 제가 Tableau로 분석한 이유는 아래 GPT가 답변해준 내용을 참고하시면 좋을 것 같습니다.
아래는 Tableau와 Looker Studio의 주요 장단점을 비교한 내용입니다.
아래는 Tableau와 Looker Studio의 주요 장단점을 비교한 내용입니다.
Tableau
장점:
풍부한 시각화 기능: 다양한 차트와 커스터마이징 옵션을 제공하여 복잡한 데이터 분석 및 고급 시각화를 지원합니다.
대규모 데이터 처리: 대용량 데이터셋과 복잡한 계산을 효율적으로 처리할 수 있습니다.
다양한 데이터 소스 연동: 여러 종류의 데이터베이스, 클라우드 서비스 및 파일 형식을 지원하여 통합 분석이 용이합니다.
고급 분석 기능: 예측, 클러스터링, 계산 필드 등 고급 분석 도구를 제공하여 심도 있는 인사이트 도출이 가능합니다.
단점:
비용: 라이선스 비용이 상대적으로 높아 중소기업이나 개인 사용자에게는 부담이 될 수 있습니다.
학습 곡선: 기능이 많고 복잡하여 처음 사용하는 사용자에게는 학습 및 숙련에 시간이 필요합니다.
리소스 요구량: 고성능 분석을 위해서는 하드웨어 자원 및 관리 인력이 필요할 수 있습니다.
Looker Studio (이전 Google Data Studio)
장점:
무료 사용: Google 계정만 있으면 무료로 사용할 수 있어 비용 부담이 적습니다.
Google 서비스와의 통합: Google Analytics, Google Ads, BigQuery 등 Google 생태계와의 원활한 연동이 가능합니다.
사용자 친화적 인터페이스: 직관적인 드래그 앤 드롭 방식의 UI로 빠르게 리포트를 작성할 수 있어 초보자에게 적합합니다.
실시간 업데이트: 데이터 소스를 연결하면 실시간 데이터 업데이트가 가능하여 최신 정보를 반영할 수 있습니다.
단점:
기능 한계: Tableau에 비해 고급 시각화나 복잡한 데이터 분석 기능은 제한적입니다.
커스터마이징 제약: 디자인 및 레이아웃 커스터마이징 옵션이 상대적으로 적어 유연성이 떨어질 수 있습니다.
데이터 소스 제한: 기본적으로 Google 서비스 중심의 데이터 연결에 최적화되어 있어 다른 데이터 소스와의 연동은 추가 설정이 필요할 수 있습니다.
결론
Tableau는 고급 분석과 복잡한 데이터 시각화가 필요하거나, 다양한 데이터 소스를 한 번에 다루고자 하는 기업에 적합합니다.
Looker Studio는 비용 효율적이고 Google 생태계를 중심으로 간단하고 빠르게 리포트를 작성해야 하는 사용자에게 유리합니다.
📊 Tableau vs Looker Studio 비교표
네, Tableau와 Looker Studio 간의 차트 기능은 꽤 큰 차이가 있습니다.
기능의 폭, 시각화의 자유도, 인터랙션, 커스터마이징 수준에서 뚜렷한 차이가 나요.
아래에 차트 관련 주요 차이점을 정리해볼게요.
🔍 Tableau vs Looker Studio 차트 비교
항목
Tableau
Looker Studio
지원 차트 종류
30종+ (기본 제공 + 고급 분석형 + 커스텀)
15~20종 (기본형 중심, 제한 적)
시각화 자유도
매우 높음 (차트 레이아웃, 색상, 마크, 툴팁, 서식 완전 제어 가능)
제한적 (일부 스타일 및 색상 변경은 가능하나 유연성 떨어짐)
고급 분석 차트
가능 (박스플롯, 파레토, 이중축, Gantt 등 포함)
일부는 불가능 (이중축, 파레토, Gantt 등 지원 안 함)
지도 시각화
고급 지도 시각화 가능 (심볼맵, 밀도맵 등)
기본 세계지도만 제공, 커스터마이징 제한
차트 인터랙션
매우 강력 (드릴다운, 하이라이트, 매개변수 기반 뷰 변화 등)
필터링, 드릴다운 가능하나 기능 제한적
커스텀 차트 추가
Tableau Extensions나 외부 도구와 연동해 가능
Community Visualization을 통해 일부 가능 (제한적)
계층적 표현
가능 (계층 필드 설정, 트리 구조 표현 등)
불가능 또는 매우 제한적
조건부 포맷
다양하고 유연하게 설정 가능
일부 차트만 제한적으로 제공
✅ 요약하자면:
상황
추천 도구
복잡 한 데이터 구조 & 정교한 분석, 시각화가 필요할 때
✅ Tableau
간단한 보고서나 마케팅/GA 리포트를 빠르게 만들고 싶을 때
✅ Looker Studio
둘 다 "시각화 도구"이지만 Tableau는 데이터 분석 전문가용, Looker Studio는 대시보드 초급 사용자 또는 마케터용 도구에 더 가깝다고 볼 수 있어요.
항목
Tableau
Looker Studio (구 Google Data Studio)
라이선스 / 가격
유료 (약 $70/월, Public은 무료)
무료 (Google 계정만 있으면 사용 가능)
목표 사용자
데이터 분석가, 기업 BI팀
마케터, 초급 사용자, Google 애널리틱스 사용자
사용 난이도
중~상 (학습 필요)
하 (직관적 UI)
시각화 종류
매우 다양 (30종 이상 + 커스터마이징 자유)
기본 차트 위주 (약 15종 + 제한적 커스터마이징)
고급 분석 기능
예측, 군집화, 계산필드, LOD 표현식 등 가능
제한적 (간단한 계산식 및 필터)
데이터 처리 성능
대용량 데이터 처리에 강함
중소규모 데이터에 적합
데이터 소스 연동
광범위 (RDB, 클라우드, API 등)
Google 생태계 최적화 (BigQuery, GA, Sheets 등)
인터랙션 기능
풍부 (필터, 드릴다운, 하이라이트 등)
기본 필터 및 드릴다운만 제공
지도 시각화
고급 지도 분석 가능 (심볼맵, 밀도맵 등)
기본 지도만 제공, 커스터마이징 제한적
협업 및 공유
Tableau Server/Cloud 기반 협업 (보안성 높음)
Google Drive 기반 실시간 공유 (간편)
커스터마이징
색상, 레이아웃, 마크, 툴팁 등 자유도 매우 높음
스타일 변경 가능하지만 제한적
모바일 지원
강력한 반응형 레이아웃 지원
제한적, 모바일에서 보기엔 불편할 수 있음
확장성 (스크립트 등)
Python, R 연동 가능 (고급 분석 가능)
외부 연동 어려움, 단순 리포팅 중심
✅ 요약 정리
사용 목적
적합한 도구
📈 고급 분석 + 정교한 대시보드
Tableau
📊 간단한 리포트 + Google 연동
Looker Studio
Tableau는 전문 분석 도구라서 다양한 동적 시각화를 포함한 다양한 시각화 분석이 가능한데, 무료로도 사용할 수 있는 방법이 있습니다.
Tableau Public은 Online 기반으로 개인당 10G의 공간을 제공하고 있고, 다양한 커넥터로 연계하여 Local PC로 분석해보고자 한다면 Tableau Desktop 버전을 학생 인증 받으면 무료 사용이 가능합니다.
또한 생성한 대시보드는 Tableau Public을 통해 온라인 공유가 가능합니다.
진행 방법
15기 Apps Script 스터디 계획
사실 Tableau Dashboard는 이미 거의 완성을 한 상태인데, 이번 스터디를 하면서 연계할 데이터를 Apps Script를 통해 자동으로 수집하는 것을 목표로 하고 있습니다.
그중 첫번째는 공공데이터포털 API에서 데이터를 Apps Script로 가져오는 것, 두번째는 네이버부동산 매물을 Apps Script로 크롤링 하는 것입니다.
1단계 : 공공데이터 API 데이터 수집 (Apps Script 활용)
클로트에 어떻게 해야되는지 물어보고 얻은 답변입니다.
1. 공공데이터포털 API 키 발급받기
공공데이터포털(https://www.data.go.kr)에 회원가입 후 로그인합니다.
'아파트 실거래가 API' 서비스를 검색하고 활용신청을 합니다.
승인 후 발급받은 API 키를 확인합니다.
2. 구글 스프레드시트 생성 및 Apps Script 열기
구글 드라이브에서 새 스프레드시트를 생성합니다.
상단 메뉴에서 [확장 프로그램] > [Apps Script]를 클릭합니다.
3. Apps Script 코드 작성하기
4. 코드 실행 방법
위 코드에서
serviceKey
변수에 발급받은 API 키를 입력합니다.LAWD_CD
와DEAL_YMD
변수를 원하는 지역코드와 조회 년월로 변경합니다.지역코드는 법정동코드 앞 5자리입니다 (예: 서울시 종로구는 11110)
스크립트 편집기에서 [파일] > [저장]을 클릭합니다.
onOpen
함수를 실행하기 위해 스프레드시트로 돌아가 새로고침합니다.스프레드시트 상단에 '아파트 실거래가' 메뉴가 생성됩니 다.
이 메뉴에서 '데이터 가져오기'를 클릭하면 API 요청이 수행되고 결과가 시트에 표시됩니다.
5. 주의사항
공공데이터포털 API는 일일 트래픽 제한이 있으므로 과도한 요청은 피해야 합니다.
처음 실행 시 권한 승인 창이 뜨면 '승인'을 클릭해야 합니다.
지역코드는 행정표준코드관리시스템(https://www.code.go.kr)에서 확인할 수 있습니다.
그리고 감사하게도 스터디장이신 스마트대디님께서 웹앱 가이드를 제공해주셨습니다.
아래 가이드 참고해서 진행 예정입니다.
// code.gs
/**
* 국토교통부 아파트 매매 실거래가 조회 웹앱
*
* 작성자: Claude
* 작성일: 2025-03-28
*
* 이 스크립트는 국토교통부에서 제공하는 아파트 매매 실거래가 정보 오픈 API를 활용하여
* 지역별, 계약년월별 실거래 정보를 조회하는 Google Apps Script 웹 애플리케이션입니다.
*/
// API 기본 설정
const API_BASE_URL = 'https://apis.data.go.kr/1613000/RTMSDataSvcAptTrade/getRTMSDataSvcAptTrade';
/**
* 웹 애플리케이션의 진입점
* 사용자에게 HTML 페이지를 제공합니다.
*/
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('국토교통부 아파트 실거래가 조회')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* HTML 파일에 포함할 JavaScript와 CSS를 가져옵니다.
*/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/**
* API 키 저장 함수
*/
function saveApiKey(apiKey) {
PropertiesService.getUserProperties().setProperty('API_KEY', apiKey);
return true;
}
/**
* API 키 조회 함수
*/
function getApiKey() {
return PropertiesService.getUserProperties().getProperty('API_KEY') || '';
}
/**
* API 키 삭제 함수
*/
function deleteApiKey() {
PropertiesService.getUserProperties().deleteProperty('API_KEY');
return true;
}
/**
* API 키 존재 여부 확인 함수
*/
function hasApiKey() {
const apiKey = getApiKey();
return apiKey !== null && apiKey !== '';
}
/**
* 지역 코드 목록을 반환합니다.
* 주요 지역만 포함하고 있습니다.
*/
function getRegionCodes() {
return [
// 서울특별시
{ code: '11110', name: '서울특별시 종로구' },
{ code: '11140', name: '서울특별시 중구' },
{ code: '11170', name: '서울특별시 용산구' },
{ code: '11200', name: '서울특별시 성동구' },
{ code: '11215', name: '서울특별시 광진구' },
{ code: '11230', name: '서울특별시 동대문구' },
{ code: '11260', name: '서울특별시 중랑구' },
{ code: '11290', name: '서울특별시 성북구' },
{ code: '11305', name: '서울특별시 강북구' },
{ code: '11320', name: '서울특별시 도봉구' },
{ code: '11350', name: '서울특별시 노원구' },
{ code: '11380', name: '서울특별시 은평구' },
{ code: '11410', name: '서울특별시 서대문구' },
{ code: '11440', name: '서울특별시 마포구' },
{ code: '11470', name: '서울특별시 양천구' },
{ code: '11500', name: '서울특별시 강서구' },
{ code: '11530', name: '서울특별시 구로구' },
{ code: '11545', name: '서울특별시 금천구' },
{ code: '11560', name: '서울특별시 영등포구' },
{ code: '11590', name: '서울특별시 동작구' },
{ code: '11620', name: '서울특별시 관악구' },
{ code: '11650', name: '서울특별시 서초구' },
{ code: '11680', name: '서울특별시 강남구' },
{ code: '11710', name: '서울특별시 송파구' },
{ code: '11740', name: '서울특별시 강동구' },
// 부산광역시
{ code: '26110', name: '부산광역시 중구' },
{ code: '26140', name: '부산광역시 서구' },
{ code: '26170', name: '부산광역시 동구' },
{ code: '26200', name: '부산광역시 영도구' },
{ code: '26230', name: '부산광역시 부산진구' },
{ code: '26260', name: '부산광역시 동래구' },
{ code: '26290', name: '부산광역시 남구' },
{ code: '26320', name: '부산광역시 북구' },
{ code: '26350', name: '부산광역시 해운대구' },
{ code: '26380', name: '부산광역시 사하구' },
{ code: '26410', name: '부산광역시 금정구' },
{ code: '26440', name: '부산광역시 강서구' },
{ code: '26470', name: '부산광역시 연제구' },
{ code: '26500', name: '부산광역시 수영구' },
{ code: '26530', name: '부산광역시 사상구' },
{ code: '26710', name: '부산광역시 기장군' },
// 기타 주요 지역
{ code: '27110', name: '대구광역시 중구' },
{ code: '28110', name: '인천광역시 중구' },
{ code: '29110', name: '광주광역시 동구' },
{ code: '30110', name: '대전광역시 동구' },
{ code: '31110', name: '울산광역시 중구' },
{ code: '36110', name: '세종특별자치시' },
{ code: '41110', name: '경기도 수원시 장안구' },
{ code: '41111', name: '경기도 수원시 권선구' },
{ code: '41113', name: '경기도 수원시 팔달구' },
{ code: '41115', name: '경기도 수원시 영통구' },
{ code: '41130', name: '경기도 성남시 수정구' },
{ code: '41131', name: '경기도 성남시 중원구' },
{ code: '41133', name: '경기도 성남시 분당구' },
{ code: '41280', name: '경기도 고양시 덕양구' },
{ code: '41281', name: '경기도 고양시 일산동구' },
{ code: '41285', name: '경기도 고양시 일산서구' },
{ code: '41360', name: '경기도 남양주시' },
{ code: '41461', name: '경기도 용인시 기흥구' },
{ code: '41463', name: '경기도 용인시 수지구' },
{ code: '41590', name: '경기도 화성시' }
];
}
/**
* 아파트 매매 실거래가 정보를 조회합니다.
*/
function getAptTradeInfo(regionCode, yearMonth) {
try {
// API 키 확인
const apiKey = getApiKey();
if (!apiKey) {
return {
success: false,
message: 'API 키가 설정되지 않았습니다. 먼저 API 키를 설정해주세요.',
data: null
};
}
// API 요청 URL 구성
const url = `${API_BASE_URL}?serviceKey=${encodeURIComponent(apiKey)}&LAWD_CD=${regionCode}&DEAL_YMD=${yearMonth}&numOfRows=1000&pageNo=1`;
// API 요청 수행
const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
// 응답 확인
const responseCode = response.getResponseCode();
if (responseCode !== 200) {
return {
success: false,
message: `API 호출 실패: HTTP 상태 코드 ${responseCode}`,
data: null
};
}
// XML 응답 파싱
const xmlContent = response.getContentText();
const document = XmlService.parse(xmlContent);
const root = document.getRootElement();
// 응답 결과 코드 확인
const header = root.getChild('header');
if (header) {
const resultCode = header.getChildText('resultCode');
const resultMsg = header.getChildText('resultMsg');
if (resultCode !== '00' && resultCode !== '000') {
return {
success: false,
message: `API 오류: ${resultMsg} (코드: ${resultCode})`,
data: null
};
}
}
// 데이터 추출
const items = [];
const body = root.getChild('body');
if (body) {
const itemsElement = body.getChild('items');
if (itemsElement) {
const itemElements = itemsElement.getChildren('item');
for (const item of itemElements) {
const aptData = {
거래금액: item.getChildText('거래금액'),
건축년도: item.getChildText('건축년도'),
년: item.getChildText('년'),
월: item.getChildText('월'),
일: item.getChildText('일'),
아파트: item.getChildText('아파트'),
전용면적: item.getChildText('전용면적'),
지번: item.getChildText('지번'),
지역코드: item.getChildText('지역코드'),
층: item.getChildText('층'),
법정동: item.getChildText('법정동'),
중개사소재지: item.getChildText('중개사소재지')
};
items.push(aptData);
}
}
}
return {
success: true,
message: '데이터를 성공적으로 조회했습니다.',
data: items
};
} catch (error) {
console.log("API 호출 중 오류:", error.toString());
return {
success: false,
message: `오류 발생: ${error.message}`,
data: null
};
}
}
/**
* 거래금액 등의 값을 정리하는 함수
*/
function cleanupValue(value) {
if (!value) return value;
// 쉼표와 공백 제거
return value.replace(/,|\s/g, '');
}
/**
* XML 요소에서 특정 이름을 가진 모든 하위 요소를 찾는 헬퍼 함수
*/
function getElements(element, name) {
const result = [];
function traverse(elm) {
// 현재 요소 이름 확인
if (elm.getName && elm.getName() === name) {
result.push(elm);
}
// 자식 요소 확인
const children = elm.getChildren ? elm.getChildren() : [];
for (let i = 0; i < children.length; i++) {
traverse(children[i]);
}
}
traverse(element);
return result;
}
/**
* XML 요소에서 특정 이름을 가진 하위 요소의 텍스트 내용을 가져오는 헬퍼 함수
*/
function getElementText(element, name) {
try {
// 직접 자식요소 확인
const children = element.getChildren();
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.getName() === name) {
return child.getText() || '';
}
}
// 네임스페이스 고려
const namespaces = element.getNamespaces();
for (let j = 0; j < namespaces.length; j++) {
const ns = namespaces[j];
const child = element.getChild(name, ns);
if (child) {
return child.getText() || '';
}
}
return '';
} catch (e) {
console.log(`${name} 요소 텍스트 가져오기 오류:`, e.toString());
return '';
}
}
/**
* 테스트 API 호출
* 개발 및 디버깅 용도
*/
function testApiCall() {
const regionCode = '11680'; // 서울특별시 강남구
const yearMonth = '202401'; // 2024년 1월
const result = getAptTradeInfo(regionCode, yearMonth);
console.log(result);
return result;
}
// index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<title>국토교통부 아파트 실거래가 조회</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: #f8f9fa;
padding: 20px;
}
.card {
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.api-key-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.api-key-modal-content {
position: relative;
background-color: #fff;
margin: 5% auto;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 600px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.close-button {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
#resultCount {
font-weight: bold;
color: #0d6efd;
}
.no-data {
text-align: center;
padding: 50px;
color: #6c757d;
}
</style>
</head>
<body>
<div class="container">
<!-- 헤더 -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h1 class="card-title text-center">국토교통부 아파트 실거래가 조회</h1>
<p class="card-text text-center text-muted">
국토교통부 실거래가 정보 오픈 API를 활용한 아파트 매매 실거래가 조회 서비스
</p>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openApiKeyModal()">
<i class="bi bi-key"></i> API 키 설정
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 검색 폼 -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">검색 조건</h5>
</div>
<div class="card-body">
<form id="searchForm">
<div class="row g-3">
<!-- 지역 선택 -->
<div class="col-md-6">
<label for="regionCode" class="form-label">지역 선택</label>
<select class="form-select" id="regionCode" required>
<option value="" selected disabled>지역을 선택하세요</option>
<!-- 지역 코드는 JavaScript에서 동적으로 추가됩니다 -->
</select>
</div>
<!-- 계약년월 선택 -->
<div class="col-md-6">
<label for="yearMonth" class="form-label">계약년월 (YYYYMM)</label>
<input type="month" class="form-control" id="yearMonth" required>
<small class="form-text text-muted">예: 2024년 1월</small>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> 검색
</button>
<button type="reset" class="btn btn-secondary">
<i class="bi bi-arrow-counterclockwise"></i> 초기화
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 로딩 표시 -->
<div id="loading" class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">로딩 중...</span>
</div>
<p class="mt-2">데이터를 불러오는 중입니다...</p>
</div>
<!-- 결과 메시지 -->
<div id="resultMessage" class="alert" style="display: none;"></div>
<!-- 검색 결과 -->
<div id="resultSection" style="display: none;">
<div class="card">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">검색 결과</h5>
<span>총 <span id="resultCount">0</span>건</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-light">
<tr>
<th>아파트명</th>
<th>법정동</th>
<th>거래금액(만원)</th>
<th>전용면적(㎡)</th>
<th>층</th>
<th>건축년도</th>
<th>계약일</th>
</tr>
</thead>
<tbody id="resultTable">
<!-- 결과 데이터가 여기에 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
<div id="noData" class="no-data" style="display: none;">
<p>검색 결과가 없습니다.</p>
</div>
</div>
</div>
</div>
<!-- API 키 설정 모달 -->
<div id="apiKeyModal" class="api-key-modal">
<div class="api-key-modal-content">
<span class="close-button" onclick="closeApiKeyModal()">×</span>
<h2 class="mb-4">API 키 설정</h2>
<p>국토교통부 실거래가 정보 오픈 API 사용을 위한 인증키를 설정합니다.</p>
<form id="apiKeyForm">
<div class="mb-3">
<label for="apiKey" class="form-label">API 키 (서비스 키)</label>
<input type="text" class="form-control" id="apiKey" placeholder="공공데이터포털에서 발급받은 API 키를 입력하세요">
<div class="form-text">
공공데이터포털(data.go.kr)에서 발급받은 인증키를 입력하세요.
<a href="https://www.data.go.kr/tcs/dss/selectApiDataDetailView.do?publicDataPk=15126469" target="_blank">API 신청 바로가기</a>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="button" class="btn btn-danger" onclick="deleteApiKey()">
<i class="bi bi-trash"></i> 삭제
</button>
<button type="button" class="btn btn-primary" onclick="saveApiKey()">
<i class="bi bi-save"></i> 저장
</button>
</div>
</form>
<div id="apiKeyMessage" class="alert mt-3" style="display: none;"></div>
<hr class="my-4">
<h5>API 키 발급 방법</h5>
<ol>
<li>공공데이터포털(<a href="https://www.data.go.kr" target="_blank">data.go.kr</a>)에 회원가입 및 로그인합니다.</li>
<li><a href="https://www.data.go.kr/tcs/dss/selectApiDataDetailView.do?publicDataPk=15126469" target="_blank">국토교통부_아파트 매매 실거래가 자료</a> 페이지에서 '활용신청' 버튼을 클릭합니다.</li>
<li>활용 목적과 상세 기능을 입력하고 신청합니다.</li>
<li>승인 후 '마이페이지 > 오픈API > 개발계정'에서 인증키를 확인합니다.</li>
<li>발급받은 인증키를 위 입력란에 붙여넣고 저장합니다.</li>
</ol>
</div>
</div>
<!-- 푸터 -->
<footer class="mt-5 text-center text-muted">
<p>본 서비스는 <a href="https://www.data.go.kr" target="_blank">공공데이터포털</a>에서 제공하는 국토교통부 실거래가 정보 오픈 API를 활용합니다.</p>
<p>© 2025 국토교통부 실거래가 정보 조회 서비스</p>
</footer>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 페이지 로드 시 실행
document.addEventListener('DOMContentLoaded', function() {
// API 키 확인
checkApiKey();
// 지역 코드 목록 로드
loadRegionCodes();
// 현재 날짜로 계약년월 기본값 설정
setDefaultDate();
// 검색 폼 제출 이벤트 처리
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
searchAptTradeInfo();
});
});
// API 키 확인 함수
function checkApiKey() {
google.script.run
.withSuccessHandler(function(hasKey) {
if (!hasKey) {
showMessage('API 키가 설정되지 않았습니다. API 키 설정 버튼을 클릭하여 API 키를 설정해주세요.', 'warning');
}
})
.withFailureHandler(function(error) {
console.error('API 키 확인 오류:', error);
})
.hasApiKey();
}
// 지역 코드 목록 로드 함수
function loadRegionCodes() {
google.script.run
.withSuccessHandler(function(regionCodes) {
const select = document.getElementById('regionCode');
// 지역 코드를 시/도별로 그룹화
const groupedCodes = {};
regionCodes.forEach(function(region) {
const parts = region.name.split(' ');
const cityName = parts[0]; // 첫 번째 부분을 시/도 이름으로 사용
if (!groupedCodes[cityName]) {
groupedCodes[cityName] = [];
}
groupedCodes[cityName].push(region);
});
// 그룹별로 옵션 추가
Object.keys(groupedCodes).sort().forEach(function(cityName) {
const optgroup = document.createElement('optgroup');
optgroup.label = cityName;
groupedCodes[cityName].forEach(function(region) {
const option = document.createElement('option');
option.value = region.code;
option.textContent = region.name;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
});
})
.withFailureHandler(function(error) {
console.error('지역 코드 로드 오류:', error);
showMessage('지역 코드를 불러오는 중 오류가 발생했습니다.', 'error');
})
.getRegionCodes();
}
// 현재 날짜로 계약년월 기본값 설정 함수
function setDefaultDate() {
const today = new Date();
const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1);
const year = lastMonth.getFullYear();
const month = String(lastMonth.getMonth() + 1).padStart(2, '0');
document.getElementById('yearMonth').value = `${year}-${month}`;
}
// API 키 모달 열기
function openApiKeyModal() {
// 저장된 API 키 가져오기
google.script.run
.withSuccessHandler(function(apiKey) {
document.getElementById('apiKey').value = apiKey;
})
.getApiKey();
document.getElementById('apiKeyModal').style.display = 'block';
}
// API 키 모달 닫기
function closeApiKeyModal() {
document.getElementById('apiKeyModal').style.display = 'none';
document.getElementById('apiKeyMessage').style.display = 'none';
}
// API 키 저장
function saveApiKey() {
const apiKey = document.getElementById('apiKey').value.trim();
if (!apiKey) {
showApiKeyMessage('API 키를 입력해주세요.', 'warning');
return;
}
google.script.run
.withSuccessHandler(function() {
showApiKeyMessage('API 키가 성공적으로 저장되었습니다.', 'success');
// 3초 후 모달 닫기
setTimeout(closeApiKeyModal, 3000);
})
.withFailureHandler(function(error) {
showApiKeyMessage('API 키 저장 중 오류가 발생했습니다: ' + error, 'danger');
})
.saveApiKey(apiKey);
}
// API 키 삭제
function deleteApiKey() {
if (confirm('정말로 API 키를 삭제하시겠습니까?')) {
google.script.run
.withSuccessHandler(function() {
document.getElementById('apiKey').value = '';
showApiKeyMessage('API 키가 삭제되었습니다.', 'success');
})
.withFailureHandler(function(error) {
showApiKeyMessage('API 키 삭제 중 오류가 발생했습니다: ' + error, 'danger');
})
.deleteApiKey();
}
}
// API 키 모달 메시지 표시
function showApiKeyMessage(message, type) {
const messageElement = document.getElementById('apiKeyMessage');
messageElement.textContent = message;
messageElement.style.display = 'block';
messageElement.className = 'alert mt-3';
switch (type) {
case 'success':
messageElement.classList.add('alert-success');
break;
case 'warning':
messageElement.classList.add('alert-warning');
break;
case 'danger':
messageElement.classList.add('alert-danger');
break;
default:
messageElement.classList.add('alert-info');
}
}
// 아파트 매매 실거래가 정보 검색
function searchAptTradeInfo() {
// 입력값 가져오기
const regionCode = document.getElementById('regionCode').value;
const yearMonthInput = document.getElementById('yearMonth').value;
// 입력값 검증
if (!regionCode || !yearMonthInput) {
showMessage('모든 검색 조건을 입력해주세요.', 'error');
return;
}
// YYYY-MM 형식을 YYYYMM 형식으로 변환
const yearMonthParts = yearMonthInput.split('-');
const yearMonth = yearMonthParts[0] + yearMonthParts[1];
// 로딩 표시 및 결과 영역 초기화
showLoading(true);
hideResults();
// 서버에 API 요청
google.script.run
.withSuccessHandler(handleSearchResults)
.withFailureHandler(function(error) {
showLoading(false);
showMessage('오류가 발생했습니다: ' + error, 'error');
})
.getAptTradeInfo(regionCode, yearMonth);
}
// 검색 결과 처리
function handleSearchResults(response) {
showLoading(false);
console.log("API 응답:", JSON.stringify(response));
if (!response.success) {
showMessage(response.message, 'error');
return;
}
const data = response.data;
if (!data || data.length === 0) {
showMessage('검색 결과가 없습니다.', 'info');
document.getElementById('resultSection').style.display = 'block';
document.getElementById('noData').style.display = 'block';
document.getElementById('resultCount').textContent = '0';
return;
}
// 결과 테이블에 데이터 표시
const tbody = document.getElementById('resultTable');
tbody.innerHTML = '';
data.forEach(function(item) {
const row = document.createElement('tr');
// 값이 없는 경우 처리
const aptName = item.아파트 || '-';
const dong = item.법정동 || '-';
const price = item.거래금액 ? formatNumber(item.거래금액) : '-';
const area = item.전용면적 ? parseFloat(item.전용면적).toFixed(2) : '-';
const floor = item.층 || '-';
const buildYear = item.건축년도 || '-';
const dealDate = (item.년 && item.월 && item.일) ?
`${item.년}.${item.월}.${item.일}` : '-';
row.innerHTML = `
<td>${aptName}</td>
<td>${dong}</td>
<td class="text-end">${price}</td>
<td class="text-end">${area}</td>
<td class="text-center">${floor}</td>
<td class="text-center">${buildYear}</td>
<td class="text-center">${dealDate}</td>
`;
tbody.appendChild(row);
});
// 결과 개수 표시 및 결과 영역 표시
document.getElementById('resultCount').textContent = data.length;
document.getElementById('resultSection').style.display = 'block';
document.getElementById('noData').style.display = 'none';
showMessage(`${data.length}건의 실거래가 정보를 조회했습니다.`, 'success');
}
// 메시지 표시
function showMessage(message, type) {
const messageElement = document.getElementById('resultMessage');
messageElement.textContent = message;
messageElement.style.display = 'block';
// 메시지 타입에 따른 스타일 설정
messageElement.className = 'alert';
switch (type) {
case 'success':
messageElement.classList.add('alert-success');
break;
case 'error':
messageElement.classList.add('alert-danger');
break;
case 'warning':
messageElement.classList.add('alert-warning');
break;
default:
messageElement.classList.add('alert-info');
}
// 5초 후 메시지 자동 숨김
setTimeout(function() {
messageElement.style.display = 'none';
}, 5000);
}
// 로딩 표시
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
// 결과 영역 숨김
function hideResults() {
document.getElementById('resultSection').style.display = 'none';
document.getElementById('resultMessage').style.display = 'none';
}
// 숫자 포맷팅 (천 단위 쉼표)
function formatNumber(num) {
return parseInt(num).toLocaleString();
}
</script>
</body>
</html>
# 국토교통부 아파트 실거래가 조회 웹앱 사용 가이드
## 1. 시작하기
### 1.1 API 키 발급받기
1. [국토교통부 공공데이터 포털](https://www.data.go.kr/data/15058747/openapi.do)에 접속합니다.
2. 회원가입 후 로그인합니다.
3. "활용신청" 버튼을 클릭하여 API 키를 발급받습니다.
4. 발급받은 API 키를 복사합니다.
### 1.2 웹앱 설정하기
1. Google Apps Script 프로젝트를 생성합니다.
2. 제공된 코드를 복사하여 붙여넣습니다.
3. 웹앱을 배포합니다:
- "배포" > "새 배포" 클릭
- "액세스 권한" 설정에서 "모든 사용자" 선택
- "배포" 버튼 클릭
## 2. 웹앱 사용하기
### 2.1 API 키 등록
1. 웹앱에 처음 접속하면 API 키를 입력하라는 메시지가 표시됩니다.
2. 발급받은 API 키를 입력하고 "저장" 버튼을 클릭합니다.
### 2.2 거래 정보 조회
1. "지역 선택" 드롭다운에서 원하는 지역을 선택합니다.
2. "년월 선택" 드롭다운에서 조회하고 싶은 년월을 선택합니다.
3. "조회" 버튼을 클릭합니다.
### 2.3 조회 결과 확인
- 조회된 아파트 거래 정보는 테이블 형태로 표시됩니다.
- 각 행에는 다음 정보가 포함됩니다:
- 거래금액
- 건축년도
- 거래일자 (년/월/일)
- 아파트명
- 전용면적
- 지번
- 층수
- 법정동
- 중개사소재지
## 3. 주의사항
1. API 키는 안전하게 보관하세요.
2. API 호출 횟수에 제한이 있을 수 있습니다.
3. 데이터는 매월 업데이트됩니다.
## 4. 문제 해결
1. API 키 오류
- API 키가 올바르게 입력되었는지 확인
- API 키가 활성화되어 있는지 확인
2. 데이터가 표시되지 않는 경우
- 선택한 지역과 년월에 거래 데이터가 있는지 확인
- 인터넷 연결 상태 확인
3. 기타 문제
- 브라우저 캐시 삭제 후 재시도
- 다른 브라우저로 시도
## 5. 지원
문제가 발생하면 다음 정보를 확인해주세요:
- 선택한 지역 코드
- 선택한 년월
- 표시되는 오류 메시지
2단계 : Tableau Prep 전처리
3단계 : Tableau Dashboard
4단계 : Tableau Public
결과와 배운 점
데이터 분석과 시각화 도구로 태블로를 사용함에 있어 데이터 수집을 Apps Script로 자동화하면 전체 프로세스를 완성할 수 있을 것 같습니다. 이제 막 Apps Script를 시작한터라 아직은 갈 길이 멀지만, 이번 스터디에서 완성해서 게시글 업데이트 하도록 하겠습니다.