관심상점 및 관심업종 구독 모델 통합

관심상점 및 관심업종 구독 모델 통합

ADR-180 관심상점 및 관심업종 구독 모델 통합

1. Metadata

  • ADR ID: ADR-180
  • Status: draft
  • Date: 2026-03-11
  • Owner: YSY

2. Context

  • 관심상점/관심업종은 개인화 신호이지만, 사용자가 보는 슬롯 리스트의 필터와 충돌할 수 있다.
  • 관심업종 알림은 유용성이 크지만, 이벤트 밀도가 높은 업종에서 푸시 폭주 리스크가 있다.
  • 슬롯 카드는 최소 정보 전달이 원칙이므로 관심상점 표시는 밀도를 해치지 않는 수준으로 제한해야 한다.

3. Domain Decision

  1. 관심상점은 user_favorite_store 도메인 모델로 관리한다.
  2. 관심업종은 user_interest_category 도메인 모델로 관리하며 category_id는 ADR-005 canonical 업종 코드 체계를 따른다.
  3. user_favorite_store(user_id, store_id) 유니크 제약을 강제한다.
  4. user_interest_category(user_id, category_id) 유니크 제약을 강제한다.
  5. 관심 구독 데이터는 알림/추천 입력으로 사용하되, 슬롯 노출/수량/상태 전이의 필수 조건으로 사용하지 않는다.
  6. 구독 등록/해제 요청은 멱등 처리한다.
  7. 슬롯 응답은 사용자 컨텍스트가 있을 때 is_favorite_store 판별 결과를 포함한다.
  8. 관심업종은 강제 노출 조건이 아니라 후순위 랭킹/알림 신호로 정의한다.
  9. 관심업종은 데이터 모델은 유지하되, 기본 운영 상태는 보류(비활성)로 둔다.

4. Product Decision

  1. 관심상점/관심업종 구독은 사용자 선택 기반(옵트인)으로 운영한다.
  2. 구독 알림은 빈도 상한과 수신 거부 경로를 기본 정책으로 제공한다.
  3. 구독 데이터는 개인화 노출/알림의 보조 신호로만 사용하고, 예약 성공 보장을 암시하지 않는다.
  4. 구독 대상 삭제/비활성 시 해당 구독은 자동 비활성 처리한다.
  5. 슬롯 리스트에서는 관심상점에만 채워진 별 마크를 노출하고 비관심 슬롯에는 별 마크를 노출하지 않는다.
  6. 관심업종 등록만으로 현재 적용된 검색 필터를 우회해 강제 노출하지 않는다.
  7. 노출 우선순위는 사용자 명시 필터 > 정책/가용성 조건 > 관심신호 기반 랭킹으로 고정한다.
  8. 기본 정렬은 기존 슬롯 정렬(시간/거리/가용성)을 유지하고, 관심신호는 소폭 가중치(soft boost)만 허용한다.
  9. 관심매장 우선은 기본값이 아닌 사용자 선택 정렬 옵션으로만 제공한다.
  10. 1단계 운영 범위는 관심매장으로 제한하고, 관심업종 사용자 기능은 보류한다.
  11. 관심매장 알림은 즉시 알림을 허용한다.
  12. 보류 중인 관심업종은 UI 진입/알림 발송을 기본 비활성으로 유지한다.
  13. 관심업종 재개는 전환율, 수신거부율, 알림 볼륨 지표가 목표 범위를 충족할 때만 결정한다.
  14. 관심매장 등록의 1차 목적은 재방문 매장의 재예약 시간 단축과 신뢰 매장 재발견 비용 절감으로 정의한다.
  15. 1단계 관심매장 등록 기본 경로는 슬롯 탭 -> 상세 화면으로 고정하고 스와이프 액션은 포함하지 않는다.
  16. 보조 경로는 기본 등록 흐름을 대체하지 않는 범위에서 허용하며, 저장 탭의 최근 본 항목에서 관심매장 추가와 같은 명시적 보조 액션을 제공할 수 있다.
  17. 파트너 화면에는 내 매장을 관심매장으로 등록한 사용자 수를 노출한다.
  18. 파트너용 관심등록 수는 실시간 증감이 아닌 시간 단위(1시간) 집계값으로 제공한다.
  19. 관심매장관심매장 알림은 동일 개념으로 취급하지 않고, 저장/재방문 신호와 알림 발송 권한을 구분된 제품 상태로 다룬다.
  20. 사용자가 알림을 꺼도 관심매장 자체의 효용은 유지되며, 최소 효용은 저장 탭 재진입, 탐색 재노출, 관심매장 우선 정렬 후보, 신뢰 매장 표식으로 정의한다.
  21. 1단계 관심매장 알림은 복잡한 사용자별 세부 필터보다 전역 on/off + 서버 측 빈도 제한을 우선 적용한다.
  22. 사용자별 알림 필터는 필요성이 확인될 경우 즉시/요약, 주중/주말 같은 소수 옵션부터 단계적으로 도입하고, 초기부터 상세 요일/시간/매장별 조건 편집을 기본 제공하지 않는다.
  23. 탐색 카드의 기본 표식은 계속 관심매장(별) 중심으로 유지하고, 알림 상태는 관심매장 맥락이 명확한 화면에서만 추가 표현할 수 있다.
  24. 관심매장 목록 또는 관심매장 맥락에서 다시 노출되는 슬롯에는 알림 상태를 표현하는 아이콘을 둘 수 있다.

5. UX Decision

  1. 사용자가 관심상점을 등록/해제할 수 있는 관리 화면을 제공한다.
  2. 슬롯 카드의 관심상점 마크는 매장명 행 오른쪽 끝(헤더 우측)에 고정 배치한다.
  3. 슬롯 리스트의 마크는 상태 인지용으로 사용하고, 기본 등록/해제 액션은 상세 화면에서 수행한다.
  4. 토글은 낙관적 업데이트를 적용하되 실패 시 즉시 롤백하고 오류 토스트를 노출한다.
  5. 비로그인 상태에서 토글 시 로그인 인터셉트를 호출하고, 로그인 후 원래 액션을 1회 재시도한다.
  6. 구독 해제는 1탭으로 처리하고 되돌리기 경로를 제공한다.
  7. 구독은 알림 보조 기능임을 명시하고 예약 확정 보장으로 오인되지 않게 안내한다.
  8. 관심업종 메뉴/토글은 보류 상태에서 사용자에게 노출하지 않는다.
  9. 저장 탭의 최근 본 항목은 보조 경로로만 관심매장 등록을 지원할 수 있으며, 카드 내부의 명시적 관심매장 추가 액션을 제공할 수 있다.
  10. 이 보조 액션은 최근 본의 원래 목적(상세 재진입)을 해치지 않도록 카드 기본 탭 동작과 명확히 분리되어야 한다.
  11. 보조 액션은 제스처 학습을 요구하지 않는 명시적 버튼 또는 동등한 수준의 직접 조작으로 제공한다.
  12. 보조 액션 성공 시 즉시 관심매장 목록에 반영하고, 이미 등록된 매장/로그인 필요/실패 상태는 인라인 피드백으로 구분해 안내한다.
  13. 저장 탭 보조 액션의 상세 문구/접근성 규칙은 본 ADR에서 확정하지 않고 별도 문서에서 정의한다.
  14. 관심매장 토글과 알림 상태는 UI에서 분리된 의미로 다루고, 사용자가 알림을 끄더라도 별표(저장 상태)는 유지되어야 한다.
  15. 탐색 메인 슬롯 카드에는 기본적으로 별표만 유지하고, 종 아이콘까지 함께 올려 카드 의미를 과도하게 늘리지 않는다.
  16. 알림 아이콘은 저장 탭의 관심매장 목록, 관심매장 섹션, 또는 관심매장으로 재맥락화된 슬롯 목록처럼 이 슬롯이 왜 여기 보이는지가 명확한 문맥에서만 노출한다.
  17. 알림 품질 제어는 초기부터 상세 필터 UI를 크게 노출하기보다 서버 측 중복 억제, 발송 빈도 제한, 전역 on/off를 우선 적용한다.
  18. 사용자별 세부 필터 UI가 필요해지면 즉시/요약, 주중/주말 순으로 도입을 검토하고, 상세 요일/시간대 편집은 후순위로 둔다.
  19. 관심매장 맥락에서 노출되는 알림 아이콘은 상태 인지용으로 사용하고, 필요 시 해당 매장의 알림 on/off 또는 알림 관리 화면 진입점으로 연결할 수 있다.

6. Tech Decision

  1. 관심상점/관심업종 저장 API는 멱등 업서트(upsert)로 구현한다.
  2. 중복 삽입 방지는 (user_id, store_id), (user_id, category_id) 유니크 인덱스로 보장한다.
  3. 슬롯 조회 API는 사용자별 is_favorite_store:boolean을 계산해 반환한다.
  4. 관심상점 토글 API는 등록 upsert해제 delete 모두 멱등 처리한다.
  5. 구독 변경 이벤트는 감사로그와 알림 파이프라인 입력 이벤트로 분리 기록한다.
  6. 알림 후보 생성은 폴링 기반 클라이언트 감시가 아니라 서버 이벤트 트리거 + 비동기 큐로 처리한다.
  7. 관심업종 푸시는 사용자별 일/시간 상한, 업종별 쿨다운, 동일 매장 중복 제거 키를 적용한다.
  8. 이벤트 누락 복구를 위해 저빈도 재조정 배치를 운영하되 실시간 판단 소스는 이벤트 파이프라인으로 고정한다.
  9. 푸시 발송 대상 단말은 user_push_tokens를 SoT로 관리하고, 관심매장 알림 큐는 사용자 단위 레코드로 적재한다.
  10. 관심매장 슬롯 오픈 알림은 slots 발행 이벤트에서 user_notification_queue에 적재한 뒤, 서버 워커/함수에서 실제 푸시 발송을 수행한다.
  11. 관심업종 사용자 기능 플래그는 기본 off로 유지하고, 운영 승인 시에만 on 전환한다.
  12. 관심업종 기능이 off인 환경에서는 관련 알림 후보를 생성하지 않는다.
  13. 슬롯 목록 조회는 선택 정렬 값으로 favorite_first를 받을 수 있으나 기본값은 기존 정렬을 유지한다.
  14. user_favorite_store를 SoT로 두고 partners.favorite_user_count는 1시간 주기 재계산 캐시로 관리한다.
  15. 관심매장 등록/해제 시 카운트 컬럼을 즉시 증감하지 않고 재집계 배치에서만 반영한다.

7. Ops Decision

  1. 관심구독 알림 실패/지연/중복 발송 대응 런북을 운영한다.
  2. 사용자 수신 거부/민원 접수 시 처리 SLA와 재발 방지 절차를 적용한다.
  3. 알림 품질 지표(발송 성공률, 중복률, 거부율)를 주기적으로 모니터링한다.
  4. 장애 복구 재발송은 사용자별 빈도 상한 정책을 위반하지 않도록 제한한다.
  5. 관심업종 푸시 볼륨 급증 시 즉시 적용 가능한 카테고리 푸시 kill-switch를 운영한다.
  6. 관심매장/관심업종별 전환율(열람-예약 전환)과 수신 거부율을 분리 추적한다.
  7. 관심업종은 보류 상태 동안 알림 비활성이 유지되는지 점검 체크를 운영한다.
  8. 관심업종 재개 판단은 월 단위 운영 리뷰에서만 수행한다.
  9. 관심매장 카운트 재집계 배치(1시간)의 성공률/지연/누락을 모니터링한다.

8. Implementation Contract (Optional)

8.1 API Contract

  • 관심상점/관심업종 등록은 업서트(upsert)로 처리한다.
  • 해제 요청은 멱등 삭제로 처리한다.
  • 슬롯 목록 응답은 is_favorite_store를 포함한다.
  • 슬롯 목록 정렬 파라미터는 기본값을 기존 정렬로 유지하고, 선택값 favorite_first를 허용한다.
  • 비로그인 슬롯 목록 응답은 is_favorite_store=false로 고정하고 토글 액션은 인증 필요 오류를 반환한다.
  • 관심업종 기능 플래그가 off이면 관심업종 등록/해제 API는 FEATURE_DISABLED를 반환한다.

8.2 Data Contract

  • user_favorite_store(user_id, store_id) 유니크 제약을 강제한다.
  • user_interest_category(user_id, category_id) 유니크 제약을 강제한다.
  • user_interest_category.category_id는 ADR-005 canonical 업종(DINING|CAFE_DESSERT|BAR|BEAUTY|FITNESS|ACTIVITY|WELLNESS|ETC)만 허용한다.
  • user_push_tokens.expo_push_token은 전역 유니크여야 하며, 동일 토큰은 동시에 하나의 사용자에게만 활성 연결된다.
  • user_notification_queue는 최소 user_id, slot_id, channel, status, payload, dedupe_key를 포함해야 한다.
  • 알림 레이트 리밋 키는 최소 user_id + subscription_type + time_bucket 단위로 저장한다.
  • partners.favorite_user_count는 캐시 컬럼이며 SoT는 user_favorite_store로 고정한다.

8.3 Error/Observability Contract

  • 비활성/삭제 대상 구독 요청은 SUBSCRIPTION_TARGET_NOT_AVAILABLE로 반환한다.
  • 구독 변경 이벤트와 알림 발송 이벤트를 분리 기록한다.
  • 관심업종 푸시 상한 초과는 PUSH_RATE_LIMITED 이벤트로 기록한다.
  • 슬롯 카드 별표 토글 실패는 favorite_toggle_failed 이벤트로 기록한다.

8.4 Test/Acceptance Contract

  • 동일 대상 중복 등록 시 레코드가 중복 생성되지 않아야 한다.
  • 구독 해제 후 알림 후보에서 즉시 제외되어야 한다.
  • 슬롯 카드에서 관심상점에만 채워진 별 마크가 노출되고 비관심 슬롯에는 별 마크가 노출되지 않아야 한다.
  • 업종 필터가 카페일 때 관심업종 마트 슬롯은 강제 노출되지 않아야 한다.
  • 기본 정렬에서는 기존 슬롯 정렬 기준이 유지되어야 하고, favorite_first 선택 시에만 관심매장 우선 정렬이 적용되어야 한다.
  • 관심매장 등록 기본 흐름은 슬롯 탭 -> 상세 화면에서 완료되어야 한다.
  • 1단계 릴리스에서 스와이프 액션 기반 관심매장 등록은 제공되지 않아야 한다.
  • 롱프레스 보조 경로를 제공하는 경우 팝업에 관심매장 등록/해제 액션이 노출되어야 한다.
  • 파트너 매장정보에서 관심등록 수가 표시되어야 하며 최대 1시간 지연을 허용해야 한다.
  • 관심매장 알림은 즉시형을 허용해야 한다.
  • 사용자가 관심매장 알림을 꺼도 관심매장 저장 상태 자체는 유지되어야 한다.
  • 탐색 메인 슬롯 카드에는 기본적으로 별표만 노출되고, 알림 아이콘은 관심매장 맥락 화면에서만 노출되어야 한다.
  • 관심업종 기능 재개 시 알림은 사용자별 일 상한을 초과해 발송되지 않아야 한다.
  • 관심업종 기능 플래그가 off인 동안 관심업종 UI가 노출되지 않아야 한다.
  • 관심업종 기능 플래그가 off인 동안 관심업종 알림 후보가 생성되지 않아야 한다.

8.5 Scenario Contract

  • 시나리오 A (리스트 탐색): 사용자가 슬롯 리스트를 볼 때 관심매장에만 헤더 우측 별 마크가 보여야 하며 리스트 밀도가 과도하게 증가하지 않아야 한다.
  • 시나리오 B (정렬 선택): 기본 진입에서는 기존 정렬이 유지되고, 사용자가 관심매장 우선 정렬을 선택한 경우에만 관심매장이 앞쪽에 배치되어야 한다.
  • 시나리오 C (등록 동선): 사용자가 슬롯을 탭해 상세로 진입한 뒤 관심매장 등록/해제를 수행할 수 있어야 한다.
  • 시나리오 C-1 (보조 경로): 롱프레스가 활성인 빌드에서는 간단 팝업에서 관심매장 등록/해제 액션을 수행할 수 있어야 한다.
  • 시나리오 C-2 (파트너 지표): 파트너가 매장정보 화면에서 관심등록 수를 확인할 수 있어야 하며 값은 시간 단위로 갱신되어야 한다.
  • 시나리오 C-3 (알림 분리): 사용자가 관심매장을 유지한 채 알림만 끌 수 있어야 하며, 저장/재방문 기능은 계속 동작해야 한다.
  • 시나리오 D (필터 충돌): 사용자가 업종 필터를 적용하면 관심업종 신호는 랭킹에만 영향 주고 필터 결과 자체를 바꾸지 않아야 한다.
  • 시나리오 E (보류 상태): 관심업종 기능이 비활성인 릴리스에서는 관심업종 UI/알림이 동작하지 않아야 한다.
  • 시나리오 F (재개 이후): 관심업종 기능 재개 시 일 상한/쿨다운/중복제거 정책으로 푸시 폭탄이 발생하지 않아야 한다.
  • 시나리오 G (실시간성): 슬롯 오픈 이벤트가 발생하면 서버 이벤트 파이프라인을 통해 관심구독 알림 후보가 생성되어야 하며 클라이언트 폴링은 필수가 아니어야 한다.

9. Validation

  • Domain/Product/UX/Tech/Ops 결정이 충돌하지 않는다.
  • 구현 기준은 SPEC과 정합성을 유지한다.
  • 운영 절차는 RUNBOOK으로 연결된다.
  • 필터 우선순위(명시 필터 > 정책/가용성 > 관심신호)가 앱/서버에서 동일하게 적용된다.
  • 관심업종 푸시 상한 및 kill-switch가 운영 대시보드에서 즉시 제어 가능하다.
  • 관심업종 보류 상태에서 UI/알림/API가 비활성 정책과 일치한다.