#에이전트 #Agent #n8n #MCP #A2A #Supabase #Postgres #공공데이터 #GIS <div style=" box-sizing: border-box; width: 100%; max-width: 600px; margin: 0 auto; padding: 60px 45px; border-radius: 20px; text-align: center; color: #0f172a; position: relative; overflow: hidden; border: 1px solid #bae6fd; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(180deg, #f0f9ff 0%, #ffffff 100%); background-color: #f0f9ff; "> <!-- 모눈종이 배경 --> <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(rgba(14, 165, 233, 0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(14, 165, 233, 0.06) 1px, transparent 1px); background-size: 24px 24px; pointer-events: none; z-index: 0;"></div> <!-- 컨텐츠 래퍼 --> <div style="position: relative; z-index: 1;"> <!-- Badge --> <div style="margin-bottom: 24px;"> <span style="display: inline-block; padding: 6px 14px; background: #ffffff; border: 1px solid #bae6fd; border-radius: 100px; font-size: 12px; font-weight: 700; color: #0284c7;"> ● ✨ 서울시 실시간 데이터 연동 </span> </div> <!-- Main Title --> <h1 style="margin: 0 0 20px 0; font-size: 42px; font-weight: 900; line-height: 1.15; letter-spacing: -0.03em; color: #0f172a;"> 우리 동네<br> <span style="color: #0284c7;">AI</span> <span style="color: #16a34a;">생활 비서</span> </h1> <!-- Subtitle --> <p style="margin: 0 0 18px 0; font-size: 17px; color: #64748b; font-weight: 500; line-height: 1.6;"> "이번 주말 홍대 축제 정보 알려줘"<br> "강남역 상권 매출 추이 어때?" </p> <!-- Description --> <p style="margin: 0 0 28px 0; font-size: 14px; color: #94a3b8; line-height: 1.7;"> 따릉이 · 지하철 · 날씨 · 문화행사 통합 조회<br> 복잡한 검색 없이 대화하듯 물어보면 AI가 찾아줍니다 </p> <!-- Icon Grid --> <div style="margin-bottom: 32px; padding: 24px 20px; background: rgba(255, 255, 255, 0.8); border-radius: 12px; border: 1px solid #bae6fd;"> <table style="width: 100%; border: none; border-collapse: collapse;"> <tr> <td style="text-align: center; padding: 12px 8px; width: 25%; border: none;"><div style="font-size: 24px;">🚲</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">따릉이</div></td> <td style="text-align: center; padding: 12px 8px; width: 25%; border: none;"><div style="font-size: 24px;">🚇</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">지하철</div></td> <td style="text-align: center; padding: 12px 8px; width: 25%; border: none;"><div style="font-size: 24px;">🌤️</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">날씨</div></td> <td style="text-align: center; padding: 12px 8px; width: 25%; border: none;"><div style="font-size: 24px;">🅿️</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">주차장</div></td> </tr> <tr> <td style="text-align: center; padding: 12px 8px; border: none;"><div style="font-size: 24px;">🎉</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">문화행사</div></td> <td style="text-align: center; padding: 12px 8px; border: none;"><div style="font-size: 24px;">💳</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">상권분석</div></td> <td style="text-align: center; padding: 12px 8px; border: none;"><div style="font-size: 24px;">👥</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">인구혼잡</div></td> <td style="text-align: center; padding: 12px 8px; border: none;"><div style="font-size: 24px;">⚡</div><div style="font-size: 11px; font-weight: 700; color: #475569; margin-top: 6px;">전기차</div></td> </tr> </table> </div> <!-- Chat Example --> <div style="margin-bottom: 32px; text-align: left;"> <div style="font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 12px; text-align: center;">💬 이렇게 물어보세요</div> <div style="padding: 12px 16px; background: white; border-radius: 10px; border: 1px solid #e2e8f0; font-size: 13px; color: #475569; margin-bottom: 10px;"> <span style="color: #0284c7;">☕</span> "성수동 카페거리 요즘 매출 어때?" </div> <div style="padding: 12px 16px; background: #f0f9ff; border-radius: 10px; border: 1px solid #bae6fd; font-size: 13px; color: #0369a1;"> <span style="color: #0284c7;">🤖</span> 20대 여성 소비 비율 <strong>42%</strong>, 매출 <strong>15% 증가</strong>했습니다. </div> </div> <!-- CTA Button --> <div style="margin-bottom: 28px; text-align: center;"> <a href="https://daniel8824.app.n8n.cloud/webhook/seoul-udt" target="_blank" style="display: inline-block; padding: 14px 32px; background: #0284c7; color: white; text-decoration: none; border-radius: 10px; font-size: 15px; font-weight: 700;"> 🚀 내 주변 정보 확인하기 </a> </div> <!-- Stats --> <div style="padding-top: 24px; border-top: 1px solid #e2e8f0;"> <table style="width: 100%; border: none; border-collapse: collapse;"> <tr> <td style="text-align: center; padding: 10px; width: 25%; border: none;"><div style="font-size: 24px; font-weight: 800; color: #0f172a;">120+</div><div style="font-size: 11px; color: #64748b; margin-top: 4px;">주요 지역</div></td> <td style="text-align: center; padding: 10px; width: 25%; border: none;"><div style="font-size: 24px; font-weight: 800; color: #0f172a;">0.5s</div><div style="font-size: 11px; color: #64748b; margin-top: 4px;">응답 속도</div></td> <td style="text-align: center; padding: 10px; width: 25%; border: none;"><div style="font-size: 24px; font-weight: 800; color: #22c55e;">Live</div><div style="font-size: 11px; color: #64748b; margin-top: 4px;">실시간 연동</div></td> <td style="text-align: center; padding: 10px; width: 25%; border: none;"><div style="font-size: 24px; font-weight: 800; color: #0f172a;">24/7</div><div style="font-size: 11px; color: #64748b; margin-top: 4px;">상시 대기</div></td> </tr> </table> </div> </div> </div> ![[Pasted image 20251217194123.png]] ## Step 1: 트리거 세팅 ### 1-1. Schedule Trigger - 노드 기능 - On a schedule - 노드 이름 - Schedule Trigger - 노드 설정 - Trigger Rules - Trigger Interval - Minutes - Minutes Between Triggers - 10 ## Step 2: API 발급 ### 2-1. 서울시 공공자전거 실시간 대여정보 - [크롬 웹 스토어](https://chromewebstore.google.com) - [JSON-handle](https://chromewebstore.google.com/detail/json-handle/iahnhfdhidomcpggpaimmmahffihkfnj?hl=ko&utm_source=ext_sidebar) - 크롬에 추가 - 확장 프로그램 추가 - [서울 열린데이터 광장](https://data.seoul.go.kr) - 서울시 통합 회원가입 - 로그인 - [서울시 공공자전거 실시간 대여정보](https://data.seoul.go.kr/dataList/OA-15493/A/1/datasetView.do) - Open API - 인증키 신청 - 서비스(사용) 환경 - 웹 사이트 개발 - 사용URL - `https://www.naver.com` - 관리용 대표 이메일 - 본인 이메일 - 활용용도 - 데이터 분석 - 내용 - 데이터 분석 - 인증키 발급 알림 - 인증키 안내 - 인증키 목록 - 인증키 복사 - 샘플 URL - `http://openapi.seoul.go.kr:8088/(인증키)/json/bikeList/1/5/` ### 2-2. 서울시 공공자전거 대여소 정보 - [서울시 공공자전거 대여소 정보](https://data.seoul.go.kr/dataList/OA-13252/F/1/datasetView.do) - Open API - 인증키 신청 - 샘플 URL - `http://openapi.seoul.go.kr:8088/(인증키)/json/tbCycleStationInfo/1/5/` ### 2-3. 서울시 역사 마스터 정보 - [서울시 역사 마스터 정보](https://data.seoul.go.kr/dataList/OA-21232/S/1/datasetView.do) - Open API - 인증키 신청 - 샘플 URL - `http://openapi.seoul.go.kr:8088/(인증키)/xml/subwayStationMaster/1/5/` ### 2-4. 서울시 실시간 도시 데이터 - [서울시 실시간 도시 데이터](https://data.seoul.go.kr/dataList/OA-21285/F/1/datasetView.do) - Open API - 인증키 신청 - 샘플 URL - `http://openapi.seoul.go.kr:8088/(인증키)/xml/citydata/1/5/광화문·덕수궁` - CSV 명세서 - [구글 시트](https://docs.google.com/spreadsheets/d/1lGLqPAYzJ_VG4JdS5FOLNARXo6jJG8BsQfxHXvzS4Z4/edit?usp=sharing) 클릭 - 사본 만들기 ### 2-5. API Fields - 노드 기능 - Edit Fields (Set) - 노드 이름 - Edit Fields - 노드 설정 - Mode - Manual Mapping - Fields to Set - `API KEY` - String - `7370516a74693634393954584a416d` - Include Other Input Fields - 🔴비활성화 - Options - Add Option - Ignore Type Conversion Errors - 🟢활성화 ## Step 3: 데이터 수집 ### 3-1. Bycicle API (1-1000) - 노드 기능 - HTTP Request - 노드 이름 - Bycicle API (1-1000) - 노드 설정` - Method - GET - URL - `http://openapi.seoul.go.kr:8088/{{ $json['API KEY'] }}/json/bikeList/1/1000` - Authentication - None - Send Query Parameters - 🔴비활성화 - Send Headers - 🔴비활성화 - Send Body - 🔴비활성화 ### 3-2. Bycicle API (1001-2000) - 노드 기능 - HTTP Request - 노드 이름 - Bycicle API (1001-2000) - 노드 설정` - Method - GET - URL - `http://openapi.seoul.go.kr:8088/{{ $json['API KEY'] }}/json/bikeList/1001/2000` - Authentication - None - Send Query Parameters - 🔴비활성화 - Send Headers - 🔴비활성화 - Send Body - 🔴비활성화 ### 3-3. Bycicle API (2001-3000) - 노드 기능 - HTTP Request - 노드 이름 - Bycicle API (2001-3000) - 노드 설정` - Method - GET - URL - `http://openapi.seoul.go.kr:8088/{{ $json['API KEY'] }}/json/bikeList/2001/3000` - Authentication - None - Send Query Parameters - 🔴비활성화 - Send Headers - 🔴비활성화 - Send Body - 🔴비활성화 ### 3-4. Merge - 노드 기능 - [[Merge]] - 노드 이름 - Merge - 노드 설정 - Mode - Append - Number of Inputs - 3 - Input 1 - Bycicle API (1-1000) ▶️ Merge - Input 2 - Bycicle API (1001-2000) ▶️ Merge - Input 3 - Bycicle API (2001-3000) ▶️ Merge ### 3-5. Split Out - 노드 기능 - Split Out - 노드 이름 - Split Out - 노드 설정 - Field To Split Out - `rentBikeStatus.row` - Include - No Other Fields ## Step 4: 데이터 저장 ### 4-1. Convert to File - 노드 기능 - Convert to File - Convert to CSV - 노드 이름 - Convert to CSV - 노드 설정 - Operation - Convert to CSV - Put Output File in Field - data - Options - Add option - File Name - bicycle.csv ### 4-2. Supabase 프로젝트 생성 - [Supabase](https://supabase.com) 접속 - Sign In - Sign In Now - Create a new organization - Name - 본인 이름 - Type - Personal - Plan - Free - $0/month - Create organization🖱️ - Create a new project - Project name - n8n-data-project - Database password - 본인 비밀번호 - Region - Northeast Asia (Seoul) - Create new project🖱️ ### 4-3. Supabase 실시간 대여정보 테이블 - 왼쪽 사이드바 - SQL Editor - ⬇️⬇️⬇️ 아래 쿼리 실행 > [!important] 서울시 공공자전거 실시간 대여정보 테이블 생성 > ```sql > -- 기본 테이블 생성 > CREATE TABLE bicycle_seoul ( > stationId text NOT NULL, > stationName text NOT NULL, > rackTotCnt int8 NOT NULL, > parkingBikeTotCnt int8 NOT NULL, > shared int8 NOT NULL, > stationLatitude float8 NOT NULL, > stationLongitude float8 NOT NULL, > PRIMARY KEY (stationId) > ); > > -- 인덱스 생성 (검색 성능 향상) > CREATE INDEX idx_stations_name ON bicycle_seoul(stationName); > CREATE INDEX idx_stations_location ON bicycle_seoul(stationLatitude, stationLongitude); > ``` ### 4-4. Bycicle Seoul - 노드 기능 - Postgres - Insert or update rows in a table - 노드 이름 - Bicycle Seoul - 계정 연결 - Credential to connect with - Create new credential - Connect - Method - Session pooler - View parameters - 계정 이름 - Postgres Bicycle - Host - aws-1-ap-northeast-2.pooler.supabase.com - Database - postgres - User - 사용자 엔드포인트 - Password - 사용자 패스워드 - Port - 5432 - 노드 설정 - Operation - Insert or Update - Schema - public - Table - bicycle_seoul - Mapping Columns to match on - stationId - Values to Send - stationid (using to match) - `{{ $('Split Out').item.json.stationId }}` - stationname - `{{ $json.stationName }}` - racktotcnt - `{{ $json.rackTotCnt }}` - parkingbiketotcnt - `{{ $json.parkingBikeTotCnt }}` - shared - `{{ $json.shared }}` - stationlatitude - `{{ $json.stationLatitude }}` - stationlongitude - `{{ $json.stationLongitude }}` ### 4-5. 그 외 데이터 수집 및 저장 - 서울시 공공자전거 대여소 정보 - 데이터 수집 및 저장 - 서울시 역사 마스터 정보 - 데이터 수집 및 저장 - 서울시 실시간 도시 데이터 - 데이터 실시간 수집 및 저장 불가 - n8n 워크플로우 - [[서울시 공공자전거 대여소 및 지하철 JSON 스크립트]] 복사 및 붙여넣기 ### 4-6. Supabase 대여소 정보 테이블 - 왼쪽 사이드바 - SQL Editor - ⬇️⬇️⬇️ 아래 쿼리 실행 > [!quote] 서울시 공공자전거 대여소 정보 테이블 생성 > ```sql > -- 정거장 마스터 데이터 테이블 생성 > CREATE TABLE bicycle_station ( > rent_id text NOT NULL PRIMARY KEY, > sta_loc text NOT NULL, > rent_no text NOT NULL, > rent_nm text NOT NULL, > rent_id_nm text NOT NULL, > hold_num int8 NOT NULL, > sta_add1 text NOT NULL, > sta_add2 text, > sta_lat float8 NOT NULL, > sta_long float8 NOT NULL, > start_index int8, > end_index int8, > rnum int8 > ); > > -- 인덱스 생성 (검색 성능 향상) > CREATE INDEX idx_bicycle_station_loc ON bicycle_station(sta_loc); > CREATE INDEX idx_bicycle_station_name ON bicycle_station(rent_nm); > CREATE INDEX idx_bicycle_station_location ON bicycle_station(sta_lat, sta_long); > CREATE INDEX idx_bicycle_station_rent_no ON bicycle_station(rent_no); > > -- 코멘트 추가 (문서화) > COMMENT ON TABLE bicycle_station IS '서울시 공공자전거 정거장 마스터 데이터'; > COMMENT ON COLUMN bicycle_station.rent_id IS '정거장 고유 ID (예: ST-10)'; > COMMENT ON COLUMN bicycle_station.sta_loc IS '위치 구 (예: 마포구)'; > COMMENT ON COLUMN bicycle_station.rent_no IS '대여소 번호 (예: 00108)'; > COMMENT ON COLUMN bicycle_station.rent_nm IS '대여소 이름 (예: 서교동 사거리)'; > COMMENT ON COLUMN bicycle_station.rent_id_nm IS '대여소 전체 이름'; > COMMENT ON COLUMN bicycle_station.hold_num IS '거치대 총 개수'; > COMMENT ON COLUMN bicycle_station.sta_add1 IS '주소1 (도로명 주소)'; > COMMENT ON COLUMN bicycle_station.sta_add2 IS '주소2 (상세 주소)'; > COMMENT ON COLUMN bicycle_station.sta_lat IS '위도'; > COMMENT ON COLUMN bicycle_station.sta_long IS '경도'; > ``` ### 4-7. Bicycle Station - 노드 기능 - Supabase - Create a row - 노드 이름 - Bicycle Station - 계정 연결 - Credential to connect with - Create new credential - 계정 이름 - Supabase Bicycle - Host - [Supabase](https://supabase.com) - Project Settings - Data API - URL - Service Role Secret - [Supabase](https://supabase.com) - Project Settings - API Keys - Secret keys - 노드 설정 - Resource - Row - Operation - Create - Table Name or ID - bicycle_station - Data to Send - Define Below for Each Column - Fields to Send - rent_id (string) - `{{ $json.RENT_ID }}` - sta_loc (string) - `{{ $json.STA_LOC }}` - rent_no (string) - `{{ $json.RENT_NO }}` - rent_nm (string) - `{{ $json.RENT_NM }}` - rent_id_nim (string) - `{{ $json.RENT_ID_NM }}` - hold_num (integer) - `{{ $json.HOLD_NUM && $json.HOLD_NUM !== '' ? parseInt($json.HOLD_NUM) : 0 }}` - sta_add1 (string) - `{{ $json.STA_ADD1 }}` - sta_add2 (string) - `{{ $json.STA_ADD2 }}` - sta_lat (number) - `{{ parseFloat($json.STA_LAT) }}` - sta_long (number) - `{{ parseFloat($json.STA_LONG) }}` - start_index (integer) - `{{ parseInt($json.START_INDEX) }}` - end_index (integer) - `{{ parseInt($json.END_INDEX) }}` - rnum (integer) - `{{ parseInt($json.RNUM) }}` - 노드 세팅 - 데이터 저장 후 Deactivated ### 4-8. Supabase 관계형 테이블 - 왼쪽 사이드바 - SQL Editor - ⬇️⬇️⬇️ 아래 쿼리 실행 > [!example] 누락된 대여소 정보 자동 추가 > ```sql > -- 모든 누락된 정거장 자동 추가 > INSERT INTO bicycle_station ( > rent_id, sta_loc, rent_no, rent_nm, rent_id_nm, > hold_num, sta_add1, sta_lat, sta_long > ) > SELECT DISTINCT ON (stationid) > stationid, > '미등록', -- sta_loc (기본값) > '99999', -- rent_no (임시 번호) > stationname, > stationname, -- rent_id_nm (이름 복사) > racktotcnt, > '주소 미등록', -- sta_add1 (기본값) > stationlatitude, > stationlongitude > FROM bicycle_seoul > WHERE stationid IN ( > 'ST-3411', 'ST-3412', 'ST-3413', 'ST-3414', 'ST-3415', > 'ST-3416', 'ST-3417', 'ST-3418', 'ST-3419', 'ST-3420', > 'ST-3421', 'ST-3422', 'ST-3423', 'ST-3424' > ); > ``` > [!warning] 외래키(Foreign Key) 연결 설정 > ```sql > -- bicycle_seoul → bicycle_station 외래키 추가 > ALTER TABLE bicycle_seoul > ADD CONSTRAINT fk_bicycle_seoul_station > FOREIGN KEY (stationid) > REFERENCES bicycle_station(rent_id) > ON DELETE RESTRICT > ON UPDATE CASCADE; > ``` ### 4-9. Supabase 역사 마스터 테이블 - 왼쪽 사이드바 - SQL Editor - ⬇️⬇️⬇️ 아래 쿼리 실행 > [!success] 서울시 역사 마스터 정보 테이블 생성 > ```sql > -- 지하철역 마스터 데이터 테이블 생성 > CREATE TABLE subway_station ( > station_id text NOT NULL PRIMARY KEY, > station_nm text NOT NULL, > line_nm text NOT NULL, > station_lat float8 NOT NULL, > station_long float8 NOT NULL > ); > > -- 인덱스 생성 (검색 성능 향상) > CREATE INDEX idx_subway_station_name ON subway_station(station_nm); > CREATE INDEX idx_subway_station_line ON subway_station(line_nm); > CREATE INDEX idx_subway_station_location ON subway_station(station_lat, station_long); > > -- 코멘트 추가 (문서화) > COMMENT ON TABLE subway_station IS '서울 지하철역 마스터 데이터'; > COMMENT ON COLUMN subway_station.station_id IS '역 고유 ID (예: 0150)'; > COMMENT ON COLUMN subway_station.station_nm IS '역 이름 (예: 서울역)'; > COMMENT ON COLUMN subway_station.line_nm IS '호선명 (예: 1호선)'; > COMMENT ON COLUMN subway_station.station_lat IS '위도 (LAT)'; > COMMENT ON COLUMN subway_station.station_long IS '경도 (LOT)'; > ``` ### 4-10. Subway Station - 노드 기능 - Supabase - Create a row - 노드 이름 - Bicycle Station - 계정 연결 - Supabase Bicycle - 노드 설정 - Resource - Row - Operation - Create - Table Name or ID - subway_station - Data to Send - Define Below for Each Column - Fields to Send - station_id (string) - `{{ $json.BLDN_ID }}` - station_nm (string) - `{{ $json.BLDN_NM }}` - line_nm (string) - `{{ $json.ROUTE }}` - station_lat (number) - `{{ parseFloat($json.LAT) }}` - station_long (number) - `{{ parseFloat($json.LOT) }}` - 노드 세팅 - 데이터 저장 후 Deactivated ### 4-11. PostGIS 활성화 - 왼쪽 사이드바 - SQL Editor - ⬇️⬇️⬇️ 아래 쿼리 실행 > [!note] PostGIS 확장기능 활성화 (공간 데이터 처리) > ```sql > -- PostGIS 활성화 > CREATE EXTENSION IF NOT EXISTS postgis; > > -- 설치 확인 (버전 조회) > SELECT PostGIS_Version(); > ``` > [!danger] 자전거 정거장 공간 데이터 변환 및 인덱싱 > ```sql > -- 1. geometry 컬럼 추가 (이미 있으면 스킵) > DO $ > BEGIN > IF NOT EXISTS ( > SELECT 1 > FROM information_schema.columns > WHERE table_name = 'bicycle_station' > AND column_name = 'location' > ) THEN > ALTER TABLE bicycle_station > ADD COLUMN location geography(Point, 4326); > END IF; > END $; > > -- 2. 기존 위도/경도로 geometry 생성 > -- 주의: ST_MakePoint는 (경도, 위도) 순서입니다. (X, Y) > UPDATE bicycle_station > SET location = ST_SetSRID( > ST_MakePoint(sta_long, sta_lat), > 4326 > )::geography > WHERE location IS NULL; -- location이 비어있는 행만 업데이트 > > -- 3. 인덱스 생성 (공간 검색 성능 최적화) > CREATE INDEX IF NOT EXISTS idx_bicycle_station_location > ON bicycle_station USING GIST(location); > > -- 4. NOT NULL 제약 추가 (선택사항) > DO $ > BEGIN > ALTER TABLE bicycle_station > ALTER COLUMN location SET NOT NULL; > EXCEPTION > WHEN others THEN > RAISE NOTICE 'NOT NULL constraint already exists or failed'; > END $; > ``` > [!important] 지하철역 공간 데이터 변환 및 인덱싱 > ```sql > -- 1. geometry 컬럼 추가 > DO $ > BEGIN > IF NOT EXISTS ( > SELECT 1 > FROM information_schema.columns > WHERE table_name = 'subway_station' > AND column_name = 'location' > ) THEN > ALTER TABLE subway_station > ADD COLUMN location geography(Point, 4326); > END IF; > END $; > > -- 2. 기존 위도/경도로 geometry 생성 (올바른 컬럼명 확인) > UPDATE subway_station > SET location = ST_SetSRID( > ST_MakePoint(station_long, station_lat), > 4326 > )::geography > WHERE location IS NULL; > > -- 3. 인덱스 생성 > CREATE INDEX IF NOT EXISTS idx_subway_station_location > ON subway_station USING GIST(location); > > -- 4. NOT NULL 제약 추가 > DO $ > BEGIN > ALTER TABLE subway_station > ALTER COLUMN location SET NOT NULL; > EXCEPTION > WHEN others THEN > RAISE NOTICE 'NOT NULL constraint already exists or failed'; > END $; > ``` ## Step 5: MCP 서버 세팅 ### 5-1. Seoul City MCP Server - 워크플로우 - 🟢활성화 - 노드 기능 - MCP Server Trigger - 노드 이름 - Seoul City MCP Server - 노드 설정 - MCP URL - Production URL - Authentication - Bearer Auth - Credential for Bearer Auth - Create new credential - 계정 이름 - n8n Bearer account - Bearer Token - [UUID](https://www.uuidgenerator.net/) 복사 및 저장 - Allowed HTTP Request Domains - All - Path - seoul-city ### 5-2. Get City Location - 툴 설정 - Google Sheets Tool - 노드 이름 - Get City Location - 계정 연결 - n8n 클라우드 - 구글 계정 연동 - 별도 호스팅 - [[n8n과 Google 계정 연동하기]] - 노드 설정 - CSV 명세서 - [구글 시트](https://docs.google.com/spreadsheets/d/1lGLqPAYzJ_VG4JdS5FOLNARXo6jJG8BsQfxHXvzS4Z4/edit?usp=sharing) 클릭 - 사본 만들기 - Tool Description - Set Automatically - Resource - Sheet Within Document - Operation - Get Row(s) - Document - From list - 서울시 주요 120 장소 목록 - Sheet - From list - 시트1 - Filters - Add Filter - Column - AREA_NM - Value - `🌟Let the model define this parameter` - Add a description - ⬇️⬇️⬇️ 아래 내용 입력 > [!quote] 서울시 주요 120개 장소 Description > ```markdown > 시스템 역할 > 사용자의 자연어 입력(예: "광화문", "강남", "홍대")을 분석하여, > 서울시 실시간 도시데이터(Seoul City Data)가 제공하는 > 120개 주요 장소(POI) 목록 중 가장 적합한 표준 지역명(AREA_NM)을 매칭합니다. > > 매칭 예시 (User Input -> AI Output) > - "광화문 가고 싶어" -> "광화문·덕수궁" > - "강남역 근처 사람 많아?" -> "강남역" > - "홍대 약속 있어" -> "홍대입구역(2호선)" > - "성수동 카페 갈래" -> "성수카페거리" > > 관리 대상 장소 카테고리 (총 120개 POI) > 1. 관광특구 (7곳) > - 강남 MICE, 동대문, 명동, 이태원, 잠실, 종로·청계, 홍대 관광특구 > > 2. 주요 지하철역 > - 강남역, 홍대입구역(2호선), 잠실역, 신촌·이대역, 서울역, > - 고속터미널역, 교대역, 건대입구역, 구로디지털단지역 등 > > 3. 발달상권 > - 가로수길, 성수카페거리, 압구정로데오, 연남동, 용리단길, > - 익선동, 북촌한옥마을, 해방촌 등 > > 4. 공원 > - 뚝섬한강공원, 망원한강공원, 반포한강공원, 여의도한강공원, > - 잠실한강공원, 남산공원, 서울숲공원, 올림픽공원 등 > ``` ### 5-3. Get City Data - 툴 설정 - HTTP Request Tool - 노드 이름 - Get City Data - 노드 설정 - Description - ⬇️⬇️⬇️ 아래 내용 입력` - Method - GET - URL - `http://openapi.seoul.go.kr:8088/5a45416d4b693634373754567a5465/json/citydata/1/5/{{ encodeURIComponent($fromAI('area_name', '지역명 (예: 강남역, 홍대입구역(2호선), 광화문·덕수궁)', 'string')) }}` - Authentication - None - Send Query Parameters - 🔴비활성화 - Send Headers - 🔴비활성화 - Send Body - 🔴비활성화 - Optimize Response - 🔴비활성화 > [!example] 서울시 실시간 도시데이터 API Description > ```text > 서울시 실시간 도시데이터 API - 특정 지역의 통합 실시간 정보 조회 > > 【API 개요】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 사용자가 입력한 장소명을 기반으로 120개 POI 중 매칭된 지역의 실시간 데이터를 제공합니다. > > 【1. 공통 응답 필드】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > - list_total_count: 총 데이터 건수 > - RESULT.CODE: 요청결과 코드 > - RESULT.MESSAGE: 요청결과 메시지 > - AREA_NM: 핫스팟 장소명 > - AREA_CD: 핫스팟 코드명 > > 【2. 실시간 인구 현황 (LIVE_PPLTN_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 혼잡도 관련: > - AREA_CONGEST_LVL: 장소 혼잡도 지표 (여유/보통/약간붐빔/붐빔) > - AREA_CONGEST_MSG: 장소 혼잡도 메시지 > - AREA_PPLTN_MIN/MAX: 실시간 인구 지표 최소/최대값 > > 성별/연령별 비율: > - MALE_PPLTN_RATE: 남성 인구 비율 > - FEMALE_PPLTN_RATE: 여성 인구 비율 > - PPLTN_RATE_0: 0~10세 비율 > - PPLTN_RATE_10~70: 10대~70대 비율 > > 거주지 구분: > - RESNT_PPLTN_RATE: 상주 인구 비율 > - NON_RESNT_PPLTN_RATE: 비상주 인구 비율 > - PPLTN_TIME: 데이터 업데이트 시간 > - REPLACE_YN: 대체 데이터 여부 > > 예측 데이터: > - FCST_YN: 예측값 제공 여부 > - FCST_PPLTN: 인구 예측값 > - FCST_TIME: 인구 예측시점 > - FCST_CONGEST_LVL: 예측 혼잡도 > - FCST_PPLTN_MIN/MAX: 예측 인구 최소/최대값 > > 【3. 도로 소통 현황 (ROAD_TRAFFIC_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 전체 도로 소통: > - ROAD_TRAFFIC_SPD: 전체 도로 소통 평균 속도 > - ROAD_TRAFFIC_IDX: 전체 도로 소통 평균 현황 > - ROAD_TRAFFIC_TIME: 데이터 업데이트 시간 > - ROAD_MSG: 전체 도로 소통 메시지 > > 개별 도로 구간: > - LINK_ID: 도로구간 LINK ID > - ROAD_NM: 도로명 > - START_ND_CD/NM/XY: 시작 노드 코드/명/좌표 > - END_ND_CD/NM/XY: 종료 노드 코드/명/좌표 > - DIST: 도로구간 길이 > - SPD: 도로구간 평균 속도 > - IDX: 도로구간 소통 지표 > - XYLIST: 링크 아이디 좌표(보간점) > > 【4. 주차장 현황 (PRK_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 기본 정보: > - PRK_NM: 주차장명 > - PRK_CD: 주차장 코드 > - PRK_TYPE: 주차장 구분 > - CPCTY: 주차장 수용 가능 면수 > - CUR_PRK_CNT: 주차 가능 면수 > - CUR_PRK_TIME: 데이터 업데이트 시간 > - CUR_PRK_YN: 실시간 주차 현황 제공 여부 > > 요금 정보: > - PAY_YN: 유무료 여부 > - RATES: 기본 주차 요금 > - TIME_RATES: 기본 주차 단위 시간 > - ADD_RATES: 추가 주차 단위 요금 > - ADD_TIME_RATES: 추가 주차 단위 시간 > > 위치 정보: > - ROAD_ADDR: 도로명 주소 > - ADDRESS: 지번 주소 > - LAT: 위도 > - LNG: 경도 > > 【5. 지하철 실시간 도착 현황 (SUB_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 역 정보: > - SUB_STN_NM: 지하철역명 > - SUB_STN_LINE: 지하철역 호선 > - SUB_STN_RADDR: 도로명 주소 > - SUB_STN_JIBUN: 지번 주소 > - SUB_STN_X/Y: 좌표 (경도/위도) > - SUB_NT_STN: 다음역 코드 > - SUB_BF_STN: 이전역 코드 > > 열차 도착 정보: > - SUB_ROUTE_NM: 지하철 노선명 > - SUB_LINE: 지하철 호선 > - SUB_ORD: 도착 예정 열차 순번 > - SUB_DIR: 지하철 방향 > - SUB_TERMINAL: 종착역 > - SUB_ARVTIME: 열차 도착 시간 > - SUB_ARMG1/2: 열차 도착 메시지 > - SUB_ARVINFO: 열차 도착 코드 정보 > > 교통약자 시설: > - SUB_FACINFO: 교통약자 이용시설 현황 > - ELVTR_NM: 승강기명 > - OPR_SEC: 운행 구간 > - INSTL_PSTN: 설치 위치 > - USE_YN: 운행 상태 > - ELVTR_SE: 승강기 구분 > > 실시간 승하차 인원: > - LIVE_SUB_PPLTN: 실시간 지하철 승하차 인원 > - SUB_ACML_GTON_PPLTN_MIN/MAX: 누적 승차 인원 > - SUB_ACML_GTOFF_PPLTN_MIN/MAX: 누적 하차 인원 > - SUB_30WTHN_GTON_PPLTN_MIN/MAX: 30분 이내 승차 > - SUB_30WTHN_GTOFF_PPLTN_MIN/MAX: 30분 이내 하차 > - SUB_10WTHN_GTON_PPLTN_MIN/MAX: 10분 이내 승차 > - SUB_10WTHN_GTOFF_PPLTN_MIN/MAX: 10분 이내 하차 > - SUB_5WTHN_GTON_PPLTN_MIN/MAX: 5분 이내 승차 > - SUB_5WTHN_GTOFF_PPLTN_MIN/MAX: 5분 이내 하차 > - SUB_STN_CNT: 장소 내 지하철역 개수 > - SUB_STN_TIME: 기준 년월 > > 【6. 버스정류소 현황 (BUS_STN_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 정류소 정보: > - BUS_RESULT_MSG: 버스 데이터 호출 메시지 > - BUS_STN_ID: 정류소 ID > - BUS_ARS_ID: 정류소 고유번호 > - BUS_STN_NM: 정류소명 > - BUS_STN_X/Y: 좌표 (경도/위도) > > 노선 정보: > - RTE_STN_NM: 노선 조회 기준 정류장명 > - RTE_NM: 노선명 > - RTE_ID: 노선 ID > - RTE_SECT: 노선 구간 > - RTE_CONGEST: 노선 혼잡도 > - RTE_ARRV_TM: 노선 예상 도착 시간 > - RTE_ARRV_STN: 노선 최근 도착 정류장 > > 실시간 승하차 인원: > - LIVE_BUS_PPLTN: 실시간 버스 승하차 인원 > - BUS_ACML_GTON_PPLTN_MIN/MAX: 누적 승차 인원 > - BUS_ACML_GTOFF_PPLTN_MIN/MAX: 누적 하차 인원 > - BUS_30WTHN_GTON_PPLTN_MIN/MAX: 30분 이내 승차 > - BUS_30WTHN_GTOFF_PPLTN_MIN/MAX: 30분 이내 하차 > - BUS_10WTHN_GTON_PPLTN_MIN/MAX: 10분 이내 승차 > - BUS_10WTHN_GTOFF_PPLTN_MIN/MAX: 10분 이내 하차 > - BUS_5WTHN_GTON_PPLTN_MIN/MAX: 5분 이내 승차 > - BUS_5WTHN_GTOFF_PPLTN_MIN/MAX: 5분 이내 하차 > - BUS_STN_CNT: 장소 내 버스정류장 개수 > - BUS_STN_TIME: 기준 년월 > > 【7. 사고통제 현황 (ACDNT_CNTRL_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > - ACDNT_OCCR_DT: 사고 발생 일시 > - EXP_CLR_DT: 통제 종료 예정 일시 > - ACDNT_TYPE: 사고 발생 유형 > - ACDNT_DTYPE: 사고 발생 세부 유형 > - ACDNT_INFO: 사고 통제 내용 > - ACDNT_X/Y: 사고 통제 지점 좌표 > - ACDNT_TIME: 데이터 업데이트 시간 > > 【8. 전기차충전소 현황 (CHARGER_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 충전소 정보: > - STAT_NM: 전기차 충전소명 > - STAT_ID: 전기차 충전소 ID > - STAT_ADDR: 전기차 충전소 주소 > - STAT_X/Y: 좌표 (경도/위도) > - STAT_USETIME: 운영 시간 > - STAT_PARKPAY: 주차료 유무료 여부 > - STAT_LIMITYN: 이용자 제한 > - STAT_LIMITDETAIL: 이용 제한 사유 > - STAT_KINDDETAIL: 상세 유형 > > 충전기 정보: > - CHARGER_ID: 충전기 ID > - CHARGER_TYPE: 충전기 타입 > - CHARGER_STAT: 충전기 상태 > - STATUPDDT: 상태 갱신 일시 > - LASTTSDT: 마지막 충전 시작 일시 > - LASTTEDT: 마지막 충전 종료 일시 > - NOWTSDT: 충전중 시작 일시 > - OUTPUT: 충전 용량 > - METHOD: 충전 방식 > > 【9. 따릉이 현황 (SBIKE_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > - SBIKE_SPOT_NM: 따릉이 대여소명 > - SBIKE_SPOT_ID: 따릉이 대여소 ID > - SBIKE_SHARED: 따릉이 거치율 (%) > - SBIKE_PARKING_CNT: 따릉이 주차 건수 (현재 자전거 수) > - SBIKE_RACK_CNT: 따릉이 거치대 개수 (총 거치대) > - SBIKE_X: 따릉이 대여소 X 좌표 (경도) > - SBIKE_Y: 따릉이 대여소 Y 좌표 (위도) > > 【10. 날씨 현황 (WEATHER_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 기본 기상: > - TEMP: 기온 > - SENSIBLE_TEMP: 체감 온도 > - MAX_TEMP: 일 최고 온도 > - MIN_TEMP: 일 최저 온도 > - HUMIDITY: 습도 > - WIND_DIRCT: 풍향 > - WIND_SPD: 풍속 > - PRECIPITATION: 강수량 > - PRECPT_TYPE: 강수 형태 > - PCP_MSG: 강수 관련 메시지 > - SUNRISE: 일출 시각 > - SUNSET: 일몰 시각 > > 자외선/대기질: > - UV_INDEX_LVL: 자외선 지수 단계 > - UV_INDEX: 자외선 지수 > - UV_MSG: 자외선 메시지 > - PM25_INDEX: 초미세먼지 지표 > - PM25: 초미세먼지 농도 > - PM10_INDEX: 미세먼지 지표 > - PM10: 미세먼지 농도 > - AIR_IDX: 통합 대기 환경 등급 > - AIR_IDX_MVL: 통합 대기 환경 지수 > - AIR_IDX_MAIN: 통합 대기 환경 지수 결정 물질 > - AIR_MSG: 통합 대기 환경 등급별 메시지 > - WEATHER_TIME: 데이터 업데이트 시간 > > 기상특보: > - NEWS_LIST: 기상 관련 특보 > - WARN_VAL: 기상 특보 종류 > - WARN_STRESS: 기상 특보 강도 > - ANNOUNCE_TIME: 기상 특보 발효 시각 > - COMMAND: 기상 특보 발표 코드 > - CANCEL_YN: 기상 특보 취소 구분 > - WARN_MSG: 기상 특보별 행동 강령 > > 24시간 예보: > - FCST24HOURS: 24시간 예보 > - FCST_DT: 예보 시간 > - RAIN_CHANCE: 강수 확률 > - SKY_STTS: 하늘 상태 > > 【11. 문화행사 현황 (CULTURALEVENTINFO)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > - EVENT_NM: 문화행사명 > - EVENT_PERIOD: 문화행사 기간 > - EVENT_PLACE: 문화행사 장소 > - EVENT_X/Y: 좌표 (경도/위도) > - PAY_YN: 유무료 여부 > - THUMBNAIL: 문화행사 대표 이미지 > - URL: 문화행사 상세정보 URL > - EVENT_ETC_DETAIL: 문화행사 기타 세부정보 > > 【12. 실시간 상권 현황 (LIVE_CMRCL_STTS)】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 장소 상권: > - AREA_CMRCL_LVL: 장소 실시간 상권 현황 > - AREA_SH_PAYMENT_CNT: 장소 실시간 신한카드 결제 건수 > - AREA_SH_PAYMENT_AMT_MIN/MAX: 장소 실시간 신한카드 결제 금액 > > 업종별 상권: > - RSB_LRG_CTGR: 업종 대분류 > - RSB_MID_CTGR: 업종 중분류 > - RSB_PAYMENT_LVL: 업종 실시간 상권 현황 > - RSB_SH_PAYMENT_CNT: 업종 실시간 신한카드 결제 건수 > - RSB_SH_PAYMENT_AMT_MIN/MAX: 업종 실시간 신한카드 결제 금액 > - RSB_MCT_CNT: 업종 가맹점 수 > - RSB_MCT_TIME: 업종 가맹점 수 업데이트 월 > > 소비자 분석: > - CMRCL_MALE_RATE: 남성 소비 비율 > - CMRCL_FEMALE_RATE: 여성 소비 비율 > - CMRCL_10_RATE: 10대 이하 소비 비율 > - CMRCL_20~60_RATE: 20대~60대 이상 소비 비율 > - CMRCL_PERSONAL_RATE: 개인 소비 비율 > - CMRCL_CORPORATION_RATE: 법인 소비 비율 > - CMRCL_TIME: 데이터 업데이트 시간 > > 【응답 구조】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > [ > { > "list_total_count": 1, > "RESULT": { > "RESULT.CODE": "INFO-000", > "RESULT.MESSAGE": "정상 처리되었습니다." > }, > "CITYDATA": { > "AREA_NM": "강남 MICE 관광특구", > "AREA_CD": "POI001", > > "LIVE_PPLTN_STTS": [ > { > "AREA_CONGEST_LVL": "여유", > "AREA_PPLTN_MIN": "2000", > "AREA_PPLTN_MAX": "2500", > "MALE_PPLTN_RATE": "55.9", > ... > } > ], > > "ROAD_TRAFFIC_STTS": [ > { > "ROAD_TRAFFIC_SPD": "35.2", > "ROAD_NM": "테헤란로", > "SPD": "40", > ... > } > ], > > "PRK_STTS": [ > { > "PRK_NM": "코엑스 주차장", > "CPCTY": "500", > "CUR_PRK_CNT": "120", > ... > } > ], > > "SUB_STTS": [ > { > "SUB_STN_NM": "삼성역", > "SUB_ARVTIME": "3분", > ... > } > ], > > "BUS_STN_STTS": [ > { > "BUS_STN_NM": "삼성역", > "RTE_NM": "140", > "RTE_ARRV_TM": "5분", > ... > } > ], > > "SBIKE_STTS": [ > { > "SBIKE_SPOT_NM": "102. 광화문역 2번출구 앞", > "SBIKE_PARKING_CNT": 12, > "SBIKE_RACK_CNT": 20, > "SBIKE_SHARED": 60.0, > ... > } > ], > > "WEATHER_STTS": [ > { > "TEMP": "5.2", > "SENSIBLE_TEMP": "3.1", > "PM10": "15", > "PM25": "8", > ... > } > ], > > "CHARGER_STTS": [ > { > "STAT_NM": "코엑스 충전소", > "CHARGER_STAT": "충전가능", > ... > } > ], > > "CULTURALEVENTINFO": [ > { > "EVENT_NM": "강남페스티벌", > "EVENT_PERIOD": "2025-01-10~2025-01-20", > ... > } > ], > > "LIVE_CMRCL_STTS": [ > { > "AREA_CMRCL_LVL": "활발", > "AREA_SH_PAYMENT_CNT": "1500", > ... > } > ] > } > } > ] > > 【사용 예시】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > Q: "광화문 따릉이 있어?" > → CITYDATA.SBIKE_STTS[].SBIKE_PARKING_CNT 확인 > > Q: "강남역 지금 혼잡해?" > → CITYDATA.LIVE_PPLTN_STTS[0].AREA_CONGEST_LVL 확인 > > Q: "홍대 주차 가능해?" > → CITYDATA.PRK_STTS[].CUR_PRK_CNT 확인 > > Q: "여의도 날씨 어때?" > → CITYDATA.WEATHER_STTS[0].TEMP, PRECPT_TYPE 확인 > > Q: "잠실 지하철 언제 와?" > → CITYDATA.SUB_STTS[].SUB_ARVTIME 확인 > > 【주의사항】 > - 응답은 배열 형태 [] 로 감싸져 있음 > - 실제 데이터는 CITYDATA 객체 안에 위치 > - 각 카테고리(SBIKE_STTS, SUB_STTS 등)는 배열 형태 > - 일부 지역은 특정 카테고리 데이터가 없을 수 있음 (빈 배열 []) > ``` ### 5-4. Seoul Bicycle MCP Server - 노드 기능 - MCP Server Trigger - 노드 이름 - Seoul Bicycle MCP Server - 노드 설정 - MCP URL - Production URL - Authentication - Bearer Auth - Credential for Bearer Auth - Create new credential - 계정 이름 - n8n Bearer account - Path - seoul-bicycle ### 5-5. Get Bicycle Current - 툴 설정 - Supabase Tool - 노드 이름 - Get Bicycle Current - 계정 연결 - Supabase Bicycle - 노드 설정 - Tool Description - Set Automatically - Use Custom Schema - 🔴비활성화 - Resource - Row - Operation - Get Many - Table Name or ID - bicycle_seoul - Return All - 🔴비활성화 - Limit - `🌟Let the model define this parameter` - Add a description - 조회할 행의 개수입니다. - Filter - String - Filters (String) - `🌟Let the model define this parameter` - Add a description - ⬇️⬇️⬇️ 아래 내용 입력 > [!warning] Supabase PostgREST API 필터링 문법 (URL 파라미터) > ```text > Supabase PostgREST 필터 문법을 사용합니다. > > 【사용 예시】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > 1. 특정 자전거 대여소 조회 (ID 일치) > ?stationid=eq.ST-4015 > > 2. "광화문"이 포함된 대여소 검색 (패턴 매칭) > ?stationname=like.*광화문* > > 3. 자전거가 10대 이상 있는 곳 (숫자 비교) > ?parkingbiketotcnt=gte.10 > > 4. 빈 거치대가 5개 이상인 곳 > ?shared=gte.5 > > 5. 복합 조건 (광화문에 있고 AND 자전거 1대 이상) > ?stationname=like.*광화문*&parkingbiketotcnt=gte.1 > > 【주요 연산자 목록】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > [비교 연산자] > - eq : 같음 (Equal) -> id=eq.1 > - neq : 다름 (Not Equal) -> id=neq.1 > - gt : 큼 (Greater Than) -> price=gt.1000 > - gte : 크거나 같음 (>=) -> price=gte.1000 > - lt : 작음 (Less Than) -> price=lt.500 > - lte : 작거나 같음 (<=) -> price=lte.500 > > [패턴 매칭] > - like : 패턴 매칭 (대소문자 구분 O) -> name=like.*Korea* > - ilike : 패턴 매칭 (대소문자 구분 X) -> name=ilike.*korea* > (*는 와일드카드: 모든 문자를 의미) > > [집합/NULL 처리] > - in : 목록에 포함됨 -> status=in.(active,pending) > - is : NULL 여부 확인 -> deleted_at=is.null > > 【정렬 및 페이지네이션】 > ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > - order : 정렬 (asc/desc) -> order=stationid.desc > - limit : 개수 제한 -> limit=10 > - offset : 건너뛰기 (페이지네이션) -> offset=0 > > 【주의사항】 > - 모든 값은 URL 인코딩이 필요할 수 있습니다. (특히 한글/특수문자) > - 클라이언트 라이브러리(JS/Python) 사용 시 문법이 다를 수 있습니다. (.eq(), .gt() 등 함수 형태) > ``` ### 5-6. Postgres RPC 함수 생성 - 왼쪽 사이드바 - SQL Editor - ⬇️⬇️⬇️ 아래 쿼리 실행 > [!note] RPC 함수 기능 매핑 (Function Mapping) > ```text > 1. get_station_detail > - 기능: 특정 대여소 상세 조회 (지도 마커 클릭) > - 설명: 지도에서 핀을 눌렀을 때, 그 대여소의 현재 자전거 수와 주소를 보여줍니다. > > 2. get_nearby_stations > - 기능: 내 주변 대여소 찾기 (GPS 기반) > - 설명: "내 위치에서 500m 이내"에 있는 대여소 목록을 가까운 순서대로 가져옵니다. > > 3. get_all_stations > - 기능: 전체 대여소 목록 로딩 (초기 지도 세팅) > - 설명: 앱을 켰을 때 서울시 전체 대여소 위치를 지도에 뿌려주기 위해 사용합니다. > > 4. compare_routes > - 기능: 최적 이동 경로 계산 (Route Planning) > - 설명: 출발지와 도착지 좌표를 주면, "출발지에서 가장 가까운 대여소"와 "도착지와 가장 가까운 대여소"를 자동으로 짝지어줍니다. > > 5. get_stations_along_route > - 기능: 이동 경로상 대여소 검색 (환승/반납 추천) > - 설명: A에서 B로 가는 직선 경로(Line) 주변에 있는 대여소들을 찾습니다. 가는 길에 힘들어서 반납하고 싶을 때 씁니다. > > 6. find_nearby_bicycle > - 기능: 주변 대여소 검색 (고급/안전 모드) > - 설명: 2번 함수와 비슷하지만, 실시간 데이터가 잠깐 끊긴 정거장(Master 데이터 기준)까지 모두 보여주어 더 안정적입니다. > > 7. find_nearby_subway > - 기능: 근처 지하철역 찾기 (환승 정보) > - 설명: 현재 자전거 위치에서 가장 가까운 지하철역이 어디인지, 몇 미터 떨어져 있는지 알려줍니다. > ``` > [!success] Supabase RPC 함수 모음 (공간 연산 & 조회) > ```sql > -- ============================================================ > -- 0️⃣ [초기화] 기존 함수 삭제 (충돌 방지) > -- ============================================================ > DROP FUNCTION IF EXISTS find_nearby_bicycle(FLOAT, FLOAT, INT); > DROP FUNCTION IF EXISTS find_nearby_subway(FLOAT, FLOAT, INT); > DROP FUNCTION IF EXISTS get_bicycle_route_stats(TEXT, TEXT); > DROP FUNCTION IF EXISTS get_popular_stations(INT); > DROP FUNCTION IF EXISTS get_station_detail(TEXT); > DROP FUNCTION IF EXISTS compare_routes(FLOAT, FLOAT, FLOAT, FLOAT); > DROP FUNCTION IF EXISTS get_nearby_stations(FLOAT, FLOAT, INT); > DROP FUNCTION IF EXISTS get_all_stations(); > DROP FUNCTION IF EXISTS get_stations_along_route(FLOAT, FLOAT, FLOAT, FLOAT, INT); > > -- ============================================================ > -- 1️⃣ get_station_detail: 특정 대여소 상세 정보 조회 > -- ============================================================ > CREATE OR REPLACE FUNCTION get_station_detail(station_id_input text) > RETURNS TABLE( > station_id text, > station_name text, > current_bikes bigint, > empty_racks bigint, > total_racks bigint, > address text, > latitude double precision, > longitude double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > SELECT > bc.stationid, > bc.stationname, > bc.parkingbiketotcnt, > bc.shared, > bc.racktotcnt, > bs.sta_add1 || ' ' || COALESCE(bs.sta_add2, '') as address, > bs.sta_lat, > bs.sta_long > FROM bicycle_seoul bc > JOIN bicycle_station bs ON bc.stationid = bs.rent_id > WHERE bc.stationid = station_id_input > LIMIT 1; > END; > $; > > -- ============================================================ > -- 2️⃣ get_nearby_stations: 내 위치 주변 대여소 검색 (PostGIS) > -- ============================================================ > CREATE OR REPLACE FUNCTION get_nearby_stations( > lat double precision, > lng double precision, > radius_meters integer DEFAULT 500 > ) > RETURNS TABLE( > station_id text, > station_name text, > current_bikes bigint, > empty_racks bigint, > total_racks bigint, > address text, > latitude double precision, > longitude double precision, > distance_meters double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > SELECT > bc.stationid, > bc.stationname, > bc.parkingbiketotcnt, > bc.shared, > bc.racktotcnt, > bs.sta_add1 || ' ' || COALESCE(bs.sta_add2, ''), > bs.sta_lat, > bs.sta_long, > ST_Distance( > ST_SetSRID(ST_MakePoint(lng, lat), 4326)::geography, > bs.location > ) as distance_meters > FROM bicycle_seoul bc > JOIN bicycle_station bs ON bc.stationid = bs.rent_id > WHERE ST_DWithin( > ST_SetSRID(ST_MakePoint(lng, lat), 4326)::geography, > bs.location, > radius_meters > ) > AND bs.location IS NOT NULL > ORDER BY distance_meters > LIMIT 10; > END; > $; > > -- ============================================================ > -- 3️⃣ get_all_stations: 전체 대여소 목록 조회 > -- ============================================================ > CREATE OR REPLACE FUNCTION get_all_stations() > RETURNS TABLE( > station_id text, > station_name text, > current_bikes bigint, > empty_racks bigint, > total_racks bigint, > address text, > latitude double precision, > longitude double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > SELECT > bc.stationid, > bc.stationname, > bc.parkingbiketotcnt, > bc.shared, > bc.racktotcnt, > bs.sta_add1 || ' ' || COALESCE(bs.sta_add2, ''), > bs.sta_lat, > bs.sta_long > FROM bicycle_seoul bc > JOIN bicycle_station bs ON bc.stationid = bs.rent_id > ORDER BY bc.stationname; > END; > $; > > -- ============================================================ > -- 4️⃣ compare_routes: 출발지-도착지 최적 경로 비교 > -- ============================================================ > CREATE OR REPLACE FUNCTION compare_routes( > start_lat double precision, > start_lng double precision, > end_lat double precision, > end_lng double precision > ) > RETURNS TABLE( > start_station_id text, > start_station_name text, > start_bikes bigint, > start_distance double precision, > end_station_id text, > end_station_name text, > end_racks bigint, > end_distance double precision, > total_distance double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > WITH start_stations AS ( > SELECT > bc.stationid, > bc.stationname, > bc.parkingbiketotcnt, > ST_Distance( > ST_SetSRID(ST_MakePoint(start_lng, start_lat), 4326)::geography, > bs.location > ) as dist > FROM bicycle_seoul bc > JOIN bicycle_station bs ON bc.stationid = bs.rent_id > WHERE bc.parkingbiketotcnt > 0 > AND bs.location IS NOT NULL > ORDER BY dist > LIMIT 1 > ), > end_stations AS ( > SELECT > bc.stationid, > bc.stationname, > bc.shared, > ST_Distance( > ST_SetSRID(ST_MakePoint(end_lng, end_lat), 4326)::geography, > bs.location > ) as dist > FROM bicycle_seoul bc > JOIN bicycle_station bs ON bc.stationid = bs.rent_id > WHERE bc.shared > 0 > AND bs.location IS NOT NULL > ORDER BY dist > LIMIT 1 > ) > SELECT > ss.stationid, > ss.stationname, > ss.parkingbiketotcnt, > ss.dist, > es.stationid, > es.stationname, > es.shared, > es.dist, > ss.dist + es.dist as total_dist > FROM start_stations ss > CROSS JOIN end_stations es; > END; > $; > > -- ============================================================ > -- 5️⃣ get_stations_along_route: 경로 상의 대여소 검색 > -- ============================================================ > CREATE OR REPLACE FUNCTION get_stations_along_route( > start_lat double precision, > start_lng double precision, > end_lat double precision, > end_lng double precision, > buffer_meters integer DEFAULT 300 > ) > RETURNS TABLE( > station_id text, > station_name text, > current_bikes bigint, > empty_racks bigint, > latitude double precision, > longitude double precision, > distance_from_start double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > SELECT > bc.stationid, > bc.stationname, > bc.parkingbiketotcnt, > bc.shared, > bs.sta_lat, > bs.sta_long, > ST_Distance( > ST_SetSRID(ST_MakePoint(start_lng, start_lat), 4326)::geography, > bs.location > ) as dist > FROM bicycle_seoul bc > JOIN bicycle_station bs ON bc.stationid = bs.rent_id > WHERE bs.location IS NOT NULL > AND ST_DWithin( > ST_MakeLine( > ST_SetSRID(ST_MakePoint(start_lng, start_lat), 4326), > ST_SetSRID(ST_MakePoint(end_lng, end_lat), 4326) > )::geography, > bs.location, > buffer_meters > ) > ORDER BY dist; > END; > $; > > -- ============================================================ > -- 6️⃣ find_nearby_bicycle: 추가 대여소 검색 (bicycle_station 우선) > -- ============================================================ > CREATE OR REPLACE FUNCTION find_nearby_bicycle( > user_lat double precision, > user_lon double precision, > radius integer DEFAULT 500 > ) > RETURNS TABLE( > station_id text, > name text, > distance_m integer, > current_bikes bigint, > empty_racks bigint, > available_bikes bigint, > address text, > latitude double precision, > longitude double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > SELECT > bs.rent_id, > bs.rent_nm, > ROUND(ST_Distance( > bs.location, > ST_SetSRID(ST_MakePoint(user_lon, user_lat), 4326)::geography > ))::integer, > COALESCE(bc.parkingbiketotcnt, 0::bigint), > COALESCE(bc.shared, 0::bigint), > bs.hold_num, > bs.sta_add1 || ' ' || COALESCE(bs.sta_add2, ''), > bs.sta_lat, > bs.sta_long > FROM bicycle_station bs > LEFT JOIN bicycle_seoul bc ON bs.rent_id = bc.stationid > WHERE bs.location IS NOT NULL > AND ST_DWithin( > bs.location, > ST_SetSRID(ST_MakePoint(user_lon, user_lat), 4326)::geography, > radius > ) > ORDER BY bs.location <-> ST_SetSRID(ST_MakePoint(user_lon, user_lat), 4326)::geography > LIMIT 10; > END; > $; > > -- ============================================================ > -- 7️⃣ find_nearby_subway: 지하철역 검색 > -- ============================================================ > CREATE OR REPLACE FUNCTION find_nearby_subway( > user_lat double precision, > user_lon double precision, > radius integer DEFAULT 500 > ) > RETURNS TABLE( > station_name text, > line_name text, > distance_m integer, > latitude double precision, > longitude double precision > ) > LANGUAGE plpgsql > AS $ > BEGIN > RETURN QUERY > SELECT > ss.station_nm, > ss.line_nm, > ROUND(ST_Distance( > ss.location, > ST_SetSRID(ST_MakePoint(user_lon, user_lat), 4326)::geography > ))::integer as distance_m, > ss.station_lat, > ss.station_long > FROM subway_station ss > WHERE ss.location IS NOT NULL > AND ST_DWithin( > ss.location, > ST_SetSRID(ST_MakePoint(user_lon, user_lat), 4326)::geography, > radius > ) > ORDER BY ss.location <-> ST_SetSRID(ST_MakePoint(user_lon, user_lat), 4326)::geography > LIMIT 10; > END; > $; > > -- ============================================================ > -- ✅ 완료 메시지 > -- ============================================================ > DO $ > BEGIN > RAISE NOTICE '✅ 7개 RPC 함수 생성 완료!'; > RAISE NOTICE ' 1. get_station_detail'; > RAISE NOTICE ' 2. get_nearby_stations'; > RAISE NOTICE ' 3. get_all_stations'; > RAISE NOTICE ' 4. compare_routes'; > RAISE NOTICE ' 5. get_stations_along_route'; > RAISE NOTICE ' 6. find_nearby_bicycle'; > RAISE NOTICE ' 7. find_nearby_subway'; > END $; > ``` ### 5-7. Get Bicycle Nearby - 툴 설정 - Postgres Tool - 노드 이름 - Get Bicycle Nearby - 계정 연결 - Postgres Bicycle - 노드 설정 - Tool Description - Set Manually - Description - ⬇️⬇️⬇️ 아래 내용 입력 - Operation - Execute Query - Query - `SELECT * FROM find_nearby_bicycle({{ $fromAI('latitude', '위도', 'number') }}, {{ $fromAI('longitude', '경도', 'number') }}, {{ $fromAI('radius', '반경(m)', 'number', '', 500) }})` > [!danger] 내 위치 주변 따릉이 대여소 검색 (RPC) > ```text > 【내 위치 주변 따릉이 대여소 검색】 > 사용자의 현재 위치(위도/경도)를 기준으로 지정된 반경 내의 대여소를 거리 순으로 최대 10개 반환합니다. PostGIS 지리 계산 사용. > > 파라미터: > - lat (double precision): 사용자 위도 (예: 37.5665) > - lng (double precision): 사용자 경도 (예: 126.9780) > - radius_meters (integer, 기본값: 500): 검색 반경(미터) > > 반환값: > - station_id, station_name, current_bikes, empty_racks, total_racks, address, latitude, longitude > - distance_meters: 사용자로부터의 거리(미터) > > 사용 예시: > Q: "내 근처 따릉이 대여소 찾아줘" (위치 정보 필요) > → get_nearby_stations(37.5665, 126.9780, 500) > ``` ### 5-8. 그 외 Supbase 및 Postgres Tools - n8n 워크플로우 - ⬇️⬇️⬇️ 아래 스크립트 복사 및 붙여넣기 > [!important] n8n AI Agent Tools (Bicycle & Subway) JSON > ```json > { > "nodes": [ > { > "parameters": { > "operation": "getAll", > "tableId": "bicycle_station", > "limit": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Limit', `조회할 행의 개수입니다. `, 'number') }}", > "filterType": "string", > "filterString": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Filters__String_', `Supabase PostgREST 필터 문법을 사용합니다.\n\n예시:\n- rent_nm=like.*광화문* (이름에 광화문 포함)\n- hold_num=gte.30 (거치대 30개 이상)\n- sta_add1=like.*종로구* (종로구 소재)\n- hold_num=gte.25&sta_add1=like.*강남구* (복합 조건, AND)\n\n연산자:\n- eq (같음), neq (다름)\n- gt (크다), gte (크거나 같다)\n- lt (작다), lte (작거나 같다)\n- like (패턴, .*는 와일드카드`, 'string') }}" > }, > "type": "n8n-nodes-base.supabaseTool", > "typeVersion": 1, > "position": [ > -864, > 544 > ], > "id": "dffd99cf-7865-4914-b2e4-99b7a039eed8", > "name": "Get Bicycle Station", > "credentials": { > "supabaseApi": { > "id": "3N1oaJrQ9pJNKmw5", > "name": "Supabase Bicycle" > } > } > }, > { > "parameters": { > "operation": "getAll", > "tableId": "subway_station", > "limit": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Limit', `조회할 행의 개수입니다.`, 'number') }}", > "filterType": "string", > "filterString": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Filters__String_', `Supabase PostgREST 필터 문법을 사용합니다.\n\n예시:\n- line_nm=like.*2호선* (2호선 역들)\n- station_nm=like.*강남* (역명에 강남 포함)\n- line_nm=like.*2호선*&station_nm=like.*역삼* (복합 조건, AND)\n\n연산자:\n- eq (같음), neq (다름)\n- gt (크다), gte (크거나 같다)\n- lt (작다), lte (작거나 같다)\n- like (패턴, .*는 와일드카드)`, 'string') }}" > }, > "type": "n8n-nodes-base.supabaseTool", > "typeVersion": 1, > "position": [ > -224, > 544 > ], > "id": "3d9810d0-1d80-4898-a1d5-e30d3e1396b5", > "name": "Get Subway Station", > "credentials": { > "supabaseApi": { > "id": "3N1oaJrQ9pJNKmw5", > "name": "Supabase Bicycle" > } > } > }, > { > "parameters": { > "descriptionType": "manual", > "toolDescription": "【특정 따릉이 대여소 상세 정보 조회】\n대여소 이름(예: \"102. 광화문역 2번출구 앞\")으로 해당 대여소의 현재 자전거 수, 빈 거치대 수, 주소, 좌표 정보를 반환합니다.\n\n파라미터:\n- station_id_input (text): 대여소 이름 또는 ID\n\n반환값:\n- station_id: 대여소 ID\n- station_name: 대여소 이름\n- current_bikes: 현재 이용 가능한 자전거 수\n- empty_racks: 빈 거치대 수\n- total_racks: 전체 거치대 수\n- address: 주소\n- latitude/longitude: 위도/경도\n\n사용 예시:\nQ: \"광화문역 2번출구 대여소 정보 알려줘\"\n→ get_station_detail('102. 광화문역 2번출구 앞')", > "operation": "executeQuery", > "query": "SELECT * FROM get_station_detail('{{ $fromAI('station_id', '대여소 ID', 'string') }}')", > "options": {} > }, > "type": "n8n-nodes-base.postgresTool", > "typeVersion": 2.6, > "position": [ > -384, > 544 > ], > "name": "Get Bicycle Detail", > "id": "42a3ec13-2ef0-45ea-a6ad-069bf464e20d", > "credentials": { > "postgres": { > "id": "NX5FuBJvOyaJIfMd", > "name": "Postgres Bicycle" > } > } > }, > { > "parameters": { > "descriptionType": "manual", > "toolDescription": "【내 위치 주변 지하철역 검색】\n사용자의 현재 위치를 기준으로 지정된 반경 내의 지하철역을 거리 순으로 최대 10개 반환합니다.\n\n파라미터:\n- user_lat, user_lon: 사용자 위도/경도\n- radius (integer, 기본값: 500): 검색 반경(미터)\n\n반환값:\n- station_name: 지하철역 이름\n- line_name: 호선 정보\n- distance_m: 거리(미터)\n- latitude, longitude: 위도/경도\n\n사용 예시:\nQ: \"내 근처 지하철역 알려줘\"\n→ find_nearby_subway(37.5665, 126.9780, 500)\n\nQ: \"여기서 1km 이내 지하철역 찾아줘\"\n→ find_nearby_subway(37.5665, 126.9780, 1000)", > "operation": "executeQuery", > "query": "SELECT * FROM find_nearby_subway({{ $fromAI('latitude', '위도', 'number') }}, {{ $fromAI('longitude', '경도', 'number') }}, {{ $fromAI('radius', '반경(m)', 'number', '', 500) }})", > "options": {} > }, > "type": "n8n-nodes-base.postgresTool", > "typeVersion": 2.6, > "position": [ > -64, > 544 > ], > "name": "Get Subway Nearby", > "id": "b2dc2d3c-faca-47ee-844b-c5519cfbeb6e", > "credentials": { > "postgres": { > "id": "NX5FuBJvOyaJIfMd", > "name": "Postgres Bicycle" > } > } > }, > { > "parameters": { > "descriptionType": "manual", > "toolDescription": "【출발지-도착지 최적 따릉이 경로 비교】\n출발지 좌표에서 가장 가까운 자전거 있는 대여소, 도착지 좌표에서 가장 가까운 빈 거치대 있는 대여소를 찾아 총 이동 거리와 함께 반환합니다.\n\n파라미터:\n- start_lat, start_lng: 출발지 위도/경도\n- end_lat, end_lng: 도착지 위도/경도\n\n반환값:\n- start_station_id, start_station_name, start_bikes, start_distance\n- end_station_id, end_station_name, end_racks, end_distance\n- total_distance: 총 거리(미터)\n\n사용 예시:\nQ: \"강남역에서 홍대입구까지 따릉이 경로 추천해줘\"\n→ compare_routes(37.4979, 127.0276, 37.5572, 126.9239)", > "operation": "executeQuery", > "query": "SELECT * FROM compare_routes({{ $fromAI('start_lat', '출발 위도', 'number') }}, {{ $fromAI('start_lon', '출발 경도', 'number') }}, {{ $fromAI('end_lat', '도착 위도', 'number') }}, {{ $fromAI('end_lon', '도착 경도', 'number') }})", > "options": {} > }, > "type": "n8n-nodes-base.postgresTool", > "typeVersion": 2.6, > "position": [ > 112, > 544 > ], > "name": "Get Compare Routes", > "id": "ad31635e-9b2f-485b-8332-994dc1844ff0", > "credentials": { > "postgres": { > "id": "NX5FuBJvOyaJIfMd", > "name": "Postgres Bicycle" > } > } > }, > { > "parameters": { > "descriptionType": "manual", > "toolDescription": "【경로 상의 따릉이 대여소 검색】\n출발지-도착지 직선 경로의 지정된 버퍼(기본 300m) 내에 있는 모든 대여소를 출발지로부터 거리 순으로 반환합니다.\n\n파라미터:\n- start_lat, start_lng: 출발지 위도/경도\n- end_lat, end_lng: 도착지 위도/경도\n- buffer_meters (integer, 기본값: 300): 경로로부터의 검색 버퍼(미터)\n\n반환값:\n- station_id, station_name, current_bikes, empty_racks, latitude, longitude\n- distance_from_start: 출발지로부터의 거리(미터)\n\n사용 예시:\nQ: \"A에서 B로 가는 길에 있는 따릉이 대여소 알려줘\"\n→ get_stations_along_route(37.5665, 126.9780, 37.5172, 127.0473, 300)", > "operation": "executeQuery", > "query": "SELECT * FROM get_stations_along_route({{ $fromAI('start_lat', '출발 위도', 'number') }}::double precision, {{ $fromAI('start_lng', '출발 경도', 'number') }}::double precision, {{ $fromAI('end_lat', '도착 위도', 'number') }}::double precision, {{ $fromAI('end_lng', '도착 경도', 'number') }}::double precision, {{ $fromAI('buffer_meters', '경로 버퍼(m)', 'number', '', 300) }})", > "options": {} > }, > "type": "n8n-nodes-base.postgresTool", > "typeVersion": 2.6, > "position": [ > 272, > 544 > ], > "name": "Get Find Routes", > "id": "9ae597bf-e0e1-404d-af48-c412b61f0fe4", > "credentials": { > "postgres": { > "id": "NX5FuBJvOyaJIfMd", > "name": "Postgres Bicycle" > } > } > }, > { > "parameters": { > "descriptionType": "manual", > "toolDescription": "고급 사용자를 위한 직접 SQL 쿼리 실행 도구입니다. \n복잡한 통계 분석, 다중 테이블 JOIN, 집계 함수가 필요한 경우에만 사용하세요.\n\n사용 가능한 테이블:\n- bicycle_seoul: 현재 대여소 상태 (stationid, stationname, parkingbiketotcnt, shared, racktotcnt)\n- bicycle_station: 대여소 기본 정보 (rent_id, rent_nm, sta_add1, sta_lat, sta_long, hold_num, location)\n- subway_station: 지하철역 정보 (station_nm, line_nm, sta_lat, sta_lon, sta_add, location)\n\n예시 쿼리:\n- SELECT stationname, parkingbiketotcnt FROM bicycle_seoul WHERE parkingbiketotcnt > 20 ORDER BY parkingbiketotcnt DESC LIMIT 10\n- SELECT bs.rent_nm, bc.parkingbiketotcnt, bc.shared FROM bicycle_station bs JOIN bicycle_seoul bc ON bs.rent_id::text = bc.stationid WHERE bc.shared >= 5\n\n⚠️ 주의: 일반 검색/필터는 다른 전용 도구를 사용하세요.", > "operation": "executeQuery", > "query": "{{ $fromAI('query', 'SQL statement', 'string') }}", > "options": {} > }, > "type": "n8n-nodes-base.postgresTool", > "typeVersion": 2.6, > "position": [ > 400, > 544 > ], > "id": "118d0c38-0473-4667-9742-20dc2506e39b", > "name": "Get SQL Query", > "credentials": { > "postgres": { > "id": "NX5FuBJvOyaJIfMd", > "name": "Postgres Bicycle" > } > } > } > ], > "connections": { > "Get Bicycle Station": { > "ai_tool": [ > [] > ] > }, > "Get Subway Station": { > "ai_tool": [ > [] > ] > }, > "Get Bicycle Detail": { > "ai_tool": [ > [] > ] > }, > "Get Subway Nearby": { > "ai_tool": [ > [] > ] > }, > "Get Compare Routes": { > "ai_tool": [ > [] > ] > }, > "Get Find Routes": { > "ai_tool": [ > [] > ] > }, > "Get SQL Query": { > "ai_tool": [ > [] > ] > } > }, > "pinData": {}, > "meta": { > "templateCredsSetupCompleted": true, > "instanceId": "ed29603280f689e433d162d6eb2f4c0ef594feb614602d9f72d06ccb3a8d3e19" > } > } > ``` ## Step 6: AI 에이전트 설정 ### 6-1. When chat message received - 노드 기능 - Chat Trigger - 노드 이름 - When chat message received - 노드 설정 - Make Chat Publicly Available - 🟢활성화 - Mode - Embedded Chat - Authentication - None - Options - Add Field - Allowed Origins (CORS) - `*` ### 6-2. AI Agent - 노드 기능 - AI Agent - 노드 이름 - AI Agent - 노드 설정 - Source for Prompt - Connected Chat Trigger Node - Prompt (User Message) - `{{ $json.chatInput }}` - Require Specific Output Format - 🔴비활성화 - Enable Fallback Model - 🔴비활성화 - Options - Add Option - System Message - [[서울시 UDT 우리 동네 에이전트 프롬프트]] ### 6-3. Google Gemini Chat Model - 모델 설정 - Google Gemini Chat Model - 노드 이름 - Google Gemini Chat Model - 계정 연결 - Credential to connect with - Create new credential - Host - `https://generativelanguage.googleapis.com` - API Key - [구글 AI 스튜디오 API 키](https://aistudio.google.com/api-keys) - Get API key - 새 키 만들기 - 키 이름 지정 - 프로젝트 선택 - Create project - 프로젝트 이름 지정 - 할당량 등급 - 결제 설정 - 결제 계정 연결 혹은 관리 - Allowed HTTP Request Domains - All - 노드 설정 - Model - models/gemini-3-pro-preview ### 6-4. Simple Memory - 메모리 설정 - Simple Memory - 노드 이름 - Simple Memory - 노드 설정 - Session ID - Connected Chat Trigger Node - Session Key From Previous Node - `{{ $json.sessionId }}` - Context Window Length - 10 ### 6-5. Think - 툴 설정 - Think Tool - 노드 이름 - Think - 노드 설정 - Think Tool Description - 기본값 사용 ### 6-6. Seoul City Tools - 툴 설정 - MCP Client Tool - 노드 이름 - Seoul City Tools - 노드 설정 - Endpoint - Seoul City MCP Server - MCP URL - Production URL - Server Transport - HTTP Streamable - Authentication - Bearer Auth - Credential for Bearer Auth - n8n Bearer account - Tools to Include - All ### 6-7. Seoul Bicycle Tools - 툴 설정 - MCP Client Tool - 노드 이름 - Seoul Bicycle Tools - 노드 설정 - Endpoint - Seoul Bicycle MCP Server - MCP URL - Production URL - Server Transport - HTTP Streamable - Authentication - Bearer Auth - Credential for Bearer Auth - n8n Bearer account - Tools to Include - All ## Step 7: 웹훅 페이지 설정 ### 7-1. Webhook - 노드 기능 - Webhook - 노드 이름 - Webhook - 노드 설정 - Webhook URLs - Production URL - HTTP Method - GET - Path - seoul-udt - Authentication - None - Respond - Using 'Response to Wehbook' Node - Options - Add Option - Allowed Origins (CORS) - `* ### 7-2. Response to Webhook - 노드 기능 - Response to Webhok - 노드 이름 - Response to Webhok - 노드 설정 - Respond With - Text - Response Body - [[서울시 UDT 우리 동네 에이전트 웹훅 스크립트]] - const N8N_WEBHOOK_URL = 'YOUR Webhook URL"; - n8n Chat Trigger 노드 설정에서 "Webhook URL"을 복사해서 넣으세요. # Step 8: Genspark MCP Server ### 8-1. 젠스파크 MCP 추가 - 새로운 MCP 서버 추가 - Genspark - 도구 선택 - 추가 - 새로운 MCP 서버 추가 - Seoul City MCP, Seoul Bicycle MCP - 서버 이름 - 각 MCP 이름 - 서버 유형 - SSE - 서버 URL - 각 MCP 엔드포인트 - 예시 - `https://daniel8824.app.n8n.cloud/mcp/seoul-city` - 설명 - 각 MCP 설명 - 요청 헤더 - `{"Authorization": "Bearer MY_N8N_AUTH_TOKEN"}` ### 관련 노트 [[MOC_AI_자동화]] - [[에이전트 클래스 Chapter 7]] ## 🧠 Connected Insights > 📅 Last analyzed: 2026. 3. 14. 오후 8:12:37 > 💰 Analysis cost: $0.0172 ### 🔗 Related Notes - 🔗 [[자동화 클래스/자동화 클래스 Chapter 7.md]] - related: 두 노트 모두 n8n, Supabase, GIS, 데이터 자동화 및 시각화 등 유사한 기술 스택과 데이터 활용 흐름을 다루며, 공공데이터와 데이터 분석에 초점을 맞추고 있음. - Confidence: ████░ (80%) - 🔗 [[자동화 클래스/자동화 클래스 Chapter 8.md]] - related: 에이전트와 n8n을 활용한 자동화 및 데이터 연동, 그리고 AI 에이전트의 실제 적용 사례를 다루는 등 개념적으로 밀접하게 연결됨. - Confidence: ████░ (77%) - 🔼 [[에이전트 클래스/에이전트 클래스 Chapter 6.md]] - extends: Chapter 6에서 n8n AI 에이전트 제작 기초를 다루고, Chapter 8에서 실제 공공데이터와 GIS 데이터 연동 등 실전 적용 사례로 확장하고 있음. - Confidence: ████░ (76%) - 🔼 [[에이전트 클래스/에이전트 클래스 Chapter 7.md]] - extends: Chapter 7에서 n8n 기반 AI 에이전트의 텔레그램 연동 등 다양한 외부 서비스 통합을 다루고, Chapter 8에서는 실시간 공공데이터와의 연동 및 생활형 AI 비서 구현으로 내용을 확장함. - Confidence: ████░ (76%) - ✅ [[데이터 클래스/데이터 클래스 Chapter 11.md]] - supports: Chapter 11은 n8n과 Supabase, RAG, Embeddings 등 지식 베이스 구축과 AI 에이전트의 데이터 활용을 다루며, Chapter 8의 공공데이터 연동 및 AI 비서 구현을 기술적으로 뒷받침함. - Confidence: ████░ (75%) ### 📚 Knowledge Gaps - 🔴 **실제 AI 에이전트의 분석/추천 로직 상세화** - 노트는 다양한 공공데이터(따릉이, 지하철, 문화행사 등) 통합과 생활형 AI 비서의 인터페이스를 소개하지만, 사용자가 질문했을 때 AI가 어떤 방식으로 데이터를 분석·추천하는지 구체적인 로직이나 워크플로우가 부족함. - Suggested resources: https://docs.n8n.io/workflows/ai/, https://developers.naver.com/docs/serviceapi/search/news/news.md - 🟡 **보안 및 개인정보 보호** - 공공데이터와 사용자의 위치/개인정보를 다루는 서비스 특성상, 데이터 보안 및 개인정보 보호에 대한 언급이 필요함. - Suggested resources: https://www.kisa.or.kr/, https://gdpr-info.eu/ - 🟡 **실전 사례 및 한계(운영 경험)** - 실제 서비스 운영 시 발생할 수 있는 문제(예: API 장애, 데이터 품질, 실시간성 한계 등)와 이를 극복한 사례 또는 한계점에 대한 논의가 부족함. - Suggested resources: https://engineering.linecorp.com/ko/blog/line-ai-agent/, https://medium.com/n8n-io - 🟢 **사용자 피드백 및 서비스 개선 루프** - AI 비서 서비스의 품질 향상을 위한 사용자 피드백 수집, 분석, 개선 프로세스가 다뤄지지 않음. - Suggested resources: https://uxdesign.cc/feedback-loops-in-ai-products-6e7e2e2f2c9a, https://www.productplan.com/glossary/feedback-loop/ ### 💡 AI Insights ‘에이전트 클래스 Chapter 8’ 노트는 n8n 기반 AI 생활 비서의 실제 구현 사례를 공공데이터 연동, GIS, Supabase 등과 결합하여 구체적으로 제시하고 있다. 관련 노트들과 비교해볼 때, 이 노트는 실전 적용에 초점을 맞추고 있으나, 분석 로직의 구체화, 보안/운영상의 실무적 이슈, 사용자 피드백 등 실전 서비스 관점에서의 심화 내용이 부족하다. 따라서, 기술적 구현과 실제 서비스 운영 사이의 연결고리를 강화하면 지식 체계가 더욱 견고해질 것이다.