From 5d3ce40f773ccc77278e42cbcf2b1b7aa891dadd Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Wed, 9 Sep 2026 11:31:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(discovery):=20=EC=9D=98=EB=A3=8C=EA=B4=80?= =?UTF-8?q?=EA=B4=91=20=EC=97=B0=EA=B2=B0=20=EC=84=B9=EC=85=98=20=E2=80=94?= =?UTF-8?q?=20TourAPI=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EC=A3=BC=EB=B3=80?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20+=20=ED=9A=8C=EB=B3=B5=20=EB=8F=99?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /discovery/:id 에 외국인 환자용 주변 정보를 붙였다. 숙박·음식점·웰니스·관광·문화· 쇼핑·축제 125건과 이동 반경 기준 회복 동선. 다음 뷰성형외과 미팅용 프로토타입이다. 데이터 (scripts/fetch_medical_tourism.py → src/data/medicalTourism.json) 한국관광공사 TourAPI 영문(EngService2) 사전 수집. 실시간 호출이 아닌 이유는 영문 커버리지가 얇아 필터링이 많이 필요하고, 시연 중 외부 API 상태에 화면이 종속되지 않게 하기 위해서다. 좌표는 Naver 지역검색 실측(뷰성형외과의원, 봉은사로 107). 실측으로 드러난 것과 대응 - 영문 관광지(76)의 대부분이 관광지가 아니다. 반경 5km 100건 중 92건이 분류코드 A02020500 "Medical Tourism Sites"(categoryCode2 로 확인)이며 병원과 유치 에이전시다. 제목 키워드로는 "하이안과", "닥파인더코리아" 를 놓쳐 코드로 걸렀다. 거리순 상위 100건이 거의 다 여기라 페이징을 넣어야 실제 관광지가 나온다. - 스파·찜질방(A02020300/400)은 회복 여정에 맞아 wellness 로 분리했다. - 영문 축제는 areacode 필드가 비어 있어 areaCode 필터가 0건을 반환한다. 좌표 거리로 걸렀다. - TourAPI 이미지 URL 이 http 라 https 사이트에서 혼합 콘텐츠로 차단된다. 수집 단계에서 https 로 올린다. - 반경을 목적별로 나눴다. 회복기에 자주 가는 숙박·식사는 5km, 관광은 10km. 만들지 않은 것 - 평점·리뷰·영업시간: TourAPI 미제공이라 null 로 두고 화면에 "미연동" 으로 적었다. Google Places 키가 아직 비어 있다(신청 중). - 수술 전후 식이 적합성: 의학 판단이라 병원 입력만 쓴다. ClinicInputsPanel 에 diet_guide 항목을 더해 supporter_inputs 로 받고, 입력 전에는 "병원 확인 대기" 로 둔다. 개별 식당의 적합 여부를 추론하지 않는다. - 회복 단계가 며칠째인지: 병원 입력(recovery). 화면의 단계는 이동 반경으로만 정의해 의학적 함의를 담지 않는다. - 한계 4가지를 meta.limits 에 담아 섹션 하단 "측정 가능 범위" 에 그대로 띄운다. FilledIcons 에 Bed·Fork·Spa·MapPin·Ticket·Bag·Theater 7종 추가(라인 아이콘·이모지 금지 규칙에 따라 채움 SVG). Co-Authored-By: Claude Opus 5 (1M context) --- scripts/fetch_medical_tourism.py | 206 ++ .../discovery/ClinicInputsPanel.tsx | 1 + .../discovery/MedicalTourismPanel.tsx | 449 ++++ src/components/icons/FilledIcons.tsx | 73 + src/data/medicalTourism.json | 1797 +++++++++++++++++ src/pages/DiscoveryReportPage.tsx | 4 + src/types/medicalTourism.ts | 88 + 7 files changed, 2618 insertions(+) create mode 100644 scripts/fetch_medical_tourism.py create mode 100644 src/components/discovery/MedicalTourismPanel.tsx create mode 100644 src/data/medicalTourism.json create mode 100644 src/types/medicalTourism.ts diff --git a/scripts/fetch_medical_tourism.py b/scripts/fetch_medical_tourism.py new file mode 100644 index 0000000..f086b5b --- /dev/null +++ b/scripts/fetch_medical_tourism.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""의료관광 연결 데이터 수집 (한국관광공사 TourAPI 다국어). + +병원 좌표를 중심으로 숙박·음식점·관광지·문화시설·쇼핑을 모으고, 진행 중인 축제를 +거리와 함께 붙여 프론트가 쓸 JSON 하나로 낸다. + +왜 사전 수집인가 + 영문 서비스는 강남 일대 커버리지가 얇고(5km 음식점 18건, 국문은 338건) 관광지의 + 79%가 의료기관이라 필터링이 많이 필요하다. 그 처리를 수집 시점에 끝내고 프론트는 + 정제된 결과만 그린다. 미팅 데모 중 외부 API 상태에 화면이 종속되지도 않는다. + +무엇을 만들지 않는가 + 없는 값을 채우지 않는다. 평점·리뷰·영업시간은 TourAPI 가 주지 않으므로 비워 두고, + Google Places 승인 후 별도 단계에서 붙인다. 식이 적합성은 병원 입력이며 여기서 + 판정하지 않는다. + + python3 scripts/fetch_medical_tourism.py + python3 scripts/fetch_medical_tourism.py --radius 5000 --out src/data/medicalTourism.json +""" +import argparse, io, json, math, os, ssl, sys, time, urllib.parse, urllib.request + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# 영문 관광지(76)의 대부분은 관광지가 아니라 의료관광 등록업소다. 반경 5km 100건 중 92건이 +# 분류코드 A02020500 = "Medical Tourism Sites"(categoryCode2 로 확인)이며 병원과 유치 에이전시가 +# 여기 들어 있다. 제목 키워드로는 "하이안과", "닥파인더코리아" 같은 것을 놓치므로 코드로 거른다. +CAT_MEDICAL_TOURISM = "A02020500" +# 스파·찜질방은 수술 후 회복 여정에 맞으므로 관광이 아니라 별도 범주로 뺀다. +CAT_WELLNESS = ("A02020300", "A02020400") # Hot Springs & Spa, Jjimjilbang + +# 다국어 ContentTypeId. 국문과 코드가 다르다. +# 반경은 목적에 따라 다르다. 회복기에 매일 가는 곳(숙박·식사·스파)은 가깝게, 관광은 넓게 본다. +TYPES = { + "76": ("attraction", "10000"), + "78": ("culture", "10000"), + "79": ("shopping", "10000"), + "80": ("stay", "5000"), + "82": ("restaurant", "5000"), +} + + +def ssl_ctx(): + try: + import certifi; return ssl.create_default_context(cafile=certifi.where()) + except Exception: + return ssl.create_default_context() + + +def load_env(path): + d = {} + for line in io.open(path, encoding="utf-8"): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, v = line.split("=", 1); d[k.strip()] = v.strip() + return d + + +def haversine_km(lat1, lng1, lat2, lng2): + r = 6371.0 + p1, p2 = math.radians(lat1), math.radians(lat2) + dp, dl = math.radians(lat2 - lat1), math.radians(lng2 - lng1) + a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 + return 2 * r * math.asin(math.sqrt(a)) + + +def call(env, op, extra, rows=100, retries=3, page=1): + base, svc, key = env["TOUR_API_BASE"], env["TOUR_API_SERVICE"], env["TOUR_API_KEY"] + p = {"MobileOS": "ETC", "MobileApp": "INFINITH", "_type": "json", + "numOfRows": str(rows), "pageNo": str(page), **extra} + url = f"{base}/{svc}/{op}?serviceKey={key}&" + urllib.parse.urlencode(p) + for i in range(retries): + try: + with urllib.request.urlopen(url, timeout=30, context=ssl_ctx()) as r: + body = r.read().decode("utf-8", "replace") + if body.lstrip().startswith("<"): + raise RuntimeError(f"XML 응답(인증·한도 확인): {body[:160]}") + j = json.loads(body)["response"] + if j["header"].get("resultCode") != "0000": + raise RuntimeError(f"{j['header'].get('resultCode')} {j['header'].get('resultMsg')}") + b = j.get("body", {}); items = b.get("items") + return b.get("totalCount", 0), ([] if not items or items == "" else items["item"]) + except Exception as e: + if i == retries - 1: raise + time.sleep(1.5 * (i + 1)) + + +def call_all(env, op, extra, max_pages=6, rows=100): + """전 페이지를 모은다. 관광지는 거리순 상위 100건이 거의 다 의료관광업소라 한 페이지로는 부족하다.""" + got, total = [], None + for page in range(1, max_pages + 1): + total, items = call(env, op, extra, rows=rows, page=page) + got += items + if not items or len(got) >= (total or 0): break + time.sleep(0.2) + return total, got + + +def bucket(it): + """분류코드로 범주를 정한다. None 이면 버린다.""" + cat3 = (it.get("cat3") or "").strip() + if cat3 == CAT_MEDICAL_TOURISM: return None + if cat3 in CAT_WELLNESS: return "wellness" + return "keep" + + +def clean(it, lat, lng): + """TourAPI 원본에서 화면이 쓸 필드만 남긴다. mapx=경도, mapy=위도 (이름과 반대라 자주 틀린다).""" + try: + ilng, ilat = float(it.get("mapx") or 0), float(it.get("mapy") or 0) + except ValueError: + ilng = ilat = 0.0 + dist = it.get("dist") + return { + "id": it.get("contentid"), + "title": (it.get("title") or "").strip(), + "address": (it.get("addr1") or "").strip(), + # TourAPI 는 이미지 URL 을 http 로 준다. https 사이트에서 혼합 콘텐츠로 차단되므로 올린다. + # tong.visitkorea.or.kr 은 https 로도 같은 파일을 준다(실측 확인). + "image": (lambda u: u.replace("http://", "https://", 1) if u else None)( + it.get("firstimage") or it.get("firstimage2") or None), + "tel": (it.get("tel") or "").strip() or None, + "lat": ilat or None, "lng": ilng or None, + "distanceM": round(float(dist)) if dist else ( + round(haversine_km(lat, lng, ilat, ilng) * 1000) if ilat and ilng else None), + # TourAPI 가 주지 않는 값. Google Places 승인 후 채운다. 지금은 비워 둔다. + "rating": None, "reviewCount": None, "openingHours": None, + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--festival-km", type=float, default=60.0, help="이 거리 안의 축제만 담는다") + ap.add_argument("--per-type", type=int, default=24, help="타입별 최대 건수") + ap.add_argument("--out", default="src/data/medicalTourism.json") + a = ap.parse_args() + + env = load_env(os.path.join(ROOT, ".env")) + for k in ("TOUR_API_KEY", "TOUR_API_BASE", "TOUR_API_SERVICE", "TOUR_API_ORIGIN_LAT", "TOUR_API_ORIGIN_LNG"): + if not env.get(k): sys.exit(f".env 에 {k} 가 비어 있습니다") + lat, lng = float(env["TOUR_API_ORIGIN_LAT"]), float(env["TOUR_API_ORIGIN_LNG"]) + + out = {"places": {}, "festivals": [], "meta": {}} + filtered_log = {} + + wellness = [] + for ct, (name, radius) in TYPES.items(): + total, rows = call_all(env, "locationBasedList2", + {"mapX": str(lng), "mapY": str(lat), "radius": radius, + "contentTypeId": ct, "arrange": "E"}) + kept, dropped = [], [] + for it in rows: + b = bucket(it) + if b is None: + dropped.append((it.get("title") or "").strip()); continue + (wellness if b == "wellness" else kept).append(clean(it, lat, lng)) + kept.sort(key=lambda x: x["distanceM"] if x["distanceM"] is not None else 10 ** 9) + out["places"][name] = kept[:a.per_type] + filtered_log[name] = {"apiTotal": total, "fetched": len(rows), "radiusM": int(radius), + "droppedMedicalTourism": len(dropped), "kept": len(kept), + "shown": len(out["places"][name]), "droppedSample": dropped[:5]} + print(f" {name:<11} r={radius:>5}m · API {total:>4} · 받음 {len(rows):>3} · 의료관광업소 제외 {len(dropped):>3} · 담음 {len(out['places'][name])}") + wellness.sort(key=lambda x: x["distanceM"] if x["distanceM"] is not None else 10 ** 9) + out["places"]["wellness"] = wellness[:a.per_type] + filtered_log["wellness"] = {"note": "관광지(76)에서 스파·찜질방 분류만 분리", "shown": len(out["places"]["wellness"])} + print(f" {'wellness':<11} 스파·찜질방 분리 · 담음 {len(out['places']['wellness'])}") + + # 영문 축제는 areacode 필드가 비어 있어 지역 필터를 쓸 수 없다. 좌표로 거리를 계산해 거른다. + today = time.strftime("%Y%m%d") + total, rows = call(env, "searchFestival2", {"eventStartDate": today, "arrange": "A"}, rows=300) + near, nocoord = [], 0 + for it in rows: + c = clean(it, lat, lng) + if c["lat"] is None: nocoord += 1; continue + km = haversine_km(lat, lng, c["lat"], c["lng"]) + if km > a.festival_km: continue + c.update(startDate=it.get("eventstartdate"), endDate=it.get("eventenddate"), + distanceKm=round(km, 1)) + near.append(c) + near.sort(key=lambda x: x["startDate"] or "") + out["festivals"] = near[:a.per_type] + print(f" festival API {total:>4}건 · 받음 {len(rows):>3} · 좌표없음 {nocoord} · {a.festival_km:.0f}km 이내 {len(near)}") + + out["meta"] = { + "source": "한국관광공사 TourAPI " + env["TOUR_API_SERVICE"], + "fetchedAt": time.strftime("%Y-%m-%dT%H:%M:%S"), + "origin": {"lat": lat, "lng": lng, "label": "뷰성형외과의원 (봉은사로 107)", + "coordSource": "Naver 지역검색 실측 2026-09-09"}, + "radiusByType": {n: int(r) for n, r in TYPES.values()}, "festivalRadiusKm": a.festival_km, + "counts": filtered_log, + # 화면이 한계를 그대로 표시할 수 있게 데이터에 적어 둔다. 없는 값을 채우지 않는다. + "limits": [ + "영문 서비스는 강남 일대 숙박·음식점 커버리지가 얇다(반경 5km 숙박 4건·음식점 18건, 국문은 43건·338건).", + "관광지 항목의 대부분(반경 5km 100건 중 92건)이 분류코드 A02020500 \"Medical Tourism Sites\" 였고 제외했다.", + "평점·리뷰·영업시간은 TourAPI 가 제공하지 않는다. Google Places 승인 후 채운다.", + "영문 축제는 areacode 필드가 비어 있어 좌표 거리로 걸렀다.", + "수술 전후 식이 적합성은 병원이 입력한 가이드로만 표시한다. 이 파일에는 없다.", + ], + } + p = os.path.join(ROOT, a.out) + os.makedirs(os.path.dirname(p), exist_ok=True) + io.open(p, "w", encoding="utf-8").write(json.dumps(out, ensure_ascii=False, indent=2)) + print(f"\n→ {a.out}") + + +if __name__ == "__main__": + main() diff --git a/src/components/discovery/ClinicInputsPanel.tsx b/src/components/discovery/ClinicInputsPanel.tsx index 6d2b2b3..b3b276c 100644 --- a/src/components/discovery/ClinicInputsPanel.tsx +++ b/src/components/discovery/ClinicInputsPanel.tsx @@ -40,6 +40,7 @@ const FIELDS: Array<{ key: string; title: string; hint: string; inputs: Array<{ { key: 'sponsorship', title: '3. 지원 관계 문구', hint: '모든 글 첫 부분에 그대로 실립니다. 예: 이 글은 OO의원의 지원(광고비)을 받아 OO 서포터즈가 작성했습니다.', inputs: [{ name: 'notice', label: '고지 문장', type: 'textarea' }] }, { key: 'quote_items', title: '4. 견적 기본 포함 항목과 결제 방식', hint: '금액은 받지 않습니다. 항목과 방식만 적어 주세요.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, { key: 'recovery', title: '5. 회복 일정표의 병원 확정값', hint: '출근 기준, 걷기 시작 시점 등 공개 자료에 없는 값.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, + { key: 'diet_guide', title: '5b. 수술 전후 식이 가이드', hint: '의료관광 연결의 음식점 추천에 그대로 실립니다. 개별 식당의 적합 여부는 저희가 판정하지 않고, 여기 적힌 분류만 표시합니다.', inputs: [{ name: 'recommended', label: '권장 음식 분류 (회복 단계별)', type: 'textarea' }, { name: 'avoid', label: '피해야 할 음식 분류', type: 'textarea' }, { name: 'stageDays', label: '각 단계가 수술 후 며칠인지' }] }, { key: 'revision_policy', title: '6. 재수술 정책', hint: '다른 병원 수술의 상담 가능 여부, 부위별 대기 기간.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, { key: 'discrepancy', title: '7. 페이지 간 표기 불일치의 정답', hint: '예: 코 실밥 제거일이 코성형 페이지 7일, 애프터케어 3일로 다릅니다.', inputs: [{ name: 'answer', label: '내용', type: 'textarea' }] }, { key: 'specialty_doctors', title: '8. 진료 분야별 담당 원장', hint: '아래 의료진 목록의 ID 를 적습니다. 글별 검토자 후보가 됩니다.', inputs: [{ name: 'eye', label: '눈' }, { name: 'nose', label: '코' }, { name: 'contour', label: '윤곽' }, { name: 'lifting', label: '리프팅' }, { name: 'breast', label: '가슴' }, { name: 'anesthesia', label: '마취' }] }, diff --git a/src/components/discovery/MedicalTourismPanel.tsx b/src/components/discovery/MedicalTourismPanel.tsx new file mode 100644 index 0000000..1513523 --- /dev/null +++ b/src/components/discovery/MedicalTourismPanel.tsx @@ -0,0 +1,449 @@ +/** + * 의료관광 연결 패널 (/discovery/:id) + * + * 외국인 환자가 수술 전후에 쓸 주변 정보를 한 곳에 모은다. + * 숙박 · 음식점 · 웰니스(스파) · 관광 · 쇼핑 · 축제, 그리고 이동 반경 기준 회복 동선. + * + * 데이터는 `scripts/fetch_medical_tourism.py` 가 한국관광공사 TourAPI 다국어(영문)에서 + * 미리 수집한 `src/data/medicalTourism.json` 이다. 사전 수집인 이유는 영문 커버리지가 + * 얇아 필터링이 많이 필요하고, 시연 중 외부 API 상태에 화면이 종속되지 않게 하기 위해서다. + * + * 만들지 않는 것 + * - 평점·리뷰·영업시간: TourAPI 미제공이므로 "미연동"으로 표시하고 비워 둔다. + * - 수술 전후 식이 적합성: 병원이 supporter_inputs('diet_guide')로 입력한 값만 쓴다. + * 입력 전에는 "병원 확인 대기"로 둔다. 개별 식당의 적합 여부를 추론하지 않는다. + * - 회복 단계가 며칠째인지: 의학 판단이라 병원 입력('recovery')을 따른다. + * 화면의 단계는 이동 반경으로만 정의한다. + */ +import { useEffect, useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; +import { motion } from 'motion/react'; +import { SectionWrapper } from '../report/ui/SectionWrapper'; +import { supabase } from '../../lib/supabase'; +import data from '../../data/medicalTourism.json'; +import type { + MedicalTourismData, + PlaceCategory, + RecoveryStage, + TourPlace, +} from '../../types/medicalTourism'; +import { + BedFilled, + ForkFilled, + SpaFilled, + MapPinFilled, + TicketFilled, + BagFilled, + TheaterFilled, +} from '../icons/FilledIcons'; + +const TOURISM = data as unknown as MedicalTourismData; + +const CATEGORY_META: Record< + PlaceCategory, + { label: string; en: string; Icon: typeof BedFilled } +> = { + stay: { label: '숙박', en: 'Stay', Icon: BedFilled }, + restaurant: { label: '음식점', en: 'Dining', Icon: ForkFilled }, + wellness: { label: '웰니스', en: 'Wellness', Icon: SpaFilled }, + attraction: { label: '관광', en: 'Attractions', Icon: MapPinFilled }, + culture: { label: '문화', en: 'Culture', Icon: TheaterFilled }, + shopping: { label: '쇼핑', en: 'Shopping', Icon: BagFilled }, +}; + +const CATEGORY_ORDER: PlaceCategory[] = [ + 'stay', + 'restaurant', + 'wellness', + 'attraction', + 'culture', + 'shopping', +]; + +/** + * 회복 동선. 며칠째인지는 병원이 정한다(§ClinicInputsPanel 'recovery'). + * 여기서는 이동 반경만 정의하므로 의학적 판단을 담지 않는다. + */ +const STAGES: RecoveryStage[] = [ + { + id: 'near', + label: '병원 도보권', + maxDistanceM: 1000, + categories: ['stay', 'restaurant'], + note: '내원이 잦은 기간에 머무는 범위입니다.', + }, + { + id: 'short', + label: '근거리 이동', + maxDistanceM: 3000, + categories: ['restaurant', 'wellness'], + note: '짧은 외출이 가능해지는 범위입니다.', + }, + { + id: 'city', + label: '시내 이동', + maxDistanceM: 10000, + categories: ['attraction', 'culture', 'shopping'], + note: '관광 일정을 넣을 수 있는 범위입니다.', + }, +]; + +function fmtDistance(m: number | null) { + if (m === null) return '거리 미상'; + return m < 1000 ? `${m}m` : `${(m / 1000).toFixed(1)}km`; +} + +function fmtDate(d: string | null) { + if (!d || d.length !== 8) return ''; + return `${d.slice(0, 4)}.${d.slice(4, 6)}.${d.slice(6, 8)}`; +} + +export function MedicalTourismPanel({ clinicId }: { clinicId: string }) { + const [tab, setTab] = useState('route'); + const [clinicInput, setClinicInput] = useState<{ + recovery?: string; + diet?: Record; + }>({}); + + // 병원이 입력한 회복·식이 값을 읽는다. 같은 clinic_id 의 최신 행이 유효값이다. + useEffect(() => { + let alive = true; + (async () => { + const { data: rows } = await supabase + .from('supporter_inputs') + .select('key,value,created_at') + .eq('clinic_id', clinicId) + .in('key', ['recovery', 'diet_guide']) + .order('created_at', { ascending: true }); + if (!alive || !rows) return; + const latest = new Map>(); + for (const r of rows) latest.set(r.key, r.value as Record); + setClinicInput({ + recovery: latest.get('recovery')?.answer, + diet: latest.get('diet_guide'), + }); + })(); + return () => { + alive = false; + }; + }, [clinicId]); + + // 단계는 누적이 아니라 구간이다. 누적으로 하면 가까운 곳이 모든 단계에 중복으로 뜬다. + const stageBuckets = useMemo( + () => + STAGES.map((s, i) => { + const minM = i === 0 ? 0 : STAGES[i - 1].maxDistanceM; + return { + stage: s, + minM, + items: s.categories + .flatMap((c) => + (TOURISM.places[c] ?? []).map((p) => ({ ...p, category: c })), + ) + .filter( + (p) => + p.distanceM !== null && + p.distanceM > minM && + p.distanceM <= s.maxDistanceM, + ) + .sort((a, b) => (a.distanceM ?? 0) - (b.distanceM ?? 0)) + .slice(0, 6), + }; + }), + [], + ); + + const totalPlaces = + CATEGORY_ORDER.reduce((n, c) => n + (TOURISM.places[c]?.length ?? 0), 0) + + TOURISM.festivals.length; + + return ( + + {/* 출처와 기준점 */} +
+
+
+
기준 좌표
+
{TOURISM.meta.origin.label}
+
+ {TOURISM.meta.origin.lat}, {TOURISM.meta.origin.lng} · {TOURISM.meta.origin.coordSource} +
+
+
+
데이터 출처
+
{TOURISM.meta.source}
+
+ 수집 {TOURISM.meta.fetchedAt.replace('T', ' ')} +
+
+
+
수집 반경
+
+ 숙박·식사 5km · 관광·문화·쇼핑 10km +
+
+ 축제 {TOURISM.meta.festivalRadiusKm}km 이내 +
+
+
+
+ + {/* 탭 */} +
+ setTab('route')}> + 회복 동선 + + {CATEGORY_ORDER.map((c) => ( + setTab(c)}> + {CATEGORY_META[c].label} {TOURISM.places[c]?.length ?? 0} + + ))} + setTab('festival')}> + 축제 {TOURISM.festivals.length} + +
+ + {tab === 'route' && ( +
+ + {stageBuckets.map(({ stage, minM, items }, i) => ( + +
+

{stage.label}

+ + 병원 반경 {minM === 0 ? '' : `${fmtDistance(minM)} ~ `} + {fmtDistance(stage.maxDistanceM)} + +
+

{stage.note}

+ {items.length === 0 ? ( + 이 반경에 담긴 영문 데이터가 없습니다. + ) : ( +
+ {items.map((p) => ( + + ))} +
+ )} +
+ ))} +
+ )} + + {tab !== 'route' && tab !== 'festival' && ( + <> + {tab === 'restaurant' && ( +
+ +
+ )} + + + )} + + {tab === 'festival' && ( +
+ {TOURISM.festivals.map((f) => ( +
+
+ {f.image ? ( + + ) : ( + 사진 없음 + )} +
+
+
+ + {f.distanceKm}km +
+

{f.title}

+

+ {fmtDate(f.startDate)} ~ {fmtDate(f.endDate)} +

+

{f.address}

+
+
+ ))} +
+ )} + + {/* 한계를 숨기지 않는다 */} +
+
+ 측정 가능 범위 +
+
    + {TOURISM.meta.limits.map((l) => ( +
  • + · + {l} +
  • + ))} +
+
+
+ ); +} + +/* ────────────────────────── 서브 컴포넌트 ────────────────────────── */ + +function TabButton({ + active, + onClick, + children, +}: { + key?: string; + active: boolean; + onClick: () => void; + children: ReactNode; +}) { + return ( + + ); +} + +function PlaceGrid({ items, category }: { items: TourPlace[]; category: PlaceCategory }) { + if (items.length === 0) return 영문 데이터가 없습니다.; + return ( +
+ {items.map((p) => ( + + ))} +
+ ); +} + +function PlaceCard({ + place, + category, + compact = false, +}: { + key?: string; + place: TourPlace; + category: PlaceCategory; + compact?: boolean; +}) { + const { Icon, label } = CATEGORY_META[category]; + return ( +
+ {!compact && ( + // 이미지가 없는 항목이 섞여 그리드가 어긋나지 않도록 자리를 항상 차지하게 둔다. +
+ {place.image ? ( + + ) : ( + 사진 없음 + )} +
+ )} +
+
+ + {label} + · {fmtDistance(place.distanceM)} +
+

+ {place.title} +

+

{place.address}

+ {/* TourAPI 미제공 항목. 채우지 않고 미연동으로 둔다. */} +
+ {place.rating === null ? '평점·영업시간 미연동' : `평점 ${place.rating}`} +
+
+
+ ); +} + +/** 병원이 채워야 하는 값의 자리. 값이 없으면 비운 채로 무엇이 필요한지 밝힌다. */ +function ClinicGate({ + title, + filled, + waiting, +}: { + title: string; + filled?: string; + waiting: string; +}) { + const has = Boolean(filled && filled.trim()); + return ( +
+
+ + + {title} + + + {has ? '병원 입력됨' : '병원 확인 대기'} + +
+

+ {has ? filled : waiting} +

+
+ ); +} + +function EmptyNote({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/icons/FilledIcons.tsx b/src/components/icons/FilledIcons.tsx index f85be25..954bed1 100644 --- a/src/components/icons/FilledIcons.tsx +++ b/src/components/icons/FilledIcons.tsx @@ -324,3 +324,76 @@ export function PrismFilled({ size = 20, className = '' }: IconProps) { ); } + +/* ── 의료관광 연결 (숙박·식사·웰니스·관광·축제) ── */ + +export function BedFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + + + + ); +} + +export function ForkFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + + ); +} + +export function SpaFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + + ); +} + +export function MapPinFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + ); +} + +export function TicketFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + + ); +} + +export function BagFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + + ); +} + +export function TheaterFilled({ size = 20, className = '' }: IconProps) { + return ( + + + + + + ); +} diff --git a/src/data/medicalTourism.json b/src/data/medicalTourism.json new file mode 100644 index 0000000..15b4677 --- /dev/null +++ b/src/data/medicalTourism.json @@ -0,0 +1,1797 @@ +{ + "places": { + "attraction": [ + { + "id": "264571", + "title": "Gangnam (강남)", + "address": "Yeoksam-dong, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/08/1984608_image2_1.jpg", + "tel": null, + "lat": 37.4970465429, + "lng": 127.0281573537, + "distanceM": 914, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1341981", + "title": "Samsung Gangnam (삼성 강남)", + "address": "411 Gangnam-daero, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/98/2489498_image2_1.jpg", + "tel": null, + "lat": 37.4963799255, + "lng": 127.0265547405, + "distanceM": 960, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2989577", + "title": "Hema Studio (헤마스튜디오)", + "address": "20-10 Nonhyeon-dong, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5134306323, + "lng": 127.021493608, + "distanceM": 1001, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1323377", + "title": "Sinsa-dong Garosu-gil Road (신사동 가로수길)", + "address": "Sinsa-dong, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/50/2933550_image2_1.bmp", + "tel": null, + "lat": 37.5215324916, + "lng": 127.0227543748, + "distanceM": 1852, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "264106", + "title": "Seolleung and Jeongneung Royal Tombs [UNESCO World Heritage] (서울 선릉과정릉) [유네스코 세계유산]", + "address": "1 Seolleung-ro 100-gil, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5074206801, + "lng": 127.0471803291, + "distanceM": 1953, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "264444", + "title": "Dosan Park (도산공원)", + "address": "20, Dosan-daero 45-gil, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5214632538, + "lng": 127.0338117034, + "distanceM": 1976, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1013448", + "title": "Jamwon Hangang Park (잠원한강공원)", + "address": "221-124 Jamwon-ro, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5207129541, + "lng": 127.0122664645, + "distanceM": 2101, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2035459", + "title": "Montmartre Park (몽마르뜨공원)", + "address": "59 Banpo-daero 37-gil, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.4955435016, + "lng": 127.0037401743, + "distanceM": 2194, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3340158", + "title": "Seoul Wave Art Center (서울웨이브아트센터)", + "address": "145-35 Jamwon-ro, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.518440907, + "lng": 127.007297537, + "distanceM": 2197, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3393199", + "title": "Gilsang Ceramics (길상도예)", + "address": "15-9 Gangnam-daero 39-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/50/2946050_image2_1.jpg", + "tel": null, + "lat": 37.48583681, + "lng": 127.0324406141, + "distanceM": 2215, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3107994", + "title": "KstarROAD 한류스타거리 (KstarROAD Hallyu Star Road)", + "address": "394, Apgujeong-dong, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5253320799, + "lng": 127.024415227, + "distanceM": 2261, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3474120", + "title": "Jennyhouse (제니하우스)", + "address": "56 Seolleung-ro 146-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/69/3474069_image2_1.jpg", + "tel": null, + "lat": 37.5213912279, + "lng": 127.0442774197, + "distanceM": 2476, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3393490", + "title": "Hyanggieok (향기억)", + "address": "23 Seolleung-ro 157-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/44/2946044_image2_1.jpg", + "tel": null, + "lat": 37.5261572277, + "lng": 127.0379584155, + "distanceM": 2601, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2037327", + "title": "Seorae Village (서래마을)", + "address": "Banpo-dong, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.496343309, + "lng": 126.9981601041, + "distanceM": 2606, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "264107", + "title": "Apgujeong Rodeo Street (압구정 로데오거리)", + "address": "Sinsa-dong, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/44/3109344_image2_1.JPG", + "tel": null, + "lat": 37.5268766055, + "lng": 127.0388971983, + "distanceM": 2710, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1013413", + "title": "Some Sevit (세빛섬)", + "address": "2085-14, Olympic-daero, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5111915303, + "lng": 126.9951432925, + "distanceM": 2778, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1011983", + "title": "Banpo Bridge Rainbow Fountain (반포대교 달빛무지개분수)", + "address": "647 Banpo-dong, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/46/3515046_image2_1.jpg", + "tel": null, + "lat": 37.5076780054, + "lng": 126.9926825046, + "distanceM": 2926, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1389275", + "title": "Cheongdam Fashion Street (청담패션거리)", + "address": "Cheongdam-dong, Gangnam-gu, Seoul-si", + "image": "https://tong.visitkorea.or.kr/cms/resource/82/2728682_image2_1.jpg", + "tel": null, + "lat": 37.5259073966, + "lng": 127.0457632671, + "distanceM": 2944, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "264594", + "title": "Seoul Bongeunsa Temple (봉은사 (서울))", + "address": "531 Bongeunsa-ro, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5158782211, + "lng": 127.0577485719, + "distanceM": 3118, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1349202", + "title": "Maeheon Citizen's Forest (매헌시민의 숲)", + "address": "99, Maeheon-ro, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/66/2668466_image2_1.jpg", + "tel": null, + "lat": 37.473038377, + "lng": 127.0369572316, + "distanceM": 3691, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "264131", + "title": "Yongsan Family Park (용산가족공원)", + "address": "185, Seobinggo-ro, Yongsan-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.520106972, + "lng": 126.9845717812, + "distanceM": 4001, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1887247", + "title": "Hangang River (한강)", + "address": "257 Gangbyeonbuk-ro, Seongdong-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5389280323, + "lng": 127.0416216085, + "distanceM": 4034, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1024837", + "title": "Itaewon Antiques Street (이태원 앤틱 가구 거리)", + "address": "109-1 Bogwang-ro, Yongsan-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/81/1020781_image2_1.jpg", + "tel": null, + "lat": 37.5326695793, + "lng": 126.9944566364, + "distanceM": 4126, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2655852", + "title": "Waterworks Museum (수도박물관)", + "address": "27, Wangsimni-ro, Seongdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/77/1570777_image2_1.jpg", + "tel": null, + "lat": 37.5398751565, + "lng": 127.0422909651, + "distanceM": 4153, + "rating": null, + "reviewCount": null, + "openingHours": null + } + ], + "culture": [ + { + "id": "616931", + "title": "Kukkiwon (World Taekwondo Headquarters) (국기원(세계태권도본부))", + "address": "(Kukkiwon), 32, Teheran-ro 7-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/77/919177_image2_1.jpg", + "tel": null, + "lat": 37.5019710091, + "lng": 127.0302792433, + "distanceM": 546, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "268118", + "title": "LG Arts Center (LG아트센터)", + "address": "(LG Arts Center Seoul), 136, Magokjungang-ro, Gangseo-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5019766908, + "lng": 127.037334157, + "distanceM": 1111, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1000217", + "title": "Seoul Center for National Intangible Cultural Asset (서울국가무형문화재전수회관)", + "address": "406, Bongeunsa-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/39/683839_image2_1.jpg", + "tel": null, + "lat": 37.5102008674, + "lng": 127.0446443698, + "distanceM": 1805, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1241100", + "title": "Yearimdang Art Hall (예림당아트홀)", + "address": "337, Eonju-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/34/1837534_image2_1.jpg", + "tel": null, + "lat": 37.4978884026, + "lng": 127.0443114151, + "distanceM": 1855, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1749152", + "title": "Korea Electric Power Corporation (KEPCO) Art Center (한전아트센터 공연장)", + "address": "60, Hyoryeong-ro 72-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/99/3109799_image2_1.JPG", + "tel": null, + "lat": 37.4856450857, + "lng": 127.0279688042, + "distanceM": 2158, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1382730", + "title": "Hallym University of Graduate Studies (한림국제대학원대학교)", + "address": "405, Yeoksam-ro, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5009992789, + "lng": 127.0513884609, + "distanceM": 2351, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "999884", + "title": "Park Ryu Sook Gallery (박여숙화랑)", + "address": "461, Apgujeong-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/33/683733_image2_1.jpg", + "tel": null, + "lat": 37.5252491662, + "lng": 127.0472214864, + "distanceM": 2969, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1312802", + "title": "Hangaram Art Museum in Seoul Arts Center (예술의전당 한가람미술관)", + "address": "2406, Nambusunhwan-ro, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.4793094549, + "lng": 127.0137702756, + "distanceM": 3032, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "736274", + "title": "COEX Aquarium (코엑스 아쿠아리움)", + "address": "513 Yeongdong-daero, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/83/3081283_image2_1.jpg", + "tel": null, + "lat": 37.5118092746, + "lng": 127.0591318945, + "distanceM": 3091, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2642344", + "title": "Starfield Library (별마당도서관)", + "address": "513, Yeongdong-daero, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/17/2507817_image2_1.JPG", + "tel": null, + "lat": 37.5118092746, + "lng": 127.0591318945, + "distanceM": 3091, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "268208", + "title": "Convention and Exhibition Center (COEX) (한국종합무역센터(코엑스))", + "address": "513, Yeongdong-daero, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5117453937, + "lng": 127.0591541159, + "distanceM": 3092, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1024886", + "title": "COEX Artium (코엑스아티움)", + "address": "513, Yeongdong-daero, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/72/2005272_image2_1.jpg", + "tel": null, + "lat": 37.5118148298, + "lng": 127.0591402265, + "distanceM": 3092, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3307357", + "title": "Ktown4u COEX (케이타운포유 코엑스)", + "address": "513 Yeongdong-daero, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/73/3305173_image2_1.png", + "tel": null, + "lat": 37.5119175967, + "lng": 127.059217995, + "distanceM": 3102, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "610753", + "title": "Korea Culture House (KOUS) (한국문화의집)", + "address": "12-9, Teheran-ro 92-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/84/1837284_image2_1.jpg", + "tel": null, + "lat": 37.5068182113, + "lng": 127.060476358, + "distanceM": 3123, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1252554", + "title": "Simsan Cultural Center (심산기념문화센터)", + "address": "55, Sapyeong-daero, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/71/1865771_image2_1.jpg", + "tel": null, + "lat": 37.4988456583, + "lng": 126.9904940651, + "distanceM": 3179, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1802823", + "title": "S.J. Cho Korean Paper Art Gallery (조수정 한지그림 갤러리)", + "address": "14, Teheran-ro 92-gil, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5070904146, + "lng": 127.0611762867, + "distanceM": 3187, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "268071", + "title": "Seoul Arts Center (예술의전당)", + "address": "2406 Nambusunhwan-ro, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.4785261578, + "lng": 127.0107150202, + "distanceM": 3214, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "268140", + "title": "National Gugak Center (국립국악원)", + "address": "2364 Nambusunhwan-ro, Seocho-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.4778373158, + "lng": 127.0088596528, + "distanceM": 3353, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1962734", + "title": "National Hangeul Museum (국립한글박물관)", + "address": "139, Seobinggo-ro, Yongsan-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5210429011, + "lng": 126.9804832384, + "distanceM": 4374, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "268217", + "title": "Leeum, Samsung Museum of Art (삼성미술관 리움)", + "address": "60-16, Itaewon-ro 55-gil, Yongsan-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5385800812, + "lng": 126.9987921858, + "distanceM": 4417, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1988054", + "title": "aT Center (에이티센터)", + "address": "27, Gangnam-daero, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/26/621926_image2_1.jpg", + "tel": null, + "lat": 37.4664808435, + "lng": 127.0391127763, + "distanceM": 4444, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1452335", + "title": "Blue Square (블루스퀘어)", + "address": "294, Itaewon-ro, Yongsan-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/77/1805577_image2_1.jpg", + "tel": null, + "lat": 37.5408548829, + "lng": 127.002458454, + "distanceM": 4475, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1215579", + "title": "Childrens Museum of the National Museum of Korea (국립중앙박물관 어린이박물관)", + "address": "137, Seobinggo-ro, Yongsan-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/06/1805106_image2_1.jpg", + "tel": null, + "lat": 37.5240675476, + "lng": 126.9803609412, + "distanceM": 4531, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3394605", + "title": "Seongsu Museum (성수미술관)", + "address": "39 Seongsuil-ro 8-gil, Seongdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/41/2946041_image2_1.jpg", + "tel": null, + "lat": 37.5458358003, + "lng": 127.0548174333, + "distanceM": 5234, + "rating": null, + "reviewCount": null, + "openingHours": null + } + ], + "shopping": [ + { + "id": "3110976", + "title": "Nonhyeon Furniture Street (논현가구거리)", + "address": "106 Hakdong-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/18/1920618_image2_1.jpg", + "tel": null, + "lat": 37.5111836894, + "lng": 127.0225185784, + "distanceM": 735, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3399449", + "title": "T1 (T1)", + "address": "627 Seolleung-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/87/2946987_image2_1.jpg", + "tel": null, + "lat": 37.512511677, + "lng": 127.0428028051, + "distanceM": 1757, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1238277", + "title": "Central City (센트럴시티)", + "address": "176, Sinbanpo-ro, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/09/1255009_image2_1.jpg", + "tel": null, + "lat": 37.505372917, + "lng": 127.0038037796, + "distanceM": 1922, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1859686", + "title": "CAOLION Cosmetics (카오리온)", + "address": "8, Apgujeong-ro 50-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/40/1859140_image2_1.jpg", + "tel": null, + "lat": 37.5277792626, + "lng": 127.0380139198, + "distanceM": 2767, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3390551", + "title": "10 Corso Como Cheongdam Branch (10꼬르소꼬모 청담점)", + "address": "416, Apgujeong-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/71/2778971_image2_1.png", + "tel": null, + "lat": 37.5269655478, + "lng": 127.0424468728, + "distanceM": 2872, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273771", + "title": "Hyundai Department Store Trade Center (현대백화점 무역센터점)", + "address": "517 Teheran-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/01/3490601_image2_1.jpg", + "tel": null, + "lat": 37.5086457635, + "lng": 127.0597319294, + "distanceM": 3077, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1984968", + "title": "Starfield COEX Mall (스타필드 코엑스몰)", + "address": "513 Yeongdong-daero, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/65/3081365_image2_1.jpg", + "tel": null, + "lat": 37.5117148467, + "lng": 127.0594429796, + "distanceM": 3116, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1985873", + "title": "Parnas Mall (파르나스몰)", + "address": "521, Teheran-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/56/2530456_image2_1.JPG", + "tel": null, + "lat": 37.5095984514, + "lng": 127.0608845757, + "distanceM": 3194, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273721", + "title": "Itaewon Shopping Street (이태원 쇼핑거리)", + "address": "Itaewon-dong, Yongsan-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/09/2933409_image2_1.bmp", + "tel": null, + "lat": 37.533861039, + "lng": 126.9906791694, + "distanceM": 4451, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3508511", + "title": "Seongsu Stage 35 (스테이지35 성수)", + "address": "35 Yeonmujang-gil, Seongdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/37/3498037_image2_1.jpg", + "tel": null, + "lat": 37.5431610875, + "lng": 127.0534981848, + "distanceM": 4918, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3417814", + "title": "Musinsa Standard Seongsu (무신사 스탠다드 성수)", + "address": "83 Yeonmujang-gil, Seongdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/02/3413102_image2_1.png", + "tel": null, + "lat": 37.5418224475, + "lng": 127.0586866388, + "distanceM": 5047, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "988219", + "title": "Hello apM (헬로에이피엠)", + "address": "253 Jangchungdan-ro, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/14/3462914_image2_1.jpg", + "tel": null, + "lat": 37.5674741142, + "lng": 127.0081710683, + "distanceM": 7104, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273747", + "title": "Hwanghak-dong Flea Market (Dokkaebi Market / Manmul Market) (황학동 벼룩시장 (도깨비시장/만물시장))", + "address": "11-7 Majang-ro 5-gil, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/45/151945_image2_1.jpg", + "tel": null, + "lat": 37.5691713182, + "lng": 127.0181228914, + "distanceM": 7154, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2710325", + "title": "Harmony Mart Myeong-dong Station Branch (하모니마트 명동역점)", + "address": "58 , Myeongdong 10-gil, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/43/2707843_image2_1.jpg", + "tel": null, + "lat": 37.5613410916, + "lng": 126.9857538405, + "distanceM": 7181, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273753", + "title": "Migliore - Myeongdong Branch (밀리오레 (명동점))", + "address": "115 Toegye-ro, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/30/3468830_image2_1.jpg", + "tel": null, + "lat": 37.5611022086, + "lng": 126.984540069, + "distanceM": 7212, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273757", + "title": "Doota Mall (두타몰)", + "address": "275, Jangchungdan-ro, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/48/3525648_image2_1.jpg", + "tel": null, + "lat": 37.5688767395, + "lng": 127.0087681967, + "distanceM": 7245, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "690197", + "title": "Pyounghwa (Pyeonghwa) Fashion Town (서울 평화시장)", + "address": "274, Cheonggyecheon-ro, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/61/614361_image2_1.jpg", + "tel": null, + "lat": 37.5694211079, + "lng": 127.0080126943, + "distanceM": 7318, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273750", + "title": "Shinsegae Department Store - Main Store (신세계백화점 (본점))", + "address": "63 Sogong-ro, Jung-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5609271625, + "lng": 126.9808542989, + "distanceM": 7366, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1327391", + "title": "Seungjin Toy (승진완구)", + "address": "30, Jong-ro 52-gil, Jongno-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/05/1309005_image2_1.jpg", + "tel": null, + "lat": 37.5713154428, + "lng": 127.0141843007, + "distanceM": 7430, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3058019", + "title": "Beauty Play (뷰티플레이)", + "address": "73 Myeongdong-gil, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/25/3564025_image2_1.jpg", + "tel": null, + "lat": 37.5643240879, + "lng": 126.9863231484, + "distanceM": 7448, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "273734", + "title": "Dongdaemun Shopping Complex·Dongdaemun Shopping Town (동대문 종합시장·동대문 쇼핑타운)", + "address": "266 Jong-ro, Jongno-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/49/710249_image2_1.jpg", + "tel": null, + "lat": 37.5707292767, + "lng": 127.0073293863, + "distanceM": 7473, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3364966", + "title": "Donga Stationery (동아완구)", + "address": "5 Jong-ro 52-gil, Jongno-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/63/3357663_image2_1.jpg", + "tel": null, + "lat": 37.5719181289, + "lng": 127.0129788379, + "distanceM": 7512, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1933245", + "title": "Seoul Folk Flea Market (서울 풍물시장)", + "address": "21 Cheonho-daero 4-gil, Dongdaemun-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/63/3107663_image2_1.jpg", + "tel": null, + "lat": 37.5727182604, + "lng": 127.025466566, + "distanceM": 7519, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3110407", + "title": "Junggok-dong Furniture Street (중곡동 가구거리)", + "address": "Cheonho-daero, Gwangjin-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5571404902, + "lng": 127.0796620717, + "distanceM": 7535, + "rating": null, + "reviewCount": null, + "openingHours": null + } + ], + "stay": [ + { + "id": "782947", + "title": "Best Western Premier Gangnam Hotel (베스트웨스턴 프리미어 강남호텔)", + "address": "139, Bongeunsa-ro, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5063677122, + "lng": 127.029790275, + "distanceM": 418, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "349317", + "title": "Imperial Palace Hotel (임피리얼 팰리스 호텔)", + "address": "640 Eonju-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/63/2048263_image2_1.jpg", + "tel": null, + "lat": 37.5136697521, + "lng": 127.0358867353, + "distanceM": 1340, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2542376", + "title": "GyoDae GuestHouse (교대게스트하우스)", + "address": "116 , Saimdang-ro 17-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/81/2529081_image2_1.jpg", + "tel": null, + "lat": 37.4933329219, + "lng": 127.0190443994, + "distanceM": 1412, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3486175", + "title": "The Hyoosik Nouvelle Seoul Itaewon (더휴식 누베르 서울 이태원점)", + "address": "11 Usadan-ro 14-gil, Yongsan-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/67/3483867_image2_1.jpg", + "tel": null, + "lat": 37.5343166442, + "lng": 126.9963480845, + "distanceM": 4157, + "rating": null, + "reviewCount": null, + "openingHours": null + } + ], + "restaurant": [ + { + "id": "3400447", + "title": "ab Cafe At Bali (에이비카페 At Bali)", + "address": "32 Gangnam-daero 102-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/96/2947096_image2_1.jpg", + "tel": null, + "lat": 37.5029569643, + "lng": 127.0279516492, + "distanceM": 318, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3556784", + "title": "Dojeong Yukgwan (도정육관)", + "address": "42 Seocho-daero 78-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/28/3551128_image2_1.jpg", + "tel": null, + "lat": 37.4940774451, + "lng": 127.0282407632, + "distanceM": 1236, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3556943", + "title": "BBQ Yul (고깃집열)", + "address": "48 Seocho-daero 78-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/22/3551922_image2_1.jpg", + "tel": null, + "lat": 37.4935414029, + "lng": 127.0286212989, + "distanceM": 1301, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3333569", + "title": "Yoojung Restaurant (유정식당)", + "address": "14 Dosan-daero 28-gil, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5184190599, + "lng": 127.0280817557, + "distanceM": 1510, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3574500", + "title": "Modern Nulang Central City Branch (모던눌랑 센트럴시티점)", + "address": "205 Sapyeong-daero, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/85/3570685_image2_1.jpg", + "tel": null, + "lat": 37.5045314196, + "lng": 127.0077895512, + "distanceM": 1568, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3574509", + "title": "Texas de Brazil Central City Branch (텍사스 데 브라질 센트럴시티점)", + "address": "205 Sapyeong-daero, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/32/3570532_image2_1.jpg", + "tel": null, + "lat": 37.5045314196, + "lng": 127.0077895512, + "distanceM": 1568, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3392013", + "title": "Cafe PLENO (카페쁠레노)", + "address": "724, Seolleung-ro, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5193525139, + "lng": 127.0408194587, + "distanceM": 2102, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3340120", + "title": "Passi 0914 (파시0914)", + "address": "15 Dosan-daero 45-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/66/2870066_image2_1.jpg", + "tel": null, + "lat": 37.5232825107, + "lng": 127.0351337538, + "distanceM": 2207, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3574457", + "title": "Buvette Main Branch (부베트 본점)", + "address": "854 Nonhyeon-ro, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/89/3570289_image2_1.jpg", + "tel": null, + "lat": 37.5253738224, + "lng": 127.0288814863, + "distanceM": 2284, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3390719", + "title": "mmn (미미네)", + "address": "39, Dosan-daero 66-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/94/2945894_image2_1.jpg", + "tel": null, + "lat": 37.5209912689, + "lng": 127.0439663475, + "distanceM": 2424, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2442632", + "title": "Ogane Jokbal (오가네족발)", + "address": "21 Nonhyeon-ro 36-gil, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.4850954296, + "lng": 127.0435674002, + "distanceM": 2734, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3580724", + "title": "Sichuan House (시추안하우스 본점)", + "address": "29 Teheran-ro 87-gil, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/39/3574239_image2_1.jpg", + "tel": null, + "lat": 37.509642839, + "lng": 127.0580070584, + "distanceM": 2943, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3333570", + "title": "Shinuiju Chapsal Sundae (신의주찹쌀순대)", + "address": "725 Yeongdong-daero, Gangnam-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/47/2833047_image2_1.jpg", + "tel": null, + "lat": 37.5225301805, + "lng": 127.05517084, + "distanceM": 3285, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3061972", + "title": "Maison Hannam (메종 한남)", + "address": "24 UN village 3-gil, Yongsan-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/58/3045758_image2_1.jpg", + "tel": null, + "lat": 37.5343030538, + "lng": 127.0129382404, + "distanceM": 3439, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3107495", + "title": "Kervan Turkish Restaurant (케르반레스토랑)", + "address": "192 Itaewon-ro, Yongsan-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5344166147, + "lng": 126.9954037239, + "distanceM": 4218, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2701185", + "title": "Sigol Bossam (시골보쌈)", + "address": "25 Bangbaecheon-ro 2-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/70/2684570_image2_1.jpg", + "tel": null, + "lat": 37.477125846, + "lng": 126.9842285813, + "distanceM": 4793, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "3409453", + "title": "Ongeundal (옹근달)", + "address": "41-1, Seongsui-ro 7-gil, Seongdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/61/2900861_image2_1.jpg", + "tel": null, + "lat": 37.5427861223, + "lng": 127.0530149062, + "distanceM": 4861, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "2946679", + "title": "Daelim Changgo Gallery (성수동 대림창고 갤러리)", + "address": "78 Seongsui-ro, Seongdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/63/2374863_image2_1.jpg", + "tel": null, + "lat": 37.5420057167, + "lng": 127.0563062954, + "distanceM": 4943, + "rating": null, + "reviewCount": null, + "openingHours": null + } + ], + "wellness": [ + { + "id": "1459282", + "title": "Spa In New York (스파인뉴욕 (SPA IN NEWYORK))", + "address": "406, Gangnam-daero, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.4989324255, + "lng": 127.0276129057, + "distanceM": 699, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1300249", + "title": "JW Spa (제이더블유스파)", + "address": "565, Nonhyeon-ro, Gangnam-gu, Seoul-si", + "image": null, + "tel": null, + "lat": 37.5065205433, + "lng": 127.0337426883, + "distanceM": 759, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "610302", + "title": "Spa Lei (스파레이)", + "address": "5, Gangnam-daero 107-gil, Seocho-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/72/2024972_image2_1.jpg", + "tel": null, + "lat": 37.5178689471, + "lng": 127.0182354456, + "distanceM": 1566, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1295190", + "title": "AMORE Spa (아모레퍼시픽 스파)", + "address": "21, Apgujeong-ro 29-gil, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5286678625, + "lng": 127.0276620598, + "distanceM": 2637, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1300201", + "title": "Q Spa & Clinic (큐스파앤클리닉)", + "address": "8 Samseong-ro 133-gil, Gangnam-gu, Seoul-si", + "image": null, + "tel": null, + "lat": 37.5197470544, + "lng": 127.0487215059, + "distanceM": 2642, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "1024874", + "title": "Will Spa (윌스파)", + "address": "524, Bongeunsa-ro, Gangnam-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.512928552, + "lng": 127.0570876042, + "distanceM": 2950, + "rating": null, + "reviewCount": null, + "openingHours": null + }, + { + "id": "669211", + "title": "Riverside Spa Land (강변스파랜드)", + "address": "2nd basement level, 45, Guuigangbyeon-ro, Gwangjin-gu, Seoul", + "image": null, + "tel": null, + "lat": 37.5348266305, + "lng": 127.0916116181, + "distanceM": 6753, + "rating": null, + "reviewCount": null, + "openingHours": null + } + ] + }, + "festivals": [ + { + "id": "4060947", + "title": "Gugak Performance Jinyeon (국악공연 진연)", + "address": "10 Insadong 5-gil, Jongno-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/35/4060435_image2_1.jpg", + "tel": "+82-2-953-0515", + "lat": 37.5728652641047, + "lng": 126.985653181823, + "distanceM": 8323, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260101", + "endDate": "20261231", + "distanceKm": 8.3 + }, + { + "id": "3113698", + "title": "APAP Artwork Tour (APAP 작품투어 (안양공공예술프로젝트))", + "address": "180 Yesulgongwon-ro, Manan-gu, Anyang-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/69/4054469_image2_1.jpg", + "tel": "Anyang Papillon: +82-31-687-0548", + "lat": 37.41945269174406, + "lng": 126.92561681569023, + "distanceM": 12963, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260310", + "endDate": "20261130", + "distanceKm": 13.0 + }, + { + "id": "4070501", + "title": "Ssokssokdeul-i Festival (국립극장 쏙쏙들이페스티벌)", + "address": "16 Heyri-ro, Tanhyeon-myeon, Paju-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/99/4104199_image2_1.jpeg", + "tel": "+82-31-870-9110", + "lat": 37.7832839326, + "lng": 126.6946859929, + "distanceM": 42492, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260314", + "endDate": "20261226", + "distanceKm": 42.5 + }, + { + "id": "3435521", + "title": "Gombaengi Teotta (곰뱅이텄다)", + "address": "경기도 안성시 보개면 남사당로 198-2", + "image": null, + "tel": null, + "lat": 37.031777162443376, + "lng": 127.31006979658326, + "distanceM": 58336, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260404", + "endDate": "20261129", + "distanceKm": 58.3 + }, + { + "id": "4108059", + "title": "COSMOS BIGBANG 20TH ANNIVERSARY MEDIA EXHIBITION", + "address": "B127 Seosomun-ro, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/94/4106894_image2_1.jpg", + "tel": "02-517-6017", + "lat": 37.5635793508, + "lng": 126.9753893632, + "distanceM": 7869, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260824", + "endDate": "20260927", + "distanceKm": 7.9 + }, + { + "id": "3514846", + "title": "Seoul International Writers' Festival (서울국제작가축제)", + "address": "26 Insadong 9-gil, Jongno-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/69/4073769_image2_1.jpg", + "tel": "+82-70-8826-5207", + "lat": 37.5734199329, + "lng": 126.9836265705, + "distanceM": 8455, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260911", + "endDate": "20260916", + "distanceKm": 8.5 + }, + { + "id": "4104955", + "title": "Seoul Architecture Festival (서울건축문화제)", + "address": "119 Sejong-daero, Jung-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/75/4103175_image2_1.png", + "tel": "070-7178-5840", + "lat": 37.5668837502, + "lng": 126.9767024196, + "distanceM": 8113, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260915", + "endDate": "20261011", + "distanceKm": 8.1 + }, + { + "id": "293144", + "title": "Bucheon International Comics Festival (부천국제만화축제)", + "address": "1, Gilju-ro, Wonmi-gu, Bucheon-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/75/3557475_image2_1.JPG", + "tel": "+82-32-310-3071", + "lat": 37.5081987483, + "lng": 126.7421932715, + "distanceM": 24986, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260918", + "endDate": "20260920", + "distanceKm": 25.0 + }, + { + "id": "1391047", + "title": "Gyeonggi Ceramics Biennale (경기도자비엔날레)", + "address": "263 Gyeongchung-daero 2697beon-gil, Icheon-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/71/4065571_image2_1.jpg", + "tel": "+31-631-6501", + "lat": 37.2769208162, + "lng": 127.4237253326, + "distanceM": 43374, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260918", + "endDate": "20261101", + "distanceKm": 43.4 + }, + { + "id": "2481976", + "title": "Siheung Gaetgol Festival (시흥갯골축제)", + "address": "287, Dongseo-ro, Siheung-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/09/3528809_image2_1.jpg", + "tel": "+82-31-310-2916, 19~20", + "lat": 37.3897711469, + "lng": 126.7812405855, + "distanceM": 25075, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260918", + "endDate": "20260920", + "distanceKm": 25.1 + }, + { + "id": "3364856", + "title": "Han River Walk (한강수계 걷기행사)", + "address": "174-3 Daegok-ri, Gapyeong-eup, Gapyeong-gun, Gyeonggi-do", + "image": null, + "tel": "+82-2-458-3315", + "lat": 37.823886379, + "lng": 127.5134174634, + "distanceM": 55700, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260919", + "endDate": "20261107", + "distanceKm": 55.7 + }, + { + "id": "715355", + "title": "Seoul Street Arts Festival (서울거리예술축제)", + "address": "10 Neungdong-ro, Gwangjin-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/56/4103256_image2_1.png", + "tel": "+82-2-2088-4957", + "lat": 37.5317034539, + "lng": 127.0667659257, + "distanceM": 4704, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260919", + "endDate": "20260920", + "distanceKm": 4.7 + }, + { + "id": "4083822", + "title": "Suwon Hwaseong Media Art (수원화성 미디어아트)", + "address": "Jangan-dong, Paldal-gu, Suwon-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/46/4076246_image2_1.jpg", + "tel": "+82-31-290-3622", + "lat": 37.285512, + "lng": 127.009673, + "distanceM": 24444, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20260919", + "endDate": "20261006", + "distanceKm": 24.4 + }, + { + "id": "697439", + "title": "Anseong Namsadang Baudeogi Festival (안성맞춤 남사당 바우덕이축제)", + "address": "198, Namsadang-ro, Bogae-myeon, Anseong-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/93/4091593_image2_1.jpg", + "tel": "+82-31-678-5414", + "lat": 37.0336344681, + "lng": 127.3054103868, + "distanceM": 57973, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261002", + "endDate": "20261005", + "distanceKm": 58.0 + }, + { + "id": "3379755", + "title": "Bongsan Mask Dance(봉산탈춤 다 모여라~ )", + "address": "서울특별시 종로구 종로1길 45 (세종로)", + "image": null, + "tel": null, + "lat": 37.574618321885374, + "lng": 126.97838493180903, + "distanceM": 8784, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261002", + "endDate": "20261004", + "distanceKm": 8.8 + }, + { + "id": "1273884", + "title": "Bupyeong Pungmul Festival (부평풍물대축제)", + "address": "34-1, Bupyeong-daero, Bupyeong-gu, Incheon", + "image": "https://tong.visitkorea.or.kr/cms/resource/77/4067377_image2_1.jpg", + "tel": "+82-32-509-7516", + "lat": 37.4944181127, + "lng": 126.7229954336, + "distanceM": 26705, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261002", + "endDate": "20261004", + "distanceKm": 26.7 + }, + { + "id": "701688", + "title": "Soraepogu Festival (소래포구 축제)", + "address": "86-17 Jangdong-ro, Namdong-gu, Incheon", + "image": "https://tong.visitkorea.or.kr/cms/resource/93/4074893_image2_1.jpg", + "tel": "+82-70-8820-4038", + "lat": 37.3979276988, + "lng": 126.7393126807, + "distanceM": 27921, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261002", + "endDate": "20261004", + "distanceKm": 27.9 + }, + { + "id": "3345888", + "title": "Gangnam Festival (강남페스티벌)", + "address": "320 Dosan-daero, Gangnam-gu, Seoul", + "image": null, + "tel": "+82-2-3423-5543", + "lat": 37.5221428648, + "lng": 127.0369343307, + "distanceM": 2162, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261003", + "endDate": "20261005", + "distanceKm": 2.2 + }, + { + "id": "4100813", + "title": "Gumcheon Siheung Haenggung Cultural Festival (금천시흥행궁문화제)", + "address": "44 Eunhaengnamu-ro, Geumcheon-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/02/4100002_image2_1.jpg", + "tel": "+82-70-4114-6352", + "lat": 37.4506976783, + "lng": 126.9074454056, + "distanceM": 12034, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261004", + "endDate": "20261004", + "distanceKm": 12.0 + }, + { + "id": "4039267", + "title": "King Jeongjo's Royal Parade (정조대왕 능행차 공동재현)", + "address": "161 Sajik-ro, Jongno-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/59/4078459_image2_1.png", + "tel": "Seoul +82-786-0610Suwon +82-31-5191-2367 Hwaseong +82-70-4202-1017", + "lat": 37.5760307, + "lng": 126.9767218661, + "distanceM": 8992, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261004", + "endDate": "20261004", + "distanceKm": 9.0 + }, + { + "id": "978249", + "title": "Suwon Hwaseong Festival (수원화성문화제)", + "address": "825, Jeongjo-ro, Paldal-gu, Suwon-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/90/4060890_image2_1.jpg", + "tel": "+82-31-5191-3647", + "lat": 37.2818820192, + "lng": 127.0143944638, + "distanceM": 24827, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261004", + "endDate": "20261011", + "distanceKm": 24.8 + }, + { + "id": "4092329", + "title": "GHOST Festival (Gyeonggi Hallyu OST Festival) (GHOST(경기 한류 OST) 페스티벌)", + "address": "148-40 Imjingak-ro, Munsan-eup, Paju-si, Gyeonggi-do", + "image": "https://tong.visitkorea.or.kr/cms/resource/98/4087098_image2_1.jpg", + "tel": "+82-70-7733-0390", + "lat": 37.8920434573, + "lng": 126.7447928585, + "distanceM": 49618, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261009", + "endDate": "20261011", + "distanceKm": 49.6 + }, + { + "id": "4088698", + "title": "Gangdong Prehistoric Culture Festival (강동선사문화축제)", + "address": "875, Olympic-ro, Gangdong-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/77/3541977_image2_1.jpg", + "tel": "+82-2-3425-5240", + "lat": 37.5590614348, + "lng": 127.1306006547, + "distanceM": 11054, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261016", + "endDate": "20261018", + "distanceKm": 11.1 + }, + { + "id": "4104871", + "title": "Geumcheon Harmony Festival (금천하모니축제)", + "address": "70 Siheung-daero 73-gil, Geumcheon-gu, Seoul", + "image": "https://tong.visitkorea.or.kr/cms/resource/92/3483192_image2_1.jpg", + "tel": "070-4118-3420", + "lat": 37.457065652, + "lng": 126.8960368503, + "distanceM": 12599, + "rating": null, + "reviewCount": null, + "openingHours": null, + "startDate": "20261017", + "endDate": "20261018", + "distanceKm": 12.6 + } + ], + "meta": { + "source": "한국관광공사 TourAPI EngService2", + "fetchedAt": "2026-09-09T11:29:26", + "origin": { + "lat": 37.5049823, + "lng": 127.0254182, + "label": "뷰성형외과의원 (봉은사로 107)", + "coordSource": "Naver 지역검색 실측 2026-09-09" + }, + "radiusByType": { + "attraction": 10000, + "culture": 10000, + "shopping": 10000, + "stay": 5000, + "restaurant": 5000 + }, + "festivalRadiusKm": 60.0, + "counts": { + "attraction": { + "apiTotal": 321, + "fetched": 321, + "radiusM": 10000, + "droppedMedicalTourism": 180, + "kept": 134, + "shown": 24, + "droppedSample": [ + "Stantop Urology & Andrology Clinic (스탠탑비뇨의학과의원)", + "View Plastic Surgery (뷰성형외과)", + "Le : Dental Clinic (르치과의원)", + "You&I Clinic Gangnam (강남유앤아이의원)", + "Healingeye Clinic (힐링안과의원)" + ] + }, + "culture": { + "apiTotal": 91, + "fetched": 91, + "radiusM": 10000, + "droppedMedicalTourism": 0, + "kept": 91, + "shown": 24, + "droppedSample": [] + }, + "shopping": { + "apiTotal": 34, + "fetched": 34, + "radiusM": 10000, + "droppedMedicalTourism": 0, + "kept": 34, + "shown": 24, + "droppedSample": [] + }, + "stay": { + "apiTotal": 4, + "fetched": 4, + "radiusM": 5000, + "droppedMedicalTourism": 0, + "kept": 4, + "shown": 4, + "droppedSample": [] + }, + "restaurant": { + "apiTotal": 18, + "fetched": 18, + "radiusM": 5000, + "droppedMedicalTourism": 0, + "kept": 18, + "shown": 18, + "droppedSample": [] + }, + "wellness": { + "note": "관광지(76)에서 스파·찜질방 분류만 분리", + "shown": 7 + } + }, + "limits": [ + "영문 서비스는 강남 일대 숙박·음식점 커버리지가 얇다(반경 5km 숙박 4건·음식점 18건, 국문은 43건·338건).", + "관광지 항목의 대부분(반경 5km 100건 중 92건)이 분류코드 A02020500 \"Medical Tourism Sites\" 였고 제외했다.", + "평점·리뷰·영업시간은 TourAPI 가 제공하지 않는다. Google Places 승인 후 채운다.", + "영문 축제는 areacode 필드가 비어 있어 좌표 거리로 걸렀다.", + "수술 전후 식이 적합성은 병원이 입력한 가이드로만 표시한다. 이 파일에는 없다." + ] + } +} \ No newline at end of file diff --git a/src/pages/DiscoveryReportPage.tsx b/src/pages/DiscoveryReportPage.tsx index fb68943..6f21bb7 100644 --- a/src/pages/DiscoveryReportPage.tsx +++ b/src/pages/DiscoveryReportPage.tsx @@ -7,6 +7,7 @@ import { AEO_GEO_RUBRIC_V2 } from '../data/aeoGeoRubricV2'; import { scoreDiscoveryV2 } from '../lib/discoveryScoreV2'; import { AeoGeoV2Panel } from '../components/discovery/AeoGeoV2Panel'; import { ClinicInputsPanel } from '../components/discovery/ClinicInputsPanel'; +import { MedicalTourismPanel } from '../components/discovery/MedicalTourismPanel'; import { DISCOVERY_RESULTS } from '../data/discoveryResults'; import { scoreDiscovery, levelToSeverity } from '../lib/discoveryScore'; import type { @@ -358,6 +359,9 @@ export default function DiscoveryReportPage() { {/* ── 5b. 서포터즈 자동 빌드 · 병원 확인 항목 (light, v2 §7-5) ── */} + {/* ── 5c. 의료관광 연결 (dark) ── */} + + {/* ── 6. 채점 기준표 (light) ── */} ; + festivals: TourFestival[]; + meta: { + source: string; + fetchedAt: string; + origin: { lat: number; lng: number; label: string; coordSource: string }; + radiusByType: Record; + festivalRadiusKm: number; + counts: Record>; + /** 화면에 그대로 표시한다. 한계를 숨기지 않는다. */ + limits: string[]; + }; +} + +/** + * 회복 단계. 며칠째인지는 의학 판단이므로 여기서 정하지 않는다. + * 이 단계는 "병원에서 얼마나 멀리 움직이는가"라는 이동 반경 기준이며, + * 각 단계가 수술 후 며칠에 해당하는지는 병원이 supporter_inputs 로 입력한다. + */ +export interface RecoveryStage { + id: string; + label: string; + /** 이 단계에서 권하는 이동 반경(m). 거리로만 정의하며 의학적 판단을 담지 않는다. */ + maxDistanceM: number; + /** 이 단계에 배치할 장소 범주. */ + categories: PlaceCategory[]; + note: string; +} + +/** 병원이 입력하는 값. 없으면 화면은 "병원 확인 대기"로 둔다. */ +export interface ClinicRecoveryInput { + /** supporter_inputs.key = 'recovery' */ + recoveryNote?: string; + /** supporter_inputs.key = 'diet_guide' */ + dietGuide?: { + /** 회복 단계별 권장 음식 분류. 병원이 적은 그대로 쓴다. */ + recommended?: string; + /** 회복 단계별 피해야 할 음식 분류. */ + avoid?: string; + /** 단계가 며칠씩인지. */ + stageDays?: string; + }; +}