v2 신규. 푸시 알림은 두 채널로 발송된다 — 하이브리드 앱(Android WebView)의 Expo Push와 웹 브라우저(PWA)의 web-push(VAPID). 수신 기기는 push_tokens에 함께 저장되고(platform으로 구분) 같은 스위치(MY-04)로 관리된다. 웹 자체 알림함(인앱 알림 목록)은 없다.
| ID | 기능명 | 상세 설명 | 처리 기준 · 예외 | 채널 |
| PUSH-04 | 발송 제외 규칙 | 본인 행동에 대한 알림은 발송하지 않음(내 글에 내가 단 댓글, 내 댓글에 내가 단 답글, 내 글 좋아요). | 게시글 작성자 = 원댓글 작성자인 경우 더 구체적인 “답글” 알림 1건만 발송(중복 방지). 댓글 수정·삭제, 댓글 좋아요는 알림 없음. 좋아요 취소 시 미발송. | 앱 · 웹 |
| PUSH-05 | 알림 탭 이동 | 알림에 해당 게시글 URL이 포함되어, 탭하면 앱/브라우저가 해당 게시글 화면을 연다. | 웹은 서비스가 열린 창이 있으면 그 창을 게시글로 이동시켜 포커스하고, 없으면 새 창을 연다. | 앱 · 웹 |
| PUSH-06 | 기기 토큰 등록 (앱) | 앱이 WebView에 푸시 토큰을 주입하면, 웹이 로그인 사용자와 연결해 서버에 자동 등록(기기당 1건, 계정 전환 시 갱신). | 비로그인 상태에서는 등록되지 않음. 만료된 토큰은 발송 실패 감지 시 자동 삭제. | 앱 |
| PUSH-11 | 웹 푸시 구독 (PWA) v2 신규 | 마이페이지 푸시 스위치(MY-04)를 웹 브라우저에서 켜면 알림 권한 요청 → 서비스 워커(sw.js) 등록 → VAPID 구독 생성 순으로 이 브라우저를 수신 기기로 등록한다(push_tokens에 platform="web"으로 저장). 페이지가 닫혀 있어도 브라우저가 워커를 깨워 알림을 표시한다. PWA 매니페스트를 제공해 홈 화면 설치를 지원한다. | iOS 사파리는 홈 화면에 설치된 PWA에서만 웹 푸시 수신 가능. 만료된 구독(알림 차단·구독 해제)은 발송 실패(404/410) 감지 시 자동 삭제. VAPID 키(환경 변수) 미설정 시 웹 채널만 건너뛴다(앱 발송 무영향). 서비스 워커는 푸시 수신·클릭 라우팅 전용(오프라인 캐싱 없음). | 로그인 |
| PUSH-07 | 수신 설정 반영 | 마이페이지 스위치(MY-04)로 끈 사용자는 발송 대상에서 제외(발송 직전 서버에서 필터링). | 알림 종류별 세분화 설정은 없음(전체 on/off 단일 설정). 설정은 웹·앱 모든 기기에 일괄 적용. | 앱 · 웹 |
| PUSH-08 | 발송 실패 무영향 | 푸시 발송은 백그라운드로 처리되어, 실패해도 댓글 작성·좋아요 등 원 기능에는 영향 없음. 채널별 발송(Expo · web-push)은 서버 공통 모듈이 담당한다. | 발송 API는 요청자 세션·소유권을 서버에서 재검증(위조 요청 차단). | 앱 · 웹 |
| PUSH-09 | 앱 링크 연결 | 도메인-앱 연결 선언 파일(assetlinks.json)을 제공해, 결제 등 외부 앱에서 서비스 주소로 복귀할 때 브라우저 대신 하이브리드 앱이 열리도록 지원. 앱이 받는 경로는 결제 복귀 경로만(/payment/* · /mypage/membership/billing). | 스토어 배포 시 릴리즈 키 지문으로 교체 필요(현재 개발용). 도메인 전체를 잡으면 README QR의 /download까지 앱 안(WebView)에서 열려 다운로드 알림이 설치로 이어지지 않는다 — 앱 v1.0.4에서 경로를 좁혔다. 앱 안에서 APK 링크를 만나면 시스템 브라우저로 넘긴다. | 앱 |
| PUSH-10 | 앱 테마 연동 | 웹의 현재 테마(라이트/다크)가 바뀔 때마다 WebView 메시지로 앱에 전달되어, 네이티브 셸(상태바 · 세이프에어리어)이 웹 테마를 따라간다. | 웹 단독(브라우저) 사용 시에는 동작하지 않음. COM-16 다크모드와 연동. | 앱 |
| PUSH-12 | 앱 오프라인 · 로드 실패 화면 v2 신규 | 앱에서 네트워크가 끊기거나(NetInfo) 페이지 로드에 실패하면(WebView onError) WebView 내장 에러 페이지 대신 앱 자체 화면을 전체 화면으로 덮는다. 구성은 404 화면(COM-11)의 디자인 언어를 그대로 옮겨 브랜드 그라데이션 원 아이콘(wifi-off) + “오프라인이에요” + “인터넷 연결을 확인한 뒤 다시 시도해 주세요.” + 상태 칩 “네트워크에 연결되어 있지 않아요” + [다시 시도] · [홈으로 이동] 2단 CTA + Wi-Fi · 데이터 · 비행기 모드 점검 칩. [다시 시도]는 연결을 다시 확인한 뒤 실패한 페이지를 새로고침하고, [홈으로 이동]은 WebView를 다시 마운트해 첫 화면부터 불러온다. | 연결이 복구되면 실패했던 페이지를 자동으로 다시 불러온다(NetInfo 이벤트 → reload). 화면 색은 웹 테마(PUSH-10)를 따라 라이트/다크로 바뀌며, 테마 신호를 받기 전(콜드 스타트 오프라인)에는 라이트. 웹 서비스 워커에는 오프라인 캐싱이 없어(PUSH-11) 브라우저 단독 사용 시에는 브라우저 기본 오프라인 페이지가 뜬다. 앱 v1.0.1(versionCode 2)부터. | 앱 |
| PUSH-13 | 앱 셸 조작 · 복구 | 네이티브 셸이 담당하는 기본 조작 — Android 당겨서 새로고침(웹 스크롤이 최상단일 때만 활성), iOS 스와이프 뒤로/앞으로 · 당겨서 새로고침, Android 하드웨어 뒤로가기(현재 경로 기준 — PUSH-14), 첫 로딩 동안 브랜드 스플래시 노출 — 시스템 스플래시는 배경색만(아이콘 없음) 보이다가 300ms 페이드로 그라데이션 + 로고 + “IVE FAN COMMUNITY” 오버레이로 이어지고, 웹 첫 로드가 끝나면 걷힌다(아이콘만 먼저 떴다가 로고+타이틀로 바뀌면 위치가 어긋나 보여 2단계를 하나로 합침). | 스플래시는 페이드 뒤 로고가 최소 1초는 보이도록 유지하고(빠른 회선의 깜빡임 방지, 로드가 그보다 길면 즉시), 10초를 넘기면 스피너를 붙이며, 15초 안전장치로 반드시 걷힌다. 히어로 배경 영상 등 무음 자동재생을 허용한다(WebView 기본값은 사용자 조작 필요). iOS에서 WebView 프로세스가 종료되면 빈 화면이 되므로 자동으로 다시 불러온다. 결제(토스 → 카드사 앱 → 복귀)가 여러 외부 도메인을 경유하므로 http/https는 전부 WebView 안에서 처리하고, intent:// 등 앱 호출 스킴만 밖으로 넘긴다. 앱 안에서는 핀치 · 더블탭 확대를 막는다 — 웹 AppViewportLock이 앱 셸(window.ReactNativeWebView)을 감지하면 viewport 메타를 maximum-scale=1, user-scalable=no로 바꾼다(Android WebView · iOS WKWebView 모두 동적 변경을 반영). 확대되면 하단 탭바 · 시트가 화면 밖으로 밀려 되돌리기 어렵기 때문이며, 모바일 브라우저는 접근성을 위해 확대를 그대로 둔다. 웹 배포만으로 적용돼 앱 업데이트가 필요 없다. Next 가 라우트 이동(뒤로가기 포함)마다 viewport 메타를 새 요소로 갈아끼우므로 MutationObserver로 매번 다시 잠그고, CSS touch-action: pan-x pan-y로 핀치 · 더블탭 제스처 자체도 막는다(2026-09-15 — 뒤로가기 뒤 다시 확대되던 문제의 수정). | 앱 |
| PUSH-14 | 앱 뒤로가기 기준 · 라우팅 브리지 v2 신규 | Android 하드웨어 뒤로가기는 웹 히스토리가 아니라 현재 경로로 판단한다(유튜브 · 인스타 방식) — 홈(/)에서는 “한 번 더 누르면 앱이 종료돼요” 토스트 뒤 2초 안에 한 번 더 누르면 종료, 하단 탭 루트(/news · /discography · /shop · /board)에서는 히스토리와 무관하게 홈으로, 그 외(상세 · 마이페이지 · 결제 · 외부 도메인)는 웹 히스토리를 따르되 뒤가 없으면(알림 딥링크 진입) 홈으로. 어느 화면에서든 “상세 → 탭 루트 → 홈 → 종료” 세 단계 안에 끝난다. 웹 NavigationBridge가 라우트가 바뀔 때마다 WebView 메시지(route)로 현재 경로를 알리고, 셸이 “홈으로”를 요청할 진입점 window.__iveNavigate(href, replace)(Next 라우터)를 열어 전체 리로드 없이 이동한다. | 셸은 WebView 내비 이벤트의 URL(외부 도메인은 무시)과 route 메시지 중 마지막 값을 쓴다. 진입점이 없는 페이지(외부 결제 도메인 · 구버전 웹)는 location.replace로 홈을 다시 연다. 하단 탭바(COM-22)는 앱 안에서만(window.ReactNativeWebView 감지) 탭 전환을 replace로 해 히스토리를 쌓지 않고, 모바일 브라우저는 push를 유지한다. 탭 루트 목록은 앱 TAB_ROOT_PATHS와 웹 GNB_ARRAY를 같이 바꿔야 한다. 이전(v1.0.5까지)에는 히스토리 우선이라 탭을 오간 만큼 뒤로가기가 이어졌다. 앱 v1.0.6(versionCode 7)부터. 시트 · 모달이 열려 있으면 경로 판단 전에 그것만 닫는다 — 웹이 열린 Radix 오버레이(role="dialog" · data-state="open")를 overlay 메시지로 알리고, 셸이 window.__iveCloseOverlay()(document Escape → 최상위 레이어만 닫힘)를 부른다. 앨범 상세 시트(DISC-05)처럼 URL 이 안 바뀌는 전체 화면 UI 에서 뒤로가기가 홈으로 가 버리던 것의 수정. 앱 v1.0.7(versionCode 8)부터. | 앱 |
| PUSH-15 | iOS 시스템 탭바 (Liquid Glass) v2 신규 | iOS 앱은 웹 하단 탭바(COM-22) 대신 iOS 시스템 탭바를 쓴다 — 홈 · 소식 · 음악 · 굿즈샵 · 자유게시판 5탭(SF Symbol 아이콘, 선택 색 보라 #A94FC0 / 다크 #DB97E9). iOS 26의 Liquid Glass가 그대로 적용돼 탭을 누르면 렌즈가 커지고, 탭바를 누른 채 끌면 렌즈가 손가락을 따라 움직이다 놓은 탭으로 이동한다. 탭마다 WebView가 따로 있어 탭별 스크롤 · 방문 기록이 유지되고, 같은 탭을 다시 누르면 그 탭 루트로 돌아간다. 알림 · 딥링크로 열리면 경로가 속한 탭(/board/… → 자유게시판)으로 전환한 뒤 그 탭에서 연다. | 웹 코드 변경 없이 앱이 페이지에 스타일을 주입(adoptedStyleSheets)해 웹 하단 탭바를 숨기고 --tabbar-h(MOB-09)를 세이프에어리어(= 시스템 탭바 높이)로 덮어써, 본문 하단 여백 · 플로팅 요소가 시스템 탭바 위로 올라간다. Android는 시스템 탭바가 Material 3 모양이라 웹 글라스 탭바를 그대로 쓴다. iOS 27은 Scene 생명주기를 쓰지 않는 앱을 실행 직후 종료하므로 셸이 Scene 방식으로 시작한다. 탭 목록은 앱 src/tabs.ts · TAB_ROOT_PATHS와 웹 GNB_ARRAY를 같이 바꿔야 한다. 앱 v1.0.8(ive-app docs/decisions.md §10)부터 — iOS 앱은 아직 배포 전(유료 개발자 계정 필요). | 앱 |
README·QR이 릴리스 파일(APK)을 직접 가리키면 인앱 브라우저에서 설치가 막힌다 — 카카오톡·QR 스캐너 등은 APK를 받아도 설치기로 넘기지 못해 “다운로드 중”에서 멈춘다. 그래서 공유 링크는 이 페이지를 가리키고, 여기서 환경을 갈라 처리한다.
| ID | 기능명 | 상세 설명 | 처리 기준 · 예외 | 권한 |
| APP-01 | 다운로드 페이지 구성 | 핑크 로고 + “IVE DIVE 앱 다운로드” 타이틀 + 환경별 안내 문구(“잠시 후 다운로드가 자동으로 시작돼요” / “외부 브라우저로 여는 중이에요…” / “iPhone·iPad는 APK 없이 웹에서 바로 설치해요”) + [APK 다시 받기] 버튼. 하단에 설치가 막힐 때의 수동 안내 2줄(⋮ 메뉴 → 다른 브라우저(Chrome)로 열기 / “출처를 알 수 없는 앱” 허용). | 스킴이 무시되는 환경(구버전 · iOS 인앱)이 있어 자동 전환에만 기대지 않고 수동 안내와 다운로드 버튼을 항상 함께 노출한다. | 전체 |
| APP-02 | 인앱 브라우저 감지 · 외부 전환 | 진입 시 User-Agent로 환경을 4갈래(ready · inapp · ios · loading)로 가른다. 카카오톡은 kakaotalk://web/openExternal 스킴으로 이 페이지를 외부 브라우저에서 다시 열고, 그 밖의 안드로이드 인앱(네이버 · 인스타그램 · 페이스북 · 라인)은 intent:// 스킴으로 기본 브라우저를 연다. 일반 브라우저와 이름 모를 WebView(카메라 QR 스캐너 등)는 바로 내려받기를 시작한다 — WebView 공통 마커(; wv)까지 인앱으로 잡아 intent://로 넘기던 분기는 일반 WebView가 그 스킴을 처리하지 못해 “외부 브라우저로 여는 중이에요…”에서 멈춰 제거했다(vidding-re · energy-meal과 동일). | iOS는 APK가 없어 다운로드 대신 홈 화면 추가(PWA) 2단계 안내와 [서비스 열기] 버튼을 보여준다(아이패드 데스크톱 UA는 maxTouchPoints로 판별). effect 안의 동기 setState가 렌더를 연쇄시키므로 상태 전환과 이동을 한 틱 미룬다. | 전체 |
| APP-03 | APK 파일 링크 — GitHub 릴리스 직결 | 내려받기는 github.com/seokachu/ive-app/releases/latest/download/ive-dive.apk를 직접 가리킨다. 릴리스마다 같은 자산명으로 올리면 README·QR·이 페이지 모두 손대지 않아도 최신을 받는다. | 한때 동일 출처 스트리밍 프록시(/download/ive-dive.apk)를 뒀으나 Range(이어받기) 요청을 처리하지 못해 폰이 네트워크 전환·화면 꺼짐 뒤 이어받기를 시도하는 순간 실패했다(curl -C 재현: 프록시 “byte ranges 미지원”, GitHub 206 정상) — 2026-08-30 제거. GitHub CDN은 Accept-Ranges/206으로 이어받고 MIME도 application/vnd.android.package-archive로 내려준다. 갓 올린 APK는 Chrome 파일 검사에 처음 걸려 커스텀 탭에서 “다운로드 중…”에 머물 수 있다 — Chrome 본체의 다운로드 목록에서 유지하면 통과한다. | 전체 |