From cf7e18798557a0f5bca4a1afa884dbde0953372d Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Fri, 5 Dec 2025 19:26:11 +0900 Subject: [PATCH] =?UTF-8?q?fix(privacy):=20=EA=B0=9C=EC=9D=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B2=98=EB=A6=AC=EB=B0=A9=EC=B9=A8=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A7=80=EC=98=A4=EC=BD=94=EB=94=A9=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스토어 설명에 네이버 지도앱 공유 링크를 수정하지 않고 그대로 붙여넣어야 한다는 안내를 추가하고, 실제 동작과 맞는 URL 사용 조건을 명시했습니다. - doc/store_desc/privacy_policy.md에 현재 구현 기준(키 기반 네이버 로컬 검색 미사용, 네이버 지도 웹/GraphQL 파싱, VWorld+Nominatim 지오코딩, 기상청 Open API, Google AdMob)을 반영한 개인정보 처리방침을 추가/정리했습니다. - lib/data/api/naver_api_client.dart에서 searchLocal 구현을 변경하여 네이버 로컬 검색 Open API를 더 이상 호출하지 않고, 항상 빈 결과를 반환하면서 디버그 로그만 남기도록 비활성화했습니다. - 네이버 URL/검색으로 가져온 식당 정보를 편집하는 뷰에서 위도/경도 필드를 선택 입력으로 완화하여, 지오코딩 실패 시에도 폼 검증만으로 저장이 막히지 않도록 조정했습니다. - AddRestaurantViewModel._resolveCoordinates에 allowFallbackWhenGeocodingFails 플래그를 추가하고, 네이버 URL 기반 추가 시에는 지오코딩 실패를 현재 위치/기본 좌표로 자동 대체하지 않고 명시적인 오류로 처리하여, 잘못된 주소로 저장되지 않도록 했습니다. --- doc/store_desc/privacy_policy.md | 191 ++++++++++++++++++ doc/store_desc/store_description.md | 4 +- lib/data/api/naver_api_client.dart | 17 +- .../widgets/fetched_restaurant_json_view.dart | 26 ++- .../add_restaurant_view_model.dart | 13 +- 5 files changed, 225 insertions(+), 26 deletions(-) create mode 100644 doc/store_desc/privacy_policy.md diff --git a/doc/store_desc/privacy_policy.md b/doc/store_desc/privacy_policy.md new file mode 100644 index 0000000..dca46c8 --- /dev/null +++ b/doc/store_desc/privacy_policy.md @@ -0,0 +1,191 @@ +# 오늘 뭐 먹Z? 개인정보 처리방침 + +오늘 뭐 먹Z? (이하 “앱”)는 사용자의 개인정보 보호를 최우선 가치로 삼습니다. + +앱은 점심 메뉴 추천 및 맛집 관리 기능 제공을 위해 최소한의 정보만을 사용하며, **개발자가 직접 운영하는 별도 서버를 두지 않습니다.** +다만, 위치 기반 추천, 날씨 정보 제공, 네이버 지도 연동, 광고 노출 및 좌표 확인을 위해 제3자 서비스와 통신할 수 있습니다. + +본 개인정보 처리방침은 앱이 어떤 정보를 어떤 목적과 방식으로 처리하는지 설명합니다. + +--- + +## 1. 수집하는 개인정보 + +### 1-1. 직접 식별 가능한 정보 + +앱은 다음과 같은 의미에서 사용자를 직접 식별할 수 있는 개인정보(이름, 이메일, 전화번호 등)를 **직접 수집하지 않습니다.** + +- 회원가입, 로그인 기능이 없습니다. +- 주민등록번호, 이름, 이메일, 전화번호, 주소 등 사용자를 직접 식별할 수 있는 정보를 요구하지 않습니다. +- 개발자가 운영하는 자체 서버로 사용자의 개인정보를 수집·저장하지 않습니다. + +### 1-2. 서비스 제공을 위해 처리되는 정보 + +앱 기능 제공을 위해 다음과 같은 정보가 기기 내에서 처리되거나 제3자 서비스로 전송될 수 있습니다. + +1) **위치 정보** + +- 내용: 현재 위치 좌표(위도, 경도) +- 사용 목적 + - 주변 맛집 추천 및 거리 계산 + - 날씨(기상청 Open API) 조회 + - 현재 위치 반경 내 추천 후보를 제한하는 데 활용 +- 처리 방식 + - 위치 정보는 주로 **실시간 계산 및 API 호출**에 사용되며, 사용자의 “위치 이력”을 장기적으로 별도 저장하지 않습니다. + - 앱에서 저장하는 위치 정보는 주로 “식당 좌표(맛집 위치)”이며, 이는 사용자의 거주지나 신원과 직접 연결되지 않습니다. + - 위치 정보는 기상청 Open API 등 날씨 서비스에 한해, 격자 좌표(nx, ny) 형태로 전송될 수 있습니다(자세한 내용은 아래 3절 참조). + +2) **맛집 및 방문 기록 정보** + +- 내용 + - 사용자가 직접 입력하거나 네이버 지도에서 가져온 식당 정보 + - 식당 이름, 카테고리, 설명, 전화번호, 도로명/지번 주소 + - 위도·경도, 주소, 영업시간 등 + - 방문 기록 및 통계 정보 + - 방문 일자, 방문 여부, 방문 횟수 등 +- 사용 목적 + - 점심 메뉴 추천, 중복 방문 방지, 방문 기록 조회, 통계 제공 등 +- 처리 방식 + - 위 정보는 모두 **사용자의 기기 내 로컬 데이터베이스**에만 저장됩니다. + - 개발자는 이 데이터를 서버를 통해 열람하거나 수집하지 않습니다. + +3) **앱 사용 설정 정보** + +- 내용: 알림 시간, 추천 거리/날씨 관련 설정, 다크모드 여부 등 앱 내 환경 설정 +- 사용 목적: 사용자 맞춤 추천 및 알림 제공 +- 처리 방식: 기기 내 로컬 저장소에만 저장되며, 서버로 전송되지 않습니다. + +--- + +## 2. 데이터 저장 및 처리 방식 + +1) **로컬 저장소(기기 내 저장소)** + +- 맛집 정보, 방문 기록, 앱 설정, 일부 캐시 데이터(날씨 등)는 + **기기 내 로컬 데이터베이스에만 저장**됩니다. +- 앱을 삭제하면 일반적으로 해당 앱과 연계된 로컬 데이터도 함께 삭제됩니다. + 다만, 운영체제나 백업 설정에 따라 일부 데이터가 OS 또는 클라우드 백업에 남을 수 있으며, 이는 각 플랫폼(예: Apple, Google)의 정책을 따릅니다. + +2) **네트워크 통신 및 제3자 전송** + +앱은 자체 서버를 운영하지 않지만, 다음과 같은 제3자 서비스와 통신합니다. + +- **지도·식당 정보 제공을 위한 네이버 지도 웹 서비스** + - 전송되는 정보(예시): 사용자가 앱에 붙여넣은 네이버 지도 URL, 해당 URL에 포함된 식당 ID 등 + - 사용 목적: 네이버 지도 페이지 및 관련 API(예: GraphQL)를 통해 식당 이름·주소·좌표·전화번호 등을 조회하고, 앱 내 식당 정보로 변환 + - 현재 버전에서는 네이버의 “로컬 검색 Open API(키 기반 검색)”를 사용하지 않으며, 사용자가 제공한 지도 링크를 기반으로 한 페이지 파싱 방식만 사용합니다. + +- **지오코딩(Geocoding) 서비스: OpenStreetMap Nominatim** + - 전송되는 정보(예시): 사용자가 입력한 식당 주소(도로명·지번 등) + - 사용 목적: 주소를 위도·경도 좌표로 변환하여 지도/거리 계산 및 추천 알고리즘에 활용 + +- **기상청 Open API(공공데이터포털)** + - 전송되는 정보(예시): 위치를 기반으로 변환된 격자 좌표(nx, ny) + - 사용 목적: 현재 및 단기(1시간 후) 날씨 정보 조회 + +- **광고 네트워크(Google AdMob 등)** + - 자세한 내용은 아래 3절 “광고 및 제3자 서비스” 참조 + +앱 개발자는 이들 제3자 서비스의 서버에 저장되는 데이터에 직접 접근하지 않으며, +제3자 서비스에서 수집·처리하는 정보는 각 서비스의 개인정보 처리방침을 따릅니다. + +--- + +## 3. 광고 및 제3자 서비스 + +앱은 무료 제공을 위해 광고를 노출할 수 있으며, 이 과정에서 **Google AdMob** 등 제3자 광고 네트워크가 참여합니다. + +### 3-1. 광고 네트워크가 수집할 수 있는 정보 + +앱은 직접 사용자의 개인정보를 수집하지 않지만, 광고 네트워크는 다음과 같은 정보를 수집·처리할 수 있습니다(예시). + +- 광고 식별자 (예: Android 광고 ID, IDFA 등) +- 기기 정보 (단말기 모델명, OS 버전, 언어/국가 설정 등) +- 대략적인 위치 정보 (국가/지역 수준, IP 기반 위치 등) +- 앱 사용 정보 (광고 조회/클릭 여부, 광고 노출 횟수 등) + +이러한 정보는 **개발자가 아닌 광고 네트워크 사업자**가 수집·처리하며, +수집 범위와 이용 목적은 해당 사업자의 개인정보 처리방침을 따릅니다. + +보다 자세한 내용은 각 서비스의 정책을 참고하세요. + +- Google AdMob / Google Mobile Ads: https://policies.google.com/privacy +- 네이버(Naver): https://policy.naver.com/ +- 공공데이터포털·기상청 Open API: 각 제공 기관의 개인정보 처리방침 + +### 3-2. 제3자 서비스 관련 안내 + +- 앱은 제3자 서비스에 사용자의 이름, 이메일, 전화번호 등 **직접 식별 정보**를 의도적으로 전송하지 않습니다. +- 위치 정보, 검색어, 기기 정보 등은 제3자 서비스의 기술적 처리 과정에서 사용될 수 있습니다. +- 제3자 서비스에서 제공하는 맞춤형 광고 또는 추천 기능 등은 해당 서비스의 정책에 따라 동작합니다. + +--- + +## 4. 권한 사용 + +앱은 기능 제공을 위해 다음과 같은 권한을 사용할 수 있습니다. +각 권한은 **명시된 목적 외의 용도로 사용되지 않습니다.** + +1) **위치 권한** + +- 사용 목적 + - 현재 위치를 기준으로 주변 맛집을 추천하기 위해 사용 + - 현재/1시간 후 날씨 정보를 조회하기 위해 사용 + - 추천 대상이 되는 식당 범위를 “현재 위치 반경”으로 제한하기 위해 사용 +- 특징 + - 위치 권한을 거부할 경우, 앱은 서울 시청(기본 좌표)을 기준으로 동작합니다. + - 사용자의 위치 이력을 장기적으로 추적하거나, 별도 계정과 연결하여 프로파일링하지 않습니다. + +2) **알림 권한** + +- 사용 목적 + - 점심 식사 후 방문 기록을 남기도록 안내하는 **방문 확인 알림** 발송 + - 알림 진동·소리, 정확한 시각의 알림 예약, 기기 재부팅 후 예약 알림 복원 +- 특징 + - 알림 내용에는 주로 추천된 식당 이름, 방문 여부 확인 요청 등의 간단한 메시지가 포함됩니다. + - 알림 권한을 거부해도 앱의 기본 사용은 가능하나, 방문 리마인더 기능은 제한될 수 있습니다. + +3) **블루투스 권한** + +- 사용 목적 + - 주변 기기와 **맛집 리스트를 공유**하기 위해 사용 + - 팀/동료와 함께 맛집 목록을 교환하는 기능에 활용 +- 특징 + - 공유 대상 데이터는 식당 이름, 주소, 카테고리 등으로, 사용자의 이름·이메일 등 직접 식별 정보는 포함하지 않습니다. + - 공유는 기기간 통신을 전제로 하며, 개발자가 운영하는 중앙 서버를 경유하지 않습니다(향후 실제 Bluetooth 스택 도입 시에도 동일 원칙을 적용합니다). + +4) **네트워크 권한** + +- 사용 목적 + - 네이버 지도 페이지 및 관련 API 호출(붙여넣은 지도 링크 기반 식당 정보 조회) + - 지오코딩 서비스(OpenStreetMap Nominatim) 및 기상청 Open API 호출(좌표·날씨 정보) + - 광고(Google AdMob) 로딩 및 통계 전송 +- 특징 + - 앱은 별도의 자체 백엔드 서버를 운영하지 않으며, 네트워크 요청은 위와 같은 제3자 API·광고 서비스에 한정됩니다. + +--- + +## 5. 아동의 개인정보 + +- 앱은 성인(직장인 등 일반 사용자)을 주요 대상으로 설계되었습니다. +- 만 14세 미만(또는 각 국가에서 정한 연령 기준 미만)의 아동을 대상으로 개인정보를 수집하려는 의도가 없습니다. +- 만약 아동의 개인정보가 부주의로 수집된 사실을 인지하게 될 경우, 가능한 한 신속히 해당 정보를 삭제하기 위한 조치를 취하겠습니다. + +--- + +## 6. 개인정보 처리방침의 변경 + +- 본 개인정보 처리방침은 서비스 개선, 관련 법령 및 가이드라인 개정, 기능 추가/변경 등에 따라 수시로 수정될 수 있습니다. +- 중요한 내용(수집 항목, 이용 목적, 제3자 제공 등)이 변경되는 경우, 앱 내 공지 또는 스토어 설명 등을 통해 변경 내용을 안내하겠습니다. +- 변경된 개인정보 처리방침은 명시된 시행일로부터 효력이 발생합니다. + +**시행일자: 2025.12.05** + +--- + +## 7. 문의처 + +앱의 개인정보 처리방침과 관련하여 문의, 의견 제출, 권리 행사(열람, 정정, 삭제 요청 등)가 필요하신 경우 아래 연락처로 문의해 주세요. + +- 담당자: 네이처브릿지AI 앱개발팀 +- 이메일: naturebridgeai@gmail.com diff --git a/doc/store_desc/store_description.md b/doc/store_desc/store_description.md index 00b36e2..bedbfc8 100644 --- a/doc/store_desc/store_description.md +++ b/doc/store_desc/store_description.md @@ -21,7 +21,7 @@ - 한식·중식·일식·카페 등 카테고리를 선택해, 그날 기분에 맞는 맛집만 골라서 추천 2. 네이버 지도 연동 맛집 수집 -- 네이버 지도앱에서 공유한 링크(naver.me 등)를 그대로 붙여넣으면, 가게 이름·주소·카테고리·좌표를 자동으로 불러와 등록 +- 네이버 지도앱의 ‘공유’ 기능으로 복사한 링크(naver.me 등)를 **수정하지 말고 그대로** 붙여넣으면, 가게 이름·주소·카테고리·좌표를 자동으로 불러와 등록 - 회사 구내식당이나 단골 분식집은 직접 입력으로 손쉽게 등록 - 메모, 전화번호까지 함께 저장해 두고, 동료에게 설명할 때도 한 번에 보여줄 수 있습니다. @@ -49,4 +49,4 @@ - 캘린더와 통계 화면 덕분에, 점심 시간이 단순 소비가 아니라 나만의 작은 라이프로그가 됩니다. - 팀 점심·회식까지 한 앱으로 해결해, 메뉴 선택 스트레스를 팀 전체에서 줄일 수 있습니다. - 지금 ‘오늘 뭐 먹Z?’를 설치하고, 한국 직장인의 가장 큰 고민 중 하나인 점심 메뉴 선택을 1분 안에 끝내 보세요. -※ 본 서비스는 현재 한국(대한민국) 지역에서만 사용 가능합니다. \ No newline at end of file +※ 본 서비스는 현재 한국(대한민국) 지역에서만 사용 가능합니다. diff --git a/lib/data/api/naver_api_client.dart b/lib/data/api/naver_api_client.dart index 1ee5b95..d7a4403 100644 --- a/lib/data/api/naver_api_client.dart +++ b/lib/data/api/naver_api_client.dart @@ -34,9 +34,12 @@ class NaverApiClient { _proxyClient = NaverProxyClient(networkClient: _networkClient); } - /// 네이버 로컬 검색 API 호출 + /// 네이버 로컬 검색 API 호출 (현재 비활성화됨) /// - /// 검색어와 좌표를 기반으로 주변 식당을 검색합니다. + /// 개인정보 처리방침 및 운영 정책에 따라 + /// 네이버 로컬 검색 Open API(키 기반 검색)는 사용하지 않는다. + /// 이 메서드는 네트워크 요청을 보내지 않고 항상 빈 리스트를 반환한다. + /// (향후 정책 변경 시, 기존 구현을 복원하여 사용할 수 있다.) Future> searchLocal({ required String query, double? latitude, @@ -45,14 +48,10 @@ class NaverApiClient { int start = 1, String sort = 'random', }) async { - return _localSearchApi.searchLocal( - query: query, - latitude: latitude, - longitude: longitude, - display: display, - start: start, - sort: sort, + AppLogger.debug( + '[NaverApiClient] searchLocal 호출됨 - 로컬 검색 Open API는 현재 비활성화 상태입니다.', ); + return []; } /// 단축 URL을 실제 URL로 변환 diff --git a/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart b/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart index ae1bcb0..0e5a559 100644 --- a/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart +++ b/lib/presentation/pages/restaurant_list/widgets/fetched_restaurant_json_view.dart @@ -183,12 +183,11 @@ class _FetchedRestaurantJsonViewState extends State { ), onChanged: widget.onFieldChanged, validator: (value) { - if (value == null || value.isEmpty) { - return '위도를 입력해주세요'; - } - final latitude = double.tryParse(value); - if (latitude == null || latitude < -90 || latitude > 90) { - return '올바른 위도값을 입력해주세요'; + if (value != null && value.isNotEmpty) { + final latitude = double.tryParse(value); + if (latitude == null || latitude < -90 || latitude > 90) { + return '올바른 위도값을 입력해주세요'; + } } return null; }, @@ -208,14 +207,13 @@ class _FetchedRestaurantJsonViewState extends State { ), onChanged: widget.onFieldChanged, validator: (value) { - if (value == null || value.isEmpty) { - return '경도를 입력해주세요'; - } - final longitude = double.tryParse(value); - if (longitude == null || - longitude < -180 || - longitude > 180) { - return '올바른 경도값을 입력해주세요'; + if (value != null && value.isNotEmpty) { + final longitude = double.tryParse(value); + if (longitude == null || + longitude < -180 || + longitude > 180) { + return '올바른 경도값을 입력해주세요'; + } } return null; }, diff --git a/lib/presentation/view_models/add_restaurant_view_model.dart b/lib/presentation/view_models/add_restaurant_view_model.dart index cf0625f..cd34f01 100644 --- a/lib/presentation/view_models/add_restaurant_view_model.dart +++ b/lib/presentation/view_models/add_restaurant_view_model.dart @@ -304,6 +304,7 @@ class AddRestaurantViewModel extends StateNotifier { jibunAddress: state.formData.jibunAddress, fallbackLatitude: fetchedData.latitude, fallbackLongitude: fetchedData.longitude, + allowFallbackWhenGeocodingFails: false, ); restaurantToSave = fetchedData.copyWith( @@ -436,6 +437,7 @@ class AddRestaurantViewModel extends StateNotifier { required String jibunAddress, double? fallbackLatitude, double? fallbackLongitude, + bool allowFallbackWhenGeocodingFails = true, }) async { final parsedLat = double.tryParse(latitudeText); final parsedLon = double.tryParse(longitudeText); @@ -468,8 +470,17 @@ class AddRestaurantViewModel extends StateNotifier { ); } else { state = state.copyWith( - geocodingStatus: '지오코딩 실패: $address, 현재 위치/기본 좌표로 대체', + geocodingStatus: '지오코딩 실패: $address, 주소를 다시 확인해 주세요.', ); + + if (!allowFallbackWhenGeocodingFails) { + state = state.copyWith( + errorMessage: + '주소가 지도에서 인식되지 않습니다. ' + '도로명 주소 전체를 정확히 입력했는지 확인해 주세요.', + ); + throw Exception('지오코딩 실패: $address'); + } } }