# 길 — 마음이 향하는 국내 여행 한국관광공사 TourAPI의 관광지·문화시설로 국내 여행 일정을 만드는 웹앱입니다. 최대 7일까지 계획하고, 날짜별 출발지와 교통편을 정한 뒤 방문 장소·이동시간·메모를 수정할 수 있습니다. ## 실행 Node.js 20 이상이 필요합니다. 별도 패키지 설치는 필요하지 않습니다. ```powershell cd 'C:\Users\User\OneDrive\문서\ChatGPT\해커톤\여행의-결' npm start ``` 브라우저에서 http://127.0.0.1:3000 을 엽니다. `index.html`을 파일로 직접 열면 API 연동이 동작하지 않습니다. 프로젝트의 `.env`에 공공데이터포털 공통 인증키를 설정합니다. 형식은 `.env.example`을 참고합니다. ```dotenv DATA_GO_KR_SERVICE_KEY=공공데이터포털_인증키 ``` `DATA_GO_KR_SERVICE_KEY`는 관광지·음식점 조회와 TAGO 서비스가 공유합니다. 공통 키를 쓰더라도 사용할 API의 활용신청은 각각 승인되어 있어야 합니다. 공공데이터포털의 디코딩 키와 인코딩 키를 모두 지원하며, 이전 `TOURAPI_SERVICE_KEY`·`TAGO_SERVICE_KEY` 설정도 호환합니다. 음식점도 기존 TourAPI로 조회하므로 별도 맛집 키는 필요하지 않습니다. 키를 바꿨으면 서버를 다시 시작합니다. ## 일정 만들기와 편집 - 여행지·시작일·기간을 선택해 날짜별 일정을 생성합니다. 기간은 해당 지역에서 구성할 수 있는 권역 수에 따라 1~7일 중 가능한 값만 표시합니다. - 여행지 목록과 관광지·문화시설의 이름·주소·좌표·유형은 TourAPI에서 불러옵니다. 실제 좌표로 가까운 장소를 묶고, 대표 장소 반경 2.5km 안에 장소가 3곳 이상인 권역을 사용합니다. 행정구역이나 실제 도로 경로를 기준으로 묶은 결과는 아닙니다. - 날짜별 방문 권역을 변경·교환하거나 같은 권역의 다른 장소로 교체할 수 있습니다. 중복 방문을 확인하고 시간을 다시 계산합니다. - 방문 권역 목록 위의 검색창에서 지역명이나 권역에 속한 관광지 이름으로 목록을 좁힐 수 있습니다. 검색과 선택만으로 일정이 바뀌지는 않으며 ‘변경 적용’을 누르면 반영합니다. - 관광지는 체류시간과 메모를, 자유 시간·식사·이동·휴식은 이름·소요시간·메모를 수정할 수 있습니다. 수정한 시간에 맞춰 뒤 일정도 이동합니다. - 출발 시각은 시·분 선택창의 **선택 완료**를 누르면 즉시 반영·저장됩니다. 별도의 ‘시간 적용’ 버튼은 없습니다. - 마지막 ‘오늘 여행 마무리’는 숙소로 돌아가기·귀가 등으로 이름을 바꾸고 이동시간을 더할 수 있습니다. 이동이 없으면 0시간 0분으로 둡니다. - 인쇄·PDF 버튼은 브라우저 인쇄 창을 엽니다. 기본 출발 시각은 09:00이며 18시 종료 제한은 없습니다. 자정을 넘으면 다음 날로 표시합니다. 기본 체류시간과 권역 안 이동시간은 앱의 예상값이며, 식사 이름은 배치 시각에 맞춰 표시합니다. 직접 수정한 식사 이름은 유지합니다. ## 출발지와 교통편 시간표 이동수단과 출발지·도착지는 날짜마다 선택합니다. 기차·시외/고속버스·비행기·배는 TAGO의 공식 역·터미널·공항·항구 목록에서 검색합니다. 버스는 고속버스와 시외버스를 구분하며 출발지와 도착지가 같은 서비스여야 시간표를 조회할 수 있습니다. 출발지와 도착지를 정한 뒤 ‘예상시간’을 펼치면 해당 날짜의 운행 시간표를 불러옵니다. TAGO의 **열차·고속버스·시외버스·국내항공·국내여객선 5개 서비스**를 연결했습니다. 교통편을 선택하면 출발·도착 시각과 소요시간을 반영하고, 뒤의 관광 일정을 함께 조정합니다. 선택한 교통편은 날짜별 `dayTransports`에 저장해 새로고침 후 복원합니다. 시외버스 API는 **한국시간 기준 당일 시간표만 조회**할 수 있습니다. 다른 날짜나 운행편이 없는 구간은 제공되는 결과를 안내하며 임의의 교통편을 만들지 않습니다. 시간표는 예정된 운행 시각이며 지연·결항 등을 반영한 실시간 도착 예측이나 예약 서비스가 아닙니다. 시내·마을버스, 지하철, 자가용·렌터카·택시·도보는 장소·주소 검색 후 이동시간을 직접 입력합니다. 이 수단들의 목적지까지 경로와 예상 도착시간은 자동 조회하지 않습니다. 장소·주소 검색은 Photon / OpenStreetMap을 사용하며, 후보의 주소를 확인해 선택합니다. 지역 간 교통편은 ‘출발지 → 도착역·터미널·공항·항구’와 ‘도착지 → 첫 관광지’를 나눕니다. 시간표를 선택해도 첫 관광지까지 이동시간은 별도로 입력합니다. 직접 입력으로 전환할 수 있으며, 출발 구간의 수동 이동시간은 최대 **72시간**까지 입력할 수 있습니다. 시간·분 입력값을 더해 예상 시각을 계산하며 대기·환승이 필요하면 직접 포함해야 합니다. 필요한 이동시간이 0분인 상태는 미정으로 표시합니다. 출발 시각이나 해당 이동시간을 직접 바꾸면 선택한 시간표 연결을 해제합니다. 출발지·도착지·이동수단을 바꾸면 관련 교통편과 구간 시간을 다시 정합니다. 방문 권역만 바꾸면 선택한 지역 간 교통편은 유지하고 새 첫 관광지까지의 이동시간을 초기화합니다. ## 식사 시간의 주변 맛집 식사 항목의 ‘지역 맛집 보기’를 펼치면 **한국관광공사 TourAPI의 음식점 정보(contentTypeId=39)**를 조회합니다. 음식 종류를 고르는 단계 없이 해당 지역의 음식점을 함께 보여줍니다. 식사 직전 관광지 주소의 시·군·구를 확인해 같은 행정구역의 음식점으로 좁힙니다. 예를 들어 대전 유성구 일정에는 동구 식당을 섞지 않습니다. 시·군·구가 없는 주소는 시도 범위임을 표시합니다. 운영 서버는 지역코드별로 최대 100건씩 3페이지를 조회하고, 요청한 시·군·구를 주소의 행정구역 토큰으로 대조합니다. 기준 좌표가 있으면 이 후보 안에서 제공 좌표 사이의 직선거리로 정렬해 최대 50곳을 반환합니다. 화면은 처음 5곳을 표시하고 ‘더 보기’로 10곳씩 추가합니다. 좌표가 없는 곳은 거리 계산에서 제외합니다. 직선거리는 도보·차량의 실제 경로나 소요시간이 아닙니다. TourAPI에 등록된 음식점을 제공하며 전국 모든 음식점이 포함되거나 맛·평점이 보증된 목록은 아닙니다. 음식점 이름·주소·제공 좌표 등을 사용합니다. 방문 전 운영 여부와 위치를 확인해야 합니다. 이전 문화정보원 API는 문화·체육·관광시설 주변 자료로 범위가 좁아 기본 조회에서 제외했으며, 기존 `.env`의 문화 API 키는 변경하지 않았습니다. 첫 조회는 제공기관의 응답에 따라 시간이 걸릴 수 있습니다. 10초가 지나면 대기 안내를 표시하며, 화면은 최대 70초까지 응답을 기다립니다. 같은 지역의 결과는 1시간 캐시합니다. ## 저장과 데이터 출처 일정·편집 내용·날짜별 출발 시각·출발/도착지·선택한 교통편은 이 브라우저의 로컬 저장소에 저장합니다. 저장된 관광 일정은 TourAPI 정보를 다시 불러와 장소 ID를 확인한 뒤 복원합니다. 조회 실패나 삭제된 장소로 복원할 수 없으면 오류를 표시하고 저장본을 보존합니다. 다른 브라우저나 기기와 자동 동기화하지 않습니다. 데이터 제공기관, 공식 API 링크와 저작권 표시는 화면 맨 아래의 **[데이터 출처 안내](sources.html)** 페이지에 모았습니다. TourAPI 관광·음식점 정보, TAGO 5개 서비스와 Photon / OpenStreetMap을 안내하며 **© OpenStreetMap contributors**, **Photon by komoot**를 표시합니다. 체류·수동 이동시간은 예상값입니다. 관광지의 운영시간·휴무·예약·요금·날씨는 일정 생성 조건에 반영하지 않습니다. 교통편의 실제 운행과 관광지·음식점의 운영 여부는 방문 전에 확인해야 합니다. ## API와 서버 브라우저는 같은 서버의 API만 호출하고 인증키는 서버에서 읽습니다. - `/api/tour/areas`, `/api/tour/places`: 여행지 목록과 관광정보. 지역별 1시간 캐시, 관광 유형별 최대 3페이지 조회. - `/api/transport/stops`, `/api/transport/trips`: 공식 교통기관 목록과 출도착 운행 시간표. 기관 목록은 24시간, 시간표는 5분 캐시. - `/api/departures/search`: Photon 장소·주소 검색. 검색 버튼이나 Enter를 누를 때만 요청하며 초당 1회로 제한하고 같은 검색어는 1시간 캐시. - `/api/restaurants`: TourAPI 지역별 음식점 전체 조회. 지역별 1시간 캐시, 기준 좌표가 있으면 직선거리 정렬. 결과가 조회 한도에서 잘리면 응답의 `truncated`로 표시합니다. 외부 API 실패를 임의의 데이터로 대체하지 않습니다. Photon 공개 데모는 과제용 검색에 사용하며 서비스 규모가 커지면 별도 운영 환경이 필요합니다. 정적 파일은 허용 목록으로 제공하므로 `.env`, 서버 소스, ZIP 파일을 웹에서 열 수 없습니다. `.env`는 Git 추적에서 제외합니다. 로컬 서버는 기본적으로 이 컴퓨터의 `127.0.0.1`에서만 실행합니다. ## Cloudflare Pages에 업로드 배포용 ZIP에는 화면 파일과 API 처리를 위한 `_worker.js`가 함께 들어갑니다. `.env`, 서버 원본, 테스트, `node_modules`는 포함하지 않습니다. 별도의 Node 서버 없이 Cloudflare Pages의 Worker에서 같은 API를 처리합니다. 배포 파일을 다시 만들 때만 개발 도구를 설치하고 다음 명령을 실행합니다. ```powershell npm ci npm run build:cloudflare ``` 1. 상위 작업 폴더의 **`길-Cloudflare-업로드.zip`**을 Cloudflare Pages의 ‘프로젝트 자산 업로드’ 칸에 넣습니다. ZIP을 풀거나 프로젝트 전체 폴더를 올릴 필요는 없습니다. 2. 처음 배포해 프로젝트를 만든 뒤 **Settings → Variables and Secrets → Add**에서 `DATA_GO_KR_SERVICE_KEY`를 추가합니다. 값은 로컬 `.env`의 기존 공통 인증키를 사용하고 **Encrypt**를 선택해 저장합니다. 키 이름과 값만 입력하며 `=`는 넣지 않습니다. 3. 같은 ZIP으로 새 배포를 만듭니다. 새로 등록한 Secret은 이후 배포부터 적용되므로, 첫 배포 뒤 키를 추가했다면 다시 배포해야 합니다. 공개 사이트에서 관광지·교통편·음식점 조회를 확인합니다. ZIP은 이미 빌드된 파일이므로 대시보드의 직접 업로드에는 빌드 명령이나 Node 호환 설정이 필요하지 않습니다. 키는 사용하는 배포 환경에 등록합니다. 실제 키를 ZIP이나 공개 파일에 넣지 않습니다. 공식 문서: [직접 업로드](https://developers.cloudflare.com/pages/get-started/direct-upload/), [Worker 고급 모드](https://developers.cloudflare.com/pages/functions/advanced-mode/), [Secret 설정](https://developers.cloudflare.com/pages/functions/bindings/#secrets). 로컬 Cloudflare 환경은 ZIP을 만든 뒤 `npm run dev:cloudflare`로 확인할 수 있습니다. Wrangler는 로컬 `.env`를 읽으며 실제 배포 시에는 대시보드의 Secret을 사용합니다. 캐시는 Worker 인스턴스별 메모리에 저장되므로 새 인스턴스에서는 다시 조회합니다. ## 검증 ```powershell npm test ``` 자동 테스트 175개가 통과했습니다. 외부 API를 대신한 테스트 응답으로 서버와 Cloudflare Worker의 오류 처리·캐시·정적 파일 차단, 관광정보 변환, 시간표 정규화·선택·복원, 음식점 거리 정렬·시군구 범위·빈 목록 형식, 일정 편집·저장과 날짜·시간 선택을 확인합니다. Worker의 HTML 주소 처리와 인증키 비노출, 외부 리디렉션 차단도 확인합니다. 실제 TourAPI 지역·관광정보 조회와 TAGO 5개 서비스의 기관 목록·운행 시간표 조회를 확인했습니다. TourAPI 부산 음식점 총301건 중 표본3건의 이름·주소·좌표를 확인했습니다. 표시 한도를50곳으로 늘린 실행 서버에서 대전 유성구 음식점41건 모두 유성구 주소임을 확인했고, 화면에서도 분류 선택 없이 시군구 범위가 표시됩니다. 제공 건수와 가능한 운행편은 조회 날짜와 제공기관의 갱신에 따라 달라집니다. Cloudflare 로컬 workerd에서도 첫 화면·출처 페이지, 실제 지역17곳·서울 관광정보363건·유성구 음식점41건·출발지 검색·열차역 검색을 확인했습니다. ZIP 압축 무결성과 실제 인증키가 포함되지 않았는지도 검사했습니다. 공개 Cloudflare 주소에서의 동작은 업로드와 Secret 등록 후 확인합니다. ## 주요 파일 - `server.mjs`: 로컬 서버, API 연결과 정적 파일 제공 - `tour-client.mjs`: Node와 Cloudflare에서 공유하는 TourAPI 클라이언트 - `cloudflare-worker.mjs`, `public-assets.mjs`: Cloudflare API 처리와 공개 파일 허용 목록 - `build-cloudflare.mjs`, `wrangler.jsonc`: Cloudflare 업로드 ZIP 생성과 로컬 실행 설정 - `tour-data.js`, `planner.js`: 관광정보 변환, 일정 생성·편집·교통편 반영 - `app.js`: 화면과 저장·복원 - `tago-server.mjs`, `transport-ui.js`: TAGO 기관·시간표 조회와 교통편 선택 - `departure-server.mjs`, `departure-ui.js`: 장소·주소 검색과 날짜별 이동수단·출발/도착지 선택 - `tour-restaurants.mjs`, `restaurant-ui.js`: 지역 음식점 조회·거리 정렬과 식사 항목의 펼침 목록 - `date-picker.js`, `time-picker.js`, `select-ui.js`: 날짜·시간·선택 목록 - `sources.html`, `sources.css`: 데이터 출처와 이용 안내 - `index.html`, `styles.css`, `free-theme.css`: 화면과 여행 노트 디자인 - `travel-sticker-v1.png`: 이미지 생성 도구로 만든 종이비행기 스티커. 프롬프트는 `artifacts/travel-sticker-prompt.txt`에 보관합니다. 복사된 과거 제출 문안과 ZIP 파일은 이전 버전의 자료입니다. 현재 기능과 실행 방법은 이 문서를 기준으로 합니다.