Files
sodalive-ios/docs/20261029_추천_서브_배너/prd.md
T

24 KiB

추천 서브 배너 PRD

문서 정보

항목 내용
문서 상태 구현·로컬 검증 완료
작성일 2026-10-29
최종 수정일 2026-10-30
대상 제품 홈·콘텐츠 추천 서브 배너
작성자·결정권자 작성: 구현 에이전트, 제품 결정: 사용자
관련 API Contract 없음, 계약은 이 PRD에 임시 기록
관련 구현 계획 plan-task.md
관련 review 통합 리뷰

요구사항 상태

상태 의미 구현 처리
확정 사용자 요청으로 명시된 요구 구현 기준
미결 최종 제품 선택이 필요한 UX 결정 전 임의 구현 금지
권고 검토를 위한 제안 기본값 승인 전 수용 기준 아님
외부 의존 서버 응답·정책 제공 필요 제공 전 추정 구현 금지

1. Overview

기존 홈 추천과 오디오 추천 조회 응답의 data.subBanners를 사용해 각 추천 화면에 배너 목록을 표시한다. 별도 endpoint나 추가 조회 없이 서버 순서와 자격 필터링 결과를 유지하고, 플랫폼별로 열 수 있는 링크만 처리한다.

2. Problem Statement

두 추천 화면에는 배치 목적이 다른 서브 배너가 필요하다. 기존 BannerCarousel은 최대 20개 제한, 정사각형 이미지, 좌우 여백을 전제로 하므로 이 요구의 배너 목록과 원본 이미지 비율을 그대로 충족하지 않는다. 기존 컴포넌트를 그대로 재사용할 수 없다.

3. Goals

  • 기존 응답의 subBanners를 홈과 콘텐츠 추천의 지정 위치에 표시한다.
  • 서버가 정한 순서를 보존하고 placement별로 분리해 보여 준다.
  • 안전한 플랫폼별 링크 열기를 제공하고, 이미지·링크 오류가 추천 화면 전체를 막지 않게 한다.

4. Non-Goals

  • /api/v2/sub-banners 추가, 두 번째 요청, placement query 추가는 하지 않는다.
  • 기존 data 필드나 banners를 바꾸지 않는다.
  • Android 전용 배너 노출 또는 Android 링크 실행을 지원하지 않는다.
  • 클라이언트 날짜 계산, 타이머를 이용한 노출 자격 판정, 설치 가능 여부 사전 필터링을 하지 않는다.
  • 기존 메인 배너 캐러셀의 동작을 변경하지 않는다.

5. Target Users and Permissions

  • 비로그인 사용자와 로그인 사용자는 모두 추천 조회 결과의 서브 배너를 본다.
  • 비로그인 요청에는 Authorization을 보내지 않는다.
  • 로그인 요청은 기존 인증 헤더를 그대로 유지한다.
  • 확인한 두 TargetType은 현재 Authorization: Bearer {token} 헤더를 항상 구성한다. 익명에서 헤더를 생략하라는 요구와 차이가 있어, 공통 plugin 동작 및 endpoint별 수정 seam은 구현 전 확인이 필요하다.

6. 핵심 사용자 흐름

  1. 홈 또는 콘텐츠 추천 화면이 기존 추천 API를 한 번 호출한다.
  2. HTTP 200이며 envelope의 success == true인 응답에서 data.subBanners를 읽는다.
  3. 화면별 placement에 해당하며 iOS 또는 웹 링크가 하나 이상 유효한 배너만 서버 순서대로 표시한다.
  4. 사용자가 배너를 탭하면 iOS 앱 링크를 먼저 열고 실패할 때 웹 링크를 한 번 시도한다.
  5. 두 링크가 모두 없거나 열기에 실패해도 추천 화면은 계속 사용할 수 있다.

7. 정보 구조와 라우팅

홈 추천: 최근 활동 크리에이터 다음
콘텐츠 추천: 기존 상단 배너 다음, 독점 시리즈 이전
공용 신규 컴포넌트 위치: SodaLive/Sources/V2/Component/Banner

8. 기능 요구사항

8.1 추천 API와 응답

ID 상태 요구사항 수용 기준 계약/Goal 연결
SB-001 확정 GET /api/v2/home/recommendations와 GET /api/v2/audio/recommendations 응답의 기존 envelope success/message/data/errorProperty 및 기존 data 필드·banners를 보존하고 data.subBanners를 추가한다. 두 경로에서 기존 필드가 유지되고 서브 배너를 같은 응답에서 읽는다. 기존 API 응답
SB-002 확정 API 성공은 HTTP 200과 success == true를 모두 만족해야 한다. HTTP 비 200 또는 success == false는 성공 데이터·빈 성공으로 취급되지 않는다. SB-001
SB-003 확정 별도 /api/v2/sub-banners 요청이나 두 번째 요청, placement query를 추가하지 않는다. 추천 화면 진입 시 서브 배너 때문에 추가 네트워크 요청이 발생하지 않는다. SB-001
SB-004 확정 비로그인 요청에서 Authorization을 생략하고 로그인 요청은 현재 endpoint의 기존 헤더를 보존한다. 양쪽 인증 상태에서 헤더가 기대값과 일치하며 다른 기존 헤더도 유지된다. 기존 인증 정책
SB-005 확정 서버는 id 오름차순으로 정렬하고 날짜 자격 조건을 적용한 목록을 내려준다. 클라이언트는 순서를 보존하며 날짜나 timer로 자격을 다시 판정하지 않는다. 제공된 계약 기반 fixture의 순서가 화면까지 유지되고 클라이언트 날짜 기반 제외·재정렬이 없다. 사용자 제공 구현 완료 계약

8.2 SubBanner 데이터와 자격

ID 상태 요구사항 수용 기준 계약/Goal 연결
SB-006 확정 SubBanner 필드는 id(JSON integer), placement(정확히 HOME_RECOMMENDATION 또는 CONTENT_RECOMMENDATION), imageUrl(절대 http(s) URL), nullable webUrl, androidDeepLink, iosDeepLink다. 추가 서버 필드를 가정하지 않는다. 제공된 필드만으로 디코딩하고 알 수 없는 필드는 요구하지 않는다. 서버 응답 계약
SB-007 확정 id는 정밀도 손실 없는 native Int decoding 전략으로 처리한다. 큰 정수 식별자가 반올림·문자열 변환 없이 정확히 보존된다. SB-006
SB-008 확정 placement를 화면별로 분리하고 리스트의 서버 순서를 그대로 유지한다. 홈에는 홈 placement만, 콘텐츠에는 콘텐츠 placement만 표시하며 각 내부 순서가 입력과 같다. SB-005, SB-006
SB-009 확정 iOS/web URL 유효성 중 하나 이상을 만족하는 항목만 노출한다. Android 링크만 있는 항목은 제외하며 앱 설치 가능 여부를 사전 확인하지 않는다. Android-only 항목은 숨기고, 설치 상태 조회로 제외되는 항목이 없다. SB-006
SB-010 확정 필터 후 목록이 비면 배너 영역과 그 영역으로 추가된 간격을 모두 숨긴다. 빈 목록 전후에 서브 배너 전용 영역 높이나 간격이 남지 않는다. SB-008, SB-009

8.3 노출과 상호작용

ID 상태 요구사항 수용 기준 계약/Goal 연결
SB-011 확정 자격을 통과한 전체 목록을 캐러셀에 제공한다. 20개 제한 없이 모든 항목을 순환 대상으로 유지한다. 21개 이상 목록의 마지막 항목까지 도달 가능하고 입력 순환 순서가 보존된다. SB-008
SB-012 확정 홈에서는 최근 활동 크리에이터 다음, 콘텐츠에서는 기존 상단 배너 다음이자 독점 시리즈 이전에 둔다. 주변 기존 섹션과 동일한 SodaSpacing.s24 간격을 사용한다. 기존 메인 배너를 이동하지 않고 새 섹션을 지정 위치에 추가하며 비어 있을 때 추가 간격이 없다. SB-010
SB-013 확정 배너는 가로 전체 폭, 수평 padding 0으로 표시하고 이미지 원본 비율을 유지한다. 양쪽 화면 경계까지 폭을 쓰며 이미지를 정사각형으로 강제하지 않는다. SB-012
SB-014 확정 5초마다 자동 순환하고 현재 이미지 원본 비율에 맞춰 높이를 변경한다. 기존 패턴의 swipe를 제공하고 항목이 하나면 timer를 실행하지 않는다. 자동 전환 전후 이미지 비율과 높이가 일치하고 빈 목록이면 컴포넌트 자체가 없다. OQ-001
SB-015 확정 기존 메인 배너 캐러셀 동작은 바꾸지 않고, 새 공용 코드는 V2/Component/Banner 아래에 둔다. 기존 메인 캐러셀에 변경이 없고 서브 배너 전용 요구가 기존 제약과 섞이지 않는다. SB-011, SB-013

8.4 링크와 오류 처리

ID 상태 요구사항 수용 기준 계약/Goal 연결
SB-016 확정 웹 URL은 scheme이 http 또는 https이고 host가 있어야 한다. 판정은 대소문자를 구분하지 않는다. host 누락, 다른 scheme, 잘못된 URL은 web 후보로 인정되지 않는다. SB-009
SB-017 확정 deep link는 절대 URI여야 하며 javascript, data, file, content scheme은 대소문자 무관하게 거부한다. 상대 URI와 금지 scheme은 실행되지 않는다. 허용 URI는 검증용 파싱 후에도 원래 문자열 그대로 전달한다. SB-009
SB-018 확정 사용자 추천안을 구현 기본값으로 채택한다. 유효한 iOS 링크를 먼저 한 번 열고, 없거나 열기에 실패하면 유효한 웹 링크를 한 번 연다. Android 링크는 쓰지 않는다. iOS 성공 시 웹 시도 0회, iOS 실패 및 웹 유효 시 웹 시도 1회, 모두 실패 시 비치명 상태를 검증한다. SB-016, SB-017
SB-019 확정 이미지 로드 실패와 개별 링크 실패는 해당 배너에만 국한한다. 실패한 이미지나 링크가 다른 배너 및 추천 화면 로딩·사용을 막지 않는다. SB-018
SB-020 확정 추천 조회 실패를 빈 성공이나 빈 배너 목록으로 바꾸지 않고 기존 추천 오류 의미를 유지한다. HTTP 비 200, success == false, 디코딩·전송 실패가 기존 추천 오류 처리로 전달되고 정상 empty와 구분된다. SB-002

9. UI/UX Expectations

  • 홈과 콘텐츠 화면의 기존 VStack 섹션 간격을 기준으로 서브 배너를 배치한다. 배너가 비면 그 섹션 때문에 추가된 간격도 없어야 한다.
  • 이미지 및 링크의 오류는 국소 처리한다.
  • page indicator는 기존 우측 상단 현재/전체 표시를 따른다. 이미지 오류는 기존 회색 placeholder 계열로 국소 처리하고 별도 오류 팝업을 추가하지 않는다.
  • 구현 기본값: 이미지 치수를 알기 전 임의 원본 비율을 만들지 않는다. 최초 치수가 없으면 이미지 영역 높이를 예약하지 않고, 다음 이미지 로딩 중에는 마지막 확인된 높이를 유지할 수 있다. 로드 성공 후 현재 이미지 비율로 높이를 갱신한다.
  • 콘텐츠의 AudioRecommendationsResponse.isEmpty는 현재 기존 배열만 검사한다. 서브 배너만 있는 응답도 표시되도록 유효 서브 배너 목록을 빈 화면 판정에 포함해야 한다.
  • 기존 BannerCarousel은 최대 20개, 5초 timer, 좌우 여백이 있는 정사각형 이미지 및 우측 상단 page count 표시를 가진다. 따라서 그대로 재사용할 수 없다.

10. API 계약

10.1 공통 규칙

  • 성공 조건: HTTP 200 및 success == true.
  • envelope: 기존 success, message, data, errorProperty 보존.
  • data의 기존 필드와 banners 보존, subBanners 추가.
  • subBanners 항목: id: JSON integer, placement, imageUrl, nullable webUrl, nullable androidDeepLink, nullable iosDeepLink.
  • 서버는 id 순서 및 날짜 자격 필터링을 담당한다. 이는 사용자가 구현 완료로 제공한 확정 계약이며 재확인으로 구현을 차단하지 않는다. 실제 서버 실행 검증 여부는 별도로 기록한다.
  • 공개 DTO에 startDate, endDate, isActive를 추가하지 않는다. 정상 빈 결과는 subBanners: []이며 삭제된 별도 endpoint의 오류 status는 가정하지 않는다.

10.2 Endpoint 추적

요구사항 Method Path 계약 상태
SB-001~020 GET /api/v2/home/recommendations 사용자 제공 구현 완료 계약, placement HOME_RECOMMENDATION
SB-001~020 GET /api/v2/audio/recommendations 사용자 제공 구현 완료 계약, placement CONTENT_RECOMMENDATION

11. 보안과 데이터 취급

  • 사용자가 제공한 URL을 실행하기 전 scheme, host, 절대 URI 조건을 검증한다.
  • 금지 deep-link scheme을 대소문자 무관하게 차단한다.
  • URI를 임의 정규화하지 않으며 검증에 통과한 입력 문자열을 그대로 연다.
  • 인증 헤더는 로그인 여부에 따른 기존 정책을 유지한다.

12. 성능과 품질 요구사항

  • 추가 요청 없이 기존 추천 응답에 포함된 목록을 쓴다.
  • 이미지 크기는 실제 이미지 치수로 계산하며 개별 로딩 실패는 추천 응답 오류로 승격하지 않는다.
  • 검증은 API 디코딩, 화면별 placement/순서, URL 판정 및 링크 fallback, 이미지·링크 실패 격리를 다룬 fixture 기반 테스트를 포함해야 한다.
  • 아래 수용 시나리오는 요구사항 검증 기준이며 아직 실행되지 않았다.
  • fixture 검증은 명시적인 테스트 모드에 한정한다. 운영 요청에서 fixture를 자동 사용하거나 통신 오류를 fixture로 대체하지 않는다.

13. 성공 기준

13.1 기능 수용 기준

  • 두 추천 endpoint의 production DTO/API/repository 검사에서 기존 필드·banners와 통합 subBanners 소비를 확인했다.
  • HTTP 200과 success == true가 함께 만족될 때만 성공이며, false/비200 실패를 실제 VM 검사로 확인했다.
  • 익명 Authorization 생략, 로그인 헤더 보존, 추가 endpoint/request/query 없음을 production API/repository 검사로 확인했다.
  • Int 정밀도 및 23개 목록 순서 보존, native에서 21번째 항목 도달을 확인했다.
  • placement 필터 분리와 빈 필터 결과를 검사하고 native composition에서 empty 추가 gap0을 확인했다.
  • web http(s)+host 및 deep 절대 URI/금지 scheme을 production URL 검사로 확인했다.
  • URI 원문 및 iOS/웹/Android 8조합의 opener 시도 순서·횟수를 driver로 확인했다. 실제 외부 앱 launch 검증과 구분한다.
  • 이미지 실패 placeholder의 국소 처리와 링크 실패 종료를 native harness/production opener 검사로 확인했다.
  • 실행하지 않은 physical input·실서비스·전체 앱 UI 검증은 별도 한계로 명시했다.

13.2 화면 수용 기준

  • 홈 위치는 최근 활동 크리에이터 다음이다.
  • 콘텐츠 위치는 상단 배너 다음이자 독점 시리즈 이전이다.
  • 섹션 간격은 기존 s24이며, 비어 있으면 컴포넌트와 추가 간격이 없다.
  • native 양 화면에서 가로 전체 폭, 수평 padding 0, 두 원본 비율 높이를 확인했다.
  • 기존 메인 배너 캐러셀의 코드·연결은 변경하지 않았다.

14. Open Questions

ID 상태 결정 필요 사항 현재 권고 결정 주체 결정 시점
OQ-001 해소 전환 주기와 높이, 빈 목록 처리 사용자 승인: 5초 자동 전환, 현재 이미지 원본 비율 높이, 빈 목록 컴포넌트 미표시. swipe/단일 항목 timer 중지는 기존 패턴을 따른다. 사용자 확인 완료
OQ-002 해소 이미지 로딩·실패 표시 기존 회색 placeholder 계열, 첫 치수 확인 전 임의 비율 없음, 개별 오류 격리. §9 구현 기본값 적용. 구현 담당 구현 기본값
OQ-003 해소 indicator의 표시 여부와 모양 기존 우측 상단 현재/전체 표시를 따른다. 구현 담당 기존 패턴
OQ-004 부분 확인 정확한 섹션 spacing과 추천 요청의 인증·실패 처리 seam 두 View의 VStack은 SodaSpacing.s24다. 두 API target의 익명 헤더 및 HTTP status 검증은 구현 단계에서 최소 수정한다. 공통 plugin 영향은 추가 코드 확인 대상이다. 구현 담당, 사용자 질문 불필요 구현 전
OQ-005 해소 새 필드 fixture와 서버 정렬·날짜 필터링 계약 사용자 메시지에 계약과 홈 두 항목/빈 목록 및 콘텐츠 빈 목록 예시가 제공됐다. 실제 서버 검증과 계약 확정은 구분한다. 구현 담당 확인 완료

15. Decision Log

날짜 ID 상태 결정 근거 영향 요구사항
2026-10-29 DEC-001 확정 기존 두 추천 응답에 subBanners를 추가하며 별도 조회는 하지 않는다. 사용자 요구 SB-001~004
2026-10-29 DEC-002 확정 링크 검증과 iOS 우선 후 웹 fallback을 요구하고 Android 링크를 사용하지 않는다. 사용자 요구 SB-016~018
2026-10-29 DEC-003 권고 5초 자동 순환 등 UX 기본값은 승인 전까지 제안으로만 둔다. 기존 BannerCarousel의 5초 설정은 참고 근거이며 신규 UX 결정은 아님 SB-014, OQ-001
2026-10-29 DEC-004 정정 대기 정확한 spacing/auth/error seam은 추가 코드 확인 전 확정하지 않는다. 현재까지 조회한 추천 화면·API와 기존 carousel 코드 OQ-004
2026-10-29 DEC-005 정정 DEC-002의 링크 우선순위는 사용자 권고안이며 최종 설계 승인 전 확정으로 취급하지 않는다. URI 안전성과 Android 실행 금지는 확정이다. 최초 요청의 계약/추천 구분 SB-018
2026-10-29 DEC-006 정정 서버 정렬·기간 필터와 응답 예시는 이미 제공된 확정 계약이다. 기존 섹션 간격 s24 및 콘텐츠 isEmpty 수정 필요를 코드로 확인했다. 사용자 계약, MainHomeRecommendationView:27, MainContentRecommendationView:28, AudioRecommendationsResponse:13 SB-005, SB-012, OQ-004~005
2026-10-29 DEC-007 확정 worktree를 만들지 않고 현재 작업 트리에서 구현. 5초 자동 전환, 현재 이미지 원본 비율 높이, 빈 목록 컴포넌트 미표시. 사용자 후속 답변 SB-010, SB-014, 실행 방식
2026-10-29 DEC-008 확정 나머지는 기존 패턴과 사용자 추천안을 기본값으로 적용: swipe/페이지 카운터, 단일 항목 timer 없음, iOS 우선 후 웹 1회 대체, 실패 팝업 추가 없음. 사용자 진행 승인 및 기존 앱 패턴 지시 SB-014, SB-018, OQ-002~003

16. 요구사항 추적표

요구사항 범위 API 계약 계획 Phase 자동 검증 수동 검증
SB-001~005 §10 미작성 fixture에서 envelope, status, auth, 요청 횟수 확인 요청 헤더와 추가 호출 없음 확인
SB-006~010 §10 미작성 lossless 정수, placement, 순서, empty gap 검사 두 화면에서 분리 및 빈 상태 확인
SB-011~015 §8.3 미작성 21개 이상 입력 및 배치 검증 폭·비율·섹션 위치 확인
SB-016~020 §8.4 미작성 URL 안전성, 원문 보존, fallback 횟수, 실패 격리 실제 대상 없이 mock open 결과로 흐름 확인

17. 변경 관리

  • UX 결정이 추가되면 먼저 Open Questions와 Decision Log를 갱신한다.
  • 사용자 답변과 설계 승인을 PRD에 반영한 뒤 계획/TASK 문서를 작성한다. 이미 제공된 서버 계약을 다시 요청하지 않는다.
  • 이 문서의 수용 시나리오는 아직 실행하지 않았으며 구현 완료를 뜻하지 않는다.

탐색 검증 기록

  • 2026-10-29: 기존 carousel, 두 추천 화면, 콘텐츠 DTO 및 관련 문서 샘플을 읽고 요구사항을 대조했다. 탐색 결과 중 기존 메인 배너 이동·정사각형 재사용 제안은 사용자 조건과 충돌하여 채택하지 않았다.
  • Xcode 26.0과 swift/xcrun은 사용 가능하다. 부팅된 시뮬레이터는 없으며 빌드·테스트·네이티브 QA는 미실행이다. 현재 도구 목록에 lsp_diagnostics가 없어 해당 검증을 수행했다고 주장하지 않는다.
  • 제품 코드 수정 없이 PRD만 작성했다. 5초 자동 전환과 서로 다른 원본 비율에 따른 현재 배너 높이 변경 질문에 사용자 답변 대기 중이다.

구현·검증 후속 기록

  • 2026-10-30: 위 탐색 기록 이후 사용자 답변 DEC-007과 실행 기준 DEC-008에 따라 구현했다. 데이터/API/opener 집중 checks, 전체16개 local scripts, 양 앱 Debug simulator build가 통과했다.
  • 최신 native 증거는 native-final/README.md다. 최종 캐러셀 hash 7e25db3076d545ffc61ea3a1c5585e7b8a617964ef406d7570286d75a75e010a와 snapshot·실행 전후 해시가 일치한다. 이전 native 증거는 최종 gesture 변경 전 기록으로 보존한다.
  • 네이티브 검증은 실제 production 두 View composition/캐러셀과 주변 대역을 사용하는 명시 harness다. physical tap/swipe, 실제 외부 앱 열기, Reduce Motion ON, 실서비스·실기기·전체 앱 UI는 미검증이며 LSP 도구는 제공되지 않았다.
  • 독립 리뷰의 소스/증거 불일치는 최신 native 재실행으로 해소했다. 최종 판정 APPROVE, 남은 차단 항목 없음. worktree 없이 구현했고 소유 QA 자원은 정리했다.

후속 버그: 스와이프 중 배너 탭 실행 (2026-10-30)

  • 사용자 재현: 다음 이미지로 넘기려고 드래그했지만 이전 이미지의 링크가 실행된다.
  • SB-021 확정: 배너를 넘기는 수평 드래그와 인식된 짧은 드래그·세로 스크롤은 링크를 실행하지 않는다. 정상 탭은 현재 표시 항목을 정확히 한 번 실행하며 스와이프 직후 탭도 유실하지 않는다.
  • 원본 비율·5초 자동 전환·전체 목록·빈 영역 숨김·두 화면 배치·링크 정책은 변경하지 않는다. worktree 없이 기존 작업에 이어 수정한다.
  • 기존 native 증거는 physical tap/swipe 미검증이었다. 이번 검증은 실제 XCTest 좌표 tap/drag를 사용하며 스크립트가 state/opener를 직접 호출한 결과로 대체하지 않는다.
  • SB-022 확정: 마지막→첫 번째 및 첫 번째→마지막 순환도 일반 전환과 같은 방향의 연속 슬라이드로 표시한다. 경계에서 전체 목록을 역방향으로 빠르게 이동하거나 순간 교체하지 않는다. 자동 5초 전환과 수동 swipe 모두 적용한다.
  • 추가 사용자 보고의 코드 원인: move(by:) 경계 분기가 disablesAnimations = true로 실제 마지막/첫 번째 사이 이동 자체를 수행한다. 기존 main BannerCarousel처럼 복제 경계 항목으로 애니메이션 후 동일 이미지 위치로만 무애니메이션 정렬하는 방식을 참고한다.
  • 후속 완료: primitive button style의 배타적인 drag/tap 인식과 전역 drag 좌표, 복제 경계 페이지의 연속 slide/동일 이미지 정렬로 수정했다. 실제 swipe 오탭 RED 및 native11개 GREEN, 자동/수동 경계 중간 프레임 확인. 부모도 native11개·기존16개 check·양 앱 build 통과를 확인했다. Phase3 리뷰 APPROVE.

후속 사용자 결정: 서브 배너 카운트 숨김 (2026-10-30)

  • DEC-009 확정: 홈·콘텐츠 공용 SubBannerCarousel의 우측 상단 현재/전체 배지만 제거한다. §9, OQ-003 및 DEC-008의 시각적 페이지 카운터 기준은 이 결정으로 대체한다.
  • 접근성 페이지/전체 라벨, 탭·스와이프·연속 순환·5초 타이머·레이아웃과 기존 메인 BannerCarousel은 그대로 유지한다. 순수 렌더링 삭제이므로 기존 UI typecheck와 소규모 native XCTest/PNG로 검증하며 새 테스트나 전체 앱 빌드는 추가하지 않는다.