diff --git a/docs/research/data-check/2026-10-29-daegu-summary.md b/docs/research/data-check/2026-10-29-daegu-summary.md new file mode 100644 index 0000000..f282c5a --- /dev/null +++ b/docs/research/data-check/2026-10-29-daegu-summary.md @@ -0,0 +1,30 @@ +# 대구 공공데이터 점검 결과 (2026-10-29) + +> **⚠ 정정 (2026-11-02)** 이 문서의 관광공사 장소·영문 장소·반려동물 동반 장소 개수는 위치 검색(`locationBasedList2`, 반려동물 서비스 포함)으로 셌는데, 위치 검색은 반경 안 장소의 일부만 돌려준다. 대구에서 시·도 지역 목록(`areaBasedList2`)과 비교하니 위치 검색은 도심 253곳 중 37곳(15%), 수성못 57곳 중 8곳, 앞산 39곳 중 15곳만 돌려줬다. 근거: `tools/data-check/curation_candidates.py` 2026-11-02 실행 결과(`data/curation/daegu/candidates-20261102-3.csv`). 반려동물 동반 장소는 도심 2 → 7(김광석다시그리기길·국채보상운동기념공원·교동시장·더현대 대구 등 추가), 수성못 0 → 수성못 유원지(일부 구역), 앞산 1 → 대구앞산공원·앞산 케이블카(켄넬) 추가로 확인됐다. **아래 관광공사 개수는 과소 집계다.** 카페·식당 영업시간이 공공데이터에 없다는 결론과 상가정보 수치는 별도 API라 이 문제와 무관하다(상가정보 반경 API의 완전성은 따로 확인하지 않았다). + +1차 출시·큐레이션 지역을 대구로 정하고 같은 방식(1차+2차)으로 쟀다. +원자료: `public-data-check-daegu-20261029-1318.json`, `public-data-check2-daegu-20261029-1319.json` · 실행: `tools/data-check/run_region.py daegu` + +## 동네별 수치 (반경 1.5km) + +| 항목 | 도심 (동성로·김광석길·근대골목) | 수성못 | 앞산 | +|---|---|---|---| +| 관광공사 장소 | 37 (관광지 12, 문화시설 1, 레포츠 3, 숙박 3, 쇼핑 2, 음식점 16) | 10 (관광지 4, 행사 1, 음식점 5) | 15 (관광지 4, 숙박 2, 음식점 9) | +| 영문 장소(국문 대비) | 26 (70%) | 2 (20%) | 4 (27%) | +| 반려동물 동반 장소 | 2 (경상감영공원 전구역, 근대골목 일부구역) | 0 | 1 (게스트하우스, 소형견) | +| 데이트 업종(상가정보) | 카페 823 · 빵/도넛 195 · 요리 주점 295 · 생맥주 46 | 카페 202 · 빵/도넛 51 · 요리 주점 168 | 카페 125 · 빵/도넛 29 · 요리 주점 55 | +| 반경 500m 음식: 상가정보 vs 관광공사 | 978 vs 1 | 186 vs 1 | 21 vs 1 | + +행정동별 업소 수(전체·음식·카페)는 원자료 JSON 의 `상가정보.dongs` 참고. 카페가 많은 동: 성내1동 243, 성내2동 140, 삼덕동 120, 두산동 72, 동인동 70, 대봉1동·대명9동 58. + +## 해석 + +1. **공공데이터 장소가 서울보다 적다.** 수성못 10곳, 앞산 15곳. 큐레이션 비중이 서울보다 크다. 대신 도심은 관광지 12곳에 영문 70%로 외국인 대응이 비교적 좋다. +2. **카페·식당 후보는 충분하다.** 세 동네 1.5km 안에 카페 1,150곳, 요리 주점 518곳. 문제는 '양'이 아니라 '고르기'다. +3. **반려동물은 도심 공원 2곳 말고는 거의 없다.** 수성못 산책로 같은 대표 반려견 장소는 큐레이션으로 넣어야 한다. +4. **이번 주말 대구에는 '축제(EV01)'가 15km 안에 0건이다.** 대신 '2026 수성국제비엔날레'(10.27–11.22)는 공연·행사 분류라 축제 필터에서 빠졌다. + 대구에서는 축제만 거르면 홈의 '이번 주말 축제'가 자주 빈다 → 축제·공연·행사를 함께 보여 주고 60일 넘는 상시 프로그램만 빼는 쪽이 낫다. +5. **음식점 영업시간은 서울과 같은 규칙이다.** 예: "11:00~21:00 (준비 시간 15:30~17:00)", "10:00~21:30 (마지막 주문 21:00)", "11:30~18:00 / ※ 재료 소진 시 조기 마감", 쉬는날 "매주 일요일". + 관광지는 "상시 개방"이 많고, 앞산카페거리·안지랑 곱창골목처럼 거리 단위 장소는 "점포별 상이"라 영업시간을 정할 수 없다. +6. **앞산 중심 좌표가 카페거리에서 조금 벗어난 것으로 보인다**(반경 500m 안 음식 21곳뿐). 큐레이션은 행정동(대명 1–11동) 단위로 하므로 문제없지만, 다음 점검 때 좌표를 카페거리 쪽으로 옮긴다. +7. 날씨는 대구 세 동네가 같은 격자(89, 90)다. 목요일 낮 발표 기준 토요일은 하루 전체, 일요일은 3시간 간격. diff --git a/docs/research/data-check/2026-10-29-summary.md b/docs/research/data-check/2026-10-29-summary.md new file mode 100644 index 0000000..704a2d6 --- /dev/null +++ b/docs/research/data-check/2026-10-29-summary.md @@ -0,0 +1,42 @@ +# 공공데이터 점검 결과 (2026-10-29) + +> **⚠ 정정 (2026-11-02)** 이 문서의 관광공사 장소 개수는 위치 검색(`locationBasedList2`, 반려동물 서비스 포함)으로 셌는데, 위치 검색은 반경 안 장소의 일부만 돌려준다. 대구에서 시·도 지역 목록(`areaBasedList2`)과 비교하니 위치 검색은 도심 253곳 중 37곳(15%), 수성못 57곳 중 8곳, 앞산 39곳 중 15곳만 돌려줬다. 근거: `tools/data-check/curation_candidates.py` 2026-11-02 실행 결과(`data/curation/daegu/candidates-20261102-3.csv`). 서울·부산은 다시 재지 않았지만 같은 방식이라 **관광공사 개수는 과소 집계일 가능성이 높다**("성수는 데이트 장소가 빈약" 같은 결론은 다시 봐야 한다). 상가정보 수치와 영업시간 형식에 대한 결론은 무관하다. + +원자료: `public-data-check-20261029-1224.json` · 스크립트: `tools/data-check/public_data_check.py` +대상: 성수(반경 2km)·연남(1.5km)·해운대(2km) · 호출 193건, 오류 0건 + +## 수치 + +| 항목 | 성수 | 연남 | 해운대 | +|---|---|---|---| +| 관광공사 국문 장소 | 121 (음식점 108, 관광지 6, 문화시설 2, 숙박 3) | 54 (음식점 22, 숙박 23, 관광지 7) | 40 (음식점 29, 관광지 4, 숙박 5) | +| 영업시간 필드 있음(표본) | 관광지·문화시설·음식점 100% | 관광지·문화시설·음식점 100% | 관광지·음식점 100% | +| 반려동물 상세(가까운 30곳) | 0곳 | 1곳 | 3곳 | +| 영문 장소(국문 대비) | 17곳 (14%) | 23곳 (43%) | 20곳 (50%) | +| 반경 500m 음식 업소: 상가정보 vs 관광공사 | 650 vs 26 | 1,155 vs 8 | 388 vs 8 | +| 단기예보 범위 | 10.29~11.2 (주말 포함) | 같음 | 같음 | + +## 해석 + +1. **영업시간은 '있음'이지 '쓸 수 있음'이 아니다.** 값이 자유 문장이다. + 예: "상시 개방", "점포별 상이", "하절기 화~일 10:00~17:00 / 동절기 화~일 11:00~16:00", "10:00~17:00
※ 방문 전 연락 필수". + AI의 "도착 시각에 영업 중" 검증을 하려면 이 문장을 시간표로 바꾸는 파서가 필요하고, 못 바꾸는 값은 '미확인'으로 둔다. + 음식점 영업시간(`opentimefood`)의 형식은 이번 표본 예시에 나오지 않아 따로 확인이 필요하다. +2. **카페·식당은 관광공사 데이터에 거의 없다.** 반경 500m 기준 관광공사 음식점은 상가정보의 1–4% 수준. + 상가정보로 이름·위치·업종은 채울 수 있지만 영업시간·반려동물 정보가 없어, 대부분의 카페는 '영업시간 미확인'이 된다. +3. **성수처럼 요즘 뜨는 동네는 관광지가 적다.** 성수 관광지 6·문화시설 2. 갤러리·팝업·카페 같은 데이트 장소가 비어 있다. +4. **반려동물 정보는 매우 적다(가까운 곳 표본 기준).** 표본이 주로 음식점·숙소라 공원 같은 관광지의 동반 정보는 제대로 재지 못했다. + 필드 이름은 확인됨: `acmpyTypeCd`(전구역/일부구역 동반가능), `acmpyPsblCpam`(가능 동물, 예: "5kg 이하"), `acmpyNeedMtr`(필요 사항), + `etcAcmpyInfo`, `relaAcdntRiskMtr`, `relaPosesFclty`, `relaFrnshPrdlst`, `relaPurcPrdlst`, `relaRntlPrdlst`. 크기는 정해진 값이 아니라 문장이다. + 관광지·문화시설 소개정보에 `chkpet`·`chkpetculture`(애완동물 동반 여부) 필드도 있다 — 아직 값 분포를 재지 않았다. +5. **영문 데이터는 국문과 장소 ID가 다르다(같은 ID 0곳).** 대신 영문 제목에 한국어 이름이 괄호로 붙어 있어(예: "Seongsu Museum (성수미술관)") 이름+좌표로 짝지을 수 있다. + 영문 분류 코드도 국문과 다르다(관광지 76, 문화시설 78, 쇼핑 79, 숙박 80, 음식점 82, 레포츠 75). +6. **'축제'에 상시 프로그램이 섞여 있다.** 성수 5km 안 결과가 "DDP 건축투어(1년 내내)", "국제선명상대회(4–11월)". 주말에 열리는 전국 85건도 상당수가 장기 행사로 보인다. + '이번 주말 축제'에는 기간이 긴 행사를 거르는 기준이 필요하다. +7. **날씨는 충분하다.** 단기예보가 오늘부터 4일 뒤(11.2)까지 닿아 주말 예보가 된다. + +## 다음 확인 거리 + +- 음식점 영업시간 형식, `chkpet` 값 분포, 공원·관광지 중심의 반려동물 상세 표본 +- 반려동물 동반여행 전용 API(15135102)의 목록 기능 — 동반 가능 장소만 모아 볼 수 있는지 +- 전시·공연 공공데이터(예: 한눈에보는문화정보 15138937)로 성수 같은 지역의 전시 보강 diff --git a/docs/research/data-check/2026-10-29-summary2.md b/docs/research/data-check/2026-10-29-summary2.md new file mode 100644 index 0000000..ef1bce8 --- /dev/null +++ b/docs/research/data-check/2026-10-29-summary2.md @@ -0,0 +1,41 @@ +# 공공데이터 2차 점검 결과 (2026-10-29) + +> **⚠ 정정 (2026-11-02)** 아래 동네별 반려동물 동반 장소 개수(성수 2·연남 1·해운대 3 등)는 위치 검색(`locationBasedList2`, 반려동물 서비스 포함)으로 셌는데, 위치 검색은 반경 안 장소의 일부만 돌려준다. 대구에서 시·도 지역 목록(`areaBasedList2`)과 비교하니 위치 검색은 도심 253곳 중 37곳(15%), 수성못 57곳 중 8곳, 앞산 39곳 중 15곳만 돌려줬다. 근거: `tools/data-check/curation_candidates.py` 2026-11-02 실행 결과(`data/curation/daegu/candidates-20261102-3.csv`). 대구는 지역 목록으로 다시 세자 도심 2 → 7곳이 됐다. 서울·부산도 **과소 집계일 가능성이 높다**("도심은 실제로 적다", "서울숲 없음"은 다시 봐야 한다). 다만 카페·식당의 반려동물 정보가 공공데이터에 거의 없다는 점은 그대로라, 카페 큐레이션이 필요하다는 결론은 유지된다. + +매뉴얼 검토(`docs/research/data-openapi-review/`)로 바로잡은 방식으로 다시 쟀다. +원자료: `public-data-check2-20261029-1256.json` · 스크립트: `tools/data-check/public_data_check2.py` · 호출 85건 + +## 반려동물 — 도심은 실제로 적다 +- 전국 동반 정보 **9,659건**(contentId 없이 일괄 조회). 전구역 동반가능 9,121 / 일부구역 526. +- 크기·무게 문구가 있는 곳은 **166건(1.7%)** 뿐이고 대부분 "전 견종 동반 가능". 문장이라 파싱해야 한다(예: "10kg미만의 애견", "5kg 이하 숙박가능한 견종"). +- 동반 가능 장소 목록(반경 1.5–2km): **성수 2곳, 연남 1곳, 해운대 3곳**(숙소·음식점). 서울숲도 목록에 없다. + → 1차의 "거의 없음"은 표본 탓만이 아니었다. 도심 데이트 지역의 반려동물 정보는 큐레이션으로 채워야 한다. +- 국문 서비스와 반려동물 전용 서비스의 동반 정보는 같은 데이터(같은 contentid, 같은 값). 국문 일괄 조회 하나로 충분하다. +- 관광지·문화시설 소개정보의 `chkpet*` 값은 표본 21곳 모두 빈 값 → 쓸 수 없다. + +## 축제 — 날짜 조회는 정확, 근처 축제는 드물다 +- `eventStartDate=eventEndDate=토요일` 로 부르면 **그날 열리는 행사만** 온다(85건, 안 겹치는 것 0건). +- 85건 중 60일 넘는 장기 행사 36건. 분류: 행사 EV03 38 · 축제 EV01 30 · 공연 EV02 17. +- 축제(EV01)만 거르면 30건. 다만 도서관 프로그램 같은 것도 축제로 분류돼 있어 기간·제목 기준을 더해야 한다. +- 지역 15km 안 축제: 성수 2건, 연남 2건, 해운대 1건. **코스 동네 안에서 축제를 찾기는 어렵고**, 시·도 단위로 보여 주고 축제를 중심으로 코스를 짜는 흐름이 맞다. + +## 음식점 영업시간 — 규칙이 있어 파싱 가능 +- 예: "11:00~22:00", "24시간", "- 11:00~21:00 / - 준비시간 15:00~17:00", "- 11:30~21:00 / - 준비시간(평일) 15:30~16:30 / - 마지막 주문 20:30" +- 쉬는날 예: "연중무휴", "매주 화요일", "설·추석 당일", "1월 1일 / 설·추석 연휴" +- 영업 시간대 + 준비시간(브레이크) + 마지막 주문 + 정기 휴무를 뽑는 파서로 대부분 처리할 수 있어 보인다. 못 읽는 값은 '미확인'. + +## 상가정보 — 성수 전체 목록 가능 +| 행정동 (adongCd) | 전체 업소 | 음식 | 카페(I21201) | +|---|---|---|---| +| 성수1가1동 (11200650) | 1,188 | 263 | 53 | +| 성수1가2동 (11200660) | 2,528 | 655 | 141 | +| 성수2가1동 (11200670) | 1,693 | 474 | 79 | +| 성수2가3동 (11200690) | 3,574 | 687 | 139 | +| **합계** | **8,983** | **2,079** | **412** | + +- 데이트 후보로 쓸 업종 코드(음식 소분류 43개 중): 카페 I21201, 빵/도넛 I21001, 아이스크림/빙수 I21008, 토스트/샌드위치/샐러드 I21005, 요리 주점 I21104, 생맥주 전문 I21103. 유흥 주점(I21101·I21102)은 제외. + +## 날씨 — 토요일은 됨, 일요일은 일부 +- 11시 발표 기준: 10.29(12시각) · 10.30(24) · 10.31(24) · 11.1(8, 3시간 간격) · 11.2(0시 1건). +- 목요일 낮에 보면 토요일은 하루 전체, 일요일은 3시간 간격만 나온다. 그 뒤 날짜는 중기예보가 필요하다. +- 강수량 값은 "강수없음", "1mm 미만", "1" 처럼 문자열 구간이다. diff --git a/docs/research/data-check/public-data-check-20261029-1224.json b/docs/research/data-check/public-data-check-20261029-1224.json new file mode 100644 index 0000000..dfec12c --- /dev/null +++ b/docs/research/data-check/public-data-check-20261029-1224.json @@ -0,0 +1,867 @@ +{ + "checked_at_kst": "2026-10-29 12:24", + "weekend_saturday": "20261031", + "areas": [ + { + "area": "성수", + "center": [ + 127.0557, + 37.5446 + ], + "radius_m": 2000, + "kor_total": 121, + "kor_by_type": { + "관광지": 6, + "문화시설": 2, + "축제·행사": 1, + "레포츠": 1, + "숙박": 3, + "음식점": 108 + }, + "kor_list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "dist", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "sigungucode", + "tel", + "title", + "zipcode" + ], + "hours": { + "관광지": { + "sampled": 6, + "with_hours": 6, + "with_restdate": 6 + }, + "문화시설": { + "sampled": 2, + "with_hours": 2, + "with_restdate": 1 + }, + "레포츠": { + "sampled": 1, + "with_hours": 0, + "with_restdate": 0 + }, + "쇼핑": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "음식점": { + "sampled": 20, + "with_hours": 20, + "with_restdate": 20 + } + }, + "hours_examples": [ + { + "title": "자양동 양꼬치거리 (중국음식문화거리)", + "type": "관광지", + "hours": "점포별 상이" + }, + { + "title": "살곶이체육공원", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "서울숲 곤충식물원", + "type": "관광지", + "hours": "하절기 화~일 10:00~17:00 / 동절기 화~일 11:00~16:00" + }, + { + "title": "한강", + "type": "관광지", + "hours": "상시 개방" + } + ], + "intro_fields": [ + "accomcount", + "accomcountculture", + "chkbabycarriage", + "chkbabycarriageculture", + "chkcreditcard", + "chkcreditcardculture", + "chkcreditcardfood", + "chkpet", + "chkpetculture", + "contentid", + "contenttypeid", + "discountinfo", + "discountinfofood", + "expagerange", + "expguide", + "firstmenu", + "heritage1", + "heritage2", + "heritage3", + "infocenter", + "infocenterculture", + "infocenterfood", + "kidsfacility", + "lcnsno", + "opendate", + "opendatefood", + "opentimefood", + "packing", + "parking", + "parkingculture", + "parkingfee", + "parkingfood", + "reservationfood", + "restdate", + "restdateculture", + "restdatefood", + "scale", + "scalefood", + "seat", + "smoking", + "spendtime", + "treatmenu", + "usefee", + "useseason", + "usetime", + "usetimeculture" + ], + "pet": { + "sampled": 30, + "with_pet_info": 0, + "with_size_info": 0 + }, + "pet_fields": [], + "pet_examples": [], + "eng_total": 17, + "eng_by_type": { + "82": 4, + "78": 2, + "76": 9, + "79": 2 + }, + "eng_same_contentid": 0, + "eng_examples": [ + { + "title": "Seongsunoru (성수노루)", + "addr1": "110 Achasan-ro, Seongdong-gu, Seoul" + }, + { + "title": "Seongsu Museum (성수미술관)", + "addr1": "39 Seongsuil-ro 8-gil, Seongdong-gu, Seoul" + }, + { + "title": "Seongsu-dong Cafe Street (성수동 카페거리)", + "addr1": "Seongsu-dong, Seongdong-gu, Seoul" + } + ], + "weather": { + "grid": [ + 61, + 126 + ], + "base": "20261029 1100", + "forecast_dates": [ + "20261029", + "20261030", + "20261031", + "20261101", + "20261102" + ], + "covers_weekend": true, + "weekend_1500_pop": "0" + }, + "store": { + "radius_m": 500, + "total": 3127, + "food": 650, + "cafe": 125, + "tour_food_same_radius": 26, + "fields": [ + "adongCd", + "adongNm", + "bizesId", + "bizesNm", + "bldMngNo", + "bldMnno", + "bldNm", + "bldSlno", + "brchNm", + "ctprvnCd", + "ctprvnNm", + "dongNo", + "flrNo", + "hoNo", + "indsLclsCd", + "indsLclsNm", + "indsMclsCd", + "indsMclsNm", + "indsSclsCd", + "indsSclsNm", + "ksicCd", + "ksicNm", + "lat", + "ldongCd", + "ldongNm", + "lnoAdr", + "lnoCd", + "lnoMnno", + "lnoSlno", + "lon", + "newZipcd", + "oldZipcd", + "plotSctCd", + "plotSctNm", + "rdnm", + "rdnmAdr", + "rdnmCd", + "signguCd", + "signguNm" + ] + } + }, + { + "area": "연남", + "center": [ + 126.9235, + 37.5627 + ], + "radius_m": 1500, + "kor_total": 54, + "kor_by_type": { + "관광지": 7, + "문화시설": 1, + "숙박": 23, + "쇼핑": 1, + "음식점": 22 + }, + "kor_list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "dist", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "sigungucode", + "tel", + "title", + "zipcode" + ], + "hours": { + "관광지": { + "sampled": 7, + "with_hours": 7, + "with_restdate": 5 + }, + "문화시설": { + "sampled": 1, + "with_hours": 1, + "with_restdate": 1 + }, + "레포츠": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "쇼핑": { + "sampled": 1, + "with_hours": 0, + "with_restdate": 1 + }, + "음식점": { + "sampled": 20, + "with_hours": 20, + "with_restdate": 20 + } + }, + "hours_examples": [ + { + "title": "연남동 공방거리", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "서울 인디스페이스", + "type": "관광지", + "hours": "매일 10:00 ~ 20:00" + }, + { + "title": "경의선책거리", + "type": "관광지", + "hours": "11:00~20:00" + }, + { + "title": "삼해소주", + "type": "관광지", + "hours": "10:00~17:00
\n※ 방문 전 연락 필수" + } + ], + "intro_fields": [ + "accomcount", + "accomcountculture", + "chkbabycarriage", + "chkbabycarriageculture", + "chkbabycarriageshopping", + "chkcreditcard", + "chkcreditcardculture", + "chkcreditcardfood", + "chkcreditcardshopping", + "chkpet", + "chkpetculture", + "chkpetshopping", + "contentid", + "contenttypeid", + "culturecenter", + "discountinfo", + "discountinfofood", + "expagerange", + "expguide", + "fairday", + "firstmenu", + "heritage1", + "heritage2", + "heritage3", + "infocenter", + "infocenterculture", + "infocenterfood", + "infocentershopping", + "kidsfacility", + "lcnsno", + "opendate", + "opendatefood", + "opendateshopping", + "opentime", + "opentimefood", + "packing", + "parking", + "parkingculture", + "parkingfee", + "parkingfood", + "parkingshopping", + "reservationfood", + "restdate", + "restdateculture", + "restdatefood", + "restdateshopping", + "restroom", + "saleitem", + "saleitemcost", + "scale", + "scalefood", + "scaleshopping", + "seat", + "shopguide", + "smoking", + "spendtime", + "treatmenu", + "usefee", + "useseason", + "usetime", + "usetimeculture" + ], + "pet": { + "sampled": 30, + "with_pet_info": 1, + "with_size_info": 1 + }, + "pet_fields": [ + "acmpyNeedMtr", + "acmpyPsblCpam", + "acmpyTypeCd", + "contentid", + "etcAcmpyInfo", + "relaAcdntRiskMtr", + "relaFrnshPrdlst", + "relaPosesFclty", + "relaPurcPrdlst", + "relaRntlPrdlst" + ], + "pet_examples": [ + { + "title": "준하우스3", + "acmpyTypeCd": "일부구역 동반가능", + "acmpyPsblCpam": "5kg 이하 숙박가능한 견종", + "acmpyNeedMtr": "자유이용" + } + ], + "eng_total": 23, + "eng_by_type": { + "76": 10, + "75": 1, + "79": 3, + "82": 4, + "80": 2, + "78": 3 + }, + "eng_same_contentid": 0, + "eng_examples": [ + { + "title": "Yeonnam-dong (연남동)", + "addr1": "Yeonnam-dong, Mapo-gu, Seoul" + }, + { + "title": "Gyeongui Line Forest Park (Yeontral Park) (경의선숲길(연트럴파크))", + "addr1": "133, Donggyo-ro 51-gil, Mapo-gu, Seoul" + }, + { + "title": "Ceramic Cafe JOOL(도자기카페 줄)", + "addr1": "11, Sinchon-ro 4-gil, Mapo-gu, Seoul" + } + ], + "weather": { + "grid": [ + 59, + 127 + ], + "base": "20261029 1100", + "forecast_dates": [ + "20261029", + "20261030", + "20261031", + "20261101", + "20261102" + ], + "covers_weekend": true, + "weekend_1500_pop": "0" + }, + "store": { + "radius_m": 500, + "total": 3131, + "food": 1155, + "cafe": 237, + "tour_food_same_radius": 8, + "fields": [ + "adongCd", + "adongNm", + "bizesId", + "bizesNm", + "bldMngNo", + "bldMnno", + "bldNm", + "bldSlno", + "brchNm", + "ctprvnCd", + "ctprvnNm", + "dongNo", + "flrNo", + "hoNo", + "indsLclsCd", + "indsLclsNm", + "indsMclsCd", + "indsMclsNm", + "indsSclsCd", + "indsSclsNm", + "ksicCd", + "ksicNm", + "lat", + "ldongCd", + "ldongNm", + "lnoAdr", + "lnoCd", + "lnoMnno", + "lnoSlno", + "lon", + "newZipcd", + "oldZipcd", + "plotSctCd", + "plotSctNm", + "rdnm", + "rdnmAdr", + "rdnmCd", + "signguCd", + "signguNm" + ] + } + }, + { + "area": "해운대", + "center": [ + 129.1604, + 35.1587 + ], + "radius_m": 2000, + "kor_total": 40, + "kor_by_type": { + "관광지": 4, + "축제·행사": 1, + "레포츠": 1, + "숙박": 5, + "음식점": 29 + }, + "kor_list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "dist", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "sigungucode", + "tel", + "title", + "zipcode" + ], + "hours": { + "관광지": { + "sampled": 4, + "with_hours": 4, + "with_restdate": 4 + }, + "문화시설": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "레포츠": { + "sampled": 1, + "with_hours": 0, + "with_restdate": 0 + }, + "쇼핑": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "음식점": { + "sampled": 20, + "with_hours": 20, + "with_restdate": 20 + } + }, + "hours_examples": [ + { + "title": "해운대 관광특구", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "동백공원", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "부산 영화의 거리", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "마린시티", + "type": "관광지", + "hours": "상시 개방" + } + ], + "intro_fields": [ + "accomcount", + "chkbabycarriage", + "chkcreditcard", + "chkcreditcardfood", + "chkpet", + "contentid", + "contenttypeid", + "discountinfofood", + "expagerange", + "expguide", + "firstmenu", + "heritage1", + "heritage2", + "heritage3", + "infocenter", + "infocenterfood", + "kidsfacility", + "lcnsno", + "opendate", + "opendatefood", + "opentimefood", + "packing", + "parking", + "parkingfood", + "reservationfood", + "restdate", + "restdatefood", + "scalefood", + "seat", + "smoking", + "treatmenu", + "useseason", + "usetime" + ], + "pet": { + "sampled": 30, + "with_pet_info": 3, + "with_size_info": 0 + }, + "pet_fields": [ + "acmpyNeedMtr", + "acmpyPsblCpam", + "acmpyTypeCd", + "contentid", + "etcAcmpyInfo", + "relaAcdntRiskMtr", + "relaFrnshPrdlst", + "relaPosesFclty", + "relaPurcPrdlst", + "relaRntlPrdlst" + ], + "pet_examples": [ + { + "title": "캔버스 블랙", + "acmpyTypeCd": "전구역 동반가능", + "acmpyNeedMtr": "목줄 착용,반려동물 유모차 탑승,이동장(켄넬)사용" + }, + { + "title": "캔버스", + "acmpyTypeCd": "전구역 동반가능", + "acmpyNeedMtr": "목줄 착용,반려동물 유모차 탑승,이동장(켄넬)사용" + }, + { + "title": "원시학", + "acmpyTypeCd": "전구역 동반가능", + "acmpyNeedMtr": "기타" + } + ], + "eng_total": 20, + "eng_by_type": { + "76": 10, + "78": 2, + "82": 3, + "75": 3, + "80": 2 + }, + "eng_same_contentid": 0, + "eng_examples": [ + { + "title": "Haeundae Beach (해운대해수욕장)", + "addr1": "264 Haeundaehaebyeon-ro, Haeundae-gu, Busan" + }, + { + "title": "Haeundae Tourist Information Center (해운대관광안내소)", + "addr1": "264 Haeundaehaebyeon-ro, Haeundae-gu, Busan" + }, + { + "title": "Hyeongje Jeontong Dwaeji Gukbap (형제전통돼지국밥)", + "addr1": "" + } + ], + "weather": { + "grid": [ + 99, + 75 + ], + "base": "20261029 1100", + "forecast_dates": [ + "20261029", + "20261030", + "20261031", + "20261101", + "20261102" + ], + "covers_weekend": true, + "weekend_1500_pop": "0" + }, + "store": { + "radius_m": 500, + "total": 1012, + "food": 388, + "cafe": 32, + "tour_food_same_radius": 8, + "fields": [ + "adongCd", + "adongNm", + "bizesId", + "bizesNm", + "bldMngNo", + "bldMnno", + "bldNm", + "bldSlno", + "brchNm", + "ctprvnCd", + "ctprvnNm", + "dongNo", + "flrNo", + "hoNo", + "indsLclsCd", + "indsLclsNm", + "indsMclsCd", + "indsMclsNm", + "indsSclsCd", + "indsSclsNm", + "ksicCd", + "ksicNm", + "lat", + "ldongCd", + "ldongNm", + "lnoAdr", + "lnoCd", + "lnoMnno", + "lnoSlno", + "lon", + "newZipcd", + "oldZipcd", + "plotSctCd", + "plotSctNm", + "rdnm", + "rdnmAdr", + "rdnmCd", + "signguCd", + "signguNm" + ] + } + } + ], + "festivals": { + "fetched": 455, + "active_on_weekend_nationwide": 85, + "near_5km": { + "성수": [ + { + "title": "2026 국제선명상대회", + "period": "20260403–20261112", + "km": 3.2 + }, + { + "title": "DDP 건축투어", + "period": "20260101–20261231", + "km": 4.7 + } + ], + "연남": [ + { + "title": "페인터즈", + "period": "20221101–20261231", + "km": 4.1 + }, + { + "title": "한강역사탐방", + "period": "20260420–20261115", + "km": 4.1 + }, + { + "title": "2026 숭례문 파수의식", + "period": "20260101–20261231", + "km": 4.6 + }, + { + "title": "서울 왕궁수문장 교대의식", + "period": "20260101–20261231", + "km": 4.7 + }, + { + "title": "2026 서울거리공연 구석구석라이브", + "period": "20260501–20261231", + "km": 4.8 + }, + { + "title": "문화가 흐르는 서울광장", + "period": "20260506–20261231", + "km": 4.9 + }, + { + "title": "팔색찬란", + "period": "20260604–20261231", + "km": 4.9 + }, + { + "title": "2026 남산 나이트 페스티벌", + "period": "20261023–20261114", + "km": 5.0 + } + ], + "해운대": [ + { + "title": "광안리 M(Marvelous) 드론 라이트쇼", + "period": "20260101–20261231", + "km": 3.8 + } + ] + }, + "list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "eventenddate", + "eventstartdate", + "festivaltype", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "progresstype", + "sigungucode", + "tel", + "title", + "zipcode" + ] + }, + "calls": { + "국문 목록": 3, + "국문 소개정보": 83, + "국문 반려동물": 90, + "영문 목록": 3, + "기상청 단기예보": 3, + "상가정보": 10, + "국문 축제": 1 + }, + "errors": [] +} \ No newline at end of file diff --git a/docs/research/data-check/public-data-check-daegu-20261029-1318.json b/docs/research/data-check/public-data-check-daegu-20261029-1318.json new file mode 100644 index 0000000..4609664 --- /dev/null +++ b/docs/research/data-check/public-data-check-daegu-20261029-1318.json @@ -0,0 +1,812 @@ +{ + "checked_at_kst": "2026-10-29 13:18", + "region": "daegu", + "weekend_saturday": "20261031", + "areas": [ + { + "area": "대구 도심", + "center": [ + 128.5966, + 35.865 + ], + "radius_m": 1500, + "kor_total": 37, + "kor_by_type": { + "관광지": 12, + "문화시설": 1, + "레포츠": 3, + "숙박": 3, + "쇼핑": 2, + "음식점": 16 + }, + "kor_list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "dist", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "sigungucode", + "tel", + "title", + "zipcode" + ], + "hours": { + "관광지": { + "sampled": 12, + "with_hours": 11, + "with_restdate": 11 + }, + "문화시설": { + "sampled": 1, + "with_hours": 1, + "with_restdate": 1 + }, + "레포츠": { + "sampled": 3, + "with_hours": 1, + "with_restdate": 0 + }, + "쇼핑": { + "sampled": 2, + "with_hours": 2, + "with_restdate": 2 + }, + "음식점": { + "sampled": 16, + "with_hours": 16, + "with_restdate": 16 + } + }, + "hours_examples": [ + { + "title": "봉산문화거리", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "2·28 기념중앙공원", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "대구향교", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "대구 동성로거리", + "type": "관광지", + "hours": "상시 개방" + } + ], + "intro_fields": [ + "accomcount", + "accomcountculture", + "accomcountleports", + "chkbabycarriage", + "chkbabycarriageculture", + "chkbabycarriageleports", + "chkbabycarriageshopping", + "chkcreditcard", + "chkcreditcardculture", + "chkcreditcardfood", + "chkcreditcardleports", + "chkcreditcardshopping", + "chkpet", + "chkpetculture", + "chkpetleports", + "chkpetshopping", + "contentid", + "contenttypeid", + "culturecenter", + "discountinfo", + "discountinfofood", + "expagerange", + "expagerangeleports", + "expguide", + "fairday", + "firstmenu", + "heritage1", + "heritage2", + "heritage3", + "infocenter", + "infocenterculture", + "infocenterfood", + "infocenterleports", + "infocentershopping", + "kidsfacility", + "lcnsno", + "opendate", + "opendatefood", + "opendateshopping", + "openperiod", + "opentime", + "opentimefood", + "packing", + "parking", + "parkingculture", + "parkingfee", + "parkingfeeleports", + "parkingfood", + "parkingleports", + "parkingshopping", + "reservation", + "reservationfood", + "restdate", + "restdateculture", + "restdatefood", + "restdateleports", + "restdateshopping", + "restroom", + "saleitem", + "saleitemcost", + "scale", + "scalefood", + "scaleleports", + "scaleshopping", + "seat", + "shopguide", + "smoking", + "spendtime", + "treatmenu", + "usefee", + "usefeeleports", + "useseason", + "usetime", + "usetimeculture", + "usetimeleports" + ], + "pet": { + "sampled": 30, + "with_pet_info": 2, + "with_size_info": 0 + }, + "pet_fields": [ + "acmpyNeedMtr", + "acmpyPsblCpam", + "acmpyTypeCd", + "contentid", + "etcAcmpyInfo", + "relaAcdntRiskMtr", + "relaFrnshPrdlst", + "relaPosesFclty", + "relaPurcPrdlst", + "relaRntlPrdlst" + ], + "pet_examples": [ + { + "title": "경상감영공원", + "acmpyTypeCd": "전구역 동반가능", + "etcAcmpyInfo": "- 맹견의 경우, 입마개 착용 필수\n- 배변봉투 지참 및 배변처리 필수", + "acmpyPsblCpam": "전 견종 동반 가능", + "acmpyNeedMtr": "목줄 착용" + }, + { + "title": "대구 근대골목(근대로의 여행)", + "acmpyTypeCd": "일부구역 동반가능", + "etcAcmpyInfo": "- 매장 입장 가능 여부 개별 문의 필요\n- 맹견의 경우, 입마개 착용 필수\n- 배변봉투 지참 및 배변처리 필수", + "acmpyPsblCpam": "전 견종 동반 가능", + "acmpyNeedMtr": "목줄 착용" + } + ], + "eng_total": 26, + "eng_by_type": { + "76": 19, + "78": 4, + "82": 2, + "79": 1 + }, + "eng_same_contentid": 0, + "eng_examples": [ + { + "title": "Lift Plastic Surgery (리프트성형외과의원)", + "addr1": "(3rd,11th,12th Floors, S Tower), 2127 Dalgubeol-daero, Jung-gu, Daegu" + }, + { + "title": "Bongsan Cultural Center (봉산문화회관)", + "addr1": "77, Bongsanmunhwa-gil, Jung-gu, Daegu" + }, + { + "title": "Allforskin Dermatology Clinic (올포스킨피부과)", + "addr1": "4th Floor, Dongwon Building, 26 Dongseong-ro 5-gil, Jung-gu, Daegu" + } + ], + "weather": { + "grid": [ + 89, + 90 + ], + "base": "20261029 1100", + "forecast_dates": [ + "20261029", + "20261030", + "20261031", + "20261101", + "20261102" + ], + "covers_weekend": true, + "weekend_1500_pop": "0" + }, + "store": { + "radius_m": 500, + "total": 3185, + "food": 978, + "cafe": 211, + "tour_food_same_radius": 1, + "fields": [ + "adongCd", + "adongNm", + "bizesId", + "bizesNm", + "bldMngNo", + "bldMnno", + "bldNm", + "bldSlno", + "brchNm", + "ctprvnCd", + "ctprvnNm", + "dongNo", + "flrNo", + "hoNo", + "indsLclsCd", + "indsLclsNm", + "indsMclsCd", + "indsMclsNm", + "indsSclsCd", + "indsSclsNm", + "ksicCd", + "ksicNm", + "lat", + "ldongCd", + "ldongNm", + "lnoAdr", + "lnoCd", + "lnoMnno", + "lnoSlno", + "lon", + "newZipcd", + "oldZipcd", + "plotSctCd", + "plotSctNm", + "rdnm", + "rdnmAdr", + "rdnmCd", + "signguCd", + "signguNm" + ] + } + }, + { + "area": "수성못", + "center": [ + 128.6176, + 35.8282 + ], + "radius_m": 1500, + "kor_total": 10, + "kor_by_type": { + "관광지": 4, + "축제·행사": 1, + "음식점": 5 + }, + "kor_list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "dist", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "sigungucode", + "tel", + "title", + "zipcode" + ], + "hours": { + "관광지": { + "sampled": 4, + "with_hours": 4, + "with_restdate": 4 + }, + "문화시설": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "레포츠": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "쇼핑": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "음식점": { + "sampled": 5, + "with_hours": 5, + "with_restdate": 5 + } + }, + "hours_examples": [ + { + "title": "봉산서원(대구)", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "공룡공원", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "고산골 메타쉐콰이어길", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "들안길먹거리타운", + "type": "관광지", + "hours": "상시 개방" + } + ], + "intro_fields": [ + "accomcount", + "chkbabycarriage", + "chkcreditcard", + "chkcreditcardfood", + "chkpet", + "contentid", + "contenttypeid", + "discountinfofood", + "expagerange", + "expguide", + "firstmenu", + "heritage1", + "heritage2", + "heritage3", + "infocenter", + "infocenterfood", + "kidsfacility", + "lcnsno", + "opendate", + "opendatefood", + "opentimefood", + "packing", + "parking", + "parkingfood", + "reservationfood", + "restdate", + "restdatefood", + "scalefood", + "seat", + "smoking", + "treatmenu", + "useseason", + "usetime" + ], + "pet": { + "sampled": 9, + "with_pet_info": 0, + "with_size_info": 0 + }, + "pet_fields": [], + "pet_examples": [], + "eng_total": 2, + "eng_by_type": { + "85": 1, + "76": 1 + }, + "eng_same_contentid": 0, + "eng_examples": [ + { + "title": "Suseong Light Art Festival (수성빛예술제)", + "addr1": "112 Muhak-ro, Suseong-gu, Daegu" + }, + { + "title": "Dinosaur Park (공룡공원)", + "addr1": "43, Yongdu 2-gil, Nam-gu, Daegu" + } + ], + "weather": { + "grid": [ + 89, + 90 + ], + "base": "20261029 1100", + "forecast_dates": [ + "20261029", + "20261030", + "20261031", + "20261101", + "20261102" + ], + "covers_weekend": true, + "weekend_1500_pop": "0" + }, + "store": { + "radius_m": 500, + "total": 359, + "food": 186, + "cafe": 32, + "tour_food_same_radius": 1, + "fields": [ + "adongCd", + "adongNm", + "bizesId", + "bizesNm", + "bldMngNo", + "bldMnno", + "bldNm", + "bldSlno", + "brchNm", + "ctprvnCd", + "ctprvnNm", + "dongNo", + "flrNo", + "hoNo", + "indsLclsCd", + "indsLclsNm", + "indsMclsCd", + "indsMclsNm", + "indsSclsCd", + "indsSclsNm", + "ksicCd", + "ksicNm", + "lat", + "ldongCd", + "ldongNm", + "lnoAdr", + "lnoCd", + "lnoMnno", + "lnoSlno", + "lon", + "newZipcd", + "oldZipcd", + "plotSctCd", + "plotSctNm", + "rdnm", + "rdnmAdr", + "rdnmCd", + "signguCd", + "signguNm" + ] + } + }, + { + "area": "앞산", + "center": [ + 128.5845, + 35.8325 + ], + "radius_m": 1500, + "kor_total": 15, + "kor_by_type": { + "관광지": 4, + "숙박": 2, + "음식점": 9 + }, + "kor_list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "dist", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "sigungucode", + "tel", + "title", + "zipcode" + ], + "hours": { + "관광지": { + "sampled": 4, + "with_hours": 3, + "with_restdate": 4 + }, + "문화시설": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "레포츠": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "쇼핑": { + "sampled": 0, + "with_hours": 0, + "with_restdate": 0 + }, + "음식점": { + "sampled": 9, + "with_hours": 9, + "with_restdate": 9 + } + }, + "hours_examples": [ + { + "title": "앞산카페거리", + "type": "관광지", + "hours": "※ 점포별 상이하므로 홈페이지 참고" + }, + { + "title": "안지랑 곱창골목", + "type": "관광지", + "hours": "※ 점포별 상이함" + }, + { + "title": "안일사(대구)", + "type": "관광지", + "hours": "상시 개방" + }, + { + "title": "앞산울진대게 본점", + "type": "음식점", + "hours": "11:30~22:30" + } + ], + "intro_fields": [ + "accomcount", + "chkbabycarriage", + "chkcreditcard", + "chkcreditcardfood", + "chkpet", + "contentid", + "contenttypeid", + "discountinfofood", + "expagerange", + "expguide", + "firstmenu", + "heritage1", + "heritage2", + "heritage3", + "infocenter", + "infocenterfood", + "kidsfacility", + "lcnsno", + "opendate", + "opendatefood", + "opentimefood", + "packing", + "parking", + "parkingfood", + "reservationfood", + "restdate", + "restdatefood", + "scalefood", + "seat", + "smoking", + "treatmenu", + "useseason", + "usetime" + ], + "pet": { + "sampled": 15, + "with_pet_info": 1, + "with_size_info": 1 + }, + "pet_fields": [ + "acmpyNeedMtr", + "acmpyPsblCpam", + "acmpyTypeCd", + "contentid", + "etcAcmpyInfo", + "relaAcdntRiskMtr", + "relaFrnshPrdlst", + "relaPosesFclty", + "relaPurcPrdlst", + "relaRntlPrdlst" + ], + "pet_examples": [ + { + "title": "유원게스트하우스", + "acmpyTypeCd": "전구역 동반가능", + "acmpyPsblCpam": "소형견" + } + ], + "eng_total": 4, + "eng_by_type": { + "76": 3, + "78": 1 + }, + "eng_same_contentid": 0, + "eng_examples": [ + { + "title": "Daegu Apsan Park (대구앞산공원)", + "addr1": "574-87 Apsansunhwan-ro, Nam-gu, Daegu" + }, + { + "title": "Apsan Cafe Street (앞산카페거리)", + "addr1": "191 Daemyeongnam-ro, Nam-gu, Daegu" + }, + { + "title": "Nakdong River Victory Memorial Hall (낙동강승전기념관)", + "addr1": "574-110, Apsansunhwan-ro, Nam-gu, Daegu" + } + ], + "weather": { + "grid": [ + 89, + 90 + ], + "base": "20261029 1100", + "forecast_dates": [ + "20261029", + "20261030", + "20261031", + "20261101", + "20261102" + ], + "covers_weekend": true, + "weekend_1500_pop": "0" + }, + "store": { + "radius_m": 500, + "total": 94, + "food": 21, + "cafe": 7, + "tour_food_same_radius": 1, + "fields": [ + "adongCd", + "adongNm", + "bizesId", + "bizesNm", + "bldMngNo", + "bldMnno", + "bldNm", + "bldSlno", + "brchNm", + "ctprvnCd", + "ctprvnNm", + "dongNo", + "flrNo", + "hoNo", + "indsLclsCd", + "indsLclsNm", + "indsMclsCd", + "indsMclsNm", + "indsSclsCd", + "indsSclsNm", + "ksicCd", + "ksicNm", + "lat", + "ldongCd", + "ldongNm", + "lnoAdr", + "lnoCd", + "lnoMnno", + "lnoSlno", + "lon", + "newZipcd", + "oldZipcd", + "plotSctCd", + "plotSctNm", + "rdnm", + "rdnmAdr", + "rdnmCd", + "signguCd", + "signguNm" + ] + } + } + ], + "festivals": { + "fetched": 455, + "active_on_weekend_nationwide": 85, + "near_5km": { + "대구 도심": [ + { + "title": "2026 수성국제비엔날레", + "period": "20261027–20261122", + "km": 4.5 + } + ], + "수성못": [ + { + "title": "2026 수성국제비엔날레", + "period": "20261027–20261122", + "km": 0.3 + } + ], + "앞산": [ + { + "title": "2026 수성국제비엔날레", + "period": "20261027–20261122", + "km": 3.3 + } + ] + }, + "list_fields": [ + "addr1", + "addr2", + "areacode", + "cat1", + "cat2", + "cat3", + "contentid", + "contenttypeid", + "cpyrhtDivCd", + "createdtime", + "eventenddate", + "eventstartdate", + "festivaltype", + "firstimage", + "firstimage2", + "lDongRegnCd", + "lDongSignguCd", + "lclsSystm1", + "lclsSystm2", + "lclsSystm3", + "mapx", + "mapy", + "mlevel", + "modifiedtime", + "progresstype", + "sigungucode", + "tel", + "title", + "zipcode" + ] + }, + "calls": { + "국문 목록": 3, + "국문 소개정보": 56, + "국문 반려동물": 54, + "영문 목록": 3, + "기상청 단기예보": 3, + "상가정보": 6, + "국문 축제": 1 + }, + "errors": [] +} \ No newline at end of file diff --git a/docs/research/data-check/public-data-check2-20261029-1256.json b/docs/research/data-check/public-data-check2-20261029-1256.json new file mode 100644 index 0000000..2df3716 --- /dev/null +++ b/docs/research/data-check/public-data-check2-20261029-1256.json @@ -0,0 +1,419 @@ +{ + "checked_at_kst": "2026-10-29 12:56", + "weekend_saturday": "20261031", + "반려동물": { + "bulk_total": 9659, + "bulk_acmpyTypeCd": [ + [ + "전구역 동반가능", + 9121 + ], + [ + "일부구역 동반가능", + 526 + ], + [ + "", + 12 + ] + ], + "bulk_with_size_text": 166, + "bulk_psblcpam_examples": [ + "전 견종 동반 가능", + "전 견종 동반 가능", + "전 견종 출입 가능(맹견의 경우, 입마개 착용 필수)", + "전 견종 동반 가능", + "이동장(켄넬)에 들어가는 전 견종 동반 가능", + "전 견종 동반 가능" + ], + "areas": [ + { + "area": "성수", + "pet_places": 2, + "by_type": { + "숙박": 1, + "음식점": 1 + }, + "with_conditions_in_bulk": 2, + "with_size_text": 1, + "examples": [ + { + "title": "리틀포레한강", + "type": "숙박", + "cond": "전구역 동반가능", + "who": "10kg미만의 애견" + }, + { + "title": "유어네이키드치즈", + "type": "음식점", + "cond": "일부구역 동반가능", + "who": "" + } + ] + }, + { + "area": "연남", + "pet_places": 1, + "by_type": { + "숙박": 1 + }, + "with_conditions_in_bulk": 1, + "with_size_text": 1, + "examples": [ + { + "title": "준하우스3", + "type": "숙박", + "cond": "일부구역 동반가능", + "who": "5kg 이하 숙박가능한 견종" + } + ] + }, + { + "area": "해운대", + "pet_places": 3, + "by_type": { + "숙박": 2, + "음식점": 1 + }, + "with_conditions_in_bulk": 3, + "with_size_text": 0, + "examples": [ + { + "title": "캔버스 블랙", + "type": "숙박", + "cond": "전구역 동반가능", + "who": "" + }, + { + "title": "캔버스", + "type": "숙박", + "cond": "전구역 동반가능", + "who": "" + }, + { + "title": "원시학", + "type": "음식점", + "cond": "전구역 동반가능", + "who": "" + } + ] + } + ], + "same_record_check": { + "contentid": "3532633", + "title": "리틀포레한강", + "kor_service": { + "contentid": "3532633", + "relaAcdntRiskMtr": "", + "acmpyTypeCd": "전구역 동반가능", + "relaPosesFclty": "배변패드, 비닐", + "relaFrnshPrdlst": "", + "etcAcmpyInfo": "", + "relaPurcPrdlst": "", + "acmpyPsblCpam": "10kg미만의 애견", + "relaRntlPrdlst": "", + "acmpyNeedMtr": "자유이용" + }, + "pet_service": { + "contentid": "3532633", + "relaAcdntRiskMtr": "", + "acmpyTypeCd": "전구역 동반가능", + "relaPosesFclty": "배변패드, 비닐", + "relaFrnshPrdlst": "", + "etcAcmpyInfo": "", + "relaPurcPrdlst": "", + "acmpyPsblCpam": "10kg미만의 애견", + "relaRntlPrdlst": "", + "acmpyNeedMtr": "자유이용" + }, + "identical": true + } + }, + "축제": { + "same_day_total": 85, + "same_day_not_overlapping": 0, + "same_day_over_60_days": 36, + "lclsSystm2_counts": [ + [ + "EV03", + 38 + ], + [ + "EV01", + 30 + ], + [ + "EV02", + 17 + ] + ], + "progresstype": [ + [ + "선택안함", + 74 + ], + [ + "", + 10 + ], + [ + "온라인", + 1 + ] + ], + "festivaltype": [ + [ + "", + 85 + ] + ], + "ev01_total": 30, + "ev01_within_15km": { + "성수": [ + "양재천을 품은 양재도서관 「물길 따라 그림책 여행」 (20261010–20261108, 8.1km)", + "노원달빛산책 (20261016–20261108, 11.7km)" + ], + "연남": [ + "고양행주한우 숯불구이축제 (20261031–20261101, 13.0km)", + "양재천을 품은 양재도서관 「물길 따라 그림책 여행」 (20261010–20261108, 13.9km)" + ], + "해운대": [ + "영도다리축제 (20261030–20261101, 11.5km)" + ] + }, + "ev01_examples": [ + "경기‧양주‧지작사 드론봇 페스티벌 (20261030–20261101)", + "경남고성공룡세계엑스포 (20260922–20261101)", + "고양도시농업축제 (20261031–20261101)", + "고양행주한우 숯불구이축제 (20261031–20261101)", + "국가유산 미디어아트 여수 진남관 (20261009–20261108)", + "노원달빛산책 (20261016–20261108)", + "단풍나무숲길 힐링축제 (20261015–20261115)", + "대장경기록문화축제 (20261009–20261101)" + ] + }, + "영업시간·동반여부": { + "food_hours_examples": [ + { + "area": "성수", + "title": "성수노루", + "opentimefood": "- 16:00~23:00 / - 마지막 주문 22:00", + "restdatefood": "설·추석 당일" + }, + { + "area": "성수", + "title": "텐동식당", + "opentimefood": "- 11:00~21:00 / - 준비시간 15:00~17:00", + "restdatefood": "연중무휴" + }, + { + "area": "성수", + "title": "탐광", + "opentimefood": "- 11:30~21:00 / - 준비시간(평일) 15:30~16:30 / - 마지막 주문 20:30", + "restdatefood": "1월 1일 / 설·추석 연휴" + }, + { + "area": "성수", + "title": "레이더", + "opentimefood": "11:00~22:00", + "restdatefood": "연중무휴" + }, + { + "area": "연남", + "title": "감나무집기사식당", + "opentimefood": "24시간", + "restdatefood": "연중무휴" + }, + { + "area": "연남", + "title": "얼스어스", + "opentimefood": "12:00~21:00", + "restdatefood": "매주 화요일" + }, + { + "area": "연남", + "title": "소이연남", + "opentimefood": "- 11:00~21:20 / - 준비시간(평일) 15:00~17:00 / - 마지막 주문 14:30, 20:50", + "restdatefood": "연중무휴" + }, + { + "area": "연남", + "title": "메종브리크", + "opentimefood": "- 11:30~22:00 / - 준비시간 15:30~17:00 / - 마지막 주문 21:00", + "restdatefood": "연중무휴" + }, + { + "area": "해운대", + "title": "일품한우", + "opentimefood": "07:30~23:00 (마지막 주문 22:00)", + "restdatefood": "연중무휴" + }, + { + "area": "해운대", + "title": "일번지자갈치산곰장어", + "opentimefood": "11:00~01:00", + "restdatefood": "연중무휴" + }, + { + "area": "해운대", + "title": "싱싱뽈락회 해운대본점", + "opentimefood": "- 월요일~토요일 12:00~22:30 / - 일요일 12:00~21:30", + "restdatefood": "연중무휴" + }, + { + "area": "해운대", + "title": "오반장", + "opentimefood": "12:00~01:00", + "restdatefood": "연중무휴" + } + ], + "chkpet_values": [ + [ + "(빈 값)", + 21 + ] + ], + "chkpet_sampled": 21 + }, + "상가정보": { + "seongsu_dongs": [ + { + "adongCd": "11200650", + "adongNm": "성수1가1동" + }, + { + "adongCd": "11200660", + "adongNm": "성수1가2동" + }, + { + "adongCd": "11200670", + "adongNm": "성수2가1동" + }, + { + "adongCd": "11200690", + "adongNm": "성수2가3동" + } + ], + "food_small_codes": [ + "I207/I20701 구내식당·뷔페>구내식당", + "I207/I20702 구내식당·뷔페>뷔페", + "I210/I21099 기타 간이>그 외 기타 간이 음식점", + "I210/I21007 기타 간이>김밥/만두/분식", + "I210/I21002 기타 간이>떡/한과", + "I210/I21004 기타 간이>버거", + "I210/I21001 기타 간이>빵/도넛", + "I210/I21008 기타 간이>아이스크림/빙수", + "I210/I21006 기타 간이>치킨", + "I210/I21005 기타 간이>토스트/샌드위치/샐러드", + "I210/I21003 기타 간이>피자", + "I206/I20601 기타 외국>분류 안된 외국식 음식점", + "I205/I20599 동남아시아>기타 동남아식 전문", + "I205/I20501 동남아시아>베트남식 전문", + "I212/I21201 비알코올 >카페", + "I204/I20401 서양식>경양식", + "I204/I20499 서양식>기타 서양식 음식점", + "I204/I20402 서양식>파스타/스테이크", + "I204/I20403 서양식>패밀리레스토랑", + "I203/I20399 일식>기타 일식 음식점", + "I203/I20303 일식>일식 면 요리", + "I203/I20302 일식>일식 카레/돈가스/덮밥", + "I203/I20301 일식>일식 회/초밥", + "I211/I21102 주점>무도 유흥 주점", + "I211/I21103 주점>생맥주 전문", + "I211/I21104 주점>요리 주점", + "I211/I21101 주점>일반 유흥 주점", + "I202/I20202 중식>마라탕/훠궈", + "I202/I20201 중식>중국집", + "I201/I20109 한식>곱창 전골/구이", + "I201/I20102 한식>국/탕/찌개류", + "I201/I20105 한식>국수/칼국수", + "I201/I20199 한식>기타 한식 음식점", + "I201/I20106 한식>냉면/밀면", + "I201/I20110 한식>닭/오리고기 구이/찜", + "I201/I20107 한식>돼지고기 구이/찜", + "I201/I20101 한식>백반/한정식", + "I201/I20113 한식>복 요리 전문", + "I201/I20108 한식>소고기 구이/찜", + "I201/I20104 한식>전/부침개", + "I201/I20103 한식>족발/보쌈", + "I201/I20112 한식>해산물 구이/찜", + "I201/I20111 한식>횟집" + ], + "cafe_like_codes": [ + { + "mcls": "I210", + "scls": "I21008", + "name": "기타 간이>아이스크림/빙수" + }, + { + "mcls": "I212", + "scls": "I21201", + "name": "비알코올 >카페" + } + ], + "seongsu_counts": [ + { + "adongNm": "성수1가1동", + "all": 1188, + "food": 263, + "scls I21008": 1, + "scls I21201": 53 + }, + { + "adongNm": "성수1가2동", + "all": 2528, + "food": 655, + "scls I21008": 2, + "scls I21201": 141 + }, + { + "adongNm": "성수2가1동", + "all": 1693, + "food": 474, + "scls I21008": null, + "scls I21201": 79 + }, + { + "adongNm": "성수2가3동", + "all": 3574, + "food": 687, + "scls I21008": 3, + "scls I21201": 139 + } + ] + }, + "날씨": { + "base": "20261029 1100", + "times_per_date": { + "20261029": 12, + "20261030": 24, + "20261031": 24, + "20261101": 8, + "20261102": 1 + }, + "last_date_times": [ + "0000" + ], + "pcp_examples": [ + "0", + "1", + "1mm 미만", + "강수없음" + ] + }, + "calls": { + "국문 반려동물 일괄": 10, + "반려 위치목록": 4, + "국문 반려동물 단건": 1, + "반려 상세": 1, + "국문 축제": 2, + "국문 목록": 3, + "국문 소개정보": 35, + "상가 코드": 2, + "상가 동별": 16, + "기상청 단기예보": 1 + }, + "errors": [ + "상가 동별: 03 NODATA_ERROR" + ] +} \ No newline at end of file diff --git a/docs/research/data-check/public-data-check2-daegu-20261029-1319.json b/docs/research/data-check/public-data-check2-daegu-20261029-1319.json new file mode 100644 index 0000000..5fd9c29 --- /dev/null +++ b/docs/research/data-check/public-data-check2-daegu-20261029-1319.json @@ -0,0 +1,533 @@ +{ + "checked_at_kst": "2026-10-29 13:19", + "region": "daegu", + "weekend_saturday": "20261031", + "반려동물": { + "bulk_total": 9659, + "bulk_acmpyTypeCd": [ + [ + "전구역 동반가능", + 9121 + ], + [ + "일부구역 동반가능", + 526 + ], + [ + "", + 12 + ] + ], + "bulk_with_size_text": 166, + "bulk_psblcpam_examples": [ + "전 견종 동반 가능", + "전 견종 동반 가능", + "전 견종 출입 가능(맹견의 경우, 입마개 착용 필수)", + "전 견종 동반 가능", + "이동장(켄넬)에 들어가는 전 견종 동반 가능", + "전 견종 동반 가능" + ], + "areas": [ + { + "area": "대구 도심", + "pet_places": 2, + "by_type": { + "관광지": 2 + }, + "with_conditions_in_bulk": 2, + "with_size_text": 0, + "examples": [ + { + "title": "경상감영공원", + "type": "관광지", + "cond": "전구역 동반가능", + "who": "전 견종 동반 가능" + }, + { + "title": "대구 근대골목(근대로의 여행)", + "type": "관광지", + "cond": "일부구역 동반가능", + "who": "전 견종 동반 가능" + } + ] + }, + { + "area": "수성못", + "pet_places": 0, + "by_type": {}, + "with_conditions_in_bulk": 0, + "with_size_text": 0, + "examples": [] + }, + { + "area": "앞산", + "pet_places": 1, + "by_type": { + "숙박": 1 + }, + "with_conditions_in_bulk": 1, + "with_size_text": 1, + "examples": [ + { + "title": "유원게스트하우스", + "type": "숙박", + "cond": "전구역 동반가능", + "who": "소형견" + } + ] + } + ], + "same_record_check": { + "contentid": "126133", + "title": "경상감영공원", + "kor_service": { + "contentid": "126133", + "relaAcdntRiskMtr": "", + "acmpyTypeCd": "전구역 동반가능", + "relaPosesFclty": "", + "relaFrnshPrdlst": "", + "etcAcmpyInfo": "- 맹견의 경우, 입마개 착용 필수 - 배변봉투 지참 및 배변처리 필수", + "relaPurcPrdlst": "", + "acmpyPsblCpam": "전 견종 동반 가능", + "relaRntlPrdlst": "", + "acmpyNeedMtr": "목줄 착용" + }, + "pet_service": { + "contentid": "126133", + "relaAcdntRiskMtr": "", + "acmpyTypeCd": "전구역 동반가능", + "relaPosesFclty": "", + "relaFrnshPrdlst": "", + "etcAcmpyInfo": "- 맹견의 경우, 입마개 착용 필수 - 배변봉투 지참 및 배변처리 필수", + "relaPurcPrdlst": "", + "acmpyPsblCpam": "전 견종 동반 가능", + "relaRntlPrdlst": "", + "acmpyNeedMtr": "목줄 착용" + }, + "identical": true + } + }, + "축제": { + "same_day_total": 85, + "same_day_not_overlapping": 0, + "same_day_over_60_days": 36, + "lclsSystm2_counts": [ + [ + "EV03", + 38 + ], + [ + "EV01", + 30 + ], + [ + "EV02", + 17 + ] + ], + "progresstype": [ + [ + "선택안함", + 74 + ], + [ + "", + 10 + ], + [ + "온라인", + 1 + ] + ], + "festivaltype": [ + [ + "", + 85 + ] + ], + "ev01_total": 30, + "ev01_within_15km": { + "대구 도심": [], + "수성못": [], + "앞산": [] + }, + "ev01_examples": [ + "경기‧양주‧지작사 드론봇 페스티벌 (20261030–20261101)", + "경남고성공룡세계엑스포 (20260922–20261101)", + "고양도시농업축제 (20261031–20261101)", + "고양행주한우 숯불구이축제 (20261031–20261101)", + "국가유산 미디어아트 여수 진남관 (20261009–20261108)", + "노원달빛산책 (20261016–20261108)", + "단풍나무숲길 힐링축제 (20261015–20261115)", + "대장경기록문화축제 (20261009–20261101)" + ] + }, + "영업시간·동반여부": { + "food_hours_examples": [ + { + "area": "대구 도심", + "title": "대하닭갈비", + "opentimefood": "11:00~21:00 (준비 시간 15:30~17:00)", + "restdatefood": "연중무휴" + }, + { + "area": "대구 도심", + "title": "중앙떡볶이", + "opentimefood": "11:30~18:00 / ※ 재료 소진 시 조기 마감", + "restdatefood": "매주 일요일" + }, + { + "area": "대구 도심", + "title": "한옥집김치찜", + "opentimefood": "10:00~21:30 (마지막 주문 21:00)", + "restdatefood": "연중무휴" + }, + { + "area": "대구 도심", + "title": "김진수로스터리카페", + "opentimefood": "- 10:00~23:00 / - 마지막 주문 22:00", + "restdatefood": "연중무휴" + }, + { + "area": "수성못", + "title": "하롱베이", + "opentimefood": "- 11:00~21:30 / - 마지막 주문 20:50", + "restdatefood": "연중무휴" + }, + { + "area": "수성못", + "title": "앙뜨레누보", + "opentimefood": "- 12:00~21:30 / - 준비시간 15:00~17:30 / - 마지막 주문 20:30", + "restdatefood": "연중무휴" + }, + { + "area": "수성못", + "title": "아라한참치", + "opentimefood": "- 14:00~24:00 / - 마지막 주문 22:00", + "restdatefood": "연중무휴" + }, + { + "area": "수성못", + "title": "데우스커피", + "opentimefood": "11:00~22:00", + "restdatefood": "매주 월요일, 명절 당일" + }, + { + "area": "앞산", + "title": "앞산울진대게 본점", + "opentimefood": "11:30~22:30", + "restdatefood": "연중무휴" + }, + { + "area": "앞산", + "title": "지오네 키친", + "opentimefood": "- 11:30~21:40 / - 준비시간 15:30~17:00 / - 마지막 주문 14:30, 20:40", + "restdatefood": "매주 월요일" + }, + { + "area": "앞산", + "title": "노이식탁", + "opentimefood": "- 11:30~22:00 / - 마지막 주문 21:00", + "restdatefood": "연중무휴" + }, + { + "area": "앞산", + "title": "앞산 큰골집", + "opentimefood": "- 11:30~21:45 / - 준비시간 15:30~17:30 / - 마지막 주문 14:30, 20:45", + "restdatefood": "설·추석 당일" + } + ], + "chkpet_values": [ + [ + "(빈 값)", + 18 + ] + ], + "chkpet_sampled": 18 + }, + "상가정보": { + "dongs": [ + { + "signguCd": "27110", + "adongCd": "27110640", + "adongNm": "남산1동", + "all": 680, + "food": 143, + "cafe": 27 + }, + { + "signguCd": "27110", + "adongCd": "27110650", + "adongNm": "남산2동", + "all": 593, + "food": 113, + "cafe": 31 + }, + { + "signguCd": "27110", + "adongCd": "27110660", + "adongNm": "남산3동", + "all": 313, + "food": 72, + "cafe": 16 + }, + { + "signguCd": "27110", + "adongCd": "27110670", + "adongNm": "남산4동", + "all": 403, + "food": 113, + "cafe": 25 + }, + { + "signguCd": "27110", + "adongCd": "27110680", + "adongNm": "대봉1동", + "all": 1117, + "food": 293, + "cafe": 58 + }, + { + "signguCd": "27110", + "adongCd": "27110690", + "adongNm": "대봉2동", + "all": 300, + "food": 83, + "cafe": 14 + }, + { + "signguCd": "27110", + "adongCd": "27110517", + "adongNm": "동인동", + "all": 1055, + "food": 348, + "cafe": 70 + }, + { + "signguCd": "27110", + "adongCd": "27110545", + "adongNm": "삼덕동", + "all": 1526, + "food": 605, + "cafe": 120 + }, + { + "signguCd": "27110", + "adongCd": "27110565", + "adongNm": "성내1동", + "all": 3406, + "food": 1029, + "cafe": 243 + }, + { + "signguCd": "27110", + "adongCd": "27110575", + "adongNm": "성내2동", + "all": 2628, + "food": 785, + "cafe": 140 + }, + { + "signguCd": "27110", + "adongCd": "27110585", + "adongNm": "성내3동", + "all": 444, + "food": 108, + "cafe": 20 + }, + { + "signguCd": "27200", + "adongCd": "27200650", + "adongNm": "대명10동", + "all": 650, + "food": 211, + "cafe": 22 + }, + { + "signguCd": "27200", + "adongCd": "27200660", + "adongNm": "대명11동", + "all": 614, + "food": 152, + "cafe": 22 + }, + { + "signguCd": "27200", + "adongCd": "27200560", + "adongNm": "대명1동", + "all": 340, + "food": 104, + "cafe": 10 + }, + { + "signguCd": "27200", + "adongCd": "27200571", + "adongNm": "대명2동", + "all": 799, + "food": 286, + "cafe": 53 + }, + { + "signguCd": "27200", + "adongCd": "27200586", + "adongNm": "대명3동", + "all": 944, + "food": 328, + "cafe": 48 + }, + { + "signguCd": "27200", + "adongCd": "27200590", + "adongNm": "대명4동", + "all": 635, + "food": 196, + "cafe": 23 + }, + { + "signguCd": "27200", + "adongCd": "27200600", + "adongNm": "대명5동", + "all": 607, + "food": 228, + "cafe": 41 + }, + { + "signguCd": "27200", + "adongCd": "27200610", + "adongNm": "대명6동", + "all": 560, + "food": 128, + "cafe": 26 + }, + { + "signguCd": "27200", + "adongCd": "27200640", + "adongNm": "대명9동", + "all": 785, + "food": 263, + "cafe": 58 + }, + { + "signguCd": "27260", + "adongCd": "27260640", + "adongNm": "두산동", + "all": 1558, + "food": 693, + "cafe": 72 + }, + { + "signguCd": "27260", + "adongCd": "27260620", + "adongNm": "상동", + "all": 986, + "food": 241, + "cafe": 47 + }, + { + "signguCd": "27260", + "adongCd": "27260570", + "adongNm": "수성1가동", + "all": 708, + "food": 165, + "cafe": 33 + }, + { + "signguCd": "27260", + "adongCd": "27260580", + "adongNm": "수성2.3가동", + "all": 722, + "food": 222, + "cafe": 32 + }, + { + "signguCd": "27260", + "adongCd": "27260590", + "adongNm": "수성4가동", + "all": 678, + "food": 234, + "cafe": 28 + }, + { + "signguCd": "27260", + "adongCd": "27260651", + "adongNm": "지산1동", + "all": 1301, + "food": 403, + "cafe": 51 + }, + { + "signguCd": "27260", + "adongCd": "27260652", + "adongNm": "지산2동", + "all": 787, + "food": 193, + "cafe": 25 + } + ], + "radius_counts": [ + { + "area": "대구 도심", + "radius_m": 1500, + "카페": 823, + "빵/도넛": 195, + "아이스크림/빙수": 4, + "샌드위치/샐러드": 15, + "요리 주점": 295, + "생맥주": 46 + }, + { + "area": "수성못", + "radius_m": 1500, + "카페": 202, + "빵/도넛": 51, + "아이스크림/빙수": 5, + "샌드위치/샐러드": 4, + "요리 주점": 168, + "생맥주": 23 + }, + { + "area": "앞산", + "radius_m": 1500, + "카페": 125, + "빵/도넛": 29, + "아이스크림/빙수": 1, + "샌드위치/샐러드": 2, + "요리 주점": 55, + "생맥주": 6 + } + ] + }, + "날씨": { + "base": "20261029 1100", + "times_per_date": { + "20261029": 12, + "20261030": 24, + "20261031": 24, + "20261101": 8, + "20261102": 1 + }, + "last_date_times": [ + "0000" + ], + "pcp_examples": [ + "0", + "1mm 미만", + "2", + "강수없음" + ] + }, + "calls": { + "국문 반려동물 일괄": 10, + "반려 위치목록": 4, + "국문 반려동물 단건": 1, + "반려 상세": 1, + "국문 축제": 2, + "국문 목록": 3, + "국문 소개정보": 30, + "상가 코드": 3, + "상가 동별": 81, + "상가 반경": 18, + "기상청 단기예보": 1 + }, + "errors": [] +} \ No newline at end of file diff --git a/docs/research/data-openapi-review/pet_and_english.md b/docs/research/data-openapi-review/pet_and_english.md new file mode 100644 index 0000000..7ae2a96 --- /dev/null +++ b/docs/research/data-openapi-review/pet_and_english.md @@ -0,0 +1,290 @@ +# 반려동물·영문 서비스 매뉴얼 검토 (2026-10-29) + +원문(`docs/data-openapi/`): +반려동물동반여행 활용매뉴얼 v4.1(2026-02-25) · 영문 활용매뉴얼 v4.4(2026-02-10) · 다국어 서비스분류코드 v4.2 · +참고: 국문 활용매뉴얼 v4.4 · 신분류체계정보 관광타입정보 연계 정의서 · 활용신청방법 매뉴얼 v3.3 + +표기: **[매뉴얼]** 원문 인용·요약 / **[추론]** 내 판단, 실제 호출로 확인 필요 + +--- + +## 핵심 요약 + +1. 반려동물 전용 API는 `KorPetTourService2`(`http://apis.data.go.kr/B551011/KorPetTourService2`)다. 지역·위치(반경 최대 20km)·키워드·동기화 **목록** 오퍼레이션이 있어 동반 가능 장소를 모아 받을 수 있다. +2. 국문 `KorService2/detailPetTour2` 는 contentId가 선택값이고 "미기입 시 반려동물 전부 출력"이라고 적혀 있다. 반려동물 정보 전체를 페이지 단위로 한꺼번에 받을 수 있다는 뜻이다. +3. 반려동물 필드 9개는 모두 자유 문장이다. `acmpyTypeCd` 도 코드표가 없고 "전구역/일부구역 동반가능" 같은 문자열이다. 크기·무게·종을 나타내는 코드 필드는 없다. +4. 반려동물 서비스는 "동반여행 가능한" 장소만 다룬다. 그래서 '불가'를 나타내는 값이 없다. 이 서비스에 없는 장소는 '불가'가 아니라 '미확인'이다. +5. 영문 레코드를 국문 contentid와 이어 주는 필드는 매뉴얼 어디에도 없다. 대신 법정동 코드·신분류(`lclsSystm`) 코드·좌표·우편번호가 언어와 관계없이 같고, 제목 괄호 안에 한국어 이름이 들어 있다. +6. 신분류(`lclsSystm`)는 언어 공통 코드다. 연계 정의서가 소분류마다 국문 타입(12…)과 다국어 타입(76…)을 함께 정해 둔다. 옛 cat1~3 분류는 v4.4부터 요청·응답 양쪽에서 삭제되었다. +7. 영문 서비스에는 반려동물 오퍼레이션이 없고 소개정보에도 `chkpet` 계열 필드가 없다. 영문 화면에 보여 줄 반려동물 정보는 짝지은 국문 데이터에서 가져와 번역해야 한다. + +--- + +## 1. 반려동물 서비스 (KorPetTourService2) + +### 1.1 서비스 정보 + +| 항목 | [매뉴얼] 내용 | +| --- | --- | +| 서비스ID | `KorPetTourService2` | +| URL (개발·운영 같음) | `http://apis.data.go.kr/B551011/KorPetTourService2` (예제는 `https://` 도 사용, 전송 레벨 "[O]HTTPS [O]HTTP") | +| 설명 | "반려동물 동반여행 가능한 관광지, 문화시설, 축제공연행사, 숙박, 음식점, 레포츠, 쇼핑의 관광정보를 제공한다." | +| 버전·시작일 | 서비스 버전 4.0, 시작일 2024-10-22 | +| 갱신 주기 | "일 1회" | +| 형식 | 기본 XML, "JSON을요청할경우“&_type=json”을추가" | +| 트래픽 | "개발계정은 일 1,000건의 트래픽을 제공합니다." / 활용신청 매뉴얼: "개발계정은 각 오퍼레이션별 일일 1,000건의 트래픽을 제공합니다." | +| 운영계정 | 활용신청 매뉴얼: "운영계정은 한국관광공사 담당자의 승인이 필요하며, 약 1~3일 정도 소요", "승인일로부터 24개월 간 활용 가능", 승인 때 "활용 어플 URL"·"개발계정의 TourAPI 호출 이력" 확인, 한국관광공사 [활용사례]에 등록되어 홍보됨. **운영계정 한도 수치는 이 문서들에 없다.** | +| 문의 | 개방데이터운영팀 / 디지털콘텐츠팀 / 070-4287-3219 / tourapi@knto.or.kr | +| 라이선스 | 이미지·콘텐츠 단위 `cpyrhtDivCd`: "Type1: 제1유형(출처표시-권장) / Type3: 제3유형(제1유형 + 변경금지)". 텍스트 전체에 대한 별도 라이선스 조항은 매뉴얼에 없다. 활용신청 때 "라이선스 표시 동의는 필수"다. | + +- data.go.kr 데이터셋 번호(15135102)는 매뉴얼에 나오지 않는다. **[추론]** 같은 서비스일 가능성이 높다. 포털 페이지의 엔드포인트가 `KorPetTourService2` 인지 한 번 확인하면 된다. +- **[추론]** data.go.kr 활용신청은 서비스 단위라서 KorService2 키가 있어도 이 서비스는 따로 신청해야 할 수 있다(매뉴얼에 명시된 내용은 아니다). + +**콘텐츠 타입 코드**: [매뉴얼] 코드표는 국문 코드 `12 관광지, 14 문화시설, 15 행사/공연/축제, 28 레포츠, 32 숙박, 38 쇼핑, 39 음식점` 이다. 여행코스(25)는 없다. +- [매뉴얼 불일치] `areaBasedList2`·`locationBasedList2`·`detailInfo2` 의 contentTypeId 설명에는 다국어 코드("75: 레포츠, 76: 관광지 … 85: 축제공연행사")가 적혀 있다. 하지만 코드표, 샘플(`contentTypeId=12`, 응답 `"contenttypeid": "12"`/`"38"`), `petTourSyncList2`·`detailIntro2` 응답 설명은 국문 코드를 쓴다. + **[추론]** 국문 코드가 맞고, 다국어 코드 문구는 영문 매뉴얼에서 복사하다 남은 오류로 보인다. + +### 1.2 오퍼레이션 (11개) + +공통 필수: `serviceKey`, `MobileOS`(IOS/AND/WIN/ETC, 코드 조회는 WEB/ETC 표기), `MobileApp`(서비스명). 공통 선택: `numOfRows`, `pageNo`, `_type`. +페이징은 `numOfRows`/`pageNo`, 응답에 `totalCount`. **numOfRows 최대값은 매뉴얼에 없다.** + +| # | 오퍼레이션 | 용도 | 추가 필수 | 추가 선택 | +| --- | --- | --- | --- | --- | +| 1 | `areaBasedList2` | 지역기반 목록 | — | `arrange`, `contentTypeId`, `modifiedtime`(YYYYMMDD), `lDongRegnCd`, `lDongSignguCd`, `lclsSystm1/2/3` | +| 2 | `locationBasedList2` | 위치기반 목록 | `mapX`, `mapY`, `radius` ("Max값 20000m=20Km") | `arrange`(+ `E`/`S` 거리순), `contentTypeId`, `modifiedtime`, `lDong*`, `lclsSystm*` | +| 3 | `searchKeyword2` | 키워드 검색 | `keyword` | `arrange`, `lDong*`, `lclsSystm*` (v4.1에서 "요청항목 (관광타입) 삭제") | +| 4 | `detailCommon2` | 공통정보 | `contentId` | — | +| 5 | `detailIntro2` | 소개정보(시간·주차 등, `chkpet` 계열 포함) | `contentId`, `contentTypeId` | — | +| 6 | `detailInfo2` | 반복정보 | `contentId`, `contentTypeId` | — | +| 7 | `detailImage2` | 이미지 | `contentId` | `imageYN` (Y=콘텐츠 이미지, N=음식점 메뉴 이미지) | +| 8 | `detailPetTour2` | 반려동물 동반 정보 | `contentId` (항목구분 1=필수) | — | +| 9 | `petTourSyncList2` | 동기화 목록 | — | `showflag`(1=표출, 0=비표출), `modifiedtime`, `arrange`, `contentTypeId`, `lDong*`, `lclsSystm*` | +| 10 | `ldongCode2` | 법정동 코드 | — | `lDongRegnCd`, `lDongListYn`(N 코드조회 / Y 전체목록) | +| 11 | `lclsSystmCode2` | 신분류 코드 | — | `lclsSystm1/2/3`, `lclsSystmListYn` | + +`arrange`: "A=제목순, C=수정일순, D=생성일순 / 대표이미지가 반드시 있는 정렬 (O=제목순, Q=수정일순, R=생성일순)", 위치기반은 E/S 거리순이 더 있다. + +**목록 응답 필드** (1·2·3·9 공통): `contentid`, `contenttypeid`, `title`, `addr1`, `addr2`, `zipcode`, `tel`, `mapx`(경도), `mapy`(위도), `mlevel`, +`firstimage`(약 500×333), `firstimage2`(약 150×100), `cpyrhtDivCd`, `createdtime`, `modifiedtime`, `lDongRegnCd`, `lDongSignguCd`, `lclsSystm1/2/3`. +위치기반은 `dist`(m), 동기화 목록은 `showflag` 가 더 붙는다. +- [매뉴얼] 목록 응답에는 동반 조건(`acmpyTypeCd` 등)이 없다. 조건을 보려면 장소마다 `detailPetTour2` 를 호출해야 한다. +- [매뉴얼] 축제 날짜 검색(`searchFestival2`)과 숙박 목록(`searchStay2`)이 없다. 반려동물 동반 행사는 `areaBasedList2&contentTypeId=15` 로 받은 뒤 `detailIntro2` 의 `eventstartdate`/`eventenddate` 로 걸러야 한다. +- [매뉴얼 불일치] 개정이력에 "동기화 오퍼레이션 / 요청항목 (이전 콘텐츠 ID) 추가"가 있지만 `petTourSyncList2` 요청·응답표에는 `oldContentid` 가 없다. 국문·영문 동기화 목록에는 있다. +- v4.1 변경 [매뉴얼]: 지역코드·서비스분류코드 오퍼레이션 삭제, "요청/응답 : 지역코드 , 시군구코드 삭제", "서비스 대분류, 중분류, 소분류 / 삭제" → `areaCode`·`cat1~3` 대신 `lDong*`·`lclsSystm*` 를 쓴다. + +### 1.3 `detailPetTour2` 응답 필드 + +| 필드 | [매뉴얼] 국문명 | [매뉴얼] 샘플 | 비고 | +| --- | --- | --- | --- | +| `acmpyTypeCd` | 동반유형코드(동반구분) | "전구역 동반가능"(반려 매뉴얼), "일부구역 동반가능"(국문 매뉴얼) | 이름은 '코드'지만 값은 한글 문장. **값 목록·코드표 없음** | +| `acmpyPsblCpam` | 동반가능동물 | "전 견종 출입 가능(맹견의 경우, 입마개 착용 필수)", "전 견종 동반 가능" | 크기·무게·종 정보는 이 문장 안에만 있다 (우리 표본: "5kg 이하 숙박가능한 견종") | +| `acmpyNeedMtr` | 동반시 필요사항 | "목줄 착용", "목줄 착용,반려동물 유모차 탑승,이동장(켄넬)사용" | 쉼표 구분 목록처럼 보임 | +| `etcAcmpyInfo` | 기타 동반 정보 | "- 목줄은 2m 이내로 유지\n- 맹견의 경우, 입마개 착용 필수\n- 배변봉투 지참 및 배변처리 필수" | `\n` 줄바꿈 포함 | +| `relaAcdntRiskMtr` | 관련 사고 대비사항 | "반려견보호자 서약서 서명" | | +| `relaPosesFclty` | 관련 구비 시설 | (빈 값) | | +| `relaFrnshPrdlst` | 관련 비치 품목 | (빈 값) | | +| `relaPurcPrdlst` | 관련 구매 품목 | (빈 값) | | +| `relaRntlPrdlst` | 관련 렌탈 품목 | (빈 값) | | +| `contentid` | 콘텐츠ID | 1059479 / 125534 | | +| `petTursmInfo` | 반려동물 관광정보 | (샘플 없음) | **KorService2 쪽 매뉴얼에만 있음** | + +- **[매뉴얼] 크기·무게·종을 나타내는 코드 필드는 없다.** `acmpyTypeCd` 에 어떤 값이 올 수 있는지도 적혀 있지 않다. +- 소개정보(`detailIntro2`)의 반려동물 필드 [매뉴얼]: `chkpet`(관광지 12), `chkpetculture`(문화시설 14), `chkpetleports`(레포츠 28), `chkpetshopping`(쇼핑 38). + 국문명은 "애완동물동반가능정보"이고 샘플 값은 모두 빈 문자열이라 **값 형식을 알 수 없다**. 숙박(32)·음식점(39)·행사(15)에는 이 필드가 없다. +- 반려 매뉴얼의 `detailIntro2` 행사(15) 필드에는 국문 매뉴얼에 없는 `festivaltype`(축제 종류)·`progresstype`(행사 진행 상황)이 있다. + +### 1.4 KorService2 `detailPetTour2` 와의 관계 + +| | `KorService2/detailPetTour2` | `KorPetTourService2/detailPetTour2` | +| --- | --- | --- | +| 등장 | 국문 매뉴얼 v4.1(2023-01-30) "반려동물 여행 정보 오퍼레이션 추가", v4.4에도 유지 | 반려 서비스 시작 2024-10-22, 매뉴얼 v4.1(2026-02-25) | +| 유형 | "조회(상세)" | "조회(목록)" | +| contentId | 선택(0): "콘텐츠ID(미기입 시 반려동물 전부 출력)" | 필수(1) | +| 응답 | 9개 필드 + `contentid` + `petTursmInfo` | 9개 필드 + `contentid` | + +- [매뉴얼] 어느 쪽도 다른 쪽을 대체하거나 폐지한다는 말이 없다. 두 매뉴얼 모두 2026년 개정판에 실려 있어 **둘 다 현행**이다. 같은 데이터인지는 적혀 있지 않다. +- **[추론]** 필드명과 값 형식이 같아 같은 원천 데이터로 보인다. 반려 매뉴얼의 contentid 예(128834 죽녹원, 264353 인사동, 1059479 여의도한강공원)도 국문 관광정보와 같은 ID 체계로 보인다. + → 같은 contentId로 두 서비스를 한 번씩 호출해 값이 같은지 확인한다. + +### 1.5 수집 방법 후보 **[추론]** + +| 방법 | 호출 | 장점 | 확인할 것 | +| --- | --- | --- | --- | +| A. 동기화 목록 → 상세 | `petTourSyncList2&showflag=1` 전체 페이지 → contentid마다 `detailPetTour2` | 동반 가능 장소 전체 + 좌표·분류를 얻음. `modifiedtime` 으로 증분, `showflag=0` 으로 삭제 감지 | 상세 호출이 장소 수만큼 필요 → 개발계정 한도(오퍼레이션당 1,000/일) 안에서 여러 날로 나눠야 할 수 있음 | +| B. 국문 일괄 조회 | `KorService2/detailPetTour2` 를 contentId 없이 페이지로 | 반려 정보 전체를 적은 호출로 받음. 우리 국문 DB와 contentid로 조인 | 실제로 전체가 나오는지, `totalCount`·최대 `numOfRows` | +| C. 화면에서 바로 | `locationBasedList2` (radius ≤ 20000) | 근처 동반 가능 장소 | 우리 원칙("AI는 저장한 DB에서만 고름")상 런타임 호출보다 A/B로 미리 저장하는 쪽이 맞음 | + +권장 순서: B로 전체 규모와 값 분포를 먼저 재고, A로 목록(좌표·분류)을 보강한다. + +--- + +## 2. 영문 서비스(EngService2)와 국문 연결 + +### 2.1 서비스 정보 [매뉴얼] + +- URL `http://apis.data.go.kr/B551011/EngService2`, 갱신 "일 1회", 개발계정 "일 1,000건". +- 오퍼레이션 12개: `areaBasedList2`, `locationBasedList2`, `searchKeyword2`, `searchFestival2`, `searchStay2`, `detailCommon2`, `detailIntro2`, `detailInfo2`, `detailImage2`, `areaBasedSyncList2`, `ldongCode2`, `lclsSystmCode2`. +- 국문과의 차이: + - **`detailPetTour2` 없음.** + - 여행코스: 코드표에 "여행코스 | 국문만 서비스". + - 교통(77): "교통(다국어만 서비스)". + - `detailInfo2`: "“숙박” 객실정보와 “여행코스” 코스정보는 국문 서비스만 제공합니다." + - 다른 언어(일·중 등) 매뉴얼은 이번 자료에 없다. 일·중 오퍼레이션 구성은 확인하지 못했다. + +### 2.2 국문과 이어 주는 고리 + +- **[매뉴얼] 국문 contentid를 가리키는 필드는 없다.** 목록·공통정보·동기화 응답표 어디에도 없다. +- `oldContentid`(영문 `areaBasedSyncList2` 요청·응답): "이전 콘텐츠ID(DB 저장 동기화시 이전 key값으로 조회 용도)". 샘플에서 `contentid` 와 `oldContentid` 가 둘 다 `3488291` 이다. + **[추론]** 같은 영문 레코드의 이전 키다. 국문 ID가 아니다. +- 언어와 관계없이 같은 값 [매뉴얼 샘플]: + - 법정동: 영문 `ldongCode2` 가 같은 코드(11/110/140)를 주고 이름만 영문이다 ("Seoul", "Jongno-gu", "Jung-gu"). + - 신분류: 영문 샘플이 국문과 같은 코드(`AC/AC04/AC040100`, `EV/EV02/EV021000`, `VE/VE07/VE070400`, `SH/SH06/SH060200`)를 쓰고, `lclsSystmCode2` 는 이름만 영문이다 ("Accommodation", "Hotels"). + - `mapx`/`mapy`(WGS84), `zipcode`. +- 제목 형식 [매뉴얼 샘플 관찰 — 규칙으로 명시되어 있지는 않다]: + "HiKR Ground (하이커 그라운드)", "Wonju Sogeumsan Ulleong Bridge (원주 소금산 울렁다리)", + "Infinity Flying(인피니티 플라잉)"(괄호 앞 공백 없음), "Bupyeong (Kkangtong) Market (부평시장(깡통시장))"(괄호 중첩, 영문 부분에도 괄호). + → 한국어 이름은 **맨 끝의 균형 잡힌 괄호 묶음**으로 뽑아야 한다. +- **[추론] 매칭 키 제안**: 같은 `lclsSystm1`(가능하면 3) + 같은 `lDongRegnCd/lDongSignguCd` + 좌표 거리 기준 + 괄호 속 한국어명 정규화 일치(+ `zipcode` 보조). + - 영문 행사 샘플은 `mapx`/`mapy` 가 빈 문자열이다(`searchFestival2` 예제). 행사는 좌표 없이 이름·법정동·기간으로 맞춰야 할 수 있다. + - 짝지은 결과는 우리 DB의 매핑 테이블로 저장하고, 어느 쪽 `modifiedtime` 이 바뀌면 다시 검증한다. + +### 2.3 소개정보(`detailIntro2`) 필드 차이 [매뉴얼 응답표 비교] + +영문 응답표에 **없는** 국문 필드: + +| 타입 (국문/영문) | 국문에만 있는 필드 | +| --- | --- | +| 관광지 12/76 | `chkbabycarriage`, `chkcreditcard`, **`chkpet`**, `heritage2`, `heritage3` | +| 문화시설 14/78 | `chkbabycarriageculture`, `chkcreditcardculture`, **`chkpetculture`**, `discountinfo` | +| 행사 15/85 | `festivalgrade` | +| 레포츠 28/75 | `chkbabycarriageleports`, `chkcreditcardleports`, **`chkpetleports`** | +| 숙박 32/80 | `barbecue`, `beauty`, `beverage`, `bicycle`, `campfire`, `fitness`, `karaoke`, `publicbath`, `publicpc`, `sauna`, `seminar`, `sports`, `refundregulation` | +| 쇼핑 38/79 | `chkbabycarriageshopping`, `chkcreditcardshopping`, **`chkpetshopping`**, `culturecenter`, `saleitemcost` | +| 음식점 39/82 | `chkcreditcardfood`, `discountinfofood`, `kidsfacility`, `packing` | + +영문에만 있는 것: 교통 77 — `chkcreditcardtraffic`, `conven`, `disablefacility`, `foreignerinfocenter`(외국인문의및안내), `infocentertraffic`, `mainroute`, `operationtimetraffic`, `parkingtraffic`, `restroomtraffic`, `shipinfo`. +**[추론]** 매뉴얼 표 기준이다. 실제 응답에 빈 키로 들어오는지는 호출로 확인해야 한다. + +### 2.4 번역 여부 [매뉴얼 샘플] + +- 영문으로 되어 있는 것: + - `overview` + - `usetime`: "November-April 09:00-17:00 / May-October 09:00-18:00
* Last ticketing 1 hr 30 min before closing" + - `restdate`: "Mondays
* If Monday falls on a public holiday, it will be closed on the following business day instead" + - `parking`: "Available" + - `detailInfo2`: `infoname` "Admission Fees", `infotext` "Adults 9,000 won / Children 5,000 won" + - 대부분의 `addr1`: "40 Cheonggyecheon-ro, Jung-gu, Seoul" +- 영문이 아닌 것: + - `areaBasedList2` 예제의 `addr1` 은 국문 "경상북도 경주시 경감로 614 (천군동)"이고 `addr2` 가 영문이다. + - `detailImage2` 의 `imgname` 은 국문이다("소금산 울렁다리(2)"). + - `tel` 은 국제 형식이다("+82-33-***-****"). +- **[추론]** 영문 시간 문장은 국문과 형식이 달라 영업시간 파서를 언어별로 두어야 한다. 국문·영문 레코드는 수정 시각이 따로라서 같은 장소라도 값이 어긋날 수 있다. + "도착 시각 영업 중" 판정은 국문 값 하나를 기준으로 하는 편이 일관된다. + +### 2.5 이미지·라이선스·한도 + +- [매뉴얼] `cpyrhtDivCd` Type1/Type3 정의는 국문과 같다. `detailImage2` 는 "이미지 저작권 공공누리 유형"을 함께 준다. 영문 샘플은 대부분 Type3이고, 이미지가 없으면 `cpyrhtDivCd` 도 빈 값이다. +- [매뉴얼 샘플] 영문 `detailCommon2` 의 `firstimage2` 와 `detailImage2` 의 `smallimageurl` 이 원본과 같은 `..._image2_1.jpg` 다. 영문은 썸네일이 원본 URL일 수 있다. +- **[추론]** Type3(변경금지) 이미지를 카드에서 잘라 보여 주는 것이 '변경'에 해당하는지는 공공누리 기준을 확인해야 한다. 출처 표시는 두 유형 모두 필요하다. + +### 2.6 기타 차이 + +- [매뉴얼 불일치] 영문 `searchKeyword2` 요청표에는 `contentTypeId` 가 남아 있다. 개정이력에는 "키워드 검색 오퍼레이션 / 요청항목 (관광타입) 삭제"라고 되어 있다. +- 영문 `searchStay2` 의 `arrange` 에 "P=조회순"이 적혀 있다. 국문 v4.1에서 조회수가 제거되었으므로 남은 문구로 보인다 [추론]. +- `detailIntro2`·`detailInfo2` 의 `contentTypeId` 에는 **해당 언어의 코드**를 넣는다(영문 예제: `contentTypeId=76`). + +--- + +## 3. 다국어 분류 코드 + +### 3.1 콘텐츠 타입 + +[매뉴얼 영문 v4.4] "다국어콘텐츠 타입(ContentTypeId)코드표 — 관광정보 (다국어 공통)". 다국어 분류코드 xlsx의 일문·중문간체·중문번체·독·불·서·러 시트도 모두 같은 번호를 쓴다. + +| 타입 | 국문 | 다국어(영·일·중 공통) | +| --- | --- | --- | +| 관광지 | 12 | 76 | +| 문화시설 | 14 | 78 | +| 행사/공연/축제 | 15 | 85 | +| 여행코스 | 25 | — (국문만) | +| 레포츠 | 28 | 75 | +| 숙박 | 32 | 80 | +| 쇼핑 | 38 | 79 | +| 음식점 | 39 | 82 | +| 교통 | — | 77 (다국어만) | + +(같은 대응표가 연계 정의서 오른쪽 표에도 있다.) + +### 3.2 신분류(`lclsSystm`) — 언어 공통 + +- [매뉴얼 연계 정의서] 신분류 소분류마다 "국문코드 | 다국어코드"를 함께 준다. 예: + - `AC010100` 호텔 → 32/80 + - `AC05` 캠핑(일반야영장·오토캠핑장·카라반·글램핑장) → **28/75 레포츠**(숙박이 아니다) + - `VE050200` 리조트 → 32/80 + - `FD050100` 카페 → 39/82 + - `VE03` 도시공원 → 12/76 + - `VE07` 전시시설 → 14/78 + - `VE10` 레저스포츠시설 → 28/75 + - `VE120100` 서점 → 14/78, `VE120200` 카지노 → 28/75 + - `EV01` 축제 → 15/85 + - `C01` 추천코스 → 25/– + - 대분류: AC 숙박, EV 축제/공연/행사, EX 체험관광, FD 음식, HS 역사관광, LS 레저스포츠, NA 자연관광, SH 쇼핑, VE 문화관광, C01 추천코스. +- [매뉴얼 영문] 영문 `lclsSystmCode2` 대분류 전체 조회 예제 응답은 `"totalCount": 9`(AC, EV, …)다. **[추론]** 다국어에는 C01(코스)이 빠진 9개다. +- **[추론]** 우리 DB 카테고리 키를 `lclsSystm3` 로 잡으면 언어별 타입 번호 차이가 사라진다. 일·중을 추가할 때도 그대로 쓸 수 있다. + 언어별 표시 이름은 각 서비스의 `lclsSystmCode2&lclsSystmListYn=Y` 로 받아 저장한다. + 같은 대분류 안에서도 타입이 갈리므로(VE → 12/14/28/32), 타입은 대분류가 아니라 **소분류 기준**으로 정해야 한다. +- [매뉴얼 이상] 반려 매뉴얼의 `lclsSystmCode2` 예제 응답 이름이 "Отель"(러시아어)로 되어 있다. 복사 오류로 보인다. + +### 3.3 옛 분류(cat1~3) + +- [매뉴얼] 다국어 서비스분류코드 v4.2는 옛 체계(`A01010100`, `B02010100` …)의 8개 언어 번역표다. + 국문·영문 v4.4 개정이력(2026-02-10)에 "서비스분류코드 오퍼레이션 삭제", "요청/응답 : 서비스 대분류, 중분류, 소분류 / 삭제"가 있다. 반려 v4.1도 같다. +- **[추론]** 현재 API는 cat 코드로 거르거나 받을 수 없다. 이 xlsx는 예전 데이터를 옮기거나 번역어를 참고할 때만 쓴다. cat ↔ lclsSystm 대응표는 이번 자료에 없다. + +--- + +## 4. 우리 가정과 다른 점 + +| 우리 가정 | 매뉴얼 | 영향 | +| --- | --- | --- | +| 전용 반려동물 서비스의 URL·오퍼레이션을 찾지 못함 | `KorPetTourService2`, 오퍼레이션 11개 (1.2) | 바로 호출해 볼 수 있다 | +| contentId마다 하나씩 조회해야 함 | 반려 서비스에 목록 4종(지역·위치·키워드·동기화)이 있다. 국문 `detailPetTour2` 는 contentId를 비우면 전체를 준다 | 표본 조사를 다시 한다. 수집은 1.5의 A/B | +| 반려동물 정보가 거의 없다(가까운 30곳 중 0/1/3) | 반려 정보는 일부 장소에만 있다. 국문 목록에서 가까운 곳을 고르면 잘 안 걸린다 | **[추론]** 표본 방식 때문에 적게 잡혔을 수 있다. 반려 서비스 목록으로 같은 지역을 다시 잰다 | +| 3상태(가능/불가/미확인) | 반려 서비스는 동반 가능 장소만 다루고 '불가' 값이 없다. `acmpyTypeCd` 는 전구역/일부구역 문장이다 | 반려 서비스에 없는 장소는 '미확인'. '불가'는 `chkpet` 문장(형식 미정)에서만 나올 수 있다. **[추론]** '일부구역'을 따로 두는 편이 정보 손실이 적다 (전구역 가능 / 일부구역 가능 / 불가 / 미확인) | +| 견종 크기 정보를 얻을 수 있다 | 구조화된 필드가 없다. `acmpyPsblCpam`·`acmpyNeedMtr`·`etcAcmpyInfo` 문장 안에만 있다 | 무게 상한(kg)·대형견 가능 여부·맹견 입마개·이동장/유모차 필수 등을 문장에서 뽑는 파서와 검수가 필요하다. 못 뽑으면 '미확인' | +| 영문↔국문은 이름+좌표로 맞춘다 | 공식 연결 키가 없다(가정과 같음) | 법정동·신분류 코드·우편번호가 언어 공통이라 매칭 키로 더 쓸 수 있다. 행사는 좌표가 비어 있을 수 있다 | +| 영문 타입 코드가 국문과 다르다 | 맞다. 공식 '다국어 공통' 코드이고 대응표가 있다(3.1) | 일·중 추가 때 그대로 재사용한다 | +| (암묵) 영문에서도 반려·편의 정보를 볼 수 있다 | 영문에는 `detailPetTour2` 와 `chkpet`·유모차·신용카드·놀이방 필드가 없다 | 영문 화면의 반려·편의 정보는 국문 짝에서 가져와 번역한다 | + +--- + +## 5. 주의점 + +1. **매뉴얼끼리 맞지 않는 곳**: + - 반려 서비스 contentTypeId 설명(다국어 코드 vs 국문 코드) + - 반려 동기화 목록에 `oldContentid` 가 빠져 있음 + - 영문 키워드 검색의 `contentTypeId` 가 남아 있음 + - 반려 `lclsSystmCode2` 예제 이름이 러시아어 + → 실제 호출로 확인한 뒤 코드에 반영한다. +2. **contentid는 바뀔 수 있다.** 국문·영문 동기화 목록에 `oldContentid` 가 있다. 우리 DB는 이전 키 → 새 키 이관을 처리해야 한다. `showflag=0` 은 비표출 전환이므로 숨김 처리한다. +3. **오류 응답은 XML뿐이다**: "공공데이터포털에서 출력되는 오류메세지는 XML로만 출력". `_type=json` 이어도 오류는 XML이다. +4. **한도는 오퍼레이션별 1,000건/일(개발계정)**이다. 반려 상세를 장소마다 호출하는 방식은 금방 한도에 닿는다. 운영계정은 실제 앱 URL과 호출 이력이 있어야 승인된다. +5. **갱신은 일 1회**다. 야간 증분 동기화(`modifiedtime`)로 충분하다. +6. 위치기반 반경은 최대 20km다. `MobileApp` 은 필수이고 통계용이므로 앱 이름을 넣는다. +7. `homepage` 는 HTML `` 태그 문자열이다. `etcAcmpyInfo` 에는 `\n`, 소개정보에는 `
` 이 섞여 있다. 표시하기 전에 정리한다. +8. 이미지 URL이 `http://` 인 경우가 많다. **[추론]** 앱(iOS ATS)에서는 `https://` 로 바꿔 열리는지 확인한다. +9. 외국인 방문자 기능에 쓸 만한 것 [매뉴얼]: + - 반복정보 유형: "외국어 안내서비스", "외국인 예약안내", "외국인 체험가능 프로그램", 쇼핑 "세금환급방법" + - 신분류 `SH040300` 사후면세점 + - 영문 교통(77) `foreignerinfocenter` + - 이 중 반복정보는 국문·영문 양쪽 매뉴얼에 있다. + +### 다음에 확인할 호출 + +- `KorService2/detailPetTour2` contentId 없이: `totalCount`, 최대 `numOfRows`, `acmpyTypeCd` 값 분포, `acmpyPsblCpam` 에서 무게·크기 언급 비율 +- `KorPetTourService2/petTourSyncList2&showflag=1` 전체 건수, contentTypeId=12로 거를 때 국문 코드가 맞는지 +- 같은 contentId로 두 `detailPetTour2` 를 비교해 값이 같은지, contentid가 KorService2와 같은지 +- 성수·연남·해운대에서 `KorPetTourService2/locationBasedList2` 로 동반 가능 장소 수 다시 재기 +- `chkpet` 계열 값 분포(국문) +- 영문 `detailIntro2` 에 `chkpet` 등이 빈 키로라도 오는지 diff --git a/docs/research/data-openapi-review/store_and_weather.md b/docs/research/data-openapi-review/store_and_weather.md new file mode 100644 index 0000000..60190b3 --- /dev/null +++ b/docs/research/data-openapi-review/store_and_weather.md @@ -0,0 +1,381 @@ +# 상가정보·기상청 단기예보 API 매뉴얼 검토 (2026-10-29) + +검토 대상 (원본: `docs/data-openapi/`) +- 소상공인시장진흥공단 상가(상권)정보 OpenAPI 활용가이드 (2026.7판, 파일명 260805) +- 기상청 단기예보 조회서비스 API 활용가이드 (261019) +- 기상청 단기예보 격자·위경도 표 (시트 "최종 업데이트 파일_20260930") + +표기: **[매뉴얼]** = 매뉴얼 원문(따옴표는 인용) · **[추정]** = 내 판단, 확인 필요 · **[실측]** = 우리 2026-10-29 점검 결과/이번 계산. +매뉴얼에 없는 코드는 "매뉴얼에 없음"으로 적었다. + +--- + +## 핵심 요약 + +1. 성수 전체 목록은 `storeListInDong`(divId=`adongCd`, key=**8자리** 행정동코드, numOfRows 최대 1000, pageNo로 반복)으로 받는다. 행정동코드는 `baroApi?resId=dong&catId=admi&signguCd=11200`로 조회한다. +2. 상가정보 데이터는 **분기마다 갱신**된다. 영업시간·전화·폐업 여부 필드는 없다. 증분 API(`storeListByDate`)는 지역 필터가 없고 신규(C)·수정(U)만 주며, 폐업 건은 오지 않는다. +3. 카페 등 업종 코드는 **매뉴얼에 표가 없다.** `I212`/`I21201`이 예시값으로만 나오고(백다방 예시), 이름은 적혀 있지 않다. `middleUpjongList`/`smallUpjongList`로 받아 고정해야 한다(2023-03 신분류, `stdrDt` 2023-02-28). +4. 단기예보는 **02~14시 발표면 글피(D+3)까지, 17~23시 발표면 그글피(D+4)까지**만 나온다. 연장된 마지막 날은 3시간 간격이고, PCP·SNO·WSD 값이 mm가 아니라 1~3 등급 코드로 온다. +5. 우리 격자 공식은 매뉴얼 C 코드와 수학적으로 같다. 격자표 3,801행 중 3,758행(98.9%)이 정확히 맞고 나머지 43행도 1칸 차이다. 성수 4개 동은 (61,126), 연남동은 (59,127), 해운대 우1동·중1동은 (99,75)로 표와 일치한다. +6. 두 매뉴얼 모두 **라이선스·이용조건·일일 트래픽을 적지 않았다.** 적힌 것은 초당 30 tps뿐이다. 이용허락범위와 일일 한도는 data.go.kr 신청 화면에서 확인해야 한다. +7. 매뉴얼의 기상청 URL은 `apihub.kma.go.kr`(authKey)다. 우리가 쓰는 `apis.data.go.kr/1360000/...`(serviceKey)는 이 매뉴얼에 나오지 않는다(실제 동작은 확인됨). + +--- + +## 1. 상가정보 오퍼레이션·파라미터 + +### 공통 +- **[매뉴얼]** 기본 URL: `http://apis.data.go.kr/B553077/api/open/sdsc2/{오퍼레이션}`. 예시는 모두 `https://`다. + 서비스 이력에 "2025-06-09 : SSL 적용(HTTPS) 및 게이트웨이 인증 모듈 적용"이 있으므로 https를 쓴다. +- **[매뉴얼]** 인증 파라미터는 `serviceKey`다. 표에는 소문자 `servicekey`, 예시에는 `ServiceKey`/`serviceKey`가 섞여 있다. + 응답 형식은 `type=xml|json`(옵션, 기본값 xml)이다. +- **[매뉴얼]** 모든 오퍼레이션에 "초당 최대 트랜잭션 [ 30 tps ]", "평균응답시간 [ 500 ms ]"가 적혀 있다. +- **[매뉴얼]** 목록형은 모두 `numOfRows` "최대 1000", `pageNo` "현재 요청 페이지번호"다. 응답 body에 `numOfRows`, `pageNo`, `totalCount`가 온다. +- **[매뉴얼]** 응답 header에 `stdrYm`(기준년월, 예 `202503`)이 온다. 적재할 때 스냅샷 버전으로 저장하는 게 좋다 **[추정]**. +- 목록형의 업종 필터(옵션, 공통): `indsLclsCd`(대, 2자리), `indsMclsCd`(중, 4자리), `indsSclsCd`(소, 6자리). **[매뉴얼]** "입력된 ○분류 업종에 해당하는 것만 조회" + +### 오퍼레이션 목록 [매뉴얼] + +| # | 오퍼레이션 | 용도 | 필수 파라미터 | 옵션 | +|---|---|---|---|---| +| 1 | `storeZoneOne` | 주요상권(전국 약 1,200개) 1건의 영역 폴리곤 | `key`=상권번호 | type | +| 2 | `storeZoneInRadius` | 반경 안의 상권 | `radius`(m, "반경제한:2km"), `cx`(경도), `cy`(위도) | type | +| 3 | `storeZoneInRectangle` | 사각형 안의 상권 | `minx, miny, maxx, maxy` | type | +| 4 | `storeZoneInAdmi` | 행정구역 단위 상권 | `divId`(ctprvnCd/signguCd/adongCd), `key` | type | +| 5 | `storeOne` | 상가업소 1건 | `key`=상가업소번호(bizesId) | type | +| 6 | `storeListInBuilding` | 건물 단위 업소 | `key`=건물관리번호(25자리) | 업종필터, numOfRows, pageNo, type | +| 7 | `storeListInPnu` | 지번(PNU) 단위 업소 | `key`=PNU(19자리) | 〃 | +| 8 | **`storeListInDong`** | **행정구역 단위 업소** | **`divId`, `key`** | 〃 | +| 9 | `storeListInArea` | 상권번호 안의 업소 | `key`=상권번호 | 〃 | +| 10 | `storeListInRadius` | 반경 안의 업소 | `radius`("미터단위, 최대2000 미터"), `cx`, `cy` | 〃 | +| 11 | `storeListInRectangle` | 사각형 안의 업소 | `minx, miny, maxx, maxy` | 〃 | +| 12 | `storeListInPolygon` | 다각형 안의 업소 | `key`=WKT `POLYGON((경도 위도, ...))`(최대 4300자) | 〃 | +| 13 | `storeListInUpjong` | 업종별 업소(**전국**) | `divId`(indsLclsCd/indsMclsCd/indsSclsCd), `key`=업종코드 | numOfRows, pageNo, type | +| 14 | `storeListByDate` | 수정일자 기준 업소 | `key`=일자 YYYYMMDD | 업종필터, numOfRows, pageNo, type | +| 15 | `reqStoreModify` | 업소정보 변경요청(등록) | 아래 참조 | — | +| 16 | `largeUpjongList` | 업종 대분류 코드 | — | type | +| 17 | `middleUpjongList` | 업종 중분류 코드 | — | `indsLclsCd`, type | +| 18 | `smallUpjongList` | 업종 소분류 코드 | — | `indsLclsCd`, `indsMclsCd`, type | +| 19 | `baroApi` | 행정구역 코드 조회 | `resId=dong`, `catId`(mega/cty/admi/zone) | 아래 참조 | + +### 8) 행정동 단위 상가업소 조회 — `storeListInDong` [매뉴얼] +- `divId` (필수, 크기 8): "시도는 ctprvnCd, 시군구는 signguCd, 행정동은 adongCd를 사용" +- `key` (필수, 크기 8): "시도는 시도코드값, 시군구는 시군구코드값, 행정동은 행정동코드값을 사용" +- `indsLclsCd` / `indsMclsCd` / `indsSclsCd` (옵션), `numOfRows` (옵션, "최대 1000"), `pageNo` (옵션), `type` (옵션) +- 매뉴얼 예시: `storeListInDong?divId=ctprvnCd&key=11&indsLclsCd=I2&indsMclsCd=I201&indsSclsCd=I20102&numOfRows=2&pageNo=1&type=xml` + - 이 예시는 서울(11)을 요청했는데 응답은 대전 업소다. 예시 응답을 그대로 믿으면 안 된다. + +### 19) 행정구역 조회 — `baroApi` [매뉴얼] +| 대상 | 요청 | 응답 주요 필드 | +|---|---|---| +| 시도 | `baroApi?resId=dong&catId=mega&type=json` | `ctprvnCd`, `ctprvnNm`, `stdrDt` | +| 시군구 | `baroApi?resId=dong&catId=cty&ctprvnCd=11&type=json` | + `signguCd`, `signguNm` | +| 행정동 | `baroApi?resId=dong&catId=admi&signguCd=11200&type=json` | + `adongCd`(예 `11740515`), `adongNm` | +| 법정동 | `baroApi?resId=dong&catId=zone&signguCd=11200&type=json` | + `ldongCd`(10자리, 예 `1174011000`), `ldongNm` | + +- **[매뉴얼]** 시군구 코드표(2026년 7월 기준)는 매뉴얼에 실려 있다: 성동구 `11200`, 마포구 `11440`, 해운대구 `26350`. 매뉴얼에 행정동 코드표는 없다(API로 조회). +- **[매뉴얼]** 상가 데이터의 `adongCd`는 **8자리**다(예시 `30170540` 변동, `11740515` 강일동). 그런데 변경요청 예시에는 `3017064000`(10자리)이 나온다. 표기가 일관되지 않다. + +#### 성수 행정동 코드 +- **[매뉴얼(기상청 격자표)]** 행정안전부 10자리 코드: 성수1가제1동 `1120065000`, 성수1가제2동 `1120066000`, 성수2가제1동 `1120067000`, 성수2가제3동 `1120069000`. 연남동은 `1144071000`, 해운대 우제1동 `2635051000`, 중제1동 `2635053000`. +- **[추정]** 상가정보 8자리 코드는 위 10자리의 앞 8자리다. 매뉴얼 예시 `11740515`(강일동)가 행안부 `1174051500`과 같은 꼴이라서다. + → 성수1가1동 `11200650`, 성수1가2동 `11200660`, 성수2가1동 `11200670`, 성수2가3동 `11200690`. + **반드시 `baroApi catId=admi signguCd=11200` 결과로 확인하고 쓴다.** 동 이름도 다를 수 있다(격자표는 "성수1가제1동", 상가정보는 "성수1가1동"일 가능성). 이름으로 조인하지 말고 코드로 조인한다. +- 대안: `divId=signguCd&key=11200`로 성동구 전체를 받고 `adongNm`/`adongCd`로 거른다. 페이지 수는 늘어난다. + +### 13) 업종별 상가업소 조회 — `storeListInUpjong` [매뉴얼] +- `divId`: "대분류는 indsLclsCd, 중분류는 indsMclsCd, 소분류는 indsSclsCd를 사용", `key`: 해당 코드값 +- **지역 파라미터가 없다** → 전국 결과다(예시 `divId=indsLclsCd&key=I2`의 totalCount `831738`). + 성수 카페만 받으려면 이 오퍼레이션 대신 `storeListInDong` + `indsSclsCd`(또는 `indsMclsCd`) 필터를 쓴다 **[추정]**. + +### 14) 수정일자기준 상가업소 조회 — `storeListByDate` [매뉴얼] +- `key`: "일자(YYYYMMDD 형식, 예:20151202)", 업종 필터·페이징 옵션 +- 설명: "수정일자를 기준으로 신규, 수정 업소목록이 필요한 경우를 위해 설계된 오퍼레이션 (제공기준 : 영업중인 상가 업소정보)" +- 응답에 추가 필드: `chgGb` "해당 상가업소정보의 데이터 변경 구분값 (생성:C, 수정:U중1)", `chgDt` 수정일자 +- **지역 필터 없음**(전국), **삭제(폐업) 구분값 없음**. "그 날짜에 바뀐 것"인지 "그 날짜 이후 바뀐 것"인지 매뉴얼에 적혀 있지 않다. +- **[추정]** 데이터 갱신이 "분기별"이므로 매일 증분 동기화는 의미가 적다. 분기마다 대상 행정동 전체를 다시 받아 `bizesId` 기준으로 비교한다(사라진 ID = 폐업 또는 ID 변경 후보). `storeListByDate`는 전국 변경분 점검용 보조 수단이다. + +### 15) 상가업소정보 변경요청 — `reqStoreModify` [매뉴얼] +- 유형 "등록". "상가업소명, 전화번호, 주소 등 상가업소 정보 정정을 요청하는 서비스" +- 파라미터(표상 **모두 필수(1)**): `bizesId`, `bizesNm`, `brchNm`, `indsSclsCd`, `adongCd`, `lnoAdr`, `rdnmAdr`, `bldNm`, `dongNo`, `flrNo`, `hoNo`, `opbizDt`(개업일자), `clbizDt`(폐업일자), `etcChgReqCnts`(기타, 1000자) +- 응답: `bizesId`, `result`("정상인 경우 00"), `message` +- `bizesId`는 "해당 번호와 일치하는 업소가 있어야 변경요청이 수락 되어 진행된다" +- **[추정]** 운영자가 현장에서 폐업·이전을 확인했을 때 수동으로 쓸 수 있는 통로다. 공공기관 데이터에 쓰는 요청이므로 자동화하지 않는다. + 매뉴얼은 GET 쿼리 예시만 보여주고 HTTP 메서드를 명시하지 않는다(인터페이스 표준 칸은 "REST (GET, POST, PUT, DELETE)"). + +### 응답 필드 (모든 상가업소 목록·단건 공통) [매뉴얼] +`bizesId, bizesNm, brchNm, indsLclsCd/Nm, indsMclsCd/Nm, indsSclsCd/Nm, ksicCd, ksicNm, ctprvnCd/Nm, signguCd/Nm, adongCd/Nm, ldongCd/Nm, lnoCd(PNU), plotSctCd/Nm, lnoMnno, lnoSlno, lnoAdr, rdnmCd, rdnm, bldMnno, bldSlno, bldMngNo, bldNm, rdnmAdr, oldZipcd, newZipcd, dongNo, flrNo, hoNo, lon, lat` +(우리 실측 필드 목록과 정확히 같다. `storeListByDate`만 `chgGb`, `chgDt`가 더 붙는다.) + +| 필드 | 매뉴얼 설명 | 메모 | +|---|---|---| +| `bizesId` | "해당 상가업소에 부여된 일련번호" (크기 20) | 안정성(불변 여부)은 **매뉴얼에 없음**. 아래 주의점 참조 | +| `bizesNm` / `brchNm` | 상호명 / "지점인 경우 지점명" | 예시 상호가 `***해장국`처럼 가려진 곳과 실명인 곳이 섞여 있다 | +| `ksicCd` / `ksicNm` | 표준산업분류코드/명 (옵션) | 예 `I56111` 한식 일반 음식점업 | +| `adongCd` | 행정동 코드 (크기 20, 예 8자리) | | +| `ldongCd` | 법정동 코드 (10자리) | 성수동1가·2가는 법정동이다 | +| `lnoCd` | "지번주소에 해당하는 PNU코드" (19자리) | | +| `bldMngNo` | "도로명주소 기준 건물관리번호" (25자리) | `storeListInBuilding`의 key | +| `bldNm` | 건물명 | 예시에서 빈 값 많음 | +| `dongNo` / `flrNo` / `hoNo` | "소재한 동정보 / 층정보 / 호정보" | 형식(지하 표기 등)은 **매뉴얼에 없음**. 예시는 `1` 또는 빈 값 | +| `lon` / `lat` | WGS84. 둘 다 설명이 "위도 좌표값"으로 적혀 있다(오기) | | +| `stdrYm` (header) | 기준년월 | | + +- 영업시간, 전화번호, 영업상태, 개업·폐업일 필드는 **응답에 없다**(개업·폐업일은 변경요청 입력 항목에만 있음). + +--- + +## 2. 업종 코드 (카페 등) + +### 분류 체계 [매뉴얼] +- "변경사항 : 2023년 03월 이후, 공공데이터에 서비스 되는 업소는 소진공에서 사용하는 업종분류의 체계가 변경되었으며 대분류(2자리), 중분류(4자리), 소분류(6자리)로 구성" +- 업종 코드 API 응답의 `stdrDt` = `2023-02-28` +- 대분류(서비스 대상) [매뉴얼]: `G2` 소매업, `I1` 숙박, `I2` 음식점, `L1` 부동산, `M1` 과학·기술 서비스, `N1` 시설관리·임대, `P1` 교육, `Q1` 보건의료, `R1` 예술·스포츠·여가, `S2` 수리·개인 서비스 + (대분류 API 예시에는 `F1` 건설업, `O1` 공공 행정·국방, `J1` 정보통신, `C1` 제조업, `S1` 협회·단체도 나온다) +- 대분류명 표기가 일관되지 않다: 코드 API는 "음식점", 업소 응답은 "음식"이다. 우리 코드의 `'음식' in indsLclsNm`은 둘 다 잡지만, **코드(`I2`)로 거르는 게 안전하다.** + +### 매뉴얼에 실제로 나오는 음식 코드 (전부) +| 코드 | 이름 | 출처 | +|---|---|---| +| `I201` | 한식 | 코드 API 예시 | +| `I20102` | 국/탕/찌개류 | 〃 | +| `I20109` | 곱창 전골/구이 | 〃 | +| `I20111` | 횟집 | 〃 | +| `I20112` | 해산물 구이/찜 | 〃 | +| `I207` | 구내식당, 뷔페 (업소 응답은 "구내식당·뷔페") | 〃 | +| `I20701` | 구내식당 | 업소 예시 | +| `I208` | 출장 음식 | 코드 API 예시 | +| `I210` | 기타 간이 | 〃 | +| `I212` | **이름 없음** — 사각형 조회의 `indsMclsCd` 샘플값 | 요청 예시 | +| `I21201` | **이름 없음** — 사각형 조회 샘플값. 변경요청 예시에서 상호 "백다방"과 함께 쓰임 | 요청 예시 | + +- **카페/커피전문점, 디저트/제과, 주점의 코드는 매뉴얼에 없다.** +- **[추정]** `I212`는 비알코올 음료점(카페류), `I21201`은 카페로 보인다. 근거는 백다방 예시와, 우리 실측에서 '비알코올'이 중분류명에 있었다는 점이다. 주점·제과는 번호를 짐작하지 않는다. +- 확정 방법(호출 3번): + 1. `largeUpjongList?type=json` + 2. `middleUpjongList?indsLclsCd=I2&type=json` + 3. `smallUpjongList?indsLclsCd=I2&type=json` (`indsMclsCd`는 옵션. 생략하면 I2 전체 소분류가 나오는지 확인 필요) + 필요하면 `R1`(예술·스포츠·여가: 공방·갤러리 후보)도 받는다. 결과를 코드표로 저장소에 고정하고, `stdrDt`가 바뀌면 다시 받는다. +- 보조 필터: `ksicCd`(통계청 표준산업분류)도 함께 온다. **[추정]** KSIC 10차 기준 커피 전문점은 `56221`(데이터 표기는 `I56221`일 것)이다. 매뉴얼에 없는 값이므로 실데이터의 `ksicNm`으로 확인한다. + +--- + +## 3. 상가정보 제한·라이선스 + +| 항목 | 내용 | +|---|---| +| 페이지 크기 | **[매뉴얼]** `numOfRows` 최대 1000 | +| 반경 | **[매뉴얼]** 업소 반경 "최대2000 미터", 상권 반경 "반경제한:2km" | +| 다각형 | **[매뉴얼]** `key` 항목 크기 4300자 | +| 속도 | **[매뉴얼]** 30 tps (오퍼레이션별) | +| 일일 한도 | **매뉴얼에 없음** → data.go.kr 마이페이지 개발계정 상세에서 확인 | +| 갱신 주기 | **[매뉴얼]** "데이터 갱신주기 분기별". 예시 `stdrYm` 202503 | +| 데이터 출처·수집 방법 | **매뉴얼에 없음**. "영업중인 상가 업소정보"(증분 API 제공기준)라는 문구만 있다 | +| 커버리지 한계 | 매뉴얼에 명시 없음. **[실측]** 성수 500m에 3,127곳으로 양은 많다. **[추정]** 분기 갱신이라 신규 개업·폐업이 최대 수개월 늦게 반영되고, 팝업·전시처럼 짧게 운영하는 곳은 없다 | +| 라이선스·이용조건 | **두 매뉴얼 모두 언급 없음.** 저장·상업 표시를 제한하는 문구도 없다. data.go.kr 해당 API 페이지의 "이용허락범위"를 확인해 이 문서에 적어 둔다. **[추정]** 공공데이터법은 영리 목적 이용을 보장하지만, 개별 데이터셋에 출처표시 조건이 붙을 수 있다 | +| 행정구역 변경 | **[매뉴얼]** 시도표에 `12` "전남광주통합특별시"가 있다(2026년 7월 기준). 기상청 격자표에도 반영되어 있다 → 코드는 바뀔 수 있으니 기준일과 함께 저장한다 | +| 오류 코드 | 상가정보 매뉴얼에는 오류 코드표가 **없다**(`resultCode` `00`=정상만 나옴) | + +--- + +## 4. 기상청 단기예보 — 오퍼레이션·발표시각·범위 + +### 공통 [매뉴얼] +- 서비스: `VilageFcstInfoService_2.0`. **서비스 URL은 `https://apihub.kma.go.kr/api/typ02/openApi/VilageFcstInfoService_2.0`**이고, 예시 요청은 `authKey=`를 쓴다. + 요청 명세 표에는 "serviceKey … 공공데이터포털에서 발급받은 인증키"라고 적혀 있다(두 체계가 섞여 있음). + 우리가 쓰는 `https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst`는 이 매뉴얼에 없다. **[실측]** 이 주소는 정상 동작했다. +- 공통 파라미터: `numOfRows`(필수, "Default: 10"), `pageNo`(필수, "Default: 1"), `dataType`(옵션 XML/JSON, "Default: XML"), `base_date`, `base_time`, `nx`, `ny`(모두 필수) +- "데이터 갱신주기 | 수시 (일 8회)", 오퍼레이션마다 "초당 최대 트랙잭션 [30] tps". 일일 한도와 라이선스는 **매뉴얼에 없음**. +- "단기예보서비스는 남한에 대해서만 제공되며, 북한 및 국외는 제공되지 않습니다." +- 오류코드 [매뉴얼]: `00` 정상, `03` NODATA_ERROR, `10` 잘못된 파라미터, `11` 필수 파라미터 없음, `12` 서비스 없음/폐기, `20` 접근거부, `21` 일시 사용불가 키, `22` 요청제한횟수 초과, `30` 미등록 키, `31` 기한만료 키, `32` 미등록 IP, `33` 서명되지 않은 호출, `99` 기타 (`01/02/04/05`는 앱·DB·HTTP·연결 오류) + +### 오퍼레이션 [매뉴얼] + +| 오퍼레이션 | 내용 | base_time | 호출 가능 시각 | 범위 | +|---|---|---|---|---| +| `getUltraSrtNcst` 초단기실황 | "예보 구역에 대한 대표 AWS 관측값" | 매시 정각 `HH00` | "매시각 10분 이후 호출". "매시간 정시에 10분이후 생성되고 10분마다 최신 정보로 업데이트" | 현재 관측 | +| `getUltraSrtFcst` 초단기예보 | "예보시점부터 6시간 이내의 예보" | 매시 30분 `HH30` | "매시각 45분 이후 호출" | 6시간, 1시간 간격 | +| `getVilageFcst` 단기예보 | 시공간 세분화 예보 | `0200, 0500, 0800, 1100, 1400, 1700, 2000, 2300` | `02:10, 05:10, …, 23:10` | 아래 | +| `getFcstVersion` 예보버전 | "수정된 예보 버전을 파악" | — | — | `ftype`(ODAM 초단기실황 / VSRT 초단기예보 / SHRT 단기예보), `basedatetime`(YYYYMMDDHHMM) → `version`(파일 생성시각) | + +### 단기예보 범위 [매뉴얼] +- "단기예보 5일 연장으로 02·05·08·11·14시 발표기준 글피 예보, 17·20·23시 발표기준 그글피 예보제공" +- "연장기간인 (02·05·08·11·14시 발표)글피, (17·20·23시 발표)그글피 예보는 3시간 간격의 자료를 제공하며, 강수량(PCP), 강설(SNO), 풍속(WSD) 요소는 정성정보(코드값)를 제공" +- 정리 **[추정: 표 해석]**: + +| 발표 | 오늘·내일·모레 | 글피(D+3) | 그글피(D+4) | +|---|---|---|---| +| 02·05·08·11·14시 | 1시간 간격, 정량값 | **3시간 간격**, PCP/SNO/WSD는 코드 | 없음 | +| 17·20·23시 | 1시간 간격, 정량값 | 1시간 간격, 정량값 | **3시간 간격**, PCP/SNO/WSD는 코드 | + +- 최저/최고기온(TMN/TMX) 제공일 [매뉴얼 표]: + - 02시 발표: 최저·최고 모두 오늘~글피 + - 05·08·11시 발표: 최저 내일~글피, 최고 오늘~글피 + - 14시 발표: 최저·최고 모두 내일~글피(**오늘 최고 없음**) + - 17·20·23시 발표: 최저·최고 모두 내일~그글피 + - TMN/TMX가 몇 시 `fcstTime`에 붙는지는 **매뉴얼에 없음**. **[추정]** 최저는 0600, 최고는 1500이다. 실데이터로 확인한다. +- `numOfRows`: 매뉴얼은 기본값 10만 말한다. 예시 totalCount는 742(연장 전 예시)다. + **[추정]** 5일 연장 후 한 번에 약 850~1,000행이다(시간당 12개 요소 × 60~72시간 + 3시간 간격 하루분 + TMN/TMX). 우리가 쓴 1500은 충분하다. 그래도 `totalCount`와 받은 건수를 비교하는 검사를 둔다. + +--- + +## 5. 기상청 카테고리 코드표 [매뉴얼] + +### 단기예보 (`getVilageFcst`) +| 코드 | 이름 | 단위 | +|---|---|---| +| `POP` | 강수확률 | % | +| `PTY` | 강수형태 | 코드: 없음(0), 비(1), 비/눈(2), 눈(3), 소나기(4) | +| `PCP` | 1시간 강수량 | 범주(1 mm) — 아래 문자열 규칙 | +| `REH` | 습도 | % | +| `SNO` | 1시간 신적설 | 범주(1 cm) — 아래 문자열 규칙 | +| `SKY` | 하늘상태 | 코드: 맑음(1), 구름많음(3), 흐림(4) (전운량 0~5 / 6~8 / 9~10) | +| `TMP` | 1시간 기온 | ℃ | +| `TMN` | 일 최저기온 | ℃ | +| `TMX` | 일 최고기온 | ℃ | +| `UUU` | 풍속(동서성분) | m/s, 동(+)/서(-) | +| `VVV` | 풍속(남북성분) | m/s, 북(+)/남(-) | +| `WAV` | 파고 | M | +| `VEC` | 풍향 | deg (16방위 변환: `(풍향 + 22.5*0.5) / 22.5` 버림 → 0=N … 15=NNW, 16=N) | +| `WSD` | 풍속 | m/s | + +### 초단기실황 (`getUltraSrtNcst`) +`T1H` 기온(℃), `RN1` 1시간 강수량(mm), `UUU`/`VVV`(m/s), `REH`(%), `PTY`(코드), `VEC`(deg), `WSD`(m/s). 응답 값 필드는 `obsrValue`다(예보는 `fcstValue`). + +### 초단기예보 (`getUltraSrtFcst`) +`T1H`, `RN1`(범주 1 mm), `SKY`, `UUU`, `VVV`, `REH`, `PTY`, **`POP`**, `LGT` 낙뢰(kA, "에너지밀도 0.2~100kA/㎢"), `VEC`, `WSD` +- "초단기예보 강수형태(POP)요소 확대 제공: 2026.6.23. 11KST 이후" — 원문은 '강수형태'라고 썼지만 코드는 POP(강수확률)다(매뉴얼 오기). +- 초단기 PTY는 0~4에 더해 빗방울(5), 빗방울눈날림(6), 눈날림(7)이 있다. + +### 특수 값 [매뉴얼] +- 강수량(RN1, PCP) 문자열 표시: + - 0.1~1.0mm 미만 → "1mm 미만" + - 1.0~30.0mm 미만 → "실수값+mm"(1.0mm~29.9mm) + - 30.0~50.0mm 미만 → "30.0~50.0mm" + - 50.0mm 이상 → "50.0mm 이상" + - "※ -, null, 0값은 '강수없음'" + - 매뉴얼 예: PCP = 6.2는 6.2mm, PCP = 30은 30.0~50.0mm + - 같은 절의 Java 예시는 "1.0~29.0mm"를 반환한다. 표와 맞지 않는 오기다. +- 신적설(SNO): + - 0.1~0.5cm 미만 → "0.5cm 미만" + - 0.5~5.0cm 미만 → "실수값+cm" + - 5.0cm 이상 → "5.0cm 이상" + - "-, null, 0값은 '적설없음'" +- 연장기간 정성 코드 ("단기예보 연장에 따른 코드값 정보(2024.11.28. 14시~)"): + - PCP: 1 약한 비(<3mm/h), 2 보통 비(3~15mm/h), 3 강한 비(≥15mm/h) + - SNO: 1 보통 눈(<1cm/h), 2 많은 눈(≥1cm/h) + - WSD: 1 약한 바람, 2 약간 강한 바람(4~9m/s), 3 강한 바람(≥9m/s) + - WSD 1의 설명이 원문에 "4m/s 이상의 약한 바람"으로 적혀 있다. 4m/s **미만**의 오기로 보인다 **[추정]**. +- 결측: "+900이상, –900 이하 값은 Missing 값으로 처리" +- 해상: "해상에는 기온군, 강수확률, 강수량/적설, 습도를 제공하지 않음(Missing값으로 마스킹처리 함)" +- **[추정]** 파서 규칙: `fcstValue`는 문자열이다. PCP/SNO는 "강수없음", "1mm 미만", "6.2mm", "30.0~50.0mm", "50.0mm 이상" 같은 문자열이 오고, 연장일에는 "1"~"3" 코드가 온다. **날짜가 연장 구간인지 보고 해석을 분기**해야 한다. + +--- + +## 6. 격자 검증 + +### 공식 [매뉴얼 C 코드 vs 우리 코드] +| 상수 | 매뉴얼 | 우리 `kma_grid` | +|---|---|---| +| Re | 6371.00877 | 6371.00877 | +| grid | 5.0 km | 5.0 | +| slat1 / slat2 | 30.0 / 60.0 | 30.0 / 60.0 | +| olon / olat | 126.0 / 38.0 | 126.0 / 38.0 | +| xo / yo | `210/grid`=42, `675/grid`=135 | 43, 136 | +| 반올림 | `(int)(x1 + 1.5)` | `floor(x + 43 + 0.5)` | + +- `42 + x + 1.5` = `43 + x + 0.5`이므로 같다. 남한 범위는 좌표가 양수라 `(int)` 절삭과 `floor`가 같다. θ 정규화도 같다. +- 매뉴얼 예제 `126.929810, 37.488201 → X=59, Y=125`를 우리 함수로 돌린 결과도 **(59,125)**다 [실측]. + +### 격자표 대조 [실측: 격자표 전 행에 우리 함수 적용] +- 좌표를 읽을 수 있는 3,801행 중 **3,758행(98.9%)이 정확히 일치**하고, 43행은 **1칸 차이**다(최대 차이 1). + 나머지 36행은 텍스트 추출 때 열이 밀려 좌표를 읽지 못해 제외했다(강원 18·전북 15·이어도 2·전남광주 1). +- 1칸 차이 예: 수유1동, 문래동, 개포3동, 부산 좌천동 등. 표의 "위치업데이트" 날짜가 있는 행이 많아(43행 중 31행) 대표 좌표만 갱신되고 격자는 그대로인 경우로 보인다 **[추정]**. + → 동 단위로 저장할 때는 **표의 nx/ny를 우선**하고, 임의 좌표(장소 좌표)는 공식으로 계산한다. + +| 지역 | 격자표(동) | 표 nx, ny | 우리 계산 | +|---|---|---|---| +| 성수 | 성수1가제1동 `1120065000` | 61, 126 | 61, 126 ✓ | +| | 성수1가제2동 `1120066000` | 61, 126 | | +| | 성수2가제1동 `1120067000` | 61, 126 | | +| | 성수2가제3동 `1120069000` | 61, 126 | | +| 연남 | 연남동 `1144071000` | 59, 127 | 59, 127 ✓ | +| 해운대 | 우제1동 `2635051000` | 99, 75 | 99, 75 ✓ | +| | 중제1동 `2635053000` | 99, 75 | | +| | (참고) 중제2동 100,75 · 좌제1~4동 100,76 · 우제2동 99,76 | | | + +- 주의: 성동구 전체 행은 (61,**127**)이다. 구 단위 격자를 쓰면 성수와 한 칸 어긋난다. + +--- + +## 7. 우리 가정과 다른 점 + +1. **"예보가 D+4까지 닿는다"는 절반만 맞다.** 11시 발표(우리 점검 시각 12:24)는 매뉴얼상 글피(D+3, 11/1)까지이고, 글피는 3시간 간격이다. + 실측의 11/2 날짜는 경계 시각(00시) 몇 건으로 보인다 **[추정, 날짜별 건수로 확인 필요]**. + 일요일(D+3) 오후 판단에 PCP를 쓰면 mm가 아니라 1~3 코드가 온다. POP 15:00은 3시간 격자에 걸려 있어 받을 수 있다. + D+4 이후는 17시 이후 발표에서만 나오고, 그 너머는 중기예보가 필요하다. +2. **기상청 엔드포인트**: 매뉴얼은 API허브(`apihub.kma.go.kr`, `authKey`)만 문서화한다. 우리는 data.go.kr 게이트웨이(`apis.data.go.kr/1360000`, `serviceKey`)를 쓴다. + 기능은 같아 보이지만 한도·키 체계가 다르다 **[추정]**. +3. **격자 상수 표기**(43/136 + 0.5)는 매뉴얼(42/135 + 1.5)과 모양만 다르고 결과가 같다. 문제 없음. +4. **카페 식별**: 키워드('카페/커피/비알코올') 대신 업종 코드로 거른다. 그런데 그 코드는 매뉴얼에 없어 `smallUpjongList`로 받아야 한다(I212/I21201은 이름 없는 예시값일 뿐이다). +5. **행정동 코드 자릿수**: 기상청 표·행안부는 10자리, 상가정보 `adongCd`·`key`는 8자리다. +6. **증분 동기화 가정**: `storeListByDate`는 지역 필터가 없고, 폐업(삭제)을 알려주지 않으며, 원천이 분기 갱신이다. "매일 변경분만 받기"는 성립하지 않는다. +7. **업종별 조회(`storeListInUpjong`)는 전국 단위**라 지역 수집에 쓰지 못한다. 지역+업종은 `storeListInDong`/`storeListInRadius`의 업종 필터로 한다. +8. **대분류명**: 코드 API는 "음식점", 업소 응답은 "음식"으로 표기가 다르다. 이름 문자열 비교를 코드 비교로 바꾼다. +9. **15시 POP 한 점**만 보는 것은 매뉴얼과 무관한 설계 이슈다 **[추정]**. + 코스 시간대 전체의 PTY·POP·PCP를 보고, 당일 출발 직전에는 초단기예보(이제 POP 포함)·초단기실황으로 덮어쓴다. + +--- + +## 8. 주의점 + +### 상가정보 +- **`bizesId` 안정성은 보장되지 않는다(매뉴얼에 없음).** 매뉴얼 안에서도 형식이 섞여 있다. + - 단건조회 예시 요청은 `key=19911025`인데 응답은 `MA010120220805432049`다. + - 요청 항목 크기는 10인데 실제 ID는 20자다. + - 변경요청 예시는 `MA10868815...`/`10868815`다. + - **[추정]** 과거에 ID 체계가 바뀐 적이 있다. 우리 장소 ID는 따로 두고 `bizesId`는 외부 참조로 저장한다. 재적재할 때 ID가 사라지면 이름+주소+좌표로 다시 짝짓는다. +- 영업시간·전화·영업상태가 없다. 운영자 검수 단계에서 채워야 할 항목이다. + 분기 갱신이라 "영업중"이라도 이미 폐업했을 수 있으니, 검수일(`verifiedAt`)을 저장한다. +- `flrNo`·`bldNm`·`dongNo`·`hoNo`는 옵션이고 예시에서 대부분 비어 있다. 지하 층 표기 형식은 실데이터로 확인한다. +- 상호명 일부가 `***`로 가려진 예시가 있다(`***해장국`). 실데이터에서 가림 비율을 확인한다. +- 매뉴얼 예시 응답 상당수가 요청과 맞지 않는다(서울 요청에 대전 응답 등). 스키마 참고용으로만 쓴다. +- 인증키는 URL 인코딩 키(Encoding)와 디코딩 키(Decoding) 중 하나를 골라 이중 인코딩하지 않는다. 우리 스크립트는 `urlencode`를 쓰므로 Decoding 키가 맞다 **[추정]**. + +### 기상청 +- base_time은 "발표 후 10분" 규칙만으로 부족할 수 있다. 지연되면 `03 NODATA_ERROR`가 오므로 직전 발표로 한 번 물러나는 재시도를 둔다 **[추정]**. + `getFcstVersion(ftype=SHRT)`로 재발표(수정) 여부를 확인할 수 있다. +- 같은 격자 안의 장소는 같은 예보를 공유한다. 격자 단위 캐시 키는 `(nx, ny, base_date, base_time)`로 하면 호출 수가 크게 준다. +- 초단기예보 표에 오기가 있다(19시 Base_time "19030", 20시 행의 "22~23시"). 규칙(매시 30분 발표, 45분 이후 호출, 6시간)만 믿는다. +- 해상 격자(해운대 해변 좌표가 바다 격자에 걸리는 경우)는 기온·강수가 결측(+900/-900)일 수 있다 **[추정]**. + 해운대 우1동·중1동 (99,75)는 육상 행정동 격자이므로, 장소 좌표가 해상 격자로 계산되면 행정동 격자로 대체한다. + +### 이 매뉴얼에 없어서 따로 필요한 것 **[추정]** +- **중기예보**(D+3/D+4 이후 ~10일, 오전/오후 단위 강수확률·하늘): 기상청 중기예보 조회서비스. 이번 주말을 목요일 오전에 계획하면 단기예보로 충분하지만, 다음 주말 코스는 중기예보가 필요하다. +- **기상특보**(호우·태풍·폭염·한파): 기상청 특보 조회서비스. "비 → 실내"보다 강한 신호(코스 취소·경고). +- **미세먼지**: 에어코리아(한국환경공단) 대기오염정보. 기상청 단기예보에는 없다. +- **자외선·체감온도 등 생활기상지수**: 기상청 생활기상지수 서비스(필요 시). +- 각 API의 이용허락범위·일일 한도: data.go.kr 신청 화면에서 확인해 이 문서에 추가한다. + +--- + +## 부록: 성수 행정동 수집 요청 형태 + +```text +# 1) 행정동 코드 확인 (1회, 결과 고정) +GET https://apis.data.go.kr/B553077/api/open/sdsc2/baroApi + ?serviceKey=…&resId=dong&catId=admi&signguCd=11200&type=json + → adongNm 에 '성수' 포함 4건의 adongCd(8자리) 사용 + +# 2) 동별 전체 업소 (totalCount 까지 pageNo 증가) +GET https://apis.data.go.kr/B553077/api/open/sdsc2/storeListInDong + ?serviceKey=…&divId=adongCd&key=&numOfRows=1000&pageNo=1&type=json + (업종만: &indsLclsCd=I2 또는 &indsMclsCd=<카페 중분류> 또는 &indsSclsCd=<카페 소분류>) + +# 3) 업종 코드표 (1회, 결과 고정) +GET …/largeUpjongList?serviceKey=…&type=json +GET …/middleUpjongList?serviceKey=…&indsLclsCd=I2&type=json +GET …/smallUpjongList?serviceKey=…&indsLclsCd=I2&type=json +``` +- 예상 adongCd **[추정, 1)로 확인]**: 성수1가1동 `11200650` · 성수1가2동 `11200660` · 성수2가1동 `11200670` · 성수2가3동 `11200690` +- 저장할 메타: 응답 header `stdrYm`, 수집 일시, 사용한 `adongCd`·업종 코드표 `stdrDt` diff --git a/docs/research/data-openapi-review/tourapi_kor.md b/docs/research/data-openapi-review/tourapi_kor.md new file mode 100644 index 0000000..255cdc2 --- /dev/null +++ b/docs/research/data-openapi-review/tourapi_kor.md @@ -0,0 +1,400 @@ +# TourAPI 국문(KorService2) 매뉴얼 검토 + +작성 2026-10-29 +대상 문서: 국문 활용매뉴얼 v4.4(2026-02-10), 신분류체계정보↔관광타입정보 연계 정의서, 활용신청방법 매뉴얼 v3.3 +원본: `docs/data-openapi/` (수정하지 않음). `L123` 은 텍스트 추출본의 줄 번호다. + +표기 +- **[매뉴얼]** 원문 그대로 인용하거나 요약한 것 +- **[관측]** 10-29 점검 결과(`docs/research/data-check/public-data-check-20261029-1224.json`)에서 확인한 것 +- **[추론]** 내 판단이라 검증이 필요한 것 + +--- + +## 핵심 요약 + +1. v4.4(2026-02-10)에서 `areacode`·`sigungucode`·`cat1~3` 와 지역코드·서비스분류코드 오퍼레이션이 **삭제**됐다. 이제 키는 법정동(`lDongRegnCd`/`lDongSignguCd`)과 분류체계(`lclsSystm1~3`) **뿐**이다. +2. 개발계정의 하루 1,000건은 **오퍼레이션마다 따로** 적용된다(신청 매뉴얼). 병목은 상세 조회고, 목록 조회는 거의 부담이 없다. +3. 야간 동기화에 쓸 `areaBasedSyncList2` 가 있다. `modifiedtime`(YYYY/YYYYMM/YYYYMMDD), `showflag`(1 표출 / 0 비표출), `oldContentid` 로 수정과 비공개 전환을 따라갈 수 있다. 데이터 갱신 주기는 하루 1회다. +4. `detailPetTour2` 는 contentId 를 비우면 **반려동물 정보 전체**를 돌려준다. 장소마다 따로 부를 필요가 없다. +5. 축제는 `lclsSystm2` 로 서버에서 가를 수 있다: EV01 축제 / EV02 공연 / EV03 행사. `eventEndDate` 요청값과 `progresstype`·`festivaltype` 응답값도 있다. +6. `usetimefestival` 은 이름과 달리 **이용요금** 필드다. 지금의 'usetime 부분일치' 방식으로는 축제 요금을 영업시간으로 잘못 읽는다. +7. 이미지는 장마다 `cpyrhtDivCd` 로 Type1(출처표시)/Type3(+변경금지)가 붙는다. 텍스트 라이선스, 운영계정 한도 수치, DB 저장 허용 여부는 명시되어 있지 않다. 다만 동기화 API 의 설명이 자체 DB 저장을 전제로 쓰여 있다. 운영계정은 "정상 서비스 확인 후" 승인된다. + +--- + +## 1. 우리 가정과 다른 점 + +### 1-1. 지역·분류 키 — "주 키"가 아니라 "유일한 키" + +- [매뉴얼] 개정이력 4.4 (L48–50): + "지역코드 오퍼레이션 삭제 / 서비스분류코드 오퍼레이션 삭제 / 요청/응답 : 지역코드 , 시군구코드 삭제 / 요청/응답 : 서비스 대분류, 중분류, 소분류 / 삭제" +- [매뉴얼] 4.3(2025-05-12)에서 법정동·분류체계 요청/응답 항목과 `ldongCode2`·`lclsSystmCode2` 오퍼레이션이 추가됐다 (L38, L46–47). +- [관측] 10-29 응답에는 `areacode, sigungucode, cat1, cat2, cat3` 키가 아직 남아 있었다(값은 기록하지 않았다). 매뉴얼의 행사 응답 예시(L461)에도 빈 문자열로 남아 있다. +- [추론] 키가 응답에 남아 있더라도 스키마·필터에는 쓰지 않는다. 매뉴얼 앞부분 호출 예시(L108, L117)의 `areaCode2` URL 은 갱신되지 않은 흔적이다. +- [매뉴얼] 필터 의존 규칙 (L234–237): `lDongSignguCd` 는 `lDongRegnCd` 가 필수이고, `lclsSystm2` 는 `lclsSystm1` 이, `lclsSystm3` 는 1·2가 필수다. +- [매뉴얼] 형식: 시도는 2자리(`11`, `26`), 시군구는 3자리(`140`, `380`) (L233–234, L1043–1049). + - 행사 오퍼레이션 예시의 `lDongRegnCd=36110`(L417)은 [추론] 매뉴얼 오기로 보인다. +- [추론] 점검 지역의 코드는 성수 11/200(성동구), 연남 11/440(마포구), 해운대 26/350 으로 보인다. 행정표준 법정동 코드로 짐작한 값이라 `ldongCode2` 로 확인해야 한다. + +### 1-2. 쿼터는 오퍼레이션별 1,000건/일 + +- [매뉴얼-국문 L102] "개발계정은 일 1,000건의 트래픽을 제공합니다." +- [매뉴얼-신청 L26] "개발계정은 **각 오퍼레이션별** 일일 1,000건의 트래픽을 제공합니다." +- [추론] `detailIntro2` 1,000, `detailCommon2` 1,000, `detailPetTour2` 1,000 … 이 각각 따로 잡힌다. 초기 적재를 오퍼레이션별로 나눠 병렬 계획을 세울 수 있다. + +### 1-3. locationBasedList2 + +- [매뉴얼 L284–299] 필수 항목은 `MobileOS`, `MobileApp`, `serviceKey`, `mapX`, `mapY`, `radius` 다. + - `radius`: "거리 반경(단위: m), Max값 20000m=20Km" + - 옵션: `contentTypeId`, `modifiedtime`(YYYYMMDD), `lDongRegnCd`/`lDongSignguCd`, `lclsSystm1~3` +- [매뉴얼 L290] `arrange` 값은 "A=제목순, C=수정일순, D=생성일순, E=거리순 / 대표 이미지가 반드시 있는 정렬 (O=제목순, Q=수정일순, R=생성일순, S=거리순)" 이다. + - 우리가 쓰는 `E` 가 맞다. + - [추론] 화면 목록에서 이미지 없는 항목을 빼야 하면 `S` 를 쓴다. +- [매뉴얼] 응답 `dist` 는 필수 항목이며 단위는 m 다 (L315). +- `numOfRows` 최대값은 **매뉴얼에 없다**. + - [관측] `searchFestival2` 에 `numOfRows=500` 을 주자 한 번에 455건이 왔다. 500은 동작한다. + +### 1-4. searchFestival2 + +- [매뉴얼 L414–421] 요청 항목: + - `eventStartDate`: 필수, YYYYMMDD + - `eventEndDate`: 옵션, YYYYMMDD + - `modifiedtime`, `lDong*`, `lclsSystm*` + - **좌표·반경 필터가 없고 거리순 정렬도 없다** +- [매뉴얼 L437–454] 응답 항목: + - `eventstartdate`, `eventenddate`: 필수 + - `progresstype`: "진행상태정보", 예시 "선택안함" + - `festivaltype`: "축제유형명", 예시는 빈 값 + - 두 필드의 값 목록은 매뉴얼에 없다 +- [관측] `eventStartDate=20260901`(토요일 60일 전)로 불렀는데 "페인터즈 20221101–20261231" 이 돌아왔다. 서버는 '시작일 ≥ 요청값'이 아니라 기간이 겹치는지(종료일 ≥ 요청값)로 거르는 것으로 보인다. +- [추론] `eventStartDate=eventEndDate=토요일` 로 부르면 그날 열리는 행사만 받을 수 있을 것 같다. 매뉴얼에 의미 정의가 없으니 한 번 호출해 검증한다. +- 상시 프로그램이 섞이는 문제는 매뉴얼에 있는 장치로 줄일 수 있다: + - `lclsSystm1=EV&lclsSystm2=EV01` 로 '축제'만 받는다 + - 소개정보의 `festivalgrade`(축제등급)를 본다 + - [추론] 그래도 기간 길이 필터(예: 60일 넘는 행사 제외)는 여전히 필요하다 +- [매뉴얼] 예시끼리 맞지 않는다. 응답 표는 `lclsSystm2=EV03` 에 `lclsSystm3=EV010200` 이고(L451–452), JSON 예시는 `EV01` 이다(L461). + - [추론] 분류 품질을 실제 데이터로 재 봐야 한다. + +### 1-5. 영업시간 감지 휴리스틱 + +- [매뉴얼 L651] `usetimefestival | 이용요금`. 개정이력 1.4 (L13)도 "행사의 이용시간을 이용 요금으로 수정" 이라고 적는다. + - 'usetime' 부분일치를 쓰면 축제(15)에서 요금을 시간으로 읽는다. + - [관측] 지금은 `HOURS_TYPES` 에 15가 없어 점검 결과에는 영향이 없었다. +- [매뉴얼] 타입별 시간 필드: + + | 타입 | 시간 필드 | + |---|---| + | 12 관광지 | `usetime` | + | 14 문화시설 | `usetimeculture` | + | 28 레포츠 | `usetimeleports`, `openperiod`(개장기간) | + | 38 쇼핑 | `opentime`, `fairday`(장서는날) | + | 39 음식점 | `opentimefood` | + | 15 행사 | `playtime`(공연시간) | + | 32 숙박 | `checkintime`/`checkouttime` | + +- [매뉴얼] 쉬는날 필드는 `restdate`, `restdateculture`, `restdateleports`, `restdateshopping`, `restdatefood` 다. 15·25·32 타입에는 없다. +- [매뉴얼] 형식은 정의되어 있지 않다. 예시 값은 "상시 개방"(L620)이고, 다른 필드 값에는 `
` 이 섞여 있다(L617). + - 정형 시간표 필드가 없으므로 '파서 + 미확인' 판단은 그대로 유효하다. +- [추론] 부분일치를 버리고 타입별 고정 필드맵으로 바꾼다 (3장 표). + +### 1-6. detailIntro2 / detailPetTour2 호출 방식 + +- [매뉴얼 L593–594] `detailIntro2` 는 `contentId` 와 `contentTypeId` 가 **둘 다 필수**다. 지금 쓰는 방식이 맞다. +- [매뉴얼 L988] `detailPetTour2`: + "`contentid | 0 | 콘텐츠ID(미기입 시 반려동물 전부 출력)`" + - 장소마다 1건씩 부른 방식(10-29에 90건)을 페이지 단위 전체 수집으로 바꿀 수 있다. +- [매뉴얼] 요청 표에는 소문자 `contentid`, 예시 URL(L1013)에는 `contentId` 로 적혀 있다. + - [관측] `contentId` 로 불러 값을 받았다. +- [매뉴얼 L1008] 응답 표에 `petTursmInfo`(반려동물 관광정보)가 있다. + - [관측] 실제 응답 키에는 없었다. 있어도 되고 없어도 되는 값으로 다룬다. +- [매뉴얼] 응답에는 제목·좌표·타입이 없고 `contentid` 와 반려동물 항목만 있다. + - [추론] 우리 장소 테이블과 `contentid` 로 조인한다. + +### 1-7. 야간 동기화 · 운영 쿼터 + +- [매뉴얼 L146] "데이터 갱신 주기 | 일 1회". 야간 1회 동기화가 주기에 맞다. 같은 날 여러 번 돌릴 이유가 없다. +- [매뉴얼-신청 L46–57] 운영계정 조건: + - "한국관광공사 담당자의 승인이 필요하며, 약 1~3일 정도 소요" + - 확인 사항: "활용 어플 URL 확인 : App/Web/기타(키오스크등)", "개발계정의 TourAPI 호출 이력" + - "**정상적인 서비스가 되고 있음이 확인 된 후** … 승인" + - "[활용사례]에 등록되어 홍보" + - "승인일로부터 24개월 … [연장신청]" + - 운영계정 한도 **수치는 어느 문서에도 없다** +- [추론] 출시 전에는 운영계정이 나오지 않을 수 있다. + - 초기 적재는 개발계정 한도(오퍼레이션별 1,000/일) 안에서 며칠에 걸쳐 끝나도록 설계한다. + - 심사용으로 스토어 링크나 웹 랜딩 URL 을 준비한다. + +--- + +## 2. 새로 쓸 만한 기능 + +### 2-1. areaBasedSyncList2 — 변경분 동기화 (L906–971) + +- [매뉴얼] 요청 항목: + - `showflag`: "콘텐츠표출여부(1=표출, 0=비표출)" + - `modifiedtime`: "콘텐츠변경일자 (수정년도,수정년월,수정년월일 입력)" — YYYY, YYYYMM, YYYYMMDD 모두 받는다 + - `arrange`, `contentTypeId`, `lDong*`, `lclsSystm*` + - `oldContentid`: "이전 콘텐츠ID(DB저장 동기화시 이전 KEY 값으로 조회 용도)" +- [매뉴얼] 응답은 목록 공통 필드에 `showflag`(필수)가 더해진다. +- [매뉴얼 L969–971] `showflag=0` 예시에서는 `firstimage`·`cpyrhtDivCd`·`lDongRegnCd`·`lclsSystm*` 가 **모두 빈 값**이다. +- [추론] 야간 잡 설계: + 1. `modifiedtime=어제(YYYYMMDD)` 로 호출한다. + 2. `showflag=1` 은 upsert, `showflag=0` 은 소프트 삭제(AI 후보에서 제외)한다. + 3. 놓친 날은 `YYYYMM` 월 단위로 다시 훑는다. +- [추론] 비표출 레코드는 법정동이 비어 있으므로, `lDongRegnCd` 로 거른 조회에서는 **삭제가 빠질 수 있다**. 비표출 확인은 지역 필터 없이 한다. +- [추론] 아래는 매뉴얼에 없으니 첫 실행에서 검증한다: + - `showflag` 를 생략하면 1·0이 함께 오는가 + - `modifiedtime` 이 '그 날짜'인가 '그 이후'인가 (예시 문구로 보면 접두 일치에 가깝다) + - 응답에 이전 ID 필드가 있는가 (`oldContentid` 는 요청 항목에만 있다) +- [추론] 초기 적재도 반경 크롤링(`locationBasedList2`) 대신 `areaBasedList2`/`areaBasedSyncList2` 를 `lDongRegnCd`(+`lDongSignguCd`) 단위로 돌리는 편이 경계 누락이 없다. +- [매뉴얼] `modifiedtime`(YYYYMMDD) 필터는 다른 목록 오퍼레이션(지역·위치·행사·숙박)에도 있다. + +### 2-2. 반려동물 + +- `detailPetTour2` 전체 목록 수집 (1-6 참조). +- [매뉴얼] 소개정보의 동반 필드는 아래 넷이며, 값 목록은 정의되어 있지 않다(예시는 빈 값, L735). + + | 타입 | 필드 | + |---|---| + | 12 관광지 | `chkpet` | + | 14 문화시설 | `chkpetculture` | + | 28 레포츠 | `chkpetleports` | + | 38 쇼핑 | `chkpetshopping` | + + - **음식점(39)·숙박(32)·행사(15)·코스(25)에는 없다.** +- [매뉴얼] KorService2 에는 반려동물 전용 contentTypeId 나 목록 필터 파라미터가 없다(타입표 L164–173, 각 요청 표). + - 서비스 개요(L91)에 별도 서비스로 "반려동물 동반여행 서비스"가 있다. 별도 매뉴얼(반려동물동반여행 v4.1)이 다룰 범위다. +- [매뉴얼] `acmpyTypeCd` 는 이름에 Cd 가 붙어 있지만 값은 "일부구역 동반가능" 같은 문장이다(L1005). + +### 2-3. 축제 거르기 · 상세 + +- 서버 필터: `lclsSystm2=EV01`(축제), `eventEndDate` (1-4 참조). +- [매뉴얼] 소개정보(15) 필드: `festivalgrade`, `playtime`, `eventplace`, `placeinfo`, `program`, `subevent`, `agelimit`, `bookingplace`, `usetimefestival`(요금), `discountinfofestival`, `spendtimefestival`, `eventhomepage`, `sponsor1`/`sponsor1tel`/`sponsor2`/`sponsor2tel`. +- [매뉴얼 L843–849] 반복정보(15) 유형: 행사소개 · 약력 · 줄거리 · 출연 · 제작 · 행사내용 · 참가안내. +- [추론] `locationBasedList2(contentTypeId=15)` 응답에는 기간 필드가 없어 상세를 따로 불러야 한다. 지금처럼 `searchFestival2` 로 전국을 받고 거리를 로컬에서 계산하는 쪽이 쿼터에 유리하다. + +### 2-4. 접근성 · 편의 · 주차 · 예약 + +- [매뉴얼] 접근성: KorService2 에 휠체어 같은 **무장애 전용 필드는 없다**. + - 있는 것은 `chkbabycarriage*`(유모차대여정보; 12/14/28/38)와, 반복정보 '장애인편의시설'(관광지 유형2·쇼핑 유형2, L829·L861; 자유문장)뿐이다. + - 별도 서비스 "국문 무장애 여행 정보 서비스"(L80)가 있지만 이 매뉴얼의 범위 밖이다. +- [매뉴얼] 기타 편의 필드: `chkcreditcard*`, `kidsfacility`(39 어린이놀이방), `smoking`(39), `packing`(39 포장), `seat`(39 좌석수), `restroom`(38 화장실설명). +- [매뉴얼] 주차 필드: + + | 타입 | 필드 | + |---|---| + | 12 | `parking` | + | 14 | `parkingculture`, `parkingfee` | + | 28 | `parkingleports`, `parkingfeeleports` | + | 32 | `parkinglodging` | + | 38 | `parkingshopping` | + | 39 | `parkingfood` | + + - 예시 값: "가능
요금 (최초 2시간 무료 / 이후 30분 당 400원씩 추가 요금 발생)" +- [매뉴얼] 예약 필드: `reservation`(28), `reservationlodging`+`reservationurl`(32), `reservationfood`(39), `bookingplace`(15). 반복정보 유형에도 예약안내·내국인예약안내·외국인예약안내·대관안내가 있다. + +### 2-5. 코스 시간 배분 힌트 + +- [매뉴얼] 관련 필드: `spendtime`(14 관람소요시간), `spendtimefestival`(15), `taketime`(25 코스총소요시간), `distance`(25 코스총거리). +- [추론] AI 가 장소별 체류시간을 정할 때 힌트로 쓸 수 있다. 값은 자유문장이다. + +### 2-6. detailInfo2 — 반복정보 (L737–864) + +- [매뉴얼] 12/14/15/28/38 타입: + - 반복 필드는 `fldgubun`, `infoname`, `infotext`, `serialnum` 이다 + - 예시: "입 장 료 / 무료", "화장실 / 있음" (L822) + - 유형표는 L824–864 에 있다 +- [매뉴얼] 25 여행코스: `subcontentid`(하위콘텐츠ID), `subname`, `subdetailoverview`, `subdetailimg`, `subdetailalt`, `subnum`. + - [추론] 관광공사 추천코스가 실제 contentid 순서로 나온다. AI 코스 평가의 기준 데이터나 템플릿으로 쓸 만하다. 분류는 C01xx(가족·나홀로·힐링·도보·캠핑·맛코스)다. +- [매뉴얼] 32 숙박: `room*` 필드와 객실 사진 `roomimg1~5`, 사진별 `cpyrhtDivCd1~5`. + +### 2-7. detailImage2 (L866–904) + +- [매뉴얼] 요청: `contentId` 필수. `imageYN` 은 "Y=콘텐츠이미지조회 / N=”음식점”타입의 음식메뉴 이미지" 다. + - `subImageYN` 은 v4.3 에서 삭제됐다(L45). +- [매뉴얼] 응답: `originimgurl`(약 500*333), `smallimageurl`(약 160*100), `imgname`, `serialnum`, 장별 `cpyrhtDivCd`. +- [추론] 대표이미지 1장으로 부족할 때만, 상세 화면에 들어갈 때 늦게 부른다(쿼터 절약). + +### 2-8. detailCommon2 (L522–576) + +- [매뉴얼] 요청 항목은 `contentId` 하나다. v4.3 에서 관광타입 ID 와 '기본정보/대표이미지/지역코드/분류코드/주소/좌표/개요 조회 여부'가 모두 삭제됐다(L44). +- [매뉴얼] 응답: `overview`(개요), `homepage`(HTML `
` 태그가 통째로 들어 있음), `telname`, 주소·좌표·법정동·분류. + - 설명란에는 "길안내 정보, 연계 관광정보"가 적혀 있지만 응답 표에 해당 필드는 없다. +- [추론] AI 설명이나 임베딩에 쓸 `overview` 는 이 오퍼레이션에서만 나오며, 1건당 1호출이다. + +### 2-9. 코드 조회 + +- [매뉴얼] `ldongCode2` (L1017–1060): + - `lDongRegnCd` 를 비우면 시도 목록, 넣으면 시군구 목록이 온다 + - `lDongListYn=Y` 면 전체 목록(`lDongRegnCd/Nm`, `lDongSignguCd/Nm`)이 온다 +- [매뉴얼] `lclsSystmCode2` (L1062–1109): + - `lclsSystmListYn=Y` 면 대·중·소 전체가 온다(`lclsSystm1Cd/Nm` … `lclsSystm3Cd/Nm`) + - 예시의 totalCount 는 243 이다 +- [추론] 두 결과를 캐시 테이블로 두고 주기적으로 갱신한다. + +### 2-10. 기타 + +- [매뉴얼 L355, L42] `searchKeyword2`: `keyword` 가 필수이고 국문은 인코딩해야 한다. `contentTypeId` 파라미터는 v4.3 에서 삭제됐으니 분류체계로 거른다. +- [매뉴얼] `arrange=O/Q/R/S` 는 대표이미지가 있는 항목만 돌려준다. + +--- + +## 3. detailIntro2 필드표 (L604–728) + +모든 값은 문자열이며 형식 정의가 없다. 공통 필드는 `contentid`, `contenttypeid` 다. 괄호 안은 매뉴얼의 국문 항목명이다. + +| 타입 | 시간 | 쉬는날 | 요금 | 주차 | 반려동물 | 예약 | 기타 | +|---|---|---|---|---|---|---|---| +| 12 관광지 | `usetime`(이용시간), `useseason`(이용시기), `opendate`(개장일) | `restdate` | (반복정보: 입장료·관람료·시설이용료·주차요금) | `parking` | `chkpet` | (반복정보 예약안내) | `accomcount`(수용인원), `chkbabycarriage`(유모차대여), `chkcreditcard`, `expagerange`(체험가능연령), `expguide`(체험안내), `heritage1/2/3`(세계문화·자연·기록유산 유무, 예시 "0"), `infocenter`(문의및안내) | +| 14 문화시설 | `usetimeculture` | `restdateculture` | `usefee`(이용요금), `discountinfo` | `parkingculture`, `parkingfee` | `chkpetculture` | – | `accomcountculture`, `chkbabycarriageculture`, `chkcreditcardculture`, `infocenterculture`, `scale`(규모), `spendtime`(관람소요시간) | +| 15 행사/공연/축제 | `playtime`(공연시간), `eventstartdate`, `eventenddate` | – | **`usetimefestival`(이용요금)**, `discountinfofestival` | – | – | `bookingplace`(예매처) | `agelimit`, `eventhomepage`, `eventplace`, `festivalgrade`(축제등급), `placeinfo`, `program`, `spendtimefestival`, `sponsor1`, `sponsor1tel`, `sponsor2`, `sponsor2tel`, `subevent` | +| 25 여행코스 | `taketime`(코스총소요시간) | – | – | – | – | – | `distance`(코스총거리), `infocentertourcourse`, `schedule`(코스일정), `theme`(코스테마) | +| 28 레포츠 | `usetimeleports`, `openperiod`(개장기간) | `restdateleports` | `usefeeleports`(입장료) | `parkingleports`, `parkingfeeleports` | `chkpetleports` | `reservation` | `accomcountleports`, `chkbabycarriageleports`, `chkcreditcardleports`, `expagerangeleports`, `infocenterleports`, `scaleleports` | +| 32 숙박 | `checkintime`, `checkouttime` | – | (반복정보 객실 요금 `room*fee*`) | `parkinglodging` | –(detailPetTour2 만) | `reservationlodging`, `reservationurl` | `accomcountlodging`, `chkcooking`, `foodplace`, `infocenterlodging`, `pickup`, `roomcount`, `roomtype`, `scalelodging`, `subfacility`, `barbecue`, `beauty`, `beverage`, `bicycle`, `campfire`, `fitness`, `karaoke`, `publicbath`, `publicpc`, `sauna`, `seminar`, `sports`, `refundregulation` | +| 38 쇼핑 | `opentime`(영업시간), `opendateshopping`(개장일), `fairday`(장서는날) | `restdateshopping` | `saleitemcost`(판매품목별가격) | `parkingshopping` | `chkpetshopping` | – | `chkbabycarriageshopping`, `chkcreditcardshopping`¹, `culturecenter`, `infocentershopping`, `restroom`, `saleitem`, `scaleshopping`, `shopguide` | +| 39 음식점 | `opentimefood`(영업시간), `opendatefood`(개업일) | `restdatefood` | `discountinfofood` | `parkingfood` | –(detailPetTour2 만) | `reservationfood` | `chkcreditcardfood`, `firstmenu`(대표메뉴), `treatmenu`(취급메뉴), `infocenterfood`, `kidsfacility`, `packing`, `scalefood`, `seat`, `smoking`, `lcnsno`(인허가번호) | + +¹ 매뉴얼 표에는 줄바꿈 때문에 "chkcreditcard / shopping" 으로 적혀 있다. [관측] 실제 응답 키는 `chkcreditcardshopping` 이다. + +[관측] 10-29 응답에서 본 소개정보 키(12/14/38/39)는 모두 이 표 안에 있었다. + +--- + +## 4. 분류 코드 (신분류체계 · 연계 정의서) + +### 4-1. 데이트 코스용 코드 [매뉴얼] + +| 목적 | 코드 | 관광타입 | +|---|---|---| +| 카페 | `FD05` 카페/찻집 → `FD050100` 카페, `FD050200` 찻집, `FD050300` 기타음료점 (+ `FD030100` 제과) | 39 | +| 술집 | `FD040100` 바/펍, `FD040200` 생맥주전문점, `FD040400` 전통주/민속주점 | 39 | +| 갤러리·전시 시설 | `VE07` 전시시설 → `VE070100` 박물관, `VE070200` 기념관, `VE070300` 전시관, `VE070400` 컨벤션센터, `VE070500` 과학관, `VE070600` 미술관/화랑 | 14 | +| 전시(기간 행사) | `EV030100` 전시회, `EV030200` 박람회 | 15 | +| 도시공원 | `VE03` 도시공원 → `VE030100` 시민공원, `VE030200` 소공원, `VE030300` 어린이공원, `VE030400` 근린공원, `VE030500` 주제공원 | 12 | +| 자연공원 | `NA04` 자연공원 → `NA040100` 국립공원, `NA040200` 도립공원, `NA040300` 군립공원, `NA040400` 지질공원, `NA040500` 생태관광지, `NA040600` 자연휴양림, `NA040700` 수목원ㆍ정원 | 12 | +| 시장 | `SH06` 시장 → `SH060100` 비상설시장, `SH060200` 상설시장 (소개정보 `fairday` 와 함께 쓴다) | 38 | +| 축제 | `EV01` 축제 → `EV010100` 문화관광축제, `EV010200` 문화예술축제, `EV010300` 지역특산물축제, `EV010400` 전통역사축제, `EV010500` 생태자연축제, `EV010600` 기타축제 | 15 | +| 공연 | `EV02` 공연 → `EV020100` 전통공연, `EV020200` 연극, `EV020300` 뮤지컬, `EV020400` 오페라, `EV020500` 무용, `EV020600` 클래식음악회, `EV020700` 대중콘서트, `EV020800` 영화, `EV020900` 기타공연, `EV021000` 넌버벌 | 15 | +| 행사 | `EV03` 행사 → `EV030100` 전시회, `EV030200` 박람회, `EV030300` 스포츠경기, `EV030400` 기타행사 | 15 | +| 동네 산책 | `VE040100` 골목길, 문화거리 · `VE040200` 마을관광지 · `VE040300` 둘레길 | 12 | +| 공방·체험 | `SH050100` 공방/공예품점(38) · `EX02` 공예체험 `EX020100`~`EX020400`(12) | 38/12 | +| 실내 데이트 | `VE060100` 공연장 · `VE060200` 영화관 · `VE120100` 서점(14) · `VE020100` 테마파크 · `VE020300` 동물원 · `VE020400` 수족관 / 아쿠라리움(12) | 14/12 | + +### 4-2. 야경 + +- [매뉴얼] '야경' 코드는 **없다**. +- [추론] 후보 코드는 아래와 같다. 개요 텍스트 판단과 우리 쪽 큐레이션 태그를 함께 써야 한다. + - `VE010200` 타워 / 전망대 + - `VE010300` 다리 / 대교 + - `VE010400` 분수 + - `NA020800` 해안절경 + - `NA020900` 해변. 해수욕장 + - `VE020500` 천문대 + +### 4-3. 매핑 주의 [매뉴얼] + +- 대분류와 contenttypeid 는 1:1 이 아니다: + - `AC05` 캠핑(`AC050100`~`AC050400`) → **28 레포츠** + - `VE050200` 리조트 → **32 숙박** + - `VE10` 레저스포츠시설, `VE120200` 카지노 → **28** + - `VE06`·`VE07`·`VE08`·`VE09`·`VE120100`·`VE120300` → **14** + - 나머지 `VE`·`EX`·`HS`·`NA` → 12 +- 다국어(영문) 관광타입 코드: 76 관광지, 78 문화시설, **85 축제/공연/행사**, 75 레포츠, 80 숙박, 79 쇼핑, 82 음식점, 여행코스 없음. 우리 요약의 영문 코드와 맞고, 85가 더해진다. +- 정의서 머리글의 "lclsSystem1", "clsSystem3" 는 오기다. API 파라미터 이름은 `lclsSystm1~3` 이다. +- 번호가 비어 있다: `VE08` 아래에는 `VE080600` 연회장만 있고 `VE11` 은 없다. +- 여행코스 분류: `C01` 추천코스 → `C0112` 가족, `C0113` 나홀로, `C0114` 힐링, `C0115` 도보, `C0116` 캠핑, `C0117` 맛코스 (소분류 8자리 `C0112001` 등). + +--- + +## 5. 라이선스 · 쿼터 + +### 5-1. 이미지 + +- [매뉴얼] `cpyrhtDivCd`: "Type1: 제1유형(출처표시-권장) / Type3: 제3유형(제1유형 + 변경금지)" + - 목록의 `firstimage` 에 붙는다 + - `detailImage2` 에서는 장마다 붙는다 + - 객실 사진에는 `cpyrhtDivCd1~5` 로 붙는다 + - 이 항목은 v4.1(2023-01-30)에 "응답항목(이미지 공공누리유형)"으로 추가됐다 (L34) +- [추론, 공공누리 일반 규정 기준] 해석: + - 제1유형: 출처를 표시하면 상업적 이용과 변경이 허용된다 + - 제3유형: 출처표시 + 변경금지. 상업적 이용은 허용된다 + - 매뉴얼은 "권장"이라고 쓰지만 공공누리 제1유형에서 출처표시는 의무로 알려져 있다. 항상 출처를 표기한다 + - Type3 이미지는 보수적으로 다룬다: 크롭·필터·텍스트 합성 없이, 원비율을 유지한 축소 표시만 한다 + - `cpyrhtDivCd` 가 빈 값이면(L971) 쓰지 않는다 + +### 5-2. 텍스트 데이터 · DB 저장 · 상업 표시 + +- 텍스트 라이선스: 국문 매뉴얼과 신청 매뉴얼 **어디에도 명시가 없다**. + - [매뉴얼-신청 L36] "활용목적, 상세기능, 라이선스 표시 동의는 필수 사항" + - [추론] data.go.kr 상세 페이지의 '이용허락범위'를 확인해 기록해 둔다 +- DB 저장: 금지 문구가 없다. + - [매뉴얼 L930] 동기화 오퍼레이션 설명("DB저장 동기화시 이전 KEY 값으로 조회 용도")이 자체 DB 저장을 **전제**하고 있다 + - [추론] 'AI 는 우리 DB 에서만 고른다'는 구조와 충돌하지 않는다 +- 상업 표시: [매뉴얼 L144] "사용 제약사항 / (비고) | N/A". + - [매뉴얼-신청 L47–48] 운영계정을 승인하면 관광공사가 "[활용사례]에 등록되어 홍보" 한다 + +### 5-3. 쿼터 · 계정 + +- [매뉴얼] 개발계정: 오퍼레이션별 1,000건/일, 자동승인, 10~30분 뒤 사용 가능 (신청 L26–27). +- [매뉴얼] 운영계정: 담당자 승인 1~3일, 정상 서비스 확인 후 승인, 24개월 유효, 연장 신청 가능. **한도 수치는 없다.** +- [매뉴얼 L145] 문의처: 개방데이터운영팀 070-4287-3219 / tourapi@knto.or.kr. 운영 한도와 텍스트 라이선스는 여기로 문의한다. +- [추론] 적재량 계산: + 1. `areaBasedList2` 의 `totalCount` 로 서울(11)·부산(26)의 타입별 건수를 먼저 잰다. 목록 호출 몇 건이면 된다. + 2. 상세(`detailCommon2`·`detailIntro2`)는 1건당 1호출이다. 1만 건이면 개발계정으로 오퍼레이션당 10일이 걸린다. + 3. 우선순위는 12/14/15/38 을 먼저, 39(음식점)는 상가정보와 겹치므로 나중이다. + +--- + +## 6. 주의점 + +1. **포털 오류는 XML 로만 온다** [매뉴얼 L187]: "공공데이터포털에서 출력되는 오류 메세지는 XML로만 출력" + - `_type=json` 이어도 키 오류나 한도 초과는 `…` 로 온다 + - 지금 `call()` 은 "JSON 아님"으로만 기록하므로 `returnReasonCode` 를 파싱해야 한다 + - 처리할 코드 [매뉴얼 L175–213]: + + | 코드 | 이름 | 대응 | + |---|---|---| + | 22 | LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR | 한도 초과 → 잡을 멈추고 다음 날 이어서 | + | 30 | SERVICE_KEY_IS_NOT_REGISTERED_ERROR | 키 문제 | + | 31 | DEADLINE_HAS_EXPIRED_ERROR | 24개월 만료 | + | 20 | SERVICE_ACCESS_DENIED_ERROR | | + | 21 | TEMPORARILY_DISABLE_THE_SERVICEKEY_ERROR | | + | 12 | NO_OPENAPI_SERVICE_ERROR | 폐기된 오퍼레이션 호출 | + | 10 | INVALID_REQUEST_PARAMETER_ERROR | | + | 11 | NO_MANDATORY_REQUEST_PARAMETERS_ERROR | | + | 03 | NODATA_ERROR | | + | 05 | SERVICETIMEOUT_ERROR | 재시도 | +2. **성공 코드가 두 가지로 적혀 있다**: 예시는 `resultCode "0000"`(L242), 코드표는 `00 NORMAL_CODE`(L197)다. 둘 다 성공으로 처리한다(현재 코드가 이미 그렇게 한다). +3. **폐기된 요청 항목은 보내지 않는다** [매뉴얼 L39–50]: + - 지역코드·시군구코드 + - 서비스 대·중·소분류 + - 목록구분 + - 공통정보의 관광타입 ID 와 각종 '조회 여부' + - 이미지의 `subImageYN` + - 키워드 검색의 관광타입 + - 숙박의 베니키아·굿스테이·한옥 여부 + - [추론] 옛 블로그·SDK 코드의 `areaCode`, `cat1`, `defaultYN`, `overviewYN`, `listYN` 등이 여기에 해당한다(이전 버전 이름이라 기억에 의존한 것이다). 거부되는지는 모르니 보내지 않는 쪽이 안전하다. +4. **서비스키 인코딩** [매뉴얼 L109–113]: `URLEncoder.encode(myKey, "UTF-8")`, 모든 문자셋은 UTF-8 이다. + - [추론] 포털이 주는 'Encoding' 키를 다시 인코딩하면 이중 인코딩이 되어 30 오류가 난다. env 에는 'Decoding' 키를 두고 한 번만 인코딩한다. +5. **keyword 는 국문 인코딩이 필요하다** (L355). +6. **이미지 URL 이 http 다**: 매뉴얼 예시 대부분이 `http://tong.visitkorea.or.kr/…` 이고, 같은 호스트의 https 예시도 있다(L439). 서비스는 HTTP/HTTPS 를 모두 지원한다(L132). + - [추론] 저장할 때 https 로 바꾼다. iOS ATS 와 Android cleartext 차단을 피하기 위해서다. +7. **HTML 이 섞인 값**: `homepage` 는 `` 태그가 통째로 들어 있고(L550, L576), `parking` 등에는 `
` 이 있다(L617). + - [추론] 원문 컬럼과 표시용 정제 컬럼을 따로 둔다. +8. **필수 `MobileOS`/`MobileApp`** (L226–227, L115–116): 서비스별 "활용 통계를 산출하기 위한 항목"이다. + - [추론] 운영계정 심사 때 호출 이력을 보므로 `MobileApp=WanderGo` 를 계속 같은 값으로 쓴다. +9. **시각 형식**: 요청 `modifiedtime` 은 YYYYMMDD 이고, 응답 `modifiedtime`/`createdtime` 은 14자리(YYYYMMDDhhmmss)다. + - [추론] KST 로 가정하고 UTC ISO 로 변환한다(api-design 규칙). +10. **좌표**: `mapx`=경도, `mapy`=위도, WGS84, 문자열이다 (L255–256). `mlevel` 은 "Map Level" 이라는 설명뿐이다. +11. **이미지 해상도**: 원본은 약 500*333, 썸네일은 약 150*100(목록)/160*100(이미지 조회)이다. + - [추론] 풀폭 히어로 이미지로 쓰기엔 해상도가 낮다. +12. **초당 속도 제한은 매뉴얼에 없다.** 일 한도만 있다. +13. **매뉴얼 예시 오류** — 그대로 복사하지 않는다: + - 키워드 예시 `keyword시장` 에 '=' 이 빠져 있다 (L394) + - 행사 예시가 `lDongRegnCd=36110` 이다 (L417) + - 축제 예시끼리 분류가 맞지 않는다 (L451 vs L461) + - 상단 예시가 `areaCode2` 를 쓴다 (L108) + - 행사 예시 제목은 "2025년 05월"인데 파라미터는 2026 전체다 (L459) +14. **매뉴얼에만 있고 실제 응답에는 없을 수 있는 항목**: `petTursmInfo`. 반대로 매뉴얼에서는 삭제됐지만 실제 응답에는 남아 있는 키: `areacode`·`cat1~3`. +15. **비표출 레코드**는 법정동·분류·이미지가 비어 있다 (L971). 지역 필터를 건 동기화 조회로는 삭제를 놓친다. diff --git a/docs/research/pre-dev-check/accounts_checklist.md b/docs/research/pre-dev-check/accounts_checklist.md new file mode 100644 index 0000000..3c6ed06 --- /dev/null +++ b/docs/research/pre-dev-check/accounts_checklist.md @@ -0,0 +1,304 @@ +# WanderGo 착수 전 계정·승인 체크리스트 (2026-10-29 조사) + +> 범위: Flutter(iOS+Android) + NestJS, 한·영 출시. 소유자가 직접 신청한다는 전제로 **무엇을, 어떤 순서로, 어떤 서류로, 얼마나 걸리는지, 무엇이 무엇을 막는지**를 정리했다. +> 기존 조사(`WanderGo 기획 검토 자료조사/public_apis.md`, `legal_policy.md`)의 쿼터·위치정보법 기본 내용은 반복하지 않고 **절차**만 더했다. +> +> **출처 표기** +> - **[fetched]** 이번 세션에 원문 페이지를 직접 열어 확인 +> - **[snippet]** 검색 결과 요약으로만 확인(원문 미열람, 신뢰도 낮음) +> - **[3rd-party]** 블로그·커뮤니티·타사 도움말(경험담) +> - **[prior]** 기존 조사 노트에서 가져옴(그 노트의 표기 유지) +> - **[inference]** 근거를 조합한 추정 +> - **unconfirmed** 확인 못 함 — 추측하지 않음 +> +> **접근 제한**: developers.naver.com 은 WebFetch 와 브라우저 도구 양쪽에서 "safety restrictions" 로 차단되었다. 네이버 로그인 검수 항목은 전부 3rd-party 근거다. law.go.kr 대신 casenote.kr 미러를 썼다(기존 노트와 같은 방식). + +--- + +## Takeaway + +1. **가장 긴 리드타임은 Google Play 신규 개인 계정의 "12명 × 연속 14일 비공개 테스트 + 프로덕션 신청 심사(≤7일)"** 다. 최소 3–4주이므로 Android 빌드가 돌아가는 즉시 테스트를 시작해야 한다. 조직 계정으로 피하려면 D-U-N-S(최대 30일)가 필요하다. +2. **도메인 + 홈페이지 + 개인정보처리방침·위치기반서비스 이용약관 + 계정 삭제 웹 페이지가 숨은 선행 조건**이다. Google 브랜드 인증, 카카오 공유 웹 도메인, Apple Services ID(Android 의 Apple 로그인), 네이버 검수, 공공데이터 활용사례, 스토어 등록이 모두 여기에 걸린다. 코드보다 먼저 만든다. +3. **사업자등록은 대부분의 API 에 필요 없다.** 공공데이터포털, 카카오(개인 개발자 비즈 앱으로 이메일 선택 동의 가능), 네이버, Apple(개인), Google Play(개인)는 모두 개인으로 된다. **실질적으로 필요한 곳은 위치기반서비스사업 신고**다(신고서에 상호·사무소, 사업자등록증 확인). Apple 의 "조직" 등록은 **법인만** 된다(개인사업자는 개인으로 등록하고, 판매자명에 실명이 나온다). +4. **한국 개발자 특유 요건 2가지**: (a) 2026-01-01부터 한국 기반 개발자는 Sign in with Apple **Services ID 등록·수정 시 서버-서버 알림 엔드포인트 필수**(Android/웹에서 Apple 로그인을 쓰면 해당). (b) 위치기반서비스사업 신고는 **수리가 필요한 신고**다(제9조⑤). 소상공인·1인 창조기업이 아니면 출시 전에 수리되어야 하고, 해당하면 개시 후 1개월 안에 신고하면 된다. +5. **출시 직전 병목**은 네이버 로그인 검수(검수 전에는 등록한 테스트 ID 만 로그인 가능 → App Store 심사원이 로그인 못 함)와 공공데이터 운영계정(활용사례에 URL·개시일·화면 필요)이다. 카카오맵 무료 쿼터는 **개발자 계정에서 처음 활성화한 앱에만** 주므로, 첫날 **실서비스용 앱**에서 카카오맵을 켠다. + +--- + +## 체크리스트 (의존 순서) + +단계: **A = 코드 작성 전**, **B = 베타(TestFlight·Play 비공개 테스트) 전**, **C = 스토어 출시 전** + +| # | 단계 | 어디서 | 사업자 필요? | 준비물 | 리드타임 | 막는 것 | 시점 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 사업 형태 결정 (개인 / 개인사업자 / 법인) | — | — | 위치 신고 특례(소상공인·1인 창조기업) 해당 여부 확인 | 결정만 | Apple 판매자명, Play 계정 유형, 위치 신고 경로 | A | +| 1 | 사업자등록 (개인사업자 기준) | 홈택스 | — | 신분증, 사업장 주소, 업종 | 원칙 2일, 확인 필요 시 연장 [3rd-party] | #13 위치 신고(소상공인확인서 포함) | A (늦어도 B) | +| 2 | 도메인 + 홈페이지 + 개인정보처리방침·위치기반서비스 이용약관 초안 + 계정 삭제 웹 페이지 | 도메인 등록업체, 정적 호스팅 | 아니오 | 앱 설명, 처리방침(위치·국외이전 포함) | 1–3일(법률 검토 별도) | #9 Services ID, #12 카카오 공유 도메인, #14 Google 브랜드 인증, #15 네이버 검수, #16 활용사례, #17 스토어 | A | +| 3 | 공공데이터포털 가입 + 개발계정 5건: 국문 KorService2, 영문 EngService2, 반려동물, 단기예보, 중기예보 | data.go.kr | 아니오 [3rd-party] | 활용 목적 | 자동승인 = 즉시 [fetched] | 백엔드 수집기 개발 | A | +| 4 | 카카오 앱 생성 → **카카오맵 사용 설정 ON(이 앱을 가장 먼저)** → 플랫폼 키(패키지명·키 해시·번들 ID) → 카카오 로그인 ON·리다이렉트 URI → 동의항목(닉네임/프로필). 이메일이 필요하면 **개인 개발자 비즈 앱 전환** | developers.kakao.com | 아니오(개인 개발자 비즈 앱 = 본인인증 + 앱 아이콘) | 번들 ID/패키지명 확정, 디버그 키 해시, 앱 아이콘 | 당일 | 로그인·장소검색·공유 개발 | A | +| 5 | 네이버 개발자센터 앱 등록(개발 중, 테스트 ID 등록) / NCP 가입 + **결제수단 등록** + Maps Application | developers.naver.com / console.ncloud.com | 아니오 [3rd-party] / NCP 개인 회원은 신용카드만 [snippet] | 패키지명·번들 ID, 콜백 | 당일 | 네이버 로그인·지도 개발 | A | +| 6 | Apple Developer Program | developer.apple.com | 개인 = 아니오 / 조직 = **법인 + D-U-N-S** | 2단계 인증 Apple 계정, 실명 | 개인: unconfirmed(보통 1–2일) / 조직: D-U-N-S 최대 5영업일 [snippet] + Apple 검토 | App ID, Sign in with Apple capability, TestFlight | A | +| 7 | Google Play Console(개인, 25달러) + 앱 생성 | play.google.com/console | 개인 = 아니오 / 조직 = D-U-N-S(최대 30일) | 신원(결제 프로필 실명·주소), 이메일·전화 OTP 인증 | 신원 인증 수일(unconfirmed) | #11 비공개 테스트 시작, Play 앱 서명 키 SHA-1 | A | +| 8 | Google Cloud OAuth: 동의 화면(Testing) + Android·iOS·Web 클라이언트 ID | console.cloud.google.com | 아니오 | 패키지명 + SHA-1, 번들 ID | 당일 | Google 로그인 개발 | A | +| 9 | Sign in with Apple 키 + **Services ID(+ 서버-서버 알림 URL, 한국 필수)** | Apple Certificates, Identifiers & Profiles | 아니오 | 배포된 HTTPS 백엔드 엔드포인트, 도메인, 리턴 URL | 당일(엔드포인트 배포가 선행) | Android 의 Apple 로그인, 탈퇴 시 토큰 폐기 | B | +| 10 | 릴리스 서명 반영: 업로드 키 + Play 앱 서명 키의 **키 해시(카카오)·SHA-1(Google)** 등록 | 카카오 / GCP | 아니오 | Play Console 앱 서명 키 정보 | 당일 | 베타에서 로그인 실패 방지 | B | +| 11 | **Play 비공개 테스트 12명 이상 × 연속 14일 → "프로덕션 신청"** | Play Console | 아니오 | 테스터 12명 이상 확보, 동작하는 빌드 | 14일 + 심사 보통 7일 이내 [fetched] | Android 출시 | B 시작 → C | +| 12 | 카카오톡 공유: [앱] > [제품 링크 관리] > 웹 도메인 등록, 템플릿 | developers.kakao.com | 아니오 | #2 도메인 | 당일 | 공유 링크 동작 | B | +| 13 | **위치기반서비스사업 신고** (방송미디어통신위원회) | 과기정통부 전자민원센터 emsit.go.kr (간편인증) | **사실상 예**(사업자등록증 확인) [inference] | 신고서, 사업계획서, 위치정보 보호조치 증빙, 설비 위치 서류 / 특례: 소상공인확인서(또는 1인 창조기업 증빙) + 사업내용 설명자료 | 서류 1–2주 [prior] + 소상공인확인서 약 1주 + 수리 최대 약 2주 [3rd-party]; 수수료 없음 [3rd-party] | 일반 신고: **출시(사업 개시)** / 특례: 개시 후 1개월 안에 신고 | B에 제출 → C | +| 14 | Google OAuth **게시(Published) + 브랜드 인증** | GCP Google Auth Platform | 아니오 | 인증된 도메인(Search Console), 홈페이지, 처리방침 | 자동 수분 / 수동 2–3영업일; 결과 7일 안에 게시 [fetched] | 테스트 사용자 외 로그인, 동의 화면 앱 이름 표시 | B 말–C | +| 15 | **네이버 로그인 검수** | 네이버 개발자센터 | 아니오 [3rd-party] | 로그인~가입 완료 전 화면 캡처, 버튼 가이드 준수, 제공 정보 = 실제 가입 항목 | 3–4영업일 [3rd-party]; 반려 대비 1–2주 확보 | 일반 사용자의 네이버 로그인, App Store 심사 | C | +| 16 | **공공데이터 운영계정**: 활용사례 등록 → 운영계정 신청(희망 트래픽 기재) | data.go.kr 마이페이지 | 아니오 | 활용사례명, 웹 URL, 서비스 개시일, 대표 이미지 [fetched gov.kr] | KTO 심의 보통 2–3일 / 기상청 자동승인 [fetched] | 출시 후 트래픽(개발계정 1,000/일) | C | +| 17 | 스토어 제출: App Store(4.8 Apple 로그인, 5.1.1(v) 앱 내 탈퇴 + 토큰 폐기), Play(데이터 보안 + 삭제 URL) | App Store Connect / Play Console | 아니오 | 처리방침 URL, 삭제 웹 링크, 심사용 계정 | App Review: unconfirmed | 출시 | C | + +**막힘 관계 요약**: #2 → #9·#12·#14·#15·#16·#17 / #1 → #13 / #7 → #10·#11 → Android 출시 / #6 → #9 → Android Apple 로그인 / #15 → iOS 심사(네이버 버튼이 있으면) / #13 수리(일반) → 출시 + +--- + +## 서비스별 근거 + +### 1. 공공데이터포털 — TourAPI · 기상청 + +**개발계정 vs 운영계정** +- 신청 순서는 "1단계 개발계정 신청 → 2단계 운영계정 신청"이다. 단계 안내는 텍스트가 아니라 이미지로만 있다 — [data.go.kr 활용방법 [fetched]](https://www.data.go.kr/ugs/selectPublicDataUseGuideMetView.do) +- 승인 방식 [fetched, 같은 페이지] + - 자동승인: "별도 확인 없이 승인신청과 동시에 자동으로 승인되는 경우" + - 심의승인: 제공기관 담당자 승인. "신청 후 보통 2~3일 정도 소요가 되며", 결과는 가입 이메일로 통보 +- 데이터셋별 심의유형·트래픽 [fetched] + + | 데이터셋 | 심의유형 | 트래픽 | + | --- | --- | --- | + | 국문 관광정보 GW 15101578 | 개발 자동 / **운영 심의** [prior] | 개발 1,000 | + | 영문 관광정보 GW 15101753 | "개발단계 : 자동승인 / 운영단계 : 심의승인" | 개발 1,000 | + | 반려동물 동반여행 15135102 | 개발 자동 / **운영 심의** [prior] | 개발 1,000 | + | 기상청 단기예보 15084084 | "개발단계 : 자동승인 / 운영단계 : 자동승인" | 개발 10,000 | + | 기상청 중기예보 15059468 | "개발단계 : 자동승인 / 운영단계 : 자동승인" | 개발 10,000 | + + 운영계정 트래픽은 모두 "활용사례 등록시 신청하면 트래픽 증가 가능"이라고만 적혀 있고 **숫자는 공개되지 않았다** — [15101753](https://www.data.go.kr/data/15101753/openapi.do), [15084084](https://www.data.go.kr/data/15084084/openapi.do), [15059468](https://www.data.go.kr/data/15059468/openapi.do) +- **활용사례 등록 항목** [fetched]: 활용 공공데이터, 활용 OpenAPI, 활용 사례명, 분류 체계, 개발유형, **웹 URL**, **서비스 개시일**, 개발자 소재지, 설명, 활용 목적, **대표 이미지**·추가 이미지. 접수기관 한국지능정보사회진흥원(1566-0025). 페이지 최종 수정 2024-10-28 — [정부24 공공데이터 활용사례 등록](https://www.gov.kr/portal/service/serviceInfo/PTR000050580) +- 운영계정 신청 위치: "[마이페이지 > API 활용현황]에서 … [운영계정신청] 버튼", 신청 시 원하는 트래픽을 적는다 — [snippet; 다른 기관 포털 안내문에서 나온 문구로, data.go.kr 원문은 이미지라 미확인] +- 사업자등록: "인증키 발급은 무료이며, 개인과 기업 모두 동일한 절차로 신청할 수 있습니다" — [studygov.kr 2026-02-06 [3rd-party]](https://studygov.kr/blog/13071-tourism-info-service/) +- 개발계정 승인 "10분 ~ 3일" — [wikidocs [3rd-party, snippet]](https://wikidocs.net/292158) + +**추정·주의 [inference]** +- 활용사례에 URL·개시일·화면이 필요하므로 운영계정은 **베타 화면이 나온 뒤**에야 신청할 수 있다. 랜딩 페이지 URL과 베타 스크린샷으로 신청할 수 있는지는 unconfirmed. +- 국문·영문·반려동물은 **각각 별도 활용신청**이므로 운영계정 신청도 3건이다. +- 개발 단계에서는 전량 동기화를 하지 말고, 지역·유형을 좁힌 증분 수집으로 1,000/일 안에서 개발한다 [prior 노트의 권고와 같음]. + +**Unconfirmed**: 운영계정 승인 후 실제 트래픽 수치, 인증키 유효기간(흔히 "2년"이라 하지만 근거 못 찾음)과 연장 절차. + +### 2. Kakao Developers — 로그인 · 공유 · 로컬(카카오맵) + +**2025-12-03 콘솔 개편** [fetched] +- 출처: [앱 키 마이그레이션](https://developers.kakao.com/docs/ko/getting-started/app-key-migration) +- 플랫폼 설정이 앱 키별로 바뀌었다. +- 리다이렉트 URI·클라이언트 시크릿은 [앱] > [플랫폼 키] > [REST API 키]로 옮겨졌다. +- 웹 도메인은 두 곳으로 나뉜다. + - API 호출 검증: [JavaScript 키] > JavaScript SDK 도메인 + - 카카오톡 메시지·공유 링크: **[앱] > [제품 링크 관리] > 웹 도메인** +- 키는 종류별로 최대 5개까지 만들 수 있다. +- 블로그·튜토리얼의 메뉴 경로는 대부분 개편 전 기준이다. +- 2026-03-19부터 REST API 테스트 도구에서 어드민 키 인증이 빠졌다 [snippet]. + +**비즈 앱** [fetched] +- 출처: [앱 설정](https://developers.kakao.com/docs/ko/app-setting/app) +- 비즈 앱에서만 되는 것: 이메일을 **필수 동의**로 설정, 테스트 앱 생성(원본 앱당 최대 5개), 와일드카드 서브도메인, 비즈니스 인증 동의항목. +- 공통 요건: 앱 아이콘. 비즈니스 정보는 오너만 등록할 수 있다. +- 국내 사업자는 사업자등록번호를 쓴다. +- **개인 개발자 비즈 앱**(사업자번호 없음) + - 경로: [앱] > [일반] > [비즈니스 정보] > [개인 개발자 비즈 앱] + - 조건: 오너가 **본인인증** 완료 + 카카오비즈니스 통합 서비스 약관 동의 + - 제한: 비즈니스 채널을 연결할 수 없다. +- 약관상 정의는 2024-12-01부터 "사업자 정보가 등록되거나 개인 인증이 완료된 애플리케이션"이다 — [DevTalk 공지 2024-10-22 [fetched]](https://devtalk.kakao.com/t/topic/140256) +- 같은 공지에서 쿼터 합산 기준이 개발자 계정에서 **앱 단위**로 바뀌었다. + +**동의항목 권한** [fetched] +- 출처: [카카오 로그인 활용](https://developers.kakao.com/docs/ko/kakaologin/utilize) +- 닉네임·프로필 사진: 모든 앱에서 기본 제공 +- 카카오계정(이메일): **비즈 앱·테스트 앱**에서만 +- 이름·성별·연령대·생일·전화번호 등: "추가 기능 신청으로 권한을 받아야" 한다. +- 검수 기간은 3–5영업일이다. 필수를 선택으로 낮추는 변경은 추가 검수가 없다 — [사전 설정 [fetched]](https://developers.kakao.com/docs/ko/kakaologin/prerequisite) +- 추가 기능 신청에는 개인정보처리방침 URL이 필요하다. 처리방침에 수집 목적(회원가입), 수집 항목, 필수·선택 여부를 적어야 하고, 신청 내용과 일치해야 한다 — [같은 페이지 [fetched]] +- "개인개발자 비즈앱도 이메일 선택동의 설정 하실 수 있습니다" — [DevTalk 2026-09-28 [3rd-party, 응답자 소속 미표시]](https://devtalk.kakao.com/t/id-1589651/151858). 개인 개발자 비즈 앱에서 **이메일 필수 동의**가 되는지는 unconfirmed. +- 비즈니스 정보 심사 반려 사유 [fetched] + - 출처: [DevTalk FAQ 2023-10-12](https://devtalk.kakao.com/t/faq/132272) + - 기재 정보와 사업자등록증 사본이 다른 경우 + - 개인정보(주민번호 등)를 가리지 않은 경우 + - 소개 페이지도 없고 앱마켓 등록 앱도 없는 경우: 권한 신청이 제한된다. + - 웹사이트가 없으면 앱마켓 정보로 대신 낼 수 있다. + +**카카오 로그인 설정** [fetched, 사전 설정 페이지] +- 상태가 OFF면 KOE004, 리다이렉트 URI가 등록값과 "완전히 동일"하지 않으면 KOE006. +- REST API 키에 클라이언트 시크릿이 켜져 있으면 토큰 요청에 시크릿을 넣어야 한다. +- **개인정보 국외 이전**(이전 국가·업체·연락처)을 입력하면 로그인 화면에 필수 동의가 추가된다. + +**연결 끊기(탈퇴 시)** [fetched] +- 출처: [REST API](https://developers.kakao.com/docs/ko/kakaologin/rest-api) +- `POST https://kapi.kakao.com/v1/user/unlink` +- 액세스 토큰 대신 **어드민 키 + `target_id_type=user_id` + `target_id`** 로 서버에서 호출할 수 있다. 사용자 토큰을 서버에 저장하지 않아도 된다. +- 사용자가 카카오 쪽에서 연결을 끊으면 연결 해제 웹훅이 온다. 서비스는 탈퇴·파기 처리 후 응답해야 한다 [fetched, utilize 페이지]. + +**플랫폼 등록** [fetched, 앱 설정] +- Android 패키지명 + **키 해시**(개발 환경별 등록), iOS 번들 ID +- 불일치 시 `invalid android_key_hash or ios_bundle_id or web_site_url` +- Flutter SDK 최소 요건: Flutter 3.38.0 / Dart 3.9.0 / Android API 21 / iOS 13 — [Flutter 시작하기 [fetched]](https://developers.kakao.com/docs/ko/flutter/getting-started) +- URL 스킴 `kakao{NATIVE_APP_KEY}`, LSApplicationQueriesSchemes에 `kakaokompassauth`, `kakaolink` 추가 — [같은 페이지] +- [inference] 릴리스 업로드 키와 Play 앱 서명 키의 키 해시도 추가해야 Play 배포본에서 로그인이 된다(일반 지식, 이번 세션 원문 미확인). + +**카카오톡 공유** [fetched] +- 출처: [공유 개요](https://developers.kakao.com/docs/ko/kakaotalk-share/common) +- SDK 전용이다(REST 미지원). Flutter SDK 2.1.0 이상. +- 비즈 앱·권한은 공유 대상 화면 커스터마이즈(친구만 보기 등)에만 필요하다. 기본 공유는 불필요. +- 링크 도메인은 [제품 링크 관리] > 웹 도메인에 등록해야 한다. 미등록 도메인은 오류가 나거나 대표 도메인으로 이동한다 [snippet, DevTalk]. +- 쿼터·가격은 [prior](commercial_map_apis.md)에 있다: 30,000/일, 초과분 프로모션 10원(2027-12-31까지). + +**로컬 API(장소 검색)** [fetched] +- 출처: [카카오맵 이해하기](https://developers.kakao.com/docs/ko/kakaomap/common) +- "[카카오맵] > [사용 설정]의 [상태]를 [ON]"으로 해야 한다. 2026-07-21부터 심사 없이 바로 활성화된다. +- "카카오맵 API는 개발자 계정 기준 첫 번째로 활성화한 앱에만 무료 쿼터를 제공합니다." 두 번째 앱이나 무료 쿼터 초과분은 "비즈월렛 연결 후 유료 API 사용 설정"이 필요하다. +- 무료량을 다 쓰면 429가 난다. +- 비즈월렛에는 **자동결제 카드**가 필수다. 후불 세금계산서 정산은 없다 — [DevTalk 2026-07-13 [fetched]](https://devtalk.kakao.com/t/topic/150648) +- 사업자 없이 비즈월렛을 만들 수 있는지는 unconfirmed. + +### 3. Naver — 네이버 로그인 · NCP Maps + +**네이버 로그인 검수** (전부 3rd-party — 공식 문서 열람 불가) +- 검수 전 상태: "서비스가 '개발중' 상태이기 때문에 등록한 테스트 아이디를 통해 로그인 테스트"를 한다 — [snippet, 쇼핑몰 플랫폼 도움말] +- 테스터 ID는 최대 20개이고, "정상 배포 후 검수 요청" 순서라는 개인 블로그가 있다 — [hajun2ya.tistory.com [snippet]](https://hajun2ya.tistory.com/m/65) +- 제출물: 회원가입 화면 + 네이버 로그인 버튼부터 가입 완료까지 노출되는 **모든 화면 캡처** — [channel.io 도움말 [snippet]](https://docs.channel.io/clickn/ko/articles/네이버-아이디로-로그인-261c8aeb) +- 대표적인 반려 사유 + - "선택한 제공 정보와 내 사이트에서 받는 실제 가입 정보가 다른 경우" [snippet] + - 버튼 디자인 가이드 미준수("버튼 검수가 상당히 꼼꼼") [snippet] + - 앱 이름이 의미를 알 수 없거나 서비스와 관련 없는 경우 [snippet] + - 쓰지 않는 정보를 요청하거나, 처리방침에 없는 정보를 처리하는 경우 — 2026-10-03 GitHub PR이 검수 기준으로 인용 [3rd-party](https://github.com/kosairesearch/kosairesearch.github.io/pull/335). 같은 PR은 "네이버 로그인 이용약관(2026-09-07 개정) 1.2 ⑨ 버튼 가이드 준수"와 버튼 규격(녹색 #03A94D, 로고 16px)을 언급한다. **공식 확인 전에는 규격 숫자를 믿지 말 것.** +- 처리 기간: "영업일 기준으로 3~4일" [snippet]. 검수 결과는 계정 이메일로 온다. +- [inference] 검수 전에는 일반 사용자와 **App Store 심사원**이 네이버로 로그인할 수 없다. 출시 빌드에 네이버 버튼을 넣으려면 검수가 먼저 끝나야 한다. 반대로 검수가 "서비스 배포 후"를 요구하면 순환이 생긴다. 해결책은 두 가지다. + - 베타(TestFlight·Play 비공개) 화면으로 검수 요청이 받아지는지 네이버에 문의한다. + - 1.0에서는 원격 플래그로 네이버 버튼을 숨기고, 검수 통과 후 켠다. +- [inference] 탈퇴 시 네이버 연동 해제(토큰 삭제 API)에는 사용자 액세스 토큰이 필요하다. 카카오처럼 어드민 키 방식이 있는지는 unconfirmed. + +**NCP(Maps)** +- "결제 수단을 등록하지 않은 계정인 경우, 콘솔에 접근할 수 없습니다." 개인·사업자 회원 모두 해당 — [NCP Organization 문제 해결, 2026-09-17 [fetched]](https://guide.ncloud-docs.com/docs/organization-troubleshoot) +- 결제수단: 국내 사업자는 신용카드·계좌이체, 국내 개인은 신용카드만 — [네이버웍스 도움말 [snippet]](https://help.worksmobile.com/ko/getting-started-guides/register-payment/) +- Maps 이용 절차 [fetched]: 콘솔에서 Application 등록 → Client ID/Secret 발급. 심사 단계는 언급이 없다. "일정 사용량 초과 시 과금되므로 이용 한도를 설정하거나 한도 초과시 통보 기능"을 쓰라고 권한다 — [NCP Maps 이용 절차](https://guide.ncloud-docs.com/docs/maps-procedure) +- 무료 이용량(특히 Mobile Dynamic Map)은 여전히 unconfirmed [prior]. + +### 4. Apple — 4.8 · 5.1.1(v) · 토큰 폐기 · 한국 Services ID 요건 · 프로그램 등록 + +**Guideline 4.8 원문** [fetched](https://developer.apple.com/app-store/review/guidelines/) +- 대상: 서드파티·소셜 로그인으로 "set up or authenticate the user's primary account"하는 앱은 "must also offer as an equivalent option another login service" 해야 한다. +- 그 대체 로그인이 갖춰야 할 세 가지: + - "limits data collection to the user's name and email address" + - "allows users to keep their email address private" + - "does not collect interactions with your app for advertising purposes without consent" +- 예외(자체 계정만 사용, 대체 마켓, 교육·기업, 정부 전자ID, 특정 서드파티 클라이언트)는 WanderGo에 해당하지 않는다 [inference]. +- 카카오·네이버·Google이 이메일 비공개를 제공하지 않으므로, 실무상 Sign in with Apple이다 [prior·inference]. + +**5.1.1(v) 원문** [fetched, 같은 페이지] +- "If your app supports account creation, you must also offer account deletion within the app." +- "The app must also include a mechanism to revoke social network credentials and disable data access between the app and social network from within the app." +- 같은 조항에 "An app may not store credentials or tokens to social networks off of the device"라는 문장이 있다. 서버에 소셜 토큰을 저장하는 설계(네이버 연동 해제용 토큰 등)와 부딪힐 수 있다. 카카오는 어드민 키 방식으로 피할 수 있다 → Open question. + +**계정 삭제 페이지** [fetched](https://developer.apple.com/support/offering-account-deletion-in-your-app/) +- 삭제 대상: 계정 기록 + 법적 보관 의무가 없는 관련 데이터 전부. 다른 사람과 공유한 UGC도 포함된다. 공동 편집 코스의 처리 정책이 필요하다(메모리의 미결 사항과 연결). +- 일시 비활성화만으로는 불충분하다. +- 웹에서 마무리하는 경우 해당 페이지로 직접 링크한다. +- 규제 산업이 아니면 전화·이메일·채팅 절차를 요구할 수 없다. +- 즉시 삭제가 아니어도 되지만 소요 기간을 알리고 완료를 확인해 줘야 한다. +- 자동 갱신 구독이 있으면 결제가 Apple을 통해 계속된다는 점을 먼저 알린다. +- Sign in with Apple은 "should use the Sign in with Apple REST API to revoke user tokens" 하고, 서버-서버 알림 엔드포인트로 계정 변경을 처리해야 한다. + +**한국 개발자 요건** [fetched](https://developer.apple.com/news/?id=j9zukcr6) +- 2025-10-09 공지: "Starting January 1, 2026, developers based in the Republic of Korea must provide a server‑to‑server notification endpoint when registering a new Services ID, or updating an existing Services ID" +- 알림 내용: 이메일 전달 설정 변경, 앱 내 계정 삭제, Apple 계정 영구 삭제. +- 포럼의 DTS 답변: 한국 기반 개발자에게만 해당한다. 실시간 처리 의무 여부는 법률 자문을 받으라고 했다 — [forum 805241 [snippet]](https://developer.apple.com/forums/thread/805241) +- [inference] Android(또는 웹)에서 Apple 로그인을 하려면 Services ID가 필요하다. 따라서 **NestJS에 서명 JWT 검증 엔드포인트를 HTTPS로 배포한 뒤**에 Services ID를 등록할 수 있다. + +**토큰 폐기 절차** (TN3194는 제목만 읽힘 → 아래는 일반 지식, 원문 대조 필요) +1. authorization code 교환 시 받은 `refresh_token`을 서버에 저장한다. +2. `.p8` 키로 ES256 client_secret JWT를 서명한다. +3. `POST https://appleid.apple.com/auth/revoke`를 호출한다(`client_id`, `client_secret`, `token`, `token_type_hint`) — [TN3194](https://developer.apple.com/documentation/technotes/tn3194-handling-account-deletions-and-revoking-tokens-for-sign-in-with-apple) + +**Developer Program 등록** [fetched](https://developer.apple.com/programs/enroll/) +- "Individual or sole proprietor/single-person business"는 개인으로 등록한다. 실명을 써야 하고, "Your name will be displayed as the seller name" +- 조직은 "must be a legal entity … We do not accept DBAs, fictitious business names, trade names, or branches" + D-U-N-S + 조직 도메인 이메일 + 실제 운영 중인 웹사이트가 필요하다. +- 연 99 USD(현지 통화로 표시). +- D-U-N-S는 "allow up to 5 business days" — [Apple D-U-N-S [snippet]](https://developer.apple.com/support/d-u-n-s/) +- [inference] 한국 개인사업자는 법인격이 없으므로 **개인으로 등록**한다. 판매자명을 "WanderGo"/회사명으로 보이게 하려면 법인이 필요하다. + +### 5. Google — Play 계정 삭제 · 비공개 테스트 · 계정 유형 · OAuth 인증 + +**Play 신규 개인 계정 테스트 요건** [fetched](https://support.google.com/googleplay/android-developer/answer/14151465) +- 2023-11-13 이후 만든 개인 계정은 비공개 테스트에 테스터 **12명 이상**이 "opted in continuously for at least 14 days" 해야 한다. +- 그 뒤 대시보드에서 "Apply for production"을 누른다. +- 신청서는 테스트 모집 난이도, 피드백, 대상 사용자, 첫해 설치 예상, 출시 준비 근거를 묻는다. +- 심사는 "usually takes seven days or less"다. 참여가 부족하면 추가 테스트를 요구받을 수 있다. + +**계정 유형** [fetched](https://support.google.com/googleplay/android-developer/answer/13628312) +- 개인: 법적 이름·주소(결제 프로필)를 인증해야 게시할 수 있다. 법적 이름·국가·개발자 이메일이 Play에 공개된다. +- 조직: D-U-N-S가 없으면 생성할 수 없다. 발급은 "can take up to 30 days" +- 연락처 이메일·전화는 OTP로 인증한다. +- 수익화 결제 프로필 인증은 최대 5일. +- 등록비 25달러와 신분증 요구 범위는 이 페이지에서 확인 못 함(unconfirmed). + +**계정 삭제** [fetched](https://support.google.com/googleplay/android-developer/answer/13327111) +- 필요한 것: + - "in-app path to delete their app accounts and associated data" + - 앱을 재설치하지 않고 요청할 수 있는 "web link resource" +- 웹 페이지는 스토어 등록명과 같은 앱·개발자 이름을 쓰고, 삭제 경로를 눈에 띄게 보여 줘야 한다. 링크는 오류 없이 동작해야 한다. +- 링크는 Play Console App content의 **Data safety 양식**에 입력한다. +- 보안·사기 방지·법령 사유로 일부를 보관할 수 있으나 고지해야 한다. + +**Google OAuth** [fetched] +- 출처: [브랜드 인증](https://developers.google.com/identity/protocols/oauth2/production-readiness/brand-verification) +- Testing 상태: "only available to users you add to the list of test users". 경고 화면이 뜨고, 사용자 수 상한과 리프레시 토큰 수명 제한이 있다. +- 브랜드 인증은 External + Published이고 로고·이름을 표시할 때 필요하다. +- Android 문서도 "Your brand must be verified for your app name to be visible to users on the Sign in with Google consent screen"이라고 한다 — [Android SIWG [fetched]](https://developer.android.com/identity/sign-in/credential-manager-siwg) +- 브랜드 인증 요건 + - Search Console로 **소유를 인증한 도메인** + - 로그인 페이지만 있는 것이 아닌 공개 홈페이지(Play 등록 페이지는 인정되지 않음) + - 같은 도메인의 개인정보처리방침 +- 소요 시간: 자동이면 수분, 수동이면 "usually takes 2-3 business days". 인증 결과는 **7일 안에 게시**하지 않으면 다시 인증해야 한다. +- openid/email/profile만 쓰면 민감·제한 범위 심사(데모 영상·보안 평가)는 해당하지 않는다. "verification review isn't mandatory" [snippet, GCP 도움말 13464323]. +- Android 클라이언트(패키지명 + SHA-1: 디버그·업로드·**Play 앱 서명 키**)와 Web 클라이언트 ID(`serverClientId`)를 등록한다 — 일반 지식, 원문에서 SHA-1 문구 미확인. + +### 6. 위치기반서비스사업 신고 (방송미디어통신위원회) + +- **누가**: "위치기반서비스사업을 하려는 자"(제9조①). 신고서에 상호·주된 사무소 소재지·사업 종류·위치정보시스템 등 주요 설비를 적는다 [prior]. +- 담당공무원이 **법인등기·사업자등록증**을 행정정보 공동이용으로 확인한다 — [정부24 민원안내 [fetched, 인코딩 일부 깨짐, 최종 수정 2021-01-26]](https://www.gov.kr/mw/AA020InfoCappView.do?HighCtgCD=A09001&CappBizCD=15701000085) +- [inference] 사업자등록이 없는 개인이 신고할 수 있는지는 **unconfirmed**다. 신고서 구조와 확인서류로 보면 **개인사업자 등록 후 신고**가 현실적이다. 위치정보지원센터 02-588-0185에 확인한다. +- **수리 요건**: 제9조⑤ "이 법에 적합하면 신고를 수리하여야 한다" — [casenote 제9조 [fetched]](https://casenote.kr/법령/위치정보의_보호_및_이용_등에_관한_법률/제9조). 처리 기한 조문은 없다. +- [inference] 수리가 필요한 신고이므로 일반 신고자는 **수리 후 사업을 개시**한다. +- **특례(제9조의2)**: 소상공인·1인 창조기업은 신고 없이 개시할 수 있고, "사업을 개시한 지 1개월이 지난 후에도" 계속하려면 신고한다 — [위치정보지원센터 [fetched]](https://www.lbsc.kr/front/content/contentViewer.do?contentId=CONTENT_0000091) +- **어디서**: 과기정통부 전자민원센터 **emsit.go.kr** 온라인 접수(특례 신고 바로가기 `Cp1440000_0182_03Reg.do`) — [lbsc.kr [fetched]]. 신청 방법은 인터넷, 처리기관은 방송미디어통신위원회(방송미디어통신사무처) — [정부24 [fetched]](https://www.gov.kr/mw/AA020InfoCappView.do?HighCtgCD=A09001&CappBizCD=15701000086) +- **서류** + - 일반: 신고서, 사업계획서(사업자 현황·사업 내용), 주요 설비 위치·설치 확인 서류, 제16조 위치정보 보호조치 증명 서류 [prior·fetched 정부24] + - 특례: 소상공인확인서 또는 1인 창조기업 확인 서류, 사업 종류·내용 설명자료(양식은 lbsc.kr) [fetched lbsc.kr] +- **발급처** [3rd-party, catchsecu 2024-10-25](https://www.catchsecu.com/archives/14060) + - 소상공인확인서: 중소기업현황정보시스템(sminfo.mss.go.kr). 재무제표 등이 필요하고 "약 1주일" + - 1인 창조기업 증빙: K-Startup 자가진단 결과 캡처 + 건강보험 자격득실확인서 + - 접수는 간편인증으로 하고, 회원가입은 필요 없다. 수리까지 "최대 2주 정도". 직접 신고하면 수수료가 없다. +- **언제** [inference] + - 비공개 베타(지인 테스터)를 "사업 개시"로 볼지는 해석이 갈린다. + - **베타 기간에 제출해 출시 전에 수리**받는 것이 가장 안전하다. + - 특례 대상이면 공개 출시일부터 1개월 안이 법정 마감이다. + - 보호조치 증빙(확인자료 자동 기록, 접근권한·접속기록, 암호화)은 **백엔드 설계 산출물**이므로 코드 작성 전에 요건으로 정해 둔다. + +### 7. 공통 — 사업자등록 + +- 사업자등록증은 원칙적으로 신청일부터 2일 안에 발급된다. 확인이 필요하면 연장되는데, 연장 기한은 출처마다 5일 또는 7일로 다르다. 사업 개시 전에도 신청할 수 있다 — [3rd-party snippet, 법령 원문 미확인](https://www.heumtax.com/contents/posts/business-registration-deadline) +- 구독 판매 시 통신판매업 신고 여부는 이번 범위 밖이다. [prior] legal_policy.md 5절 미결. + +--- + +## Open questions + +1. **사업 형태**: 개인사업자로 시작할지, 법인을 세울지. 결정에 따라 Apple 판매자명(실명 vs 회사명), Play 계정(개인 = 12명 × 14일 테스트 / 조직 = D-U-N-S 최대 30일), 위치 신고 경로(특례 가능 여부)가 갈린다. +2. 사업자등록 없는 **개인이 위치기반서비스사업 신고**를 할 수 있는지, 특례 대상 확인서를 신생 사업자가 바로 발급받을 수 있는지(재무제표가 없는 경우). 위치정보지원센터 02-588-0185에 문의. +3. **네이버 로그인 검수가 출시 전(베타 빌드) 화면으로 가능한지.** 안 되면 1.0에서 네이버 버튼을 플래그로 숨긴다. 공식 검수 가이드 원문은 이번 세션에서 열람 불가. +4. **Apple 5.1.1(v)의 "tokens to social networks off of the device" 문구**와 서버 측 토큰 저장(네이버 연동 해제, Apple refresh token)의 관계. 카카오는 어드민 키 해제로 저장을 피할 수 있다. 네이버 연동 해제에 서버 저장 없이 되는 경로가 있는지 확인. +5. 카카오 **개인 개발자 비즈 앱에서 이메일 "필수" 동의가 되는지**(선택은 커뮤니티 답변으로 확인). 이메일 자동 연결 정책을 정하기 전에 확인하되, Apple 비공개 이메일을 고려해 **이메일에 의존하지 않는 설계**를 권장한다. +6. 공공데이터 운영계정의 **실제 부여 트래픽**과, 출시 전 랜딩 URL·베타 화면으로 활용사례를 등록할 수 있는지. 인증키 유효기간과 연장 절차. +7. 카카오 **비즈월렛을 사업자 없이 만들 수 있는지**(유료 초과분이 필요해질 때만 해당). +8. NCP Maps(새 Maps)의 Mobile Dynamic Map 무료 이용량. 지도 SDK 결정과 연결된다. +9. Apple Developer Program 개인 등록 승인 소요, Play 신원 인증 소요와 신분증 요구, App Review 평균 소요 — 이번 세션에서 원문 확인 못 함. +10. 비공개 테스트 단계의 지인 테스터 위치 수집이 위치정보법상 "사업 개시"인지(전문가 확인). diff --git a/docs/research/pre-dev-check/kakao_place_id.md b/docs/research/pre-dev-check/kakao_place_id.md new file mode 100644 index 0000000..d23d938 --- /dev/null +++ b/docs/research/pre-dev-check/kakao_place_id.md @@ -0,0 +1,253 @@ +# Kakao 장소 ID만 저장한 코스는 다시 그릴 수 있나 — 2026-10-29 확인 + +> Builds on `WanderGo 기획 검토 자료조사/commercial_map_apis.md` §7. That file established the rule: Kakao Local results can't be stored; place ID and URL can. +> Legend: **(fetched)** = I opened the page and read it. **(snippet)** = search-engine summary only. **[inference]** = my reasoning, not stated by a source. +> Answers in DevTalk come from the accounts **"Map"** and **"C.L"**. The fetched page text doesn't show a staff badge, but these accounts answer policy questions on Kakao's behalf in every thread. I treat them as Kakao staff [inference]. + +--- + +## Takeaway + +1. **There's no legitimate way to turn a Kakao place ID back into its name, address or coordinates.** Kakao has said there's no single-place lookup by ID 7 times between 2020 and 2026-10-28. The `place.map.kakao.com` JSON is "공식적으로 지원하는 방법이 아닙니다". The only official use of an ID is **landing on Kakao Map**: `place_url` or `https://map.kakao.com/link/map|to|roadview/{장소ID}`. +2. **The rule got stricter in September 2026.** You can't store a Kakao name, address, coordinates or category, even if the user confirmed or edited it, even only on the device, and even just to use for re-searching. Earlier answers (July and August 2026) allowed storing the name text, but later answers replace them. Separately, **Local results must not be sent to an external LLM** (2026-09-08). This hits WanderGo's AI course builder directly. +3. **What you *can* store:** the Kakao ID and URL; text the user types themselves (not pre-filled from the API); data from independent public sources (TourAPI, 소상공인 상가정보, and similar). +4. **Recommendation:** course stops point to WanderGo's own `place` table, built from public data and user-typed places. The Kakao ID and URL are kept only as an optional "카카오맵에서 보기" link. Kakao Local is used only for the live "장소 추가" search screen. +5. **Google and Naver don't fix this.** Google only lets you store the ID for free, charges the Pro rate for names, and requires a Google map. Naver's search API 특약 (in force since 2026-09) bans storing results beyond short caches, according to secondary sources. + +--- + +## Cited findings + +### A. Is there any ID → details path? (Q1) + +| Date | Source | What it says | +|---|---|---| +| 2020-05-29 | [DevTalk 104683 (fetched)](https://devtalk.kakao.com/t/api-id-data-api/104683), C.L | "그와 같은 사용은 허용하지 않고, ID로 상세 정보를 조회하는 API는 제공 계획이 없습니다." (The asker wanted to store IDs and fetch details later.) | +| 2021-12-29 | [DevTalk 120361 (fetched)](https://devtalk.kakao.com/t/place-map-api/120361), lea.ju | On `https://place.map.kakao.com/main/v/{place_id}`: "공식적으로 지원하는 방법이 아닙니다." "REST API에서 제공하는 정보만 사용해주셔야 합니다." | +| 2026-08-21 | [DevTalk 151265 (fetched)](https://devtalk.kakao.com/t/api-id-place-url/151265), Map | Q4: "장소ID만으로 해당 장소를 단건 조회 가능한 API는 없습니다." | +| 2026-09-08 | [DevTalk 151545 (fetched)](https://devtalk.kakao.com/t/api-llm-place-url-webview/151545), Map | "현재 장소 ID 기반의 단건 조회 API는 제공되지 않습니다." | +| 2026-09-16 | [DevTalk 151647 (fetched)](https://devtalk.kakao.com/t/api/151647), Map | "장소ID 기반의 단건 조회 API는 없습니다." | +| 2026-09-16 | [DevTalk 151720 (fetched)](https://devtalk.kakao.com/t/topic/151720), Map | "장소 ID 기반 단건 호출 API는 별도로 제공되지 않으며, 카카오맵 상세 URL 연결이나 실시간 재검색 등 서비스 구조에 맞춰 직접 구현하셔야 하는 영역입니다." | +| 2026-10-23 | [DevTalk 152312 (fetched)](https://devtalk.kakao.com/t/api/152312), C.L | "장소 ID만으로 상세 정보를 다시 조회하는 공개 API는 제공하지 않으며," | +| 2026-10-28 | [DevTalk 152447 (fetched)](https://devtalk.kakao.com/t/local-api/152447), C.L | "장소 ID만으로 상세 정보를 조회하는 공개 API는 현재 제공하지 않습니다." Store details must come from live search or a link to the place URL. | + +- **REST API docs (fetched)**: the Local API has 6 endpoints: address search, coord2regioncode, coord2address, transcoord, keyword search, category search. **None of them takes a place ID as input.** `id` and `place_url` appear only in the output. Example `place_url`: `http://place.map.kakao.com/26338954` — [Kakao Local dev guide](https://developers.kakao.com/docs/ko/local/dev-guide) +- **Official ID-based links (fetched)**: the Kakao Maps API guide's "지도 URL" section lists `/link/map/장소ID`, `/link/to/장소ID` and `/link/roadview/장소ID` on base `https://map.kakao.com`, for example `https://map.kakao.com/link/map/18577297`. Multi-stop `/link/by/{mode}/...` takes only `이름,위도,경도`, not IDs. Up to 5 waypoints; transit doesn't support waypoints — [apis.map.kakao.com/web/guide](https://apis.map.kakao.com/web/guide/) +- **App URL scheme `kakaomap://place?id=`**: I found no official doc for it **(unconfirmed)**. One DevTalk asker reports that `kakaomap://look` opens at building level and can't target a specific business — [DevTalk 151335 (fetched)](https://devtalk.kakao.com/t/db-id/151335) +- **Map SDK POI tap**: the place name delivered by a base-map POI label click event also can't be stored — "카카오맵 SDK의 POI 라벨 클릭 이벤트를 통해 전달되는 장소명 역시 그대로 저장하여 반복 활용하는 방식은 허용하지 않습니다." (Map, 2026-08-24) — [DevTalk 151294 (fetched)](https://devtalk.kakao.com/t/sdk-poi/151294) + +**Answer to Q1: no.** There's no REST, SDK, deep-link or place-URL route that returns the name, address or coordinates for an ID. A deep link only opens Kakao Map, which shows the information inside Kakao's own app or page. + +### B. What may be stored — the rule as of late Oct 2026 + +**Operating policy text (fetched)** — [Kakao Developers 운영정책](https://developers.kakao.com/terms/ko/site-policies), effective **2026-04-20** (제19조): +- 제5조 제20호 prohibits "앱에서 사용자 환경을 개선하기 위한 목적 외 다른 목적으로 카카오에서 받은 데이터를 캐시하거나 캐시 후 최신 데이터로 유지하지 않는 행위". +- 제5조 제30호 prohibits copying or replicating obtained information, or entering it into a search engine or directory, without Kakao's prior consent. +- Staff said storing names or addresses in a DB "제5조 제20항(캐시 목적)에 해당하지 않아 정책상 허용되지 않습니다" (Map, 2026-09-16) — [151720 (fetched)](https://devtalk.kakao.com/t/topic/151720) + +**Storage rulings, most recent first (all fetched):** +- **2026-10-28, C.L** [152447](https://devtalk.kakao.com/t/local-api/152447): + - Storage is not allowed even on the device or as a past-visit record: "개인 단말에만 보관하거나 과거 방문·예정 기록으로 사용하는 경우에도, 카카오 API에서 받은 장소명·주소를 장기 저장하여 재사용하는 것은 허용하지 않습니다." + - The test is where the value came from: "구분 기준은 저장되는 정보의 출처입니다." "사용자가 독립적으로 작성한 메뉴명·식사 날짜·메모 등은 보관할 수 있습니다." Renaming an auto-filled field to "메모" doesn't make it user-authored. + - Cleanup: you can't relabel old API data as "수기 입력" to keep it. +- **2026-10-23, C.L** [152312](https://devtalk.kakao.com/t/api/152312): + - "사용자가 직접 선택한 장소이거나 본인에게만 보이는 기록이라도, 로컬 API에서 받은 장소명·카테고리·주소·좌표 등을 저장하여 이후에도 계속 사용하는 형태는 허용하지 않습니다." + - "장소 ID와 URL은 카카오맵으로 연결하는 용도로 보관할 수 있습니다." + - "재검색을 위한 목적이라도 응답받은 장소명·좌표를 함께 저장하여 사용하는 것은 허용 범위에 해당하지 않습니다." +- **2026-10-19/20, Map** [152228](https://devtalk.kakao.com/t/api-id/152228): + - "좌표와 주소 정보를 저장하여 활용하는 형태는 허용하지 않습니다." + - Publishing the data as static JSON is also not allowed. + - "장소ID, URL은 저장하여 카카오맵으로 랜딩하는 구조로 활용할 수 있습니다." + - **2026-10-20:** "로컬API 응답값 내 장소ID 및 URL만 저장하고 별도 확보한 주소를 공공기관 API로 변환한 좌표로 저장하여 지도에 표시하는 방식은 가능합니다." Staff added that the public data's own terms apply. + - A browser `services.Places` lookup on marker tap "해당 구조는 실시간 호출에 해당합니다". +- **2026-10-19, Map** [152223](https://devtalk.kakao.com/t/1-id/152223): asked to allow storing "ID + 장소명", staff answered "로컬 API에서 제공하는 장소명은 저장하여 활용할 수 없으며 실시간 호출로만 이용 가능합니다." +- **2026-09-17, Map** [151727](https://devtalk.kakao.com/t/id/151727): "사용자가 일부 확인, 수정 과정을 거치더라도 로컬API 결과를 기반으로 한 데이터이므로 … 정책 상 API 결과값의 DB저장에 해당" / "장소ID 및 URL은 저장 및 활용은 가능합니다." +- **2026-09-16, Map** [151647](https://devtalk.kakao.com/t/api/151647): + - "설명해 주신 일회성 조회로 활용 및 사용자 직접 입력 구조(API 호출 결과값이 아닌)는 허용됩니다." + - "장소ID는 저장 가능하며, 장소명, 주소 등의 응답값을 함께 저장하여 활용하는 형태는 허용되지 않습니다." + - The asker's case was a user typing a business name directly and pinning a location on the map, without going through a Kakao response. +- **2026-09-07, Map** [151514](https://devtalk.kakao.com/t/topic/151514): "API 응답 데이터를 그대로 DB에 저장하는 것은 허용되지 않으므로, 나)의 방식보단 가) 방식처럼 사용자 직접 텍스트 입력 구조를 권장합니다." + - (가) was an input box where the user types or confirms a name. + - [inference] Under 151727 (09-17), a box **pre-filled** with the API name no longer qualifies. The box must start empty, or the user must type independently. +- **2026-08-24, Map** [151294](https://devtalk.kakao.com/t/sdk-poi/151294): coordinates based on Local results "사용자가 화면에서 확인하거나 확정하는 절차를 거치더라도 별도로 저장하여 활용할 수 없습니다". Calling the API live at each display and drawing the returned coordinates is allowed. + +**Superseded (older, looser) answers — don't rely on them:** +- **2026-07-28, Map** [150888 (fetched)](https://devtalk.kakao.com/t/api/150888): "설명하신 사용 범위 내 장소명 텍스트를 저장하여 활용하는 것은 가능합니다." +- **2026-08-21, Map** [151265 (fetched)](https://devtalk.kakao.com/t/api-id-place-url/151265): storing "카카오 장소 ID, 장소명, place_url" for places the user saved or put in a course — "저장 범위에 허용됩니다". This was a dating/travel course app much like WanderGo. +- [inference] These conflict with the 5+ later answers from 09-16 to 10-28. Treat the later answers as the current rule. + +**Public data + Kakao ID matching (fetched):** +- **2026-08-10, Map** [151052](https://devtalk.kakao.com/t/tourapi-local-api/151052): own or public place ledger (TourAPI, public data) plus live Local checks: + - "장소 정보 매핑 관련 추가 확인을 위해 로컬API를 활용할 수 있습니다." "장소ID 및 URL은 저장하여 활용 가능합니다." + - Recording whether a place was checked, and when: "확인 여부 및 일자를 기록하는 것은 무방합니다." + - Using non-Kakao coordinates on a Kakao map: "가능합니다." + - **But** "지역 구분을 위한 기계적인 API 대량 호출은 허용하지 않습니다." +- **2026-08-26, Map** [151335](https://devtalk.kakao.com/t/db-id/151335): matching your own public-data DB to Kakao IDs and storing only the ID and `place_url` — "허용됩니다." + - The follow-up asked about a one-time operator batch over about 2,000 records and attribution. **No staff reply yet.** +- **2026-10-01, Map** [151916](https://devtalk.kakao.com/t/topic/151916): + - Live matching plus storing the Kakao ID next to your own DB row: "실시간 호출 기반이라면 허용되며, 장소ID는 저장하여 사용 가능합니다." + - Using a tapped coordinate only to find the nearest own-DB place, then storing that place's ID: "장소ID 저장은 가능합니다." + - Drawing own public-data coordinates as markers on the Kakao JS map: "이슈 없습니다." + - Showing phone or address on marker tap: staff "장소URL을 통한 카카오맵 랜딩 구조를 권장". +- **2026-10-21, C.L** [152192](https://devtalk.kakao.com/t/api/152192): using Kakao coordinates once to query your own public parcel DB, then storing the public-data ID, address and center — "독립적으로 확보한 공공데이터의 값이라면 … 일회성 조회로 판단할 수 있습니다." + +**LLM and WebView (fetched) — 2026-09-08, Map** [151545](https://devtalk.kakao.com/t/api-llm-place-url-webview/151545): +- "API 응답 데이터를 외부 LLM으로 전송하는 것은 허용되지 않습니다." This applies even with minimal fields, or only for translation or sentence generation. +- "응답 속도 개선을 위한 단기 캐싱 역시 제한 대상입니다." +- "place_url은 외부 브라우저 호출 방식으로 이용해 주시기 바랍니다." / "별도의 출처 표기 의무는 없습니다." +- ⚠️ **Conflict:** one day earlier, [151514](https://devtalk.kakao.com/t/topic/151514) (09-07) allowed rendering `place_url` in an in-app view as-is ("설명하신 구조의 활용은 허용됩니다", but don't cover the Kakao Map CI). **Use the external browser to be safe.** + +**User's own search query** — [151190 (fetched)](https://devtalk.kakao.com/t/local-api/151190), C.L 2026-08-18: "사용자가 직접 입력한 쿼리 외에 카카오맵 API 호출을 통해 응답받은 결과를 저장하여 반복 사용하는 것은 허용하지 않습니다." +- [inference] This sentence carves the user's query out of the ban. It does **not** say query storage is allowed. +- No thread I found rules directly on storing the query (recent searches) or on re-searching with a stored user-typed label. **Unconfirmed — ask.** + +### C. Other sources (Q2 c–e) + +**Public data — storable (all fetched from data.go.kr):** +- **TourAPI 국문 관광정보 서비스_GW** — [15101578](https://www.data.go.kr/data/15101578/openapi.do): + - "이용허락범위 제한 없음", free. + - Images are 공공누리 1유형 and 3유형, and use as corporate CI/BI is banned. + - Dev traffic 1,000; production traffic can be raised by registering a use case. Production approval is reviewed (심의승인). +- **한국관광공사 반려동물 동반여행 서비스** — [15135102](https://www.data.go.kr/data/15135102/openapi.do): + - "이용허락범위 제한 없음", update cycle real-time. + - Covers "관광지, 숙소, 음식점, 쇼핑시설 등" with pet conditions. Cafés aren't named. +- **소상공인시장진흥공단 상가(상권)정보 API** — [15012005](https://www.data.go.kr/data/15012005/openapi.do): + - "이용허락범위 제한 없음", auto-approval for dev and production, dev traffic 10,000. + - Fields: "상가업소번호, 상호명, 주소, 상권업종명, 표준산업분류명, 경도, 위도 등". Source: 국세청 and card companies. + - **This is the best storable POI set for 음식점/카페 with names and WGS84 coordinates.** +- **행정안전부 식품_휴게음식점 조회서비스** — [15154921](https://www.data.go.kr/data/15154921/openapi.do): + - "이용허락범위 제한 없음", updated daily. + - Coordinates are in "Bessel 중부원점TM(EPSG:5174)" and need conversion. + - Useful for 영업상태 (open/closed) checks. +- **LOCALDATA → 공공데이터포털:** MOIS moved 195 licensing datasets (including 일반음식점) and 14 convenience datasets into data.go.kr, all available as APIs. The old portal ran in parallel "4월 15일까지" — [뉴스서울 2026-01-26 (fetched)](https://newsseoul.co.kr/news/view/1065579617179088) +- Kakao staff confirmed that storing public-data coordinates and drawing them on a Kakao map is fine (151052, 151916, 152228 above). The public source's own terms apply. + +**Google Places (New):** +- Pricing (fetched, page updated 2026-10-28) — [pricing](https://developers.google.com/maps/billing-and-pricing/pricing): + + | SKU | Monthly free cap | Price per 1,000 | + |---|---|---| + | Place Details Essentials (IDs Only) | Unlimited | — | + | Place Details Essentials | 10,000 | $5 | + | Place Details Pro | 5,000 | $17 | + | Text Search Essentials (IDs Only) | Unlimited | — | + +- Fields (fetched) — [data fields](https://developers.google.com/maps/documentation/places/web-service/data-fields): + - **IDs Only** = attributions, consumerAlert, id, movedPlace, movedPlaceId, name (resource name), photos. **No `displayName`, no `location`.** + - `location` and `formattedAddress` are Essentials. **`displayName` is Pro.** + - [inference] So showing a Korean name plus a pin costs at the Pro rate: $17/1k after 5k free per month. The free SKU can only refresh IDs (`movedPlaceId`). +- Policies (fetched) — [Places policies](https://developers.google.com/maps/documentation/places/web-service/policies): + - "You can therefore store place ID values indefinitely." + - "Places API results displayed on a map must be shown on a Google Map". +- Latitude/longitude may be cached up to 30 consecutive days, per archived Service Specific Terms — [maps-service-terms (snippet; live page didn't render)](https://cloud.google.com/maps-platform/terms/maps-service-terms) +- Korean `languageCode=ko` displayName output for Korean places: **unverified.** It needs a live test. + +**Naver 검색 API (지역):** +- Official terms page `developers.naver.com/products/terms/` **could not be fetched** (tool blocked). +- Secondary sources (fetched): + - The 검색 API 특약 was announced 2026-08-07 and took effect 2026-09-07 (Developers Center). One source says 2026-09-20 for NAVER API HUB. + - Copying, storing or caching is prohibited, except server history lookup up to 21 days and personalization or device cache of 24 hours or until the next query. + - Results must be shown as Naver results, with attribution and a link to the original. + - Feeding results into AI is prohibited. + - Developers Center keys end 2027-06-30; local search moves to API HUB with metered billing. + - Sources: [waffleboard (fetched)](https://waffleboard.io/blog/naver-search-api-terms-ai-ads-storage), which cites the [official notice](https://developers.naver.com/notice/article/33400); [GitHub issue summary (fetched)](https://github.com/HITENEKEN/blog-auto-poster/issues/27); [brunch 2026-09-16 (fetched)](https://brunch.co.kr/@ainote/123) +- API limits: max 5 results per call, no paging, 25,000/day — [NCP API HUB local search (fetched)](https://api.ncloud-docs.com/docs/naver-api-hub-search-local.md) + +--- + +## Options table + +| Option | Allowed? | Cost | UX | Risk | +|---|---|---|---|---| +| **a) Store Kakao ID + URL, plus a label the user types** | **Yes.** ID/URL: 7+ staff answers. User-typed text: allowed by 151647 (09-16) and 152447 (10-28), **only if typed independently** — a box pre-filled from the API doesn't count (151727). | Kakao search only when adding a place: 100k/day free, then 2원 per call. Links are free. | Weak. No pin or route on WanderGo's map unless you re-search (option b). Name quality depends on the user. "카카오맵에서 보기" opens externally. | Low policy risk. Users type sloppy labels or skip the step, so the route has no coordinates. | +| **b) Re-search at display time** | **Partly.** Live calls are fine (151294, 152228). Using **stored API names or coordinates as the query is banned** (152312). Using the **user's own typed label** as the query is **unconfirmed** — 151190 only carves the query out of the ban. | N keyword calls per course view. At about 8k/day it stays in the free quota. | Pins can show, but matches fail when there's no `x,y` (Kakao coordinates can't be stored; public-data or region-center coordinates would work). Adds latency. | Medium: the policy is unconfirmed, and the stored ID may not be in the top 45 results. Staff also warned against "기계적인 API 대량 호출" (151052). | +| **c) Storable sources: TourAPI, 반려동물 동반여행, 소상공인 상가정보, 인허가, user-entered places** | **Yes.** "이용허락범위 제한 없음" on all four data.go.kr pages. Kakao OKs drawing these coordinates on its map and attaching Kakao IDs (151052, 151335, 151916, 152228). | Free. Engineering cost: ETL, EPSG:5174→WGS84 conversion, dedup, and periodic sync. | Good. Stored name, pin and category show offline. Shareable. Can be fed to the AI. Trendy or new spots may be missing or stale; names may differ from what users know. | Data coverage and freshness, not policy. Also check TourAPI image license types (1유형 vs 3유형). | +| **d) Google Places** | ID storage forever: yes. Name or coordinates: only by live fetch, **and only on a Google map**. Lat/lng cache ≤30 days (snippet). | IDs Only: free, but no name or coordinates. Name needs Pro: $17/1k after 5k/month. Address/coordinates need Essentials: $5/1k after 10k/month. | Needs a Google base map, which clashes with a Kakao or Naver map. Korean name output unverified. | High: can't mix with a Kakao map, unpredictable cost, weaker Korean POIs ([inference] per the earlier notes). | +| **e) Naver 지역 검색** | **No** for persistent storage (21-day/24h exceptions, secondary sources). Attribution and link required. AI input banned. | Metered on API HUB from 2026-09. 25k/day; max 5 results. | Poor: 5 results, no paging. | High: terms just tightened, and the endpoint is migrating. | + +--- + +## Recommendation (MVP) + +**Build course stops on WanderGo's own `place` table (option c). Attach Kakao ID + URL only as an outbound link (option a). Keep Kakao Local as a live search screen only, never as stored data or AI input.** + +How it works: +1. **Candidate pool for AI and curation** = TourAPI (관광지, 축제, 반려동물 동반), 소상공인 상가정보 (음식점, 카페), and optionally 인허가 영업상태 for closed-business checks. + - Sync these into `place` with a nightly or weekly batch. + - **The AI only ever sees `place` rows.** Kakao results never go to the LLM (151545). +2. **Kakao link**: when the user or operator confirms a place, store the Kakao `id` and `place_url` (plus the check date) in `place_external_link`. + - Use one live keyword search per place, at the moment someone acts on it — not a mechanical bulk sweep (151052). + - The detail screen shows our stored name and address from public data. The button "카카오맵에서 보기 / 길찾기" opens `https://map.kakao.com/link/map/{id}` or `/link/to/{id}` **in an external browser or the Kakao app**. + - Phone, hours and reviews are shown only through that link-out, never stored. +3. **"장소 직접 추가"** (a place not in our DB): + - Run a live Kakao search for display only. + - On save, show an **empty** name box the user types into (not pre-filled). + - Store the typed label, Kakao ID and URL. + - For location, either snap to the nearest own-DB place (151916 Q4), or leave the stop without coordinates. It then shows in the list and opens in Kakao, but isn't routed. + - Storing a raw map-tap coordinate is **unconfirmed**; ask before using it. +4. **Routes**: compute them live from our stored public-data coordinates. Don't cache Kakao route results. Whether routes may be cached is a separate open question. + +**DB fields:** + +`place` (our data): +- `id` +- `source` (`TOUR_API` | `TOUR_PET` | `SBIZ` | `LICENSE` | `USER`) +- `source_ref` (contentid / 상가업소번호 / 관리번호) +- `name`, `category_code` (own taxonomy), `address`, `lat`, `lng` (WGS84) — **all from the public source**, or user-typed for `USER` +- `pet_policy` (JSON from TourAPI pet) +- `license_note`, `image_license_type` +- `source_synced_at`, `is_active` + +`place_external_link`: +- `place_id`, `provider` (`KAKAO`) +- `external_id` (Kakao 장소 ID), `external_url` (`place_url`) +- `checked_at`, `match_status` (`MATCHED` | `NOT_FOUND` | `AMBIGUOUS`) +- **Nothing else from Kakao.** + +`course_stop`: +- `id`, `course_id`, `seq` +- `place_id` (nullable FK) +- `user_label` (user-typed, nullable) +- `kakao_place_id`, `kakao_place_url` (nullable, only for user-added stops without a `place` row) +- `user_memo`, `planned_start`, `stay_minutes`, `move_mode_to_next`, `created_at` + +**Never store:** Kakao `place_name`, `address_name`, `road_address_name`, `x`/`y`, `category_*`, `phone`, `distance`, POI-tap names, or route polylines and durations (pending an answer). Don't pre-fill `user_label` from the API. + +**Trade-offs:** +- Public data misses newer trendy spots and sometimes uses legal business names. Fix this with an operator curation queue and "직접 추가". +- More ETL work up front. +- In return: zero per-view API cost, offline and shareable courses, AI-safe inputs, and no dependence on Kakao's shifting DevTalk answers. + +--- + +## Open questions — draft DevTalk posts (카카오맵 API 지도/로컬 카테고리) + +**Q1. 사용자가 직접 입력한 라벨로 표시 시점 재검색** +> 안녕하세요. 데이트·당일 여행 코스 앱(WanderGo)을 개발하고 있습니다. 사용자가 로컬 API 검색 결과에서 장소를 고르면 장소 ID와 place_url만 저장합니다. 이어서 비어 있는 입력창을 띄워 사용자가 장소 이름을 직접 타이핑하게 하고, 이 텍스트를 저장합니다. 입력창에 API의 장소명을 미리 채우지 않습니다. +> 1) 이렇게 사용자가 독립적으로 입력한 라벨을 저장하는 것은 허용되는지요? +> 2) 코스 화면을 열 때 이 라벨을 query로 키워드 검색 API를 실시간 호출하고, 응답 중 저장된 장소 ID와 같은 항목의 이름·좌표를 화면에만 표시한 뒤 저장 없이 폐기하는 구조가 허용되는지요? (152312에서 "응답받은 장소명·좌표를 저장해 재검색"하는 것은 불가하다고 안내받았습니다. 이 질문은 사용자가 직접 입력한 텍스트만 query로 쓰는 경우입니다.) + +**Q2. 공공데이터 원장과 카카오 장소 ID의 운영자 배치 매칭** +> 한국관광공사 TourAPI와 소상공인 상가(상권)정보로 자체 장소 원장(이름·주소·좌표, 약 2~5만 건)을 구축합니다. 운영자가 원장 장소마다 키워드 검색(상호명 + 원장 좌표 반경 300m)을 한 번 호출합니다. 주소·거리로 명확히 일치할 때만 카카오 장소 ID와 place_url, 확인 일자를 저장하고, 그 밖의 응답값은 저장하지 않습니다. +> 1) 151335·151052에서 ID 매칭은 허용되지만 "기계적인 대량 호출"은 안 된다고 하셨는데, 위와 같은 일회성 배치는 어디에 해당하는지요? +> 2) 폐업 확인 등을 위해 같은 배치를 주기적으로(예: 분기 1회) 다시 실행해도 되는지요? + +**Q3. 지도 탭 좌표 저장** +> 사용자가 카카오맵 SDK 지도에서 빈 곳을 길게 눌러 고른 좌표(로컬 API 응답이 아닌 지도 탭 좌표)를 사용자가 직접 입력한 장소명과 함께 코스 경유지로 저장해도 되는지요? 151190에 같은 질문(추가 질문 2번)이 있었으나 답변이 없어 다시 여쭙습니다. + +**Q4. place_url 앱 내 표시 방식** +> 151514(9/7)에서는 place_url 상세 페이지를 앱 안에서 그대로 렌더링하는 구조가 허용된다고 하셨고, 151545(9/8)에서는 외부 브라우저 호출 방식으로 이용해 달라고 하셨습니다. Flutter 앱에서 `https://map.kakao.com/link/map/{장소ID}` 또는 place_url을 열 때 인앱 WebView(또는 Custom Tabs/SFSafariViewController)를 써도 되는지, 외부 브라우저·카카오맵 앱으로만 열어야 하는지 확인 부탁드립니다. + +**Q5. 경로 API 결과 저장** +> 도보·대중교통 경로 조회 API로 받은 소요 시간(분)과 경로선을 코스 공유 링크에서 다시 보여주려고 합니다. 이 결과도 로컬 API와 같이 실시간 호출만 허용되는지요? 아니면 사용자가 만든 코스의 일부로 소요 시간 숫자만 저장하는 것은 가능한지요? + +--- + +## Gaps + +- No official Kakao doc for `kakaomap://place?id=`. Only the web `map.kakao.com/link/...{장소ID}` formats are documented. +- The license terms for user-address geocoding via public APIs (juso.go.kr 좌표제공, VWorld) weren't checked. Needed only if "직접 추가" should take a typed address. +- Naver 특약 clause text wasn't read first-hand. +- The Google lat/lng 30-day rule came from snippets of archived terms only. +- Google `languageCode=ko` output for Korean places is untested. +- 소상공인 상가정보 field-level freshness and coverage of cafés, date spots and pop-ups weren't measured. diff --git a/docs/research/pre-dev-check/map_sdk.md b/docs/research/pre-dev-check/map_sdk.md new file mode 100644 index 0000000..6a31eac --- /dev/null +++ b/docs/research/pre-dev-check/map_sdk.md @@ -0,0 +1,214 @@ +# 지도 SDK 선정 (Flutter) — 2026-10-29 기준 + +> Builds on `WanderGo 기획 검토 자료조사/commercial_map_apis.md` §1, §2, §3, §7, §8. Don't re-read those for basics. +> Legend: **(fetched)** = I opened the page, API JSON or source file myself. **(snippet)** = I only saw a search-engine summary. **(inference)** = my reasoning, not stated by a source. +> Project baseline, read from `flutter/` in this repo: Flutter 3.41.4, Dart `^3.11.1`, AGP 8.11.1, Gradle 8.14, iOS deployment target 13.0. + +--- + +## Takeaway + +1. **Recommended map: Naver, via `flutter_naver_map` 1.4.4.** It's native and covers every map feature WanderGo needs: path/polyline overlays, built-in clustering with a custom cluster builder, widget-to-image markers, fitBounds, `onCameraIdle` + `getContentBounds` for "이 지역 검색", and a map `locale` setting (ko/en/ja/zh). +2. **Naver is free at our scale.** The NCP pricing page (fetched today) shows **Dynamic Map, which covers Web JS, Android SDK and iOS SDK, free up to 6,000,000 calls/month**, for one 대표 계정 only. The old AI·NAVER API map products have no free tier. This settles the conflict in the earlier notes. +3. **Neither Kakao option works for the MVP.** `kakao_maps_flutter` 0.2.2 has no polyline API and needs Flutter ≥3.44. `kakao_map_plugin` 1.0.0 is complete but runs in a WebView, and the Kakao JS API has no label-language option. The native Kakao SDK v2 has `setPoiLanguage("en")`, but neither Flutter package exposes it. +4. **Google is the fallback.** Its labels follow the device language (ko/en/ja/zh). It's best-maintained, and map loads are free and unlimited. But in Korea it has no driving, walking or biking directions and no traffic layer, and Places results can only be shown on a Google map. +5. **Data rules don't block us.** TourAPI is "이용허락범위 제한 없음", so its coordinates can go on any map. Kakao confirmed in writing again on 2026-06-29 that Local results may be shown on another vendor's SDK, real-time only, and nothing newer contradicts that. **The main risk with Naver is maintenance:** one maintainer, no release since 2025-12-20, and open bugs in clustering and multi-instance use. A 1-day spike should test exactly those. + +--- + +## Comparison table + +Assumption for the cost row: 10k MAU × 6 sessions/month × about 3 map views per session ≈ **180k map loads/month (~6k/day)** (inference). + +| | `flutter_naver_map` | `kakao_maps_flutter` | `kakao_map_plugin` | `google_maps_flutter` | +|---|---|---|---|---| +| Latest version / date | **1.4.4 / 2025-12-20** (fetched) | **0.2.2 / 2026-08-24** (fetched) | **1.0.0 / 2026-09-11** (fetched) | **2.19.0 / 2026-10-14** (fetched) | +| Publisher, 30-day downloads, likes | note11.dev (verified); 29,247; 116 | seunghwanly.xyz; 351; 9 | kkensu.com; 2,192; 48 | flutter.dev; 1,095,480; 4,630 | +| SDK constraint vs. our Flutter 3.41 | Flutter ≥3.22, Dart ≥3.0 — **OK** | Flutter ≥3.44, Dart ^3.12 — **incompatible** (needs an upgrade) | Flutter ≥3.19, Dart ≥3.3 — **OK** | 2.19.0 needs Flutter ≥3.47 — **use 2.18.1** (Flutter ≥3.38) or upgrade | +| Native or WebView | Native (Naver SDK 3.23.0, pinned) | Native (Kakao Maps SDK v2: Android 2.15.1, iOS ~>2.12.19) | **WebView** over the Kakao JS API (iframe on web) | Native | +| Min OS | Android 6.0 (SDK 23), iOS 12 | Android SDK 23, iOS 13 | Android SDK 19/20+, iOS 9+ | Android SDK 24+, iOS 15+ (from the earlier notes) | +| Polyline / route line | **Yes**: `NPathOverlay`, `NMultipartPathOverlay`, `NArrowheadPathOverlay`, `NPolylineOverlay` | **No.** The controller has no polyline/shape/route API, though the native SDK has RouteLine and Shape | **Yes**: `Polyline`, plus `getPolylineLength`, `onPolylineTap` | Yes | +| Clustering | **Yes, native**: `NClusterableMarker` + `NaverMapClusteringOptions` with a custom `clusterMarkerBuilder` | **Only LOD label competition** on mobile (overlaps hidden, not count bubbles). True clusterer is web-only | **Yes**: `Clusterer` + `ClustererStyle` | Yes (`ClusterManager` in recent versions; not re-verified today) | +| Custom markers (number circles) | `NOverlayImage.fromWidget` (rasterized, no interaction), `.fromByteArray`, `.fromAssetImage` | `MarkerStyle` from PNG bytes (render the widget yourself) | `MarkerIcon.fromWidget`, `MarkerIcon.pin`, plus live `KakaoMapWidgetOverlay` | `BitmapDescriptor` from bytes/asset; Advanced Markers need a Map ID | +| Fit to bounds / "search this area" | `NCameraUpdate.fitBounds(bounds, padding)`; `onCameraIdle` + `getContentBounds()` | `CameraUpdate.fromBounds`; `getViewportBounds()` | `fitBounds(points, padding)`, `panToBounds` | Yes (standard API) | +| Map label languages | **ko / en / ja / zh**, settable per map (`NaverMapViewOptions.locale`) | Native SDK: `setPoiLanguage("ko"/"en")` plus `getSupportedLanguages()`; **not exposed by the package** | **Korean only.** No language option found in the Kakao JS docs or the plugin | Follows the **device** language; ko, en, ja, zh-CN, zh-TW are supported | +| Cost at 10k MAU (~180k loads/mo) | **0원**: within 6M/month free (대표 계정 only). With no free tier, about 18,000원/month at 0.1원 (price unconfirmed) | **0원**: 300,000/day free | **0원**: JS SDK 300,000/day free | **$0**: mobile loads unlimited | +| Open issues / risk | 20 open, including clustering flicker and icon reverts, an Android leak on API 30–32, an iOS leak, two-map gestures, and an AGP 9 PR | 2 open, tiny user base | 1 open (AGP 9 / Gradle 9 build failure) | Low | + +--- + +## Cited findings + +### Q1. `flutter_naver_map` and NCP Maps pricing + +**Package state** +- 1.4.4 was published **2025-12-20T20:30Z**. Earlier releases: 1.4.3 on 2025-12-19, 1.4.2 on 2025-12-14 (Naver SDK upgraded to 3.23.0), 1.4.1 on 2025-07-30, and 1.4.0 on 2025-07-15. Requires Dart `>=3.0.0 <4.0.0` and Flutter `>=3.22.0`. Source: [pub.dev API](https://pub.dev/api/packages/flutter_naver_map) (fetched). +- Score 140/160, 116 likes, 29,247 downloads in 30 days. Platforms: Android and iOS only, with Swift Package Manager support. Source: [pub.dev score API](https://pub.dev/api/packages/flutter_naver_map/score) (fetched). +- Minimum OS: "flutter_naver_map은 Android 6.0(SDK 23), iOS 12.0부터 지원합니다." Source: [README](https://github.com/note11g/flutter_naver_map/blob/main/README.md) (fetched). +- At tag v1.4.4, the Android build pins `com.naver.maps:map-sdk:3.23.0` with `minSdkVersion 23` and `compileSdk 36`. The iOS podspec pins `NMapsMap 3.23.0` with `platform :ios, '12.0'`. Source: [repo @v1.4.4](https://github.com/note11g/flutter_naver_map/tree/v1.4.4) (fetched). +- The native Naver Android SDK is now at **3.24.0 (2026-09-09)**, after 3.23.1 (2026-01-26), 3.23.2 (2026-03-30) and 3.23.3 (2026-07-13). 3.23.0 (2025-09-22) added map-data attribution (`OnAttributionChangeListener`). So the plugin lags the native SDK by four releases. Source: [android-map-sdk CHANGELOG](https://github.com/navermaps/android-map-sdk/blob/master/CHANGELOG.md) (fetched). +- Repo activity: 182 stars, last push 2026-07-07. The latest commits on main are two `MissingPluginException` fixes (2026-02-12 and 2026-03-23), which are unreleased. Branches `dev/1.4.5` and `dev/v2` exist, and issue #261 "Prepare v2.0" has been open since 2024-08. Source: [GitHub API](https://api.github.com/repos/note11g/flutter_naver_map) (fetched). + +**Feature support, verified in source at v1.4.4** +- Overlays: `path_overlay.dart`, `multipart_path_overlay.dart`, `arrow_head_path_overlay.dart`, `polyline_overlay.dart`, `polygon_overlay.dart` (fetched). +- Clustering: `NClusterableMarker`, `NaverMapClusteringOptions(enableZoomRange, animationDuration, mergeStrategy, clusterMarkerBuilder)`. The doc comment says that when the builder is null, `defaultClusterMarkerBuilder` runs (fetched). +- Custom markers: `NOverlayImage.fromWidget({widget, size, context})`. The doc comment says the widget is rasterized, so it isn't interactive, and warns against putting `Image` widgets inside it (fetched). +- Camera and bounds: `NCameraUpdate.fitBounds(bounds, padding)`, `controller.getContentBounds()`, `onCameraIdle`, `onMapTapped`, `onSymbolTapped` (fetched). +- Map language: `NLocale` doc comment says "현재 네이버 지도 SDK는 한국어, 영어, 중국어, 일본어만을 지원합니다". The `NaverMapViewOptions.locale` comment says "지도의 언어를 설정합니다. (한국어, 영어, 일어, 중국어 지원)" and defaults to the system locale (fetched). +- Brand styling: `customStyle` (Map Style Editor ID) since 1.4.0. Source: [CHANGELOG](https://github.com/note11g/flutter_naver_map/blob/main/CHANGELOG.md) (fetched). +- Authentication: `FlutterNaverMap().init(clientId:, onAuthFailed:)` uses the new NCP auth (NCP console > Maps > Application > "Dynamic Map") (fetched). + +**Open issues that matter** — [issues](https://github.com/note11g/flutter_naver_map/issues) (fetched) +- #349 (2025-08-26): `NClusterableMarker` makes the whole marker set flicker, on both iOS and Android, with an Android crash log. A fix was tried on `dev/1.4.5` but is unreleased. +- #363 (2026-05-20): after clustering, markers revert to the icon they were created with, so custom icons are lost. +- #342 (2025-07-28): on iOS, `clusterMarkerBuilder` stops firing after `clearOverlays()`. +- #321 (2025-06-05): gestures misbehave when maps are used on two screens. +- #365 (2026-08-27): on Android API 30–32 only, each map screen open and close leaks the MapView tree plus about 18 MB of native heap (39 → 267 MB after 12 cycles). +- #347 (2025-08-04): iOS memory leak. +- #283 (2024-12): an Impeller crash on one device. +- #368 (2026-10-05): iOS crash on a zero-area polygon. +- #364 and #366: `forceGLSurfaceView` is broken in 1.4.4. +- #367: an open PR for AGP 9. We use AGP 8.11.1, so this doesn't affect us yet. + +**SDK-level docs** +- Locale: `NaverMap#setLocale(Locale)`, `NaverMapOptions#locale(Locale)`, and iOS `NMFMapView.locale` all say "지도의 로캘을 지정합니다. 기본값은 시스템 로캘". The supported-language list appears only in the Flutter plugin's comment above, not in the native reference. Sources: [Android NaverMap ref](https://navermaps.github.io/android-map-sdk/reference/com/naver/maps/map/NaverMap.html), [iOS NMFMapView ref](https://navermaps.github.io/ios-map-sdk/reference/Classes/NMFMapView.html) (fetched). +- Paths: "PathOverlay 는 경로선을 나타내는 데 특화된 오버레이… 테두리와 패턴 이미지… 진척률… 경로선과 겹치는 마커 및 심벌을 숨길 수도 있습니다." Source: [guide 5-7](https://navermaps.github.io/android-map-sdk/guide-ko/5-7.html) (fetched). +- Clustering: native marker clustering via `ClusteringKey`. Source: [guide 5-8](https://navermaps.github.io/android-map-sdk/guide-ko/5-8.html) (fetched). + +**NCP Maps pricing** +- Product page pricing table: "Maps 이용요금은 요청된 호출수 기준으로 부과됩니다." + - Dynamic Map: "6,000,000건 이하 무료 … ※ 대표 계정 1개에 한해 월 6,000,000건의 무료 이용량을 제공합니다." + - Static Map, Geocoding, Reverse Geocoding: 3,000,000/month each. + - Directions 5: 60,000/month. Directions 15: 3,000/month. + - The over-quota price cells show only **"-"**. + - "AI NAVER API 내 지도 API 상품은 무료 이용량을 제공하지 않습니다." + - The feature table marks Dynamic Map as available on **Web JS, Android SDK and iOS SDK (O/O/O)**, so the 6M free tier covers the mobile SDK. + - Source: [ncloud.com/product/applicationService/maps](https://www.ncloud.com/product/applicationService/maps) (fetched as raw HTML on 2026-10-29; the page is JS-rendered, so I extracted the table text). +- "Maps API는 대표 계정에 한하여 무료 이용량을 제공합니다. 대표 계정이 아닐 경우에는 과금이 되므로 주의". The 대표 계정 is the account that used the Maps API first. Default call limits per application: Dynamic Map 6,000,000 (maximum 15M/day, 45M/month). Daily and monthly limits and alerts can be set. Source: [Maps Application guide, updated 2026-10-15](https://guide.ncloud-docs.com/docs/application-maps-app-vpc) (fetched). +- The older [Maps 개요](https://guide.ncloud-docs.com/docs/maps-overview) page lists free usage only for Web Dynamic Map, Static Map and Geocoding. **The newer pages above supersede it** (inference). +- Over-quota unit price: 0.1원/건 for Dynamic Map, from the 2025 change notice (snippet, earlier notes §2). It's **not shown on the current product page, so it's unconfirmed** for the new Maps product. +- Billing unit: "Mobile Dynamic Map의 경우 지도 뷰가 1회 생성될 때 1건" (snippet, NCP forum FAQ). + +### Q2. The Kakao Flutter packages + +**`kakao_maps_flutter`** +- 0.2.2 published 2026-08-24, after 0.2.1 (2026-07-01) and 0.2.0 (2026-06-29). Requires **Dart ^3.12.0 and Flutter >=3.44.0**, which conflicts with our 3.41.4. 351 downloads in 30 days, 9 likes. Repo: 4 stars, 2 open PRs. Sources: [pub.dev API](https://pub.dev/api/packages/kakao_maps_flutter), [GitHub](https://github.com/seunghwanly/kakao_maps_flutter) (fetched). +- Native dependencies: Android `com.kakao.maps.open:android:2.15.1` (minSdk 23); iOS `KakaoMapsSDK ~> 2.12.19` (iOS 13). Source: build.gradle and podspec (fetched). +- `KakaoMapController` methods include markers, marker styles from bytes, InfoWindows, camera (`CameraUpdate.fromBounds`), `getViewportBounds`, POI visibility, logo and compass, and LOD marker layers. **There is no polyline, shape or route-line method.** `addLodMarkerLayer` is "Mobile only" and maps to native `LodLabelLayer`, which uses competition and hiding rather than count clusters. `addWebMarkerClusterer` is "Web only". There is **no language method**. Source: [controller source](https://github.com/seunghwanly/kakao_maps_flutter/blob/main/lib/src/controller/kakao_map_controller.dart) (fetched). +- The README says "Separate agreement to Kakao Maps SDK Terms of Use required" and "Quota limits may apply for commercial use". Source: [pub.dev](https://pub.dev/packages/kakao_maps_flutter) (fetched). + +**`kakao_map_plugin`** +- 1.0.0 published 2026-09-11, after 0.4.0 (2026-01-20). Requires Dart >=3.3 and Flutter >=3.19. 2,192 downloads in 30 days, 48 likes. One open issue, #72 (2026-10-23): "Android build fails with Gradle 9 / AGP 9". Sources: [pub.dev API](https://pub.dev/api/packages/kakao_map_plugin), [GitHub](https://github.com/johyunchol/kakao_map_plugin) (fetched). +- README: "네이티브 라이브러리를 사용한 것이 아닌 Javascript 라이브러리를 이용하여 제작… Android, iOS 는 `webview_flutter` 로". Support: Android SDK 19/20+, iOS 9.0+. Requires `usesCleartextTraffic="true"`. Source: [README](https://github.com/johyunchol/kakao_map_plugin/blob/main/README.md) (fetched). +- Supported features: + - `Polyline` set, `onPolylineTap`, `getPolylineLength` + - `Clusterer(markers, styles: [ClustererStyle.material(...)])` + - `MarkerIcon.fromWidget(...)`, plus `KakaoMapWidgetOverlay`: real Flutter widgets that follow the map once per frame. The README advises using `CustomOverlay` beyond a few hundred. + - `fitBounds(points, padding: 48)`, `panToBounds` + - Limitation: iOS `reload()` is unsupported + - Source: README (fetched). +- Kakao JS API documentation: the full doc page (2.1 MB) contains no `language`, `locale`, 영문 or 다국어 option. So labels are **Korean only** (fetched; absence-based inference). Source: [apis.map.kakao.com/web/documentation](https://apis.map.kakao.com/web/documentation/). + +### Q3. Multilingual labels + +- **Naver:** ko, en, ja and zh, set per map at runtime (see Q1). How completely POIs are translated is **not verified**, so the spike should check it. +- **Kakao Maps SDK v2 (native Android):** `boolean setPoiLanguage(String language)` is documented as "지도의 POI 언어를 변경한다. 한국어는 "ko", 영어는 "en"", alongside `String[] getSupportedLanguages()`. Whether ja and zh are supported is **unconfirmed**; the docs name only ko and en. The iOS reference page wasn't reachable, so iOS parity is unconfirmed. Source: [KakaoMap reference](https://apis.map.kakao.com/android_v2/reference/com/kakao/vectormap/KakaoMap.html) (fetched). Neither Flutter package wraps this method (fetched, Q2). +- **Google:** + - Supported languages include ko, ja, en, zh-CN and zh-TW. "By default the API will attempt to load the most appropriate language based on the users location or browser settings." Source: [Google Maps FAQ](https://developers.google.com/maps/faq) (fetched). + - On Android, map text labels "are displayed in the appropriate language based on the user's device settings and location". Source: [Configure a map](https://developers.google.com/maps/documentation/android-sdk/configure-map) (snippet). + - `google_maps_flutter` has no per-map language parameter, so you **can't force English on a Korean-language phone** (inference). +- **Google data limits in Korea:** the coverage table row for South Korea shows Map Tiles 2D/3D "⬤", Geocoding "⬤", and Traffic Layer, Driving Directions/Snap to Roads, Biking Directions and Walking Directions all "—" ("not available … or low data quality"). Source: [Google coverage](https://developers.google.com/maps/coverage) (fetched). + - The 1:5,000 map-data export was conditionally approved on 2026-02-27, but no navigation launch had been reported as of Aug 2026 (earlier notes §3, snippet). I found no newer launch news (snippet search, 2026-10-29). + +### Q4. Terms + +**TourAPI** +- 한국관광공사_국문 관광정보 서비스_GW: "**이용허락범위 제한 없음**". It offers only Visitkorea content "저작권 등에 구애 없이 자유롭게 활용 가능한 정보만을 선별". Photo restrictions: no defamatory use and no corporate CI/BI use; images are 공공누리 Type 1 and Type 3. Development accounts get 1,000 calls/day, and production accounts can raise that by registering a use case. Source: [data.go.kr 15101578](https://www.data.go.kr/data/15101578/openapi.do) (fetched). +- So TourAPI coordinates can be stored in our DB and shown on any map (inference). Credit photos according to their 공공누리 type. + +**Kakao Local results on a Naver map: confirmed, nothing newer contradicts it** +- 2026-06-29, reply by "Map": "말씀하신 형태로 타사 지도SDK와 혼용하여 사용하셔도 약관 상 이슈는 없습니다. 다만… 실시간 호출 기반의 사용만 가능하며, 데이터 저장 등의 목적으로는 호출이 불가". Source: [DevTalk 150397](https://devtalk.kakao.com/t/local-api-sdk/150397) (fetched). +- 2026-08-18: "다른 지도 API 와 조합하여 사용하시는 것을 제한하지는 않습니다만…". The reply has no staff badge, but the answer is official in style. Source: [DevTalk 151190](https://devtalk.kakao.com/t/local-api/151190) (fetched). +- **Newest (2026-10-19 and 10-20), reply by "Map":** + - "좌표와 주소 정보를 저장하여 활용하는 형태는 허용하지 않습니다". + - Publishing data as a static JSON file is not allowed. + - "장소ID, URL은 저장하여 카카오맵으로 랜딩하는 구조로 활용할 수 있습니다". + - "장소ID 및 URL만 저장하고 별도 확보한 주소를 공공기관 API로 변환한 좌표로 저장하여 지도에 표시하는 방식은 가능합니다". + - This doesn't contradict the earlier answers; it **adds a storable-coordinate path** through public geocoding. + - Source: [DevTalk 152228](https://devtalk.kakao.com/t/api-id/152228) (fetched). + +**Logo and attribution** +- **Naver SDK:** "네이버 로고는 비활성화할 수 없으나, 위치를 조정하거나 클릭을 비활성화할 수는 있습니다. 네이버 지도 SDK를 사용하는 앱은 반드시 네이버 로고가 앱의 UI 요소에 가려지지 않도록 해야 합니다." If logo click is disabled, the app must provide a menu that opens the legal notice and open-source license screens. Source: [guide 4-1](https://navermaps.github.io/android-map-sdk/guide-ko/4-1.html) (fetched). + - The Flutter plugin exposes `logoAlign`, `logoMargin`, `logoClickEnable`, `openLegalNotice()` and `openMapOpenSourceLicense()` (fetched). + - NCP also requires that Maps feature names stay unchanged (for example, 거리뷰 can't be called 로드뷰), and treats calls outside the official path as abuse. Source: [maps-spec](https://guide.ncloud-docs.com/docs/maps-spec) (fetched). +- **Kakao SDK v2:** the logo is bottom-right by default. "이에 대한 변경이나 가려짐은 허용되지 않습니다". Repositioning with `getLogo().setPosition()` is allowed when your UI covers it, and brief temporary covering is OK. Source: [precautions](https://apis.map.kakao.com/android_v2/docs/getting-started/precautions/) (fetched). Note that `kakao_maps_flutter` lists "Logo Visibility ❌ on Android" (fetched). +- **Google:** Places content must be shown on a Google map, and only place_id may be stored (earlier notes §3/§7, fetched). Showing our own or TourAPI data on a Google map is not restricted by that rule (inference). +- I did **not** find a Naver rule against showing third-party POIs (Kakao or TourAPI) on a Naver map. No clause-level Naver terms were read, so this is **unconfirmed**. The SDK is a general overlay API (inference). + +--- + +## Recommendation + fallback + +**MVP: Naver Maps via `flutter_naver_map` 1.4.4, with an NCP "Maps" (new product) subscription on the company's 대표 계정.** + +Why: +- It's the only option that is native, mature (about 29k downloads/month), compatible with our Flutter 3.41 and AGP 8.11 today, and covers all four map needs. It also lets us set label language per map (ko/en, with ja/zh later), independent of the device locale. +- Cost at 10k MAU is 0원 (about 180k of the 6M/month free calls). +- Pair it with: + - Kakao Local REST through the NestJS proxy (no caching; store only place_id and place_url) + - TourAPI and public-geocoded coordinates in our own DB + - Kakao or TMAP for route geometry, drawn as `NPathOverlay` +- Apply our brand style with the Map Style Editor (`customStyle`). + +**Fallback: `google_maps_flutter`.** Use 2.18.1 on the current Flutter, or 2.19+ after upgrading to Flutter ≥3.47. +- Upside: native and best-maintained, labels in ko/en/ja/zh, free map loads, and clustering and polylines are standard. +- Downsides: + - Labels follow the **device** language only. + - No Google routing or traffic in Korea, so we'd keep Kakao or TMAP routes. + - Korean base-map detail is reported to be thinner (inference). + - Google Places must not be mixed in (we don't plan to use them anyway). +- **Korean-only last resort:** `kakao_map_plugin` 1.0.0 (WebView). It has full features but carries a WebView performance risk, Korean-only labels, and an AGP 9 build issue. +- **Don't choose `kakao_maps_flutter` now.** It has no polylines, needs Flutter ≥3.44, and has about 350 downloads/month. Revisit if it adds RouteLine and language support. + +**Risk list** +1. **Single maintainer and stale release.** No release since 2025-12-20; the plugin pins Naver SDK 3.23.0 while native is at 3.24.0, and v2 is in progress on a branch. Mitigation: be ready to fork; the license is BSD-3. +2. **Clustering bugs** #349 (flicker) and #363 (custom icon reverts) hit our map tab directly. Mitigation: test in the spike; if they reproduce, cluster on the server or in Dart and draw plain `NMarker`s. +3. **Multiple map instances.** #321 (two-map gestures), #365 (Android 30–32 leak), and #347 (iOS leak) affect us, since the course map, map tab and co-edit screen each hold a map. Mitigation: reuse one map per screen and avoid stacking map routes. +4. **The NCP free tier depends on the 대표 계정.** A non-대표 account is billed from the first call, and the over-quota price isn't shown on the product page. Mitigation: register on the account that will own production, and set NCP usage limits and alerts. +5. **Label translation coverage** in en/ja/zh isn't verified. POIs we add ourselves need our own i18n names whatever the base map. +6. **AGP 9 / Gradle 9** will break both Naver (PR #367 open) and `kakao_map_plugin` (#72) when we upgrade. Hold AGP at 8.x until they're fixed. + +--- + +## 1-day spike checklist (go/no-go for `flutter_naver_map`) + +**Setup (about 1h)** +- [ ] On the account that will own production: NCP console > Maps > subscribe to the new Maps product > Application with "Dynamic Map". Confirm the 대표 계정 check passes, write down the over-quota price the console shows, and set daily and monthly limits plus an alert. +- [ ] Add `flutter_naver_map: 1.4.4` to `flutter/`. `flutter build apk` and `flutter build ios --no-codesign` must pass with Flutter 3.41.4, AGP 8.11.1 and iOS 13. Also run on an Android device with Impeller on (default). + +**Course map (about 2h)** +- [ ] Draw 6 numbered #FF5A60 circle markers via `NOverlayImage.fromWidget`, with tap → bottom sheet. +- [ ] Draw an `NPathOverlay` from real Kakao or TMAP walking-route coordinates. +- [ ] Use `NCameraUpdate.fitBounds` with bottom padding sized to the sheet height. +- [ ] Check that the Naver logo stays visible above the sheet (`logoAlign`/`logoMargin`). + +**Map tab (about 2h)** +- [ ] Add 1,000 `NClusterableMarker`s with a custom `clusterMarkerBuilder`. Pan and zoom for 2 minutes on iOS and Android, and **watch for flicker (#349) and icon reverts (#363)**. +- [ ] Wire `onCameraIdle` + `getContentBounds()` → "이 지역 검색" → NestJS proxy → Kakao Local (not cached) → replace markers. + +**Language (about 1h)** +- [ ] Switch `locale` to `NLocale('en')`, then `ja`, then `zh` at runtime. +- [ ] Take screenshots of Myeongdong, Hongdae, Gyeongbokgung, Haeundae and Jeju airport, and count untranslated labels. +- [ ] Compare with Google on an en-US device (about 30 min). + +**Stability (about 1.5h)** +- [ ] Open and close the course map 20 times while the map tab stays alive. Watch memory on an Android 11/12 (API 30–32) emulator for #365 and on iOS for #347. +- [ ] Test gestures on the second map (#321). +- [ ] Co-edit simulation: two devices; one adds or moves markers once a second over a mock WebSocket. Check that the other device updates within 300 ms and doesn't crash after 5 minutes. + +**Data (about 0.5h)** +- [ ] Load TourAPI WGS84 coordinates from our DB, and coordinates geocoded through a public geocoder, onto the Naver map. Check alignment against the base map (target: under 5 m offset). + +**Go/no-go** +- Go if: no crash, flicker that is absent or mitigable, an acceptable memory trend, and readable English labels. +- Otherwise: switch the map layer to `google_maps_flutter`. Data and routing stay the same.