PageSourceSearch

https://seonamhae-grand.pages.dev/localinfo.js

js seonamhae-grand.pages.dev collected 2026-10-03 05:32:28 UTC 29,603 bytes, 369 lines download raw bytes

1// 현지 정보(Local Info) — 한국관광 데이터랩/TourAPI 연동 + 목업 폴백
2// TourAPI 기반 카테고리는 실API(/api/tour) 우선, 실패 시 목업으로 폴백.
3// 교통·안전은 큐레이션 콘텐츠(데이터랩 API 아님) → data-i18n으로 다국어.
4
5const TOURAPI_TYPE = { stay: 32, food: 39, heritage: 14, activity: 28 };
6// TourAPI 계열 서비스 재활용(반려동물 동반여행) — KorService2와 동일 파라미터(area/sigungu via region)
7const TOURAPI_SVC = { pet: "KorPetTourService2" };
8// 중심관광지(LocgoHubTarService1)는 데이터랩 계열: 행정표준코드 signguCd 필수 → 전남 시군 매핑
9const HUB_SIGNGU = { mokpo: "46110", muan: "46840", sinan: "46910", jindo: "46900", wando: "46890", boseong: "46780", haenam: "46820",
10  yeosu: "46130", suncheon: "46150", goheung: "46770", jangheung: "46800", gangjin: "46810", yeongam: "46830", hampyeong: "46860", yeonggwang: "46870" };
11
12// 교통·안전 — 큐레이션(데이터랩 아님). data-i18n 키로 다국어.
13const CURATED = {
14  transport: [
15    { icon: "✈️", t: "local.tr.air.t", d: "local.tr.air.d", tags: ["무안국제공항 (MWX)", "목포 관문", "오사카·상하이·타이베이"] },
16    { icon: "🚄", t: "local.tr.rail.t", d: "local.tr.rail.d", tags: ["목포역 · 광주송정역", "KTX·SRT", "용산 2h30m"] },
17    { icon: "⛴", t: "local.tr.ship.t", d: "local.tr.ship.d", tags: ["목포·완도 여객선터미널", "한국해운조합 도선", "고흥 나로도항", "진도 팽목항"] },
18    { icon: "🚌", t: "local.tr.bus.t", d: "local.tr.bus.d", tags: ["전남 공영버스", "재즈 익스프레스"] },
19    { icon: "🚗", t: "local.tr.car.t", d: "local.tr.car.d", tags: ["렌터카", "천사대교·서해안고속"] },
20    { icon: "🚲", t: "local.tr.bike.t", d: "local.tr.bike.d", tags: ["증도·청산도 슬로시티", "자전거 대여"] },
21    // 다중관문체계(Multi-Gateway) — 무안공항 단일의존 대신 A~D 4단계로 설계(설계 제안).
22    { icon: "🌐", t: "local.tr.gw.t", d: "local.tr.gw.d", tags: ["A 제주공항(현재)", "B 인천공항+KTX(현재)", "C 무안공항(재개항 후)", "D 목포항·완도항(현재)"] }
23  ],
24  safety: [
25    { icon: "📡", t: "local.sf.comm.t", d: "local.sf.comm.d", tags: ["스타링크 위성 안전망", "오지 K-Eco Hotspot"] },
26    { icon: "🆘", t: "local.sf.sos.t", d: "local.sf.sos.d", tags: ["위성 SOS", "112 · 관광경찰"] },
27    { icon: "🏥", t: "local.sf.med.t", d: "local.sf.med.d", tags: ["응급의료 안내", "마을 원격 의료부스"] },
28    { icon: "🌊", t: "local.sf.sea.t", d: "local.sf.sea.d", tags: ["물때·풍랑 알림", "도선 운항 상태"] }
29  ],
30  // 고려인의 서남해 (F-07 「귀향」) — 실재 사료·현장 기반. 출처: docs/09_고려인_귀향_설계서 §10
31  koryoin: [
32    { icon: "🏛", t: "local.ko.center.t", d: "local.ko.center.d", tags: ["광주 광산구 월곡동", "2021 개관", "유물 1.2만점"] },
33    { icon: "🕊️", t: "local.ko.park.t", d: "local.ko.park.d", tags: ["다모아어린이공원", "2021 유해봉환 기념"] },
34    { icon: "🎨", t: "local.ko.art.t", d: "local.ko.art.d", tags: ["2024 개관", "고려인 화가 상주"] },
35    { icon: "🤝", t: "local.ko.support.t", d: "local.ko.support.d", tags: ["종합지원센터", "러시아어 상담·뿌리찾기 연계"] }
36  ],
37  // 2026 섬 방문의 해 — 전남광주 「섬 반값여행」(공식 발표) 8개 시군 16개 섬 + 「섬의 날(8.8)」 추천 섬 재구성.
38  // 정직 원칙: 환급(50%)이 아니라 주민환원·인바운드 자부심으로 메시지 재구성, 신분증 요건 등 한계 명시.
39  islands2026: [
40    { icon: "🏝", t: "local.is.policy.t", d: "local.is.policy.d", tags: ["전남광주통합특별시 공식 발표", "2026.8.29~11.4", "8개 시군 16개 섬"] },
41    { icon: "⚓", t: "local.is.mokpo.t", d: "local.is.mokpo.d", tags: ["목포연안여객선터미널 출항", "달리도·외달도"] },
42    { icon: "🥾", t: "local.is.yeosu.t", d: "local.is.yeosu.d", tags: ["여수 현지 여객선 경유", "하화도·사도·거문도·개도·금오도"] },
43    { icon: "🌿", t: "local.is.goheung.t", d: "local.is.goheung.d", tags: ["고흥 나로도항 출항", "쑥섬·애도·연홍도"] },
44    { icon: "🦪", t: "local.is.boseong.t", d: "local.is.boseong.d", tags: ["보성 벌교 인근 선착장", "장도(람사르 습지)"] },
45    { icon: "🌉", t: "local.is.gangjin.t", d: "local.is.gangjin.d", tags: ["강진 도보 연도교(출렁다리)", "가우도"] },
46    { icon: "🐦", t: "local.is.yeonggwang.t", d: "local.is.yeonggwang.d", tags: ["영광 현지 선착장 경유", "송이도(천연기념물 서식지)"] },
47    { icon: "🌊", t: "local.is.jindo.t", d: "local.is.jindo.d", tags: ["진도 팽목항 출항", "관매도(다도해해상국립공원)"] },
48    { icon: "💜", t: "local.is.sinan.t", d: "local.is.sinan.d", tags: ["신안 현지 여객선 경유", "흑산ë
48„·증도·반월박지도"] },
49    // 우리의 제안 — 「섬의 날(8.8)」·「섬 반값여행」 정신을 이어받은 인바운드 전용 오리지널 프로그램(설계 제안, B2G 협업 필요)
50    { icon: "📔", t: "local.is.challenge.t", d: "local.is.challenge.d", tags: ["설계 제안 · B2G 협업 필요", "디지털 섬 스탬프", "L-임팩트 뱃지 연계"] }
51  ],
52  // 서남해 섬트레일 — 산티아고 순례길·국내 동서트레일(849km·단계적 개통) 벤치마킹.
53  // 섬은 도보로 안 이어지므로 '아일랜드 호핑 트레일'로 설계. 거리는 전부 실제 확인된 값만 표기(정직 원칙).
54  // 거점(관문) 9곳 중심 구성 — 교통·숙박·관광지·문화유산 4요소를 각 게이트웨이 카드에 포함(전남광주 27개 지자체 중 섬 보유 주요 관문).
55  islandtrail: [
56    { icon: "🎒", t: "local.it.intro.t", d: "local.it.intro.d", tags: ["산티아고·동서트레일 벤치마킹", "거점 9곳·13개+ 섬", "단계적 확장"] },
57    { icon: "⚓", t: "local.it.gw.mokpo.t", d: "local.it.gw.mokpo.d", tags: ["무안공항·KTX·여객선터미널", "근대역사관(문화유산)", "외달도·달리도"] },
58    { icon: "💜", t: "local.it.gw.sinan.t", d: "local.it.gw.sinan.d", tags: ["1004대교·현지 여객선", "태평염전(국가등록문화유산)", "흑산도·증도·반월박지도"] },
59    { icon: "🚶", t: "local.it.gw.wando.t", d: "local.it.gw.wando.d", tags: ["완도 여객선터미널", "장보고 청해진 유적(문화유산)", "청산도 슬로길 42.195km"] },
60    { icon: "🥾", t: "local.it.gw.yeosu.t", d: "local.it.gw.yeosu.d", tags: ["여수여객선터미널·KTX", "거문도 등대·뱃노랫길", "금오도 18.5km·하화도 5.7km"] },
61    { icon: "🚀", t: "local.it.gw.goheung.t", d: "local.it.gw.goheung.d", tags: ["나로도항", "나로우주센터", "연홍도 둘레길 4km"] },
62    { icon: "🌊", t: "local.it.gw.jindo.t", d: "local.it.gw.jindo.d", tags: ["팽목항", "진도아리랑·강강술래(무형유산)", "관매도"] },
63    { icon: "🌉", t: "local.it.gw.gangjin.t", d: "local.it.gw.gangjin.d", tags: ["육로 연도교(배 불필요)", "다산초당(국가지정문화유산)", "가우도"] },
64    { icon: "🐦", t: "local.it.gw.yeonggwang.t", d: "local.it.gw.yeonggwang.d", tags: ["현지 선착장", "불갑사(문화유산)", "송이도 몽돌해변 4km"] },
65    { icon: "🦪", t: "local.it.gw.boseong.t", d: "local.it.gw.boseong.d", tags: ["벌교 인근 선착장", "뻘배(국가어업유산)", "장도(람사르 습지)"] },
66    { icon: "🔢", t: "local.it.tier.t", d: "local.it.tier.d", tags: ["① 1개 섬 입문형", "② 2개 섬 도전형", "③ 3개 섬+ 순례자형", "예약·결제 향후 연계"] },
67    { icon: "🌱", t: "local.it.esg.t", d: "local.it.esg.d", tags: ["디지털 스탬프·완주증", "클린 트레일(해안쓰레기 신고)", "데이터연금 연계"] },
68    // 출발 전 안전 체크리스트 — 실재 공공기관 4곳만 인용(하드웨어·기기 대여 없음, 정보 제공만). 링크는 각 기관 대표 홈페이지.
69    { icon: "📋", t: "local.it.safety.t", d: "local.it.safety.d", tags: ["바다누리(khoa.go.kr) 물때", "KOMSA(komsa.or.kr) 운항정보", "기상청(weather.go.kr) 풍랑특보", "Emergency Ready App(다국어)"] },
70    // 벤치마킹 — 실존 단체(같은 대표가 이끄는 비영리+벤처 결합 모델). '클린 트레일' 기능의 실제 선례로 인용, 협업은 제안 단계.
71    { icon: "🧹", t: "local.it.bench.t", d: "local.it.bench.d", tags: ["사단법인 섬즈업(월1회 정기방문)", "무인도섬테마연구소(해양관광벤처)", "협업 제안·설계 참고"] }
72  ],
73  // 대한민국 경계의 섬 — 영해기점 ESG 챌린지. 섬즈업·무인도섬테마연구소 모델을 계승해
74  // ①방문 서비스 ②ESG 가치실현 ③서남해 코리도 확산 로드맵으로 설계.
75  // 정직 원칙: 실제 수치(23=내륙3+유인7+무인도서13)로 정정, 무인도 세부목록은 향후 검증 단계로 명시.
76  borderislands: [
77    { icon: "🧭", t: "local.bi.intro.t", d: "local.bi.intro.d", tags: ["23개 영해기점(전국)", "전남권 유인 기점섬 5곳", "섬즈업 모델 계승"] },
78    { icon: "🏝", t: "local.bi.gageo.t", d: "local.bi.gageo.d", tags: ["신안", "최서남단", "영해기점"] },
79    { icon: "🔴", t: "local.bi.hongdo.t", d: "local.bi.hongdo.d", tags: ["신안", "다도해해상국립공원", "영해기점"] },
80    { icon: "🏆", t: "local.bi.yeoseo.t", d: "local.bi.yeoseo.d", tags: ["완도", "2025 올해의 섬", "영해기점"] },
81    { icon: "🚢", t: "local.bi.geomun.t", d: "local.bi.geomun.d", tags: ["여수", "2026 올해의 섬", "영해기점"] },
82    { icon: "⛵", t: "local.bi.hoengdo.t", d: "local.bi.hoengdo.d", tags: ["영광", "영해기점"] },
83    { icon: "🌱", t: "local.bi.esg.t", d: "local.bi.esg.d", tags: ["해양쓰레기 정화 인증", "국토경계 지킴이 배지", "데이터연금·ESG 리포트 연계"] },
84    { icon: "🗺", t: "local.bi.roadmap.t", d: "local.bi.roadmap.d", tags: ["1단계: 유인기점 5곳", "2단계: 무인도서 검증·추가", "3단계: 서남해 코리도 전체"] }
85  ],
86  // 서남해 산·섬 10개 코스 안전검증표 — 완도군·여수시 공식 관광정보 + KOMSA + 기상청 교차검증(기준시점 202
866-08-22).
87  // 원칙: 공식자료로 확인 안 된 화장실·대피로·119접근성은 임의로 채우지 않고 "불확실"로 표시(정직 원칙).
88  coursesafety: [
89    { icon: "📋", t: "local.cs.intro.t", d: "local.cs.intro.d", tags: ["대피·탈출로: 10개 코스 전부 공식 미확인", "119 접근성: 전부 불확실·현장확인 필요", "선박: KOMSA→선사→기상청→현지안내 순 확인", "국립공원(팔영산·월출산)은 별도 기준, 이 표 제외"] },
90    { icon: "📊", t: "local.cs.table.t", d: "local.cs.table.d", wide: true, tags: [] },
91    { icon: "①", t: "local.cs.c1.t", d: "local.cs.c1.d", tags: ["완도 청산도", "공식 5.71km·90분", "안전판정: 우선 검토"] },
92    { icon: "②", t: "local.cs.c2.t", d: "local.cs.c2.d", tags: ["완도 청산도", "범바위길~용길", "안전판정: 추가 현장검증 필요"] },
93    { icon: "③", t: "local.cs.c3.t", d: "local.cs.c3.d", tags: ["여수 금오도", "5km·2h·하(입문자 적합)", "안전판정: 단, 절벽 주의"] },
94    { icon: "④", t: "local.cs.c4.t", d: "local.cs.c4.d", tags: ["여수 금오도", "3.5km·1h30m·하", "안전판정: 초보자 후보"] },
95    { icon: "⑤", t: "local.cs.c5.t", d: "local.cs.c5.d", tags: ["여수 금오도", "3.5km·2h·상", "안전판정: 초보자 상품 제외 권장"] },
96    { icon: "⑥", t: "local.cs.c6.t", d: "local.cs.c6.d", tags: ["여수 하화도", "5.7~6km·3h·하", "안전판정: 조건부 적합·1순위 추천"] },
97    { icon: "⑦", t: "local.cs.c7.t", d: "local.cs.c7.d", tags: ["여수 개도", "4.5km·1h30m·하", "안전판정: 조건부"] },
98    { icon: "⑧", t: "local.cs.c8.t", d: "local.cs.c8.d", tags: ["여수 개도", "3.4km·2h30m·상", "안전판정: 초보자 제외 권장"] },
99    { icon: "⑨", t: "local.cs.c9.t", d: "local.cs.c9.d", tags: ["여수 낭도", "40분·하", "안전판정: 조건부·3순위 추천"] },
100    { icon: "⑩", t: "local.cs.c10.t", d: "local.cs.c10.d", tags: ["여수 거문도", "1.2km·1h", "안전판정: 조건부"] },
101    { icon: "🥇", t: "local.cs.rank.t", d: "local.cs.rank.d", tags: ["1순위: 하화도 꽃섬길", "2순위: 금오도 1코스", "3순위: 낭도 1코스", "4순위: 청산도 1코스"] },
102    { icon: "🌿", t: "local.cs.esg1.t", d: "local.cs.esg1.d", tags: ["청산도 슬로길", "느린여행·주민공존", "완도"] },
103    { icon: "🐚", t: "local.cs.esg2.t", d: "local.cs.esg2.d", tags: ["금오도 비렁길", "해안생태·로컬푸드", "여수"] },
104    { icon: "🌼", t: "local.cs.esg3.t", d: "local.cs.esg3.d", tags: ["하화도 꽃섬길", "야생화·지역식문화", "여수"] },
105    { icon: "🍚", t: "local.cs.esg4.t", d: "local.cs.esg4.d", tags: ["하화도 섬힐링밥상", "생태+지역경제 연결", "여수"] },
106    { icon: "🥗", t: "local.cs.esg5.t", d: "local.cs.esg5.d", tags: ["금오도 방풍나물", "지역특산 식문화체험", "여수"] },
107    { icon: "🏆", t: "local.cs.score.t", d: "local.cs.score.d", tags: ["예시·공식 인증 아님", "1위 청산도(89)", "2위 하화도+야생화(86)", "3위 금오도+방풍나물(85)"] }
108  ],
109  // K-콘텐츠 촬영지 — 전남광주통합특별시, 1990년~현재. 웹검색 교차검증(2026-08-28 기준) + 전남영상위원회 「전남필름」 공식 DB 존재 확인.
110  // 정직 원칙: 검색으로 확인 안 된 지자체는 억지로 연결하지 않고 별도 항목으로 명시. 향후 서남해코리도 확장 시 지역 코드만 추가하면 됨(설계는 docs 참고).
111  filmlocation: [
112    { icon: "🎬", t: "local.fl.intro.t", d: "local.fl.intro.d", tags: ["1990년~현재", "웹검색 교차검증(2026-08-28)", "전남영상위원회 「전남필름」 공식DB 존재"] },
113    { icon: "⚔️", t: "local.fl.wando1.t", d: "local.fl.wando1.d", tags: ["완도 청해포구 촬영장", "해신(2004)·추노(2010)·명량(2014)·해적(2014)", "사극·해양물 50여 편"] },
114    { icon: "🎞", t: "local.fl.wando2.t", d: "local.fl.wando2.d", tags: ["완도 청산도", "서편제(1993, 최초 100만 관객 영화)", "봄의 왈츠(KBS, 2006)"] },
115    { icon: "🏛", t: "local.fl.mokpo.t", d: "local.fl.mokpo.d", tags: ["목포 근대역사관", "호텔 델루나(tvN, 2019)", "1987(2017)·모래시계(1995)·야인시대(2002)"] },
116    { icon: "🍵", t: "local.fl.boseong.t", d: "local.fl.boseong.d", tags: ["보성 대한다원 녹차밭", "여름향기(2003)·푸른 바다의 전설(2016)·역적(2017)"] },
117    { icon: "🌲", t: "local.fl.damyang.t", d: "local.fl.damyang.d", tags: ["담양 죽녹원·메타세쿼이아길", "미스터 션샤인(2018)·군주(2017)", "명당·창궐·역린(영화)·일지매(2008)"] },
118    { icon: "🚂", t: "local.fl.suncheon.t", d: "local.fl.suncheon.d", tags: ["순천 드라마 촬영장", "사랑과 야망(2006)·에덴의 동쪽(2008)", "자이언트(2010)·각시탈(2012)·폭싹 속았수다(2025)"] },
119    { icon: "🌊", t: "local.fl.yeosu.t", d: "local.fl.yeosu.d", tags: ["여수 오동도·이순신광장", "대물(2010)·혼술남녀(2016)·호구의 사랑(2015)"] },
120    { icon: "💜", t: "local.fl.sinan.t", d: "local.fl.sinan.d", tags: ["신안 병풍도 '사랑의 해변'", "봄의 왈츠(2006)"] },
121    { icon: "🌾", t: "local.fl.haenam.t", d: "local.fl.haenam.d", tags: ["해남 북평면·대흥사 유선관", "호프(2026)·장군의 아들(1990)·라켓소년단(2021)"] },
122    { icon: "🏙", t: "local.fl.gwangju.t", d: "local.fl.gwangju.d", tags: ["광주 국립아시아문화전당·조선대·광주극장", "파묘(2024)·서울의 봄(2023)·종이의 집(2022)·암살(2015)"] },
123    { icon: "❓", t: "local.fl.unconfirmed.t", d: "local.fl.unconfirmed.d", tags: ["강진·영암·장흥·고흥·함평·영광·무안·진도", "촬영 이력 검색 미확인", "향후 「전남필름」 연동 시 갱신"] }
124  ],
125  // 우리의 소리(Our Sound) — 영화 속 장면이 아니라 그 장면을 만든 실제 소리·사람·장소를 잇는다.
126  // 웹검색 교차검증(2026-08-30 기준): 강산제(한국민족문화대백과사전·위키백과), 진도아리랑(기존 filmlocation·islandtrail 항목과 동일 출처 재확인).
127  // 정직 원칙: 확인 못한 뱃노래·민요는 지어내지 않고 '연결 대기'로 명시.
128  oursound: [
129    { icon: "🎶", t: "local.os.intro.t", d: "local.os.intro.d", tags: ["영화·드라마는 입구", "실제 소리·사람·장소가 본체", "웹검색 교차검증(2026-08-30)"] },
130    { icon: "🎼", t: "local.os.gangsanje.t", d: "local.os.gangsanje.d", tags: ["보성 웅치면 강산리", "판소리 유파 강산제", "박유전"] },
131    { icon: "🌄", t: "local.os.jindoarirang.t", d: "local.os.jindoarirang.d", tags: ["완도 청산도 촬영 장면", "진도 대표 민요", "무형유산"] },
132    { icon: "🕓", t: "local.os.pending.t", d: "local.os.pending.d", tags: ["뱃노래·어업요는 지역별 현장 채록 전", "지어내지 않고 대기 표시", "확인되는 대로 갱신"] }
133  ]
134};
135const CURATED_SRC = { koryoin: "local.src.koryoin", islands2026: "local.src.islands2026", islandtrail: "local.src.islandtrail", borderislands: "local.src.borderislands", coursesafety: "local.src.coursesafety", filmlocation: "local.src.filmlocation", oursound: "local.src.oursound" };
136
137let localCat = "transport";
138let localRegion = "";
139
140function t(key, fallback){
141  const all = window.I18N || {};
142  const dict = all[window.__lang] || {};
143  // 현재 언어 → 없으면 한국어 → 없으면 fallback → 키
144  return dict[key] || (all.ko && all.ko[key]) || fallback || key;
145}
146
147function selectLocalCat(el, cat){
148  document.querySelectorAll('.local-cat').forEach(c => c.classList.remove('active'));
149  el.classList.add('active');
150  localCat = cat;
151  const regionBar = document.getElementById('localRegionBar');
152  // 교통·안전은 전남 전역 공통 → 지역 필터 숨김
153  regionBar.style.display = (cat === 'transport' || cat === 'safety') ? 'none' : 'flex';
154  renderLocal();
155}
156
157function selectLocalRegion(el, region){
158  document.querySelectorAll('.local-region').forEach(r => r.classList.remove('active'));
159  el.classList.add('active');
160  localRegion = region;
161  renderLocal();
162}
163
164function renderCurated(list, srcKey){
165  const grid = document.getElementById('localGrid');
166  grid.innerHTML = list.map(item => `
167    <div class="loc-info-card${item.wide ? ' wide' : ''}">
168      <div class="loc-info-icon">${item.icon}</div>
169      <div class="loc-info-title" data-i18n="${item.t}">${t(item.t)}</div>
170      <div class="loc-info-desc" data-i18n="${item.d}">${t(item.d)}</div>
171      <div class="loc-info-tags">${(item.tags||[]).map(tag => `<span class="loc-info-tag">${tag}</span>`).join('')}</div>
172    </div>`).join('');
173  document.getElementById('localSource').innerHTML =
174    '<span class="mock">● ' + t(srcKey || 'local.src.curated', '서남해 그랜드 큐레이션 정보') + '</span>';
175}
176
177// XSS 방지: 외부(TourAPI) 데이터를 innerHTML에 넣기 전 HTML 이스케이프
178// esc()는 utils.js에서 전역 제공 (index.html 최상단 로드)
179// 이미지 URL 화이트리스트: http(s) + 따옴표/괄호/공백/꺾쇠 없는 경우만 허용 (CSS url() 탈출 차단)
180function safeImg(u){
181  u = String(u || '');
182  return /^https?:\/\/[^\s'"()<>
182]+$/i.test(u) ? u : '';
183}
184
185// 두루누비 트레킹 — 서남해 시군 필터용 (sigun/코스명에 포함되는 한글명)
186const TRAIL_REGION_KO = { gwangju:"광주", mokpo:"목포", muan:"무안", sinan:"신안", jindo:"진도", wando:"완도", boseong:"보성", haenam:"해남",
187  yeosu:"여수", suncheon:"순천", goheung:"고흥", jangheung:"장흥", gangjin:"강진", yeongam:"영암", hampyeong:"함평", yeonggwang:"영광" };
188const SEONAMHAE_KO = ["신안","완도","진도","해남","목포","무안","보성"];
189
190function trailCard(item){
191  const isBike = item.brdDiv === "DNBW";
192  const typeLabel = isBike ? t("local.trail.bike", "자전거길") : t("local.trail.walk", "걷기길");
193  const mins = parseInt(item.crsTotlRqrmHour, 10) || 0;
194  const h = Math.floor(mins / 60), m = mins % 60;
195  const timeStr = h ? `${h}h${m ? m + "m" : ""}` : (m ? `${m}m` : "-");
196  const meta = `📏 ${esc(item.crsDstnc)}km · ⏱ ${timeStr}${item.crsLevel ? ` · ⛰ Lv.${esc(item.crsLevel)}` : ""}`;
197  return `<div class="loc-card">
198    <div class="loc-card-body">
199      <span class="loc-card-region">${isBike ? "🚲" : "🥾"} ${esc(item.sigun)}</span>
200      <div class="loc-card-title">${esc(item.crsKorNm)}</div>
201      <div class="loc-card-meta" style="margin-top:6px;">
202        <div><span>${meta}</span></div>
203        <div style="margin-top:6px;"><span class="loc-info-tag">${esc(typeLabel)}</span></div>
204      </div>
205    </div>
206  </div>`;
207}
208
209// 0건일 때 표시할 안내 — 특정 지역을 골라 조회했다면 인근 지역도 확인해보라는 힌트를 덧붙임(전체 조회 시엔 생략)
210function emptyStateHTML(){
211  const base = `<div class="local-empty">${t('local.none', '이 지역·분류에 등록된 관광공사 데이터가 없습니다.')}</div>`;
212  const hint = localRegion
213    ? `<div class="local-empty-hint">${t('local.none.hint', '💡 인근 지역도 함께 확인해보세요 — 위 지역 탭에서 다른 시·군을 선택하거나 전체보기로 돌아가면 됩니다.')}</div>`
214    : '';
215  return base + hint;
216}
217
218async function renderTrail(){
219  const grid = document.getElementById("localGrid");
220  grid.innerHTML = `<div class="local-empty">${t("local.loading", "불러오는 중…")}</div>`;
221  try {
222    const res = await fetch("/api/trail?op=courseList&numOfRows=300", { signal: AbortSignal.timeout(8000) });
223    if(!res.ok) throw new Error("proxy " + res.status);
224    const data = await res.json();
225    if(data.error) throw new Error(data.error);
226    let raw = data?.response?.body?.items?.item;
227    raw = Array.isArray(raw) ? raw : (raw ? [raw] : []);
228    const names = localRegion ? [TRAIL_REGION_KO[localRegion]].filter(Boolean) : SEONAMHAE_KO;
229    const items = raw.filter(i => names.some(n => (String(i.sigun || "") + String(i.crsKorNm || "")).includes(n)));
230    document.getElementById("localSource").innerHTML =
231      '<span class="live">● ' + t("local.src.trail", "실시간 — 한국관광공사 두루누비 (섬·오지 도보/자전거길)") + "</span>";
232    grid.innerHTML = items.length
233      ? items.map(trailCard).join("")
234      : emptyStateHTML();
235  } catch (e) {
236    document.getElementById("localSource").innerHTML =
237      '<span class="mock">● ' + t("local.trail.err", "두루누비 연결 대기 — API 키/프록시 확인 필요") + "</span>";
238    grid.innerHTML = emptyStateHTML();
239  }
240}
241
242// 인기 관광지(중심관광지) — 시군별 월간 랭킹 (LocgoHubTarService1)
243function hubCard(item){
244  const cat = [item.hubCtgryLclsNm, item.hubCtgryMclsNm].filter(Boolean).map(esc).join(' · ');
245  return `<div class="loc-card">
246    <div class="loc-card-body">
247      <span class="loc-card-region">⭐ TOP ${esc(item.hubRank)} · ${esc(item.signguNm)}</span>
248      <div class="loc-card-title">${esc(item.hubTatsNm)}</div>
249      <div class="loc-card-meta">${cat ? `<div><span class="loc-info-tag">${cat}</span></div>` : ''}</div>
250    </div>
251  </div>`;
252}
253async function renderHubspot(){
254  const grid = document.getElementById('localGrid');
255  const signgu = HUB_SIGNGU[localRegion];
256  if(!signgu){
257    grid.innerHTML = `<div class="local-empty">${t('local.hub.pick','인기 관광지는 시군(목포·신안·완도 등)을 선택하면 랭킹으로 표시됩니다.')}</div>`;
258    document.getElementById('localSource').innerHTML =
259      '<span class="mock">● ' + t('local.src.curated','서남해 그랜드 큐레이션 정보') + '</span>';
260    return;
261  }
262  grid.innerHTML = `<div class="local-empty">${t('local.loading','불러오는 중…')}</div>`;
263  try{
264    let items = [];
265    for(const ym of ymCandidates(8)){
266      const res = await fetch(`/api/datalab?svc=LocgoHubTarService1&op=areaBasedList1&areaCd=46&signguCd=${signgu}&baseYm=${ym}&numOfRows=20`, { signal: AbortSignal.timeout(8000) });
267      const d = await res.json();
268      if(d.error) continue;
269      let raw = d?.response?.body?.items?.item;
270      raw = Array.isArray(raw) ? raw : (raw ? [raw] : []);
271      if(raw.length){ items = raw; break; }
272    }
273    items.sort((a,b)=> Number(a.hubRank) - Number(b.hubRank));
274    document.getElementById('localSource').innerHTML =
275      '<span class="live">● ' + t('local.src.hub','실시간 — 한국관광공사 기초지자체 중심 관광지(데이터랩)') + '</span>';
276    grid.innerHTML = items.length
277      ? items.map(hubCard).join('')
278      : emptyStateHTML();
279  }catch(e){
280    document.getElementById('localSource').innerHTML =
281      '<span class="mock">● ' + t('local.trail.err','데이터랩 연결 대기 — API 키/프록시 확인 필요') + '</span>';
282    grid.innerHTML = emptyStateHTML();
283  }
284}
285
286function cardHTML(item){
287  const regionName = item.region ? esc(t('city.' + item.region, item.region)) : '';
288  const imgUrl = safeImg(item.img);
289  const img = imgUrl
290    ? `<div class="loc-card-img" style="background-image:url('${imgUrl}')"></div>`
291    : `<div class="loc-card-img loc-card-noimg">${esc(item.icon) || '📍'}</div>`;
292  return `<div class="loc-card">
293    ${img}
294    <div class="loc-card-body">
295      ${regionName ? `<span class="loc-card-region">◈ ${regionName}</span>` : ''}
296      <div class="loc-card-title">${esc(item.title)}</div>
297      <div class="loc-card-meta">
298        ${item.addr ? `<div>📍 <span>${esc(item.addr)}</span></div>` : ''}
299        ${item.tel ? `<div>☎ <span>${esc(item.tel)}</span></div>` : ''}
300      </div>
301    </div>
302  </div>`;
303}
304
305async function renderLocal(){
306  // 뿌리찾기 지원 경로(F-07 H3)는 고려인 카테고리에서만 노출 — 다른 카테고리 전환 시 항상 비움
307  var hcBox = document.getElementById('koryoinSupport');
308  if(localCat === 'koryoin' && window.SGHomecoming) window.SGHomecoming.render();
309  else if(hcBox) hcBox.innerHTML = '';
310  // 교통·안전 → 큐레이션 (t()가 현재 언어로 렌더하므로 applyLang 재호출 불필요 = ì
310ž¬ê·€ 방지)
311  if(CURATED[localCat]){
312    renderCurated(CURATED[localCat], CURATED_SRC[localCat]);
313    return;
314  }
315  // 트레킹 → 두루누비 API
316  if(localCat === 'trail'){
317    await renderTrail();
318    return;
319  }
320  // 인기 관광지(중심관광지) → 데이터랩 LocgoHubTarService1 (시군별 랭킹)
321  if(localCat === 'hubspot'){
322    await renderHubspot();
323    return;
324  }
325  // 숙박·음식·문화유산·체험·반려동물 → TourAPI 시도, 실패 시 목업
326  const grid = document.getElementById('localGrid');
327  const typeId = TOURAPI_TYPE[localCat];
328  grid.innerHTML = `<div class="local-empty">${t('local.loading', '불러오는 중…')}</div>`;
329  try {
330    const params = new URLSearchParams({ area: '38' });
331    if(typeId) params.set('type', typeId);
332    if(TOURAPI_SVC[localCat]) params.set('service', TOURAPI_SVC[localCat]);
333    if(localRegion) params.set('region', localRegion);
334    const res = await fetch('/api/tour?' + params.toString(), { signal: AbortSignal.timeout(6000) });
335    if(!res.ok) throw new Error('proxy ' + res.status);
336    const data = await res.json();
337    if(data.error) throw new Error(data.error);
338    const raw = data?.response?.body?.items?.item;
339    const items = (Array.isArray(raw) ? raw : (raw ? [raw] : [])).map(it => ({
340      title: it.title,
341      addr: it.addr1 || '',
342      tel: it.tel || '',
343      img: it.firstimage || it.firstimage2 || '',
344      icon: '📍'
345    }));
346    // API 정상 응답: 데이터가 있으면 실데이터, 0건이면 정직하게 빈 상태(목업 아님)
347    document.getElementById('localSource').innerHTML =
348      '<span class="live">● ' + t('local.src.live', '실시간 — 한국관광공사 TourAPI') + '</span>';
349    grid.innerHTML = items.length
350      ? items.map(cardHTML).join('')
351      : emptyStateHTML();
352  } catch (e) {
353    // 프록시/네트워크/키 오류 — mock 데이터 금지 원칙: 목업 대신 정직한 "연결 대기" 안내(renderTrail/renderHubspot과 동일 패턴)
354    document.getElementById('localSource').innerHTML =
355      '<span class="mock">● ' + t('local.tour.err', '한국관광공사 TourAPI 연결 대기 — API 키/프록시 확인 필요') + '</span>';
356    grid.innerHTML = emptyStateHTML();
357  }
358}
359
360// 언어 변경 시 동적 카드도 다시 렌더 (data-i18n 미적용 항목 반영)
361window.onLangChange = function(){
362  if(document.getElementById('s-local') && document.getElementById('s-local').classList.contains('active')){
363    renderLocal();
364  }
365};
366
367document.addEventListener('DOMContentLoaded', () => {
368  if(document.getElementById('localGrid')) renderLocal();
369});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.