Vidding 기능명세서
v2.7 · As-Built(현행) · 2026-10-12
가격을 부르지 않는 경매. 가장 공감받은 이야기를 쓴 사람이 낙찰받는다.
이 문서는 배포된 서비스가 실제로 하는 일을 기준으로 쓴 현행 명세다. 설계 시점 명세(v1.0)는 기능 스펙 13건이 원본이고, 구현하면서 달라진 것은 §6에 모았다.
| 항목 | 내용 |
|---|---|
| 배포 | https://vidding-re.vercel.app (Vercel · 서울 icn1) |
| 저장소 | https://github.com/seokachu/vidding-re |
| 웹 | Next.js 16 (App Router · 서버 컴포넌트 · 서버 액션) · Tailwind CSS 4 |
| 백엔드 | Supabase — Postgres · RLS · Realtime · Auth · Storage · Vault · pg_cron · pg_net |
| 푸시 | 웹 푸시(VAPID) + Expo 푸시(FCM), 발송 분기 단일 파이프라인 |
| 앱 | Expo(React Native WebView) 하이브리드 — Android APK · iOS 는 네이티브 탭바(Liquid Glass) + 탭마다 WebView |
| 디자인 | Pencil .pen 단일 파일 · 잉크 블루 #1D48B0 · Pretendard · 390px 기준 · 시안 보기 |
1. 권한 모델 — 관계 판정
사용자 유형(role)이 없다. 경매와 나의 관계가 화면과 권한을 정한다.
| 관계 | 조건 | 할 수 있는 것 |
|---|---|---|
| 주최자 | 내가 등록한 경매 | 수정 · 삭제(사연 0건일 때) · 공감(+50) · 채팅 |
| 참여자 | 내가 사연을 쓴 경매 | 사연 수정 · 삭제 · 추가 입찰 · 공감(+10) · 채팅(낙찰 시) |
| 방문자 | 로그인, 위 둘 다 아님 | 열람 · 사연 작성(쓰는 순간 참여자로 전이) · 찜 |
| 비회원 | 미로그인 | 열람만. 그 외 액션은 로그인으로 유도 |
- 관계는 계정 속성이 아니라 경매마다 따로 정해진다. 사용자가 선언하지 않는다
- 같은 상세 화면이 관계에 따라 셋으로 갈린다 — 방문자·참여자·주최자 (S03 · S03b · S04)
- 화면 진입은 프록시가 막고, 데이터 권한의 최종 판정은 RLS가 한다
상세: 00-관계-판정
2. 기능 목록
핵심 기능 (설계 F1~F12 — 전부 배포됨)
| ID | 기능 | 요약 | 주요 화면 |
|---|---|---|---|
| F1 | 경매 등록 | 사진 · 제목 · 설명 · 기간(1 · 3 · 7일). 사연이 1건이라도 있으면 삭제 불가 | S06 · S06b |
| F2 | 경매 탐색 | 홈 최신 경매, 목록 검색 · 정렬(최신 · 마감 임박 · 사연 많은 순) | S01 · S02 |
| F3 | 사연 작성·입찰 | 사연 + 포인트 입찰, 추가 입찰 가능. 마감 전 삭제는 전액 반환 | S05 |
| F4 | 공감 | 참여자 +10 · 주최자 +50 가중치. 사연당 1회 | S03b · S04 |
| F5 | 낙찰 | 크론이 1분 주기로 마감. 총점 = 입찰 + 공감, 동점은 먼저 쓴 사연. 포인트 정산은 원장 검증 후 커밋 | S09 |
| F6 | 1:1 채팅 | 주최자–낙찰자 실시간 대화(Realtime), 읽음 표시, 배송 정보 전송 | S10 · S16 |
| F7 | 찜하기 | 경매 저장 · 해제, 마이페이지 찜 탭 | S02 · S07 |
| F8 | 마이페이지 | 프로필 · 포인트 · 배송지 + 내 경매 · 내 사연 · 찜 탭 | S07 · S15 |
| F9 | 알림 | 마감 임박 3단계 · 새 사연 · 낙찰 결과 · 새 메시지. 목록을 열면 읽음 | S08 |
| F10 | 인증 | 구글 · 카카오 소셜 로그인만. 로그인 후 원래 위치로 복귀 | S13 |
| F11 | 온보딩 | 로그인 전 3장 안내 — 사연 입찰 · 참여 방식 · 낙찰 규칙 | S12 |
| F12 | 배송지 관리 | 1건 등록 · 수정. 낙찰 후 채팅으로 전송하는 용도 | S11 |
확장 기능 (설계 이후 추가 — §6)
| ID | 기능 | 요약 | 상태 |
|---|---|---|---|
| X1 | 웹 푸시 | notifications INSERT 트리거 → pg_net 웹훅 → VAPID 발송. 구독 배너 · 해제 | 배포됨 |
| X2 | 하이브리드 앱 푸시 | WebView 앱 셸이 Expo 토큰을 페이지에 주입, 같은 파이프라인에서 FCM으로 분기 | APK 검증 완료 |
| X3 | 실시간 알림 배지 | notifications INSERT 를 Realtime 구독 → 새로고침 없이 하단 탭 점이 켜진다 | 배포됨 |
| X4 | 채팅 목록 | /chat — 상대 · 마지막 메시지 · 미읽음 수. 전 탭 헤더에 진입 아이콘 | 배포됨 |
| X5 | PWA 설치 | 매니페스트 + 설치 유도 배너 + 스플래시 — 웹을 홈 화면에 앱처럼 설치 | 배포됨 |
| X6 | 에러 처리 · 오프라인 | 전역 404 · 화면 오류(500) 화면 + 서비스 워커 오프라인 폴백 + 앱 셸의 오프라인 화면(WebView 내장 에러 페이지 차단) | 배포됨 · 앱 v1.3.1 |
| X7 | 경매 자동 활성화 | GitHub Actions 크론이 하루 1건 새 경매를 올리고, 진행중 경매에 사연 · 입찰 · 공감을 며칠에 걸쳐 나눠 채운다 | 배포됨 |
| X8 | 뒤로가기 정책 | 화면을 두 층으로 본다 — 탭 루트(1층)는 히스토리에 쌓지 않고, 2층은 히스토리 back. 앱 셸은 1층에서 두 번 눌러 종료 | 배포됨 · 앱 v1.4.0 |
| X9 | iOS 네이티브 탭 | 아이폰에서만 웹 하단 탭바 대신 iOS 기본 탭바(Liquid Glass)를 쓰고 탭마다 WebView 를 따로 둔다. 안드로이드는 그대로 | 기기 검증 · 앱 v1.5.0 (iOS 배포 채널 없음) |
3. 기능별 명세
각 기능의 원본 스펙은 기능명 · 목적 · 동작 방식 · 예외 처리 · 완료 조건 5항목으로 작성돼 있다. 여기서는 현행 기준의 핵심만 요약한다.
F1. 경매 등록
- 사진(필수 1장) · 제목 · 설명 · 기간(1 · 3 · 7일)으로 등록한다. 마감 시각은 등록 시점 + 기간
- 수정은 주최자만, 사연이 1건이라도 있으면 삭제할 수 없다 — 남의 입찰을 증발시키지 않는다
- 이미지는 Storage 버킷에 사용자별 폴더로 올린다 (RLS로 남의 폴더 접근 차단)
F2. 경매 탐색
- 홈(S01)은 최신 경매를, 목록(S02)은 검색과 정렬(최신 · 마감 임박 · 사연 많은 순)을 제공한다
- 마감 임박(1시간 이내)은 배지로 구분한다. 상태 배지는 진행중 · 마감 임박 · 마감됨 · 낙찰됨 · 유찰
F3. 사연 작성·입찰
- 사연(텍스트)과 함께 포인트를 걸어 입찰한다. 경매당 사연 1건, 포인트는 추가 입찰로 올릴 수 있다
- 마감 전 삭제는 전액 반환 + 재작성 허용. 마감 후에는 낙찰 사연만 삭제 불가
- 가입 시 5,000 P 지급. 추가 획득 경로는 경매를 열어 낙찰시키는 것뿐
F4. 공감
- 사연에 공감을 남기면 가중치가 총점에 더해진다 — 참여자 +10, 주최자 +50
- 사연당 1회, 자기 사연에는 불가. 마감 뒤에는 공감 버튼이 잠긴다
F5. 낙찰
- pg_cron이 1분 주기로 마감을 처리한다. 총점 = 건 포인트 + 받은 공감 가중치, 동점이면 먼저 작성한 사연
- 정산: 낙찰자 차감 확정 → 주최자에게 이전, 미낙찰자는 전액 반환. 사연 0건이면 유찰
- 정산은 포인트 원장으로 기록되고, "반환 + 이전 = 차감 총액"이 깨지면 롤백된다
F6. 1:1 채팅
- 낙찰이 확정되면 주최자–낙찰자 채팅방이 열린다. Supabase Realtime으로 실시간 수신, 읽음 표시
- 낙찰자는 저장된 배송지를 카드 형태 메시지로 전송할 수 있다 (보내기 전 확인 다이얼로그)
- 연결이 끊기면 상태 줄로 알리고, 재연결 시 놓친 메시지를 다시 불러온다. 전송 실패는 재전송 제공
F7. 찜하기
- 목록 · 상세에서 하트로 저장하고, 마이페이지 찜 탭에서 모아 본다. 비회원은 로그인 유도
F8. 마이페이지
- 프로필(닉네임 · 아바타는 마지막 로그인 제공자를 따른다) · 보유 포인트 · 배송지
- 탭 3개: 내 경매 / 내 사연(참여중 · 낙찰 · 미낙찰 배지) / 찜
- 포인트 내역(S15)은 원장 그대로 — 가입 축하 · 입찰 · 반환 · 낙찰 수령
F9. 알림 (+ X1 · X2 · X3)
- 종류: 마감 임박(기간에 비례한 3단계 — 3일 · 1일 · 1시간 전) · 새 사연(주최자) · 낙찰 결과(전원) · 새 메시지
- 하단 탭에 미읽음 점, 목록을 열면 읽음 처리. 알림을 누르면 관련 경매 · 채팅방으로 이동
- 현행 확장: 웹 · 앱 푸시로도 발송되고(X1 · X2), 화면을 보고 있으면 새로고침 없이 점이 켜진다(X3)
- 새 메시지 알림은 내용 미리보기 40자를 싣고 방마다 최신 1건만 남는다. 배송 정보는 개인정보라 미리보기하지 않는다
F10. 인증
- 구글 · 카카오 소셜 로그인만 제공한다 (이메일 · 비밀번호 없음)
- 로그인이 필요한 곳에서 진입하면 왜 왔는지 알리고, 로그인 후 원래 위치로 돌려보낸다
- 접근 판정은 실제로 있는 화면에만 한다. 프록시가 아는 경로 목록(
routes.ts의KNOWN_ROUTES)에 없으면 판정을 건너뛰고 404(S17)로 흘려보낸다 — 없는 주소는 로그인해도 여전히 없어서, 막아 봐야 사용자를 두 번 헛걸음시킬 뿐이다 - 목록 안에서는 막는 쪽으로 판정한다 — 어느 공개 규칙에도 안 걸리면 열지 않는다. 새 화면을 목록에 빠뜨리면 조용히 뚫리는 게 아니라 비회원에게 404 로 보인다(눈에 띄게 실패한다)
- 목록은 손으로 적지만 어긋난 채로 오래 가지 않는다 —
pnpm check:routes가src/app의 실제 라우트와 양방향으로 대조하고(빠진 것 · 지웠는데 남은 것) CI 가 매 PR 에서 돌린다. 자동 생성하지 않는 이유는, 이 목록이 공개/보호 규칙 옆에서 사람이 읽고 판단하는 값이기 때문이다
F11. 온보딩
- 로그인 전 3장: 사연으로 입찰한다 → 참여 방식 → 낙찰 규칙. 건너뛸 수 있다
F12. 배송지 관리
- 1건만 등록 · 수정한다. 우편번호 검색 포함
- 용도는 낙찰 후 배송 정보 전송(F6) 하나다. 서버가 저장된 값을 다시 읽어 보내므로 클라이언트 위조가 불가능하다
X1 · X2. 푸시 (웹 + 앱)
notificationsINSERT 트리거가 pg_net으로/api/push웹훅을 부른다. 발송 주소 · 시크릿은 Vault에 보관- 웹은 VAPID 웹 푸시, 앱 셸(WebView)은 Push API가 없어 Expo 토큰을 페이지에 주입해 같은 테이블에 저장 — 발송 시 endpoint 모양으로 웹 푸시 / FCM을 가른다
- 구독은 알림 화면 배너에서 켜고 끈다. 계정 전환 시 이전 계정의 구독 행은 회수된다
X3. 실시간 알림 배지
notificationsINSERT를 Realtime으로 구독해 서버 트리를 갱신한다 — 미읽음 수의 진실은 서버에 있으므로 클라이언트에서 따로 세지 않는다- 알림 목록을 보는 중에 온 알림은 목록에 바로 나타나고 자동으로 읽음 처리된다
- 연결이 끊겼다 붙으면(절전 등) 한 번 갱신해 놓친 알림을 따라잡는다
X4. 채팅 목록
/chat— 상대 · 경매 제목 · 마지막 메시지 · 미읽음 수. 참여자 판정은 RLS 조회가 한다- 탭 4곳 헤더 오른쪽에 진입 아이콘(+ 미읽음 점). 배송 정보 메시지는 목록에서도 미리보기하지 않는다
X5. PWA 설치
- 웹 매니페스트로 "설치할 수 있는 앱" 요건을 갖추고, 탭 4화면에서 하단 탭 위에 설치 유도 배너를 띄운다
- 설치 흐름은 플랫폼별로 갈린다 — 크로미움(안드로이드 · 데스크톱 크롬/엣지)은 네이티브 설치 프롬프트, iOS는 "공유 → 홈 화면에 추가" 가이드 바텀시트
- 도울 수 없을 때는 나타나지 않는다 — 앱 셸 안, 이미 설치됨, 인앱 브라우저, 최근 닫은 사용자(7일)
- 스플래시는 잉크 블루 배경 + 흰 로고 + 태그라인 — iOS는 기기별 이미지 17장(
pnpm splash로 생성), 안드로이드는 매니페스트에서 자동 생성 - APK 앱 셸의 스플래시는 2단계다 — 프로세스가 뜨는 순간은 네이티브가 배경색만 보이고(
vidding-appv1.3.2), RN 오버레이가 S00(로고 + 태그라인)을 웹 로드가 끝날 때까지 잇는다. 네이티브에 아이콘을 두면 아이콘은 정중앙 · RN 은 덩어리 중앙이라 인계 순간 로고가 23dp 튀었다 — 아이콘을 지우면 "튀는" 것이 "나타나는" 것이 된다 - APK 배포는
/download페이지가 맡는다 — 인앱 브라우저(카카오톡 등)는 APK 설치가 막히므로 감지해서 외부 브라우저로 자동 전환한다. README 의 QR 도 이 페이지를 가리킨다 - APK 는 arm64 전용이다 (앱 v1.4.1, 약 27 MB). CPU 4종 네이티브 라이브러리가 56 MB 였는데 폰에 필요한 것은 arm64 하나다 —
expo-build-properties의android.buildArchs. 68 MB 이던 다운로드가 몇 초로 줄었다. 32비트 전용 기기(2015년 이전)와 x86 에뮬레이터는 제외된다 - 다운로드는 페이지를 떠나지 않는다. 카메라가 여는 크롬 커스텀 탭에서는 파일 주소로의 이동이 크롬의 다운로드 화면("다운로드 중…")으로 페이지를 통째로 덮었고, 그 화면은 용량을 다 채우고도 표시가 안 바뀌는 경우가 있어 — 파일은 이미 받아져 있는데 — 멈춘 것처럼 보였다 (에뮬레이터의 일반 탭 · 커스텀 탭 두 흐름 모두 파일은 온전히 저장되는 것을 확인했다). 숨긴 iframe 으로 받아 페이지를 남기고, "알림창에서
vidding.apk를 열라"는 안내와 버전 · 용량(릴리스 API)을 보여준다. iframe 이 막히는 환경을 위해 직접 링크도 둔다
X6. 에러 처리 · 오프라인
- 404 (S17) — 어느 라우트에도 걸리지 않은 주소와
notFound()를app/not-found.tsx가 함께 받는다. 경매 상세처럼 자기 문맥이 있는 곳은 자기not-found를 그대로 쓴다 - 안내만 하고 끝내지 않고 갈 곳을 함께 준다 — 홈 · 경매 목록 버튼과 빠른 이동 칩(채팅 · 알림 · 마이). 찾지 못한 주소를 그대로 보여줘 오타인지 고장인지 구분시킨다
- 앱(WebView)은 이 404 를 그대로 본다. 앱 셸이 웹을 띄우는 구조라 앱용 화면을 따로 만들지 않았다 — 위에 네이티브 상태바만 얹힌다
- 오프라인 (S18) — 서비스 워커가 설치 시점에
/offline.html한 장만 미리 받아 두고, 화면 이동이 네트워크 실패로 끊길 때 대신 돌려준다. 연결이 돌아오면(online) 자동으로 새로고침한다 - 페이지는 캐시하지 않는다. 캐시가 늘수록 "고쳤는데 옛 화면이 뜬다"는 갱신 실패만 늘어난다. 화면 안의 조회 실패는 지금처럼
ErrorState가 제 자리에서 다룬다 - 서비스 워커 등록을 알림 배너에서 앱 시작 지점으로 옮겼다 — 푸시를 켠 사람만 오프라인 화면을 갖는 것은 이상하다. 등록은 권한을 묻지 않는다
- 앱 셸 (
vidding-appv1.3.1) — 지금까지는 로드가 실패하면 안드로이드 웹뷰의 내장 에러 페이지("웹페이지를 사용할 수 없음")가 그대로 떠서, 앱 안에서 브라우저가 튀어나온 것처럼 보였다.renderError로 그 자리를 S18 화면이 대신 채운다 renderError는 웹뷰를 대체하지 않는다. react-native-webview 는 웹뷰와renderError()결과를 형제로 나란히 그리므로, 그냥 두면 내장 에러 페이지가 위에 남고 우리 화면이 그 아래로 밀려 화면 밖으로 나간다 (v1.3.0 의 버그). 오프라인 화면을 불투명 배경으로 컨테이너 전체를 덮게 해 가린다 — 에뮬레이터에서 네이티브 경로 · 서비스 워커 경로 둘 다 확인했다- CTA 는 [다시 시도 =
reload()] + [홈으로 = 웹뷰 재마운트].reload()는 실패한 주소를 다시 부를 뿐이라, 그 주소 자체가 문제일 때 빠져나갈 길이 하나 더 필요하다 - 연결 복구는 NetInfo 가 아니라 도달 확인으로 잡는다. 네이티브 모듈이 하나 늘어나는데 알려주는 것은 "링크가 붙었는가"뿐이라, Wi-Fi 에 붙었지만 밖으로 못 나가는 상태를 못 거른다. 오프라인 화면이 떠 있는 동안에만
/offline.html을 4초 주기로 찔러보고(앱을 다시 켤 때도 한 번), 응답이 오면 자동으로 다시 불러온다. 새 네이티브 의존성은 없다 - 아이콘은 벡터 라이브러리를 새로 들이는 대신 lucide 글리프를
.pen색으로 구워assets/에 넣었다 - 비회원도 404 를 본다. 예전에는 프록시가 모르는 경로를 전부 로그인으로 보내 이 화면이 비회원에게 닿지 않았다. 이제 프록시가 아는 경로 목록에 없으면 판정을 건너뛴다 (F10 참고) — 보호 경로의 로그인 유도는 그대로다
- 화면 오류 (S19) — 404 는 "없는 주소", 이건 있는 화면인데 못 그린 경우다. 조회가 예외를 던지거나 렌더가 실패하면
app/error.tsx가, 루트 레이아웃까지 깨지면app/global-error.tsx가 대신 선다. 없을 때는 Next 기본 오류 화면이 그대로 나왔다 - 화면 *일부*의 조회 실패는 여기까지 오지 않는다 — 그건 그 자리에서
ErrorState·InlineRetry가 받는다 (F8 4 · F9 4). 빈 상태 · 조회 실패 · 화면 오류가 서로 다른 화면이다 - CTA 는 [다시 시도] + [홈으로 가기]. Next 16 은
reset이 아니라unstable_retry다 —reset은 오류 상태만 지우고 같은 데이터로 다시 그려서, 서버 조회가 원인이면 대개 그대로 또 터진다 - 서버에서 난 오류는 내용이 클라이언트로 넘어오지 않는다. 서버 로그와 맞춰볼 수 있는
digest해시를 칩으로 보여줘 제보에 실어 보낼 수 있게 한다 global-error는 루트 레이아웃을 대신하므로html·body· 전역 스타일 · 390 셸을 직접 그린다. 폰트는 따라오지 않아 시스템 서체로 떨어진다 — 오류 화면 한 장을 위해 폰트 로딩을 다시 세우지 않는다- 다만 루트 레이아웃이 서버에서 터지면 첫 응답은 Next 의 내부 오류 문서(
<html id="__next_error__">)다.global-error는 그 뒤 하이드레이션에서 그려진다 — Next 의 동작이고, 상태 코드는 500 으로 정확히 나간다 (로컬에서 확인)
X7. 경매 자동 활성화
- 비어 있는 목록은 서비스가 아니다. 포트폴리오로 열어 둔 서비스라 실사용자 유입이 없어, 며칠만 지나면 홈이 마감된 경매만 남은 화면이 된다. GitHub Actions 크론(
auction-daily.yml, 하루 4회 — 09:20 · 12:20 · 16:20 · 20:20 KST)이 그 자리를 채운다 - 하는 일은 둘이다. ① 그날(KST) 새 경매가 없으면 1건 등록 — 풀(
scripts/data/auction-daily-pool.mjs, 35건)에 안 올린 품목이 있으면 그것을, 없으면 마감된 지 7일 지난 경매를 사진째 재등록한다. 그래서 풀이 비어도 자동화는 멈추지 않는다 - ② 진행중 경매에 예정 시각이 지난 사연 · 입찰 · 공감을 그때그때 등록한다 — 한 경매가 올라온 직후엔 조용하다가 며칠에 걸쳐 반응이 쌓이는 것처럼 보인다. 하루 4회로 나눈 이유가 이것이다
- 테이블에 직접 쓰지 않는다. 입찰 · 공감을 직접 넣으면 원장과 잔액이 어긋나
close_auction()이 롤백된다. 매직링크로 로그인해 anon 키로place_bid()·toggle_episode_like()를 부르는 실제 사용자 흐름을 그대로 밟는다 — RLS 를 통과하므로 정책이 바뀌면 이 스크립트도 사용자처럼 같이 막힌다 (seed.mjs와 같은 원칙) - 시드 계정 포인트가 모자라면 그 입찰만 건너뛰고 다음 실행에 재시도한다. 진행중 경매가 마감되면 반환 · 이전이 일어나므로 시간이 지나면 저절로 풀린다
- 마감 처리는 여기서 하지 않는다 — Supabase pg_cron 이 1분 주기로 이미 하고 있다 (F5)
- 의존성 없는 스크립트라 Actions 에서
pnpm install없이 바로 돈다.pnpm auction:daily --dry-run으로 계획만 볼 수 있다
X8. 뒤로가기 정책
- 뒤로가기가 끝이 없었다. 하단 탭이
Linkpush 라 홈 → 탐색 → 상세 → 탐색 → 마이 → … 를 오간 만큼 히스토리가 쌓였고, 앱의 물리 뒤로가기는 그 길을 전부 되밟아야 "첫 화면"에 닿아 종료할 수 있었다. 몇 분 쓰고 나면 열 번을 눌러도 앱이 안 꺼졌다 - 화면을 두 층으로 본다. 하단 탭 넷(홈 · 탐색 · 알림 · 마이)이 1층, 그 밖의 전부(상세 · 작성 · 수정 · 채팅 · 포인트 · 배송지)가 2층이다. 목록은
src/lib/routes.ts의TAB_ROOTS한 곳이고 하단 탭의 활성 판정도 이걸 쓴다 - 탭 전환은 히스토리에 쌓지 않는다 (
Link replace). 탭 넷은 나란한 1층이지 위아래가 아니다. 홈에서만 그려지는 헤더 로고도 같다. 그래서 어디서 시작했든 2층에서 back 을 되밟으면 한두 번 안에 1층에 닿는다 - 2층의 뒤로가기는 언제나 히스토리 back 이다 — 상단 바 버튼도, 물리 뒤로가기도. 홈 경유 같은 우회 규칙을 두지 않는다. 작성 · 수정 저장 뒤에도 back 이라 상세가 두 번 남지 않고, 등록 성공은 서버가
RedirectType.replace로 상세를 보내 폼이 히스토리에서 사라진다 (기존 동작) - 돌아갈 히스토리가 없으면 그 화면이 속한 탭으로
replace한다 (hasHistory·tabRootOf). 공유 링크 · 푸시 알림으로 상세에 바로 들어온 사람에게router.back()은 아무 일도 하지 않아 버튼이 죽은 것처럼 보였다./auctions/…는 탐색,/mypage/…는 마이, 나머지는 홈 - 앱 셸 (
vidding-appv1.4.0) — 물리 뒤로가기는 웹뷰의canGoBack이 아니라 현재 경로로 판단한다. 1층이면 히스토리가 남아 있어도 더 물러나지 않고 2초 안에 두 번 눌러 종료한다 (토스트 "한 번 더 누르면 종료됩니다"). 2층이면goBack(), 돌아갈 곳이 없으면 종료 대신 홈을 연다. 로그인 전 첫 화면(/· 온보딩)도 1층으로 친다 — 거기서 물러나는 것은 둘러보기로 들어가겠다는 뜻이 아니라 나가겠다는 뜻이다 - 셸의
EXIT_ROOTS는 웹의TAB_ROOTS와 같은 목록이다. 탭을 늘리면 양쪽을 함께 고친다 - 홈 경유(비홈 탭에서 back → 홈 → back → 종료) 안도 검토했다. 종료까지 한 번 더 걸리는 대신 얻는 것이 없고, "왜 갑자기 홈이지"가 생긴다. 1층은 전부 같은 층이라는 단순한 규칙이 설명하기도 맞추기도 쉽다
X9. iOS 네이티브 탭
- 아이폰에서만 웹 하단 탭바 대신 iOS 기본 탭바를 쓴다 — 누르면 커지는 유리 렌즈, 탭바 위를 끌어 탭을 옮기는 Liquid Glass 동작이 그대로 난다. 안드로이드는 웹 탭바 + 단일 WebView 그대로다 (
vidding-appv1.5.0, expo-routerNativeTabs) - 웹은 고치지 않았다. 탭 넷(홈
/main· 탐색/auctions· 알림/notifications· 마이/mypage)과 라벨 · 순서는 웹 하단 탭바와 같고, 아이콘은 가장 가까운 SF Symbol(house · magnifyingglass · bell · person)이다. 셸의 탭 목록(shell/tabs.ts)도 웹TAB_ROOTS와 같은 목록이라 탭을 늘리면 함께 고친다 - 탭마다 WebView 가 따로 있다 — 탭을 오가도 스크롤 위치와 방문 기록이 탭별로 남는다. 세션 쿠키는 웹뷰끼리 공유돼 한 탭에서 로그인하면 전부 로그인 상태다. 한 번도 안 연 탭은 웹뷰를 세우지 않는다
- 웹 탭바는 셸이 CSS 로 숨긴다.
<style>은 하이드레이션 때 지워질 수 있어document.adoptedStyleSheets로 넣는다. 웹 탭바에aria-label이 없어 알림 · 마이 링크를 가진nav로 집는다. 웹 탭바의 읽지 않은 알림 점은 네이티브 알림 탭의 배지로 옮겨 단다 - 하단 여백은 셸이 잰 탭바 높이로 비운다. 탭 루트는 웹이 고정 px 로 비워 둔 자리(본문
pb-[88px]· 글쓰기 버튼 · 토스트)를 같은 간격으로 탭바 위에 다시 맞추고, 2층은 본문 끝과 하단 고정 버튼 줄을 탭바 위로 올린다.env(safe-area-inset-bottom)은 탭 화면의 WKWebView 에서 0 이었고,:has()로 페이지가 스스로 고르게 한 규칙은 SPA 이동 뒤 다시 계산되지 않아 — 경로가 바뀔 때마다 셸이 시트를 갈아 끼운다 - 네이티브 탭바는 상세에서도 남는다 — 그래야 상세에서 같은 탭을 다시 눌러 루트로 돌아간다. 내리는 곳은 키보드가 하단을 차지하는 채팅방 · 작성 · 수정 · 배송지와, 홈 탭의 로그인 흐름(입구 · 온보딩 · 로그인)뿐이다. 다른 탭이 비로그인이라 로그인 화면으로 밀려난 경우엔 남긴다 — 내리면 그 탭에 갇힌다
- 같은 탭을 다시 누르면 루트에서는 맨 위로, 2층에서는 그 탭의 루트로 간다
- 알림 · 딥링크는 그 경로가 속한 탭으로 넘어가 그 탭의 WebView 에서 연다 (
tabRootOf와 같은 규칙). 웹 안의 링크가 다른 탭의 루트로 가도 그 탭으로 옮겨 간다 — 예: 마이 탭 로그인 화면의 "먼저 둘러보기" → 홈 탭 - 스플래시 · 푸시 토큰 · 알림 응답은 탭과 무관하게 한 번만 있다 (
app/_layout.tsx). 상태바는 탭바가 나타나고 사라질 때 UIKit 이 스플래시 때의 흰 글씨로 되돌려 매번 다시 검게 건다 - iOS 27 은 Scene 생명주기를 안 쓰면 실행 즉시 종료된다.
expo-build-properties의ios.enableSceneSupport로ExpoAppSceneDelegate를 쓰게 해 prebuild 를 다시 해도 유지된다. iOS 26 시뮬레이터 · iOS 27 실기기에서 실행 확인 - iOS 는 배포 채널이 없다 — App Store · TestFlight 에 올리지 않았고, 개인 개발 팀은 Push Notifications 권한을 서명할 수 없어 실기기 개발 빌드에서는 푸시가 빠진다
4. 화면 목록
.pen 시안의 화면 코드와 배포 라우트. 상세 화면은 관계에 따라 같은 라우트가 다르게 그려진다. 전 화면 이미지는 디자인 시안에서 디자인 시스템과 함께 볼 수 있다.
| 코드 | 화면 | 라우트 |
|---|---|---|
| S00 | 스플래시 | (설치된 앱 실행 시) |
| S01 | 홈 | /main |
| S01b | 홈 · 설치 배너 | /main (미설치 브라우저) |
| S02 | 경매 탐색 | /auctions |
| S03 · S03b · S04 | 경매 상세 — 방문자 · 참여자 · 주최자 | /auctions/[id] |
| S03c | 경매 상세 · 유찰 | /auctions/[id] (마감 후) |
| S09 | 낙찰 결과 | /auctions/[id] (마감 후) |
| S05 | 사연 작성 · 입찰 | /auctions/[id]/episodes/write |
| S06 · S06b | 경매 등록 · 수정 | /auctions/write · /auctions/[id]/edit |
| S07 · S07b · S07c | 마이페이지 (기본 · 로그아웃 확인 · 내 사연) | /mypage |
| S14 · S14b | 마이페이지 빈 상태 · 내 사연 빈 상태 | /mypage |
| S15 | 포인트 내역 | /mypage/points |
| S11 | 배송지 관리 | /mypage/address |
| S08 · S08b | 알림 · 빈 상태 | /notifications |
| S10 · S10b | 채팅 · 전송 실패 | /chat/[id] |
| S16 · S16b | 채팅 목록 · 빈 상태 | /chat |
| S12 · S12-1~3 | 온보딩 3장 | /onboarding |
| S13 | 로그인 | / |
| S17 · S17b | 404 · 웹 셸 배치 | 걸리지 않는 모든 주소 |
| S18 · S18b | 오프라인 — 앱 · 웹 | /offline.html (서비스 워커 폴백) |
| S19 | 화면 오류 (500) | 렌더·조회가 실패한 모든 화면 |
5. 데이터 · 인프라 요약
| 구성 | 내용 |
|---|---|
| 테이블 | users · auctions · episodes · likes · favorites · points(원장) · notifications · chat_rooms · messages · addresses · push_subscriptions |
| RLS | 전 테이블 활성. 판정 헬퍼(is_chat_participant 등)로 중복 제거. 알 수 없는 것은 막는다 |
| RPC | 입찰 · 공감 · 마감(close_auction) · 읽음 처리 등 — 원장 정합이 깨지면 롤백 |
| 크론 | pg_cron 1분 주기 — 마감 처리 · 마감 임박 알림 |
| Realtime | messages · notifications 발행. RLS가 그대로 적용돼 본인 것만 수신 |
| 발송 | notifications INSERT 트리거 → pg_net → /api/push (시크릿 헤더 인증, Vault 보관) |
상세: 데이터 모델 명세
6. 설계에서 달라진 것
v1.0 설계 문서는 수정하지 않고 그대로 보존한다. 아래가 구현하면서 달라진 전부다.
| 영역 | 설계 (v1.0) | 현행 (v2.0) | 이유 |
|---|---|---|---|
| 푸시 | 만들지 않는다 (F9) | 웹 푸시 + 앱 푸시 도입 (X1 · X2) | 서비스 밖에서도 마감 · 낙찰을 놓치지 않게. Vault · pg_net 트리거로 우려했던 관리 비용을 줄였다 |
| 알림 갱신 | 화면 진입 시 조회 | Realtime 실시간 배지 (X3) | 열어 둔 화면에서도 점이 켜져야 알림이 알림답다 |
| 채팅 진입 | 낙찰 알림에서만 | 채팅 목록 /chat + 전 탭 헤더 아이콘 (X4) | 알림이 밀리면 대화로 돌아갈 길이 없었다 |
| 채팅 알림 | 메시지마다 1건 | 방마다 최신 1건 + 내용 미리보기 40자 | 같은 방 알림이 목록을 도배했다. 배송 정보는 미리보기 제외 |
| 내 사연 배지 | 탈락 | 미낙찰 | 사연을 꺼낸 사람에게 탈락은 시험 낙방처럼 읽힌다. 포인트 내역의 어휘와 통일 |
| 배송지 | 경매 등록의 요건 | 요건 아님 — 낙찰 후 전송 용도만 | 등록 장벽을 없앤다. 배송지가 실제로 쓰이는 곳은 전송뿐이다 |
| 프로필 | 가입 시점 고정 | 마지막 로그인 제공자를 따라간다 | 구글 ↔ 카카오를 오가도 아바타 · 닉네임이 어색하지 않게 |
| 앱 | 범위 밖 | Expo WebView 하이브리드 + FCM 푸시 | 웹 푸시가 닿지 않는 안드로이드 웹뷰를 앱 셸이 메운다 |
| 설치 | APK 배포뿐 | PWA 설치 배너 + 스플래시 (X5) | APK 를 받지 않아도 웹에서 바로 홈 화면에 앱처럼 설치되게 |
| 에러 화면 | 경매 상세의 없는 경매만 (F3 4.1) | 전역 404 · 오프라인 · 화면 오류 (X6) | 없는 주소는 Next 기본 404, 앱은 웹뷰 내장 에러 페이지가 떴다 — 서비스 밖으로 튕겨 나가는 두 자리였다 |
| 뒤로가기 | 규정 없음 — 브라우저 기본 | 두 층 규칙 (X8) — 탭은 쌓지 않고, 앱은 탭에서 두 번 눌러 종료 | 탭을 오간 만큼 뒤로가기가 되밟아 끝이 없었고, 앱은 종료에 영영 못 닿았다 |
| iOS 탭바 | 웹 하단 탭바 | 아이폰은 iOS 기본 탭바 + 탭마다 WebView (X9) | 웹 탭바는 아이폰에서 앱이 아니라 웹페이지처럼 보였다. 탭별 스크롤 · 방문 기록도 살린다 |
7. 버전 기록
| 버전 | 일자 | 상태 | 요약 |
|---|---|---|---|
| v1.0 | 2026-07-31 | 설계 | PRD · 데이터 모델 · 기능 스펙 13건 확정. 미결 정책 0건으로 구현 시작 |
| v1.1 | 2026-08-01 | 구현 | 전 화면 구현 · Vercel 배포. 백엔드(RLS · RPC · 크론) 검증 — 등록→사연→입찰→공감→마감→낙찰→채팅→알림 전 구간 · 포인트 총량 보존 확인 |
| v1.2 | 2026-08-02 | 검증 | .pen 시안 21화면을 배포본과 1:1 대조 완료 |
| v2.0 | 2026-08-03 | 확장 | 웹 푸시 (VAPID · Vault · INSERT 트리거) |
| v2.1 | 2026-08-04 | 확장 | 하이브리드 앱 푸시(Expo · FCM) · 실시간 알림 배지 · 채팅 목록 · 채팅 알림 정리(미리보기 · 방마다 1건) · 미낙찰 문구 |
| v2.2 | 2026-08-05 | 문서 | As-Built 기능명세서(이 문서) 작성 · /docs 공개 · 디자인 시안 페이지 |
| v2.3 | 2026-08-14 | 확장 | PWA 설치(X5 — 매니페스트 · 설치 배너 · 스플래시) · 스플래시 잉크 블루 리디자인 · 로그인 버튼 bfcache 잠김 수정 |
| v2.4 | 2026-08-27 | 확장 | 에러 처리 · 오프라인(X6 — 전역 404 · 서비스 워커 오프라인 폴백 · 워커 등록 상시화 · 앱 셸 오프라인 화면 v1.3.1 · 500 화면) · 접근 판정을 아는 경로에만 적용해 없는 주소는 비회원도 404 를 본다. .pen 에 S17 · S18 · S19 시안 5장 추가. 문서 누락분(X7 경매 자동 활성화) 반영 |
| v2.5 | 2026-09-06 | 확장 | 뒤로가기 정책(X8 — 탭 루트는 히스토리에 쌓지 않고, 앱 셸은 탭에서 두 번 눌러 종료 · 히스토리 없는 진입은 탭으로 대체) · 앱 셸 v1.4.0 |
| v2.6 | 2026-09-07 | 확장 | 앱 다운로드 안정화(X5) — APK arm64 전용(68 → 약 27 MB) · 다운로드 페이지가 페이지를 떠나지 않고 받으며 버전 · 용량과 설치 안내를 보여준다 · 앱 셸 v1.4.1 |
| v2.7 | 2026-10-12 | 현행 | iOS 네이티브 탭(X9 — 아이폰에서만 Liquid Glass 탭바 · 탭마다 WebView · 웹 탭바는 셸이 숨김 · iOS 27 Scene 생명주기) · 앱 셸 v1.5.0. 안드로이드 동작 · 웹 코드는 그대로 |
관련 문서
- PRD — 배경 · 문제 · 목표 · 사용자 · 시나리오 · MVP 범위
- 데이터 모델 명세 — 테이블 · RLS · 고정 상수
- 기능 스펙 (v1.0 설계) — 기능별 상세 13건
- 관계 판정 — 모든 권한의 공통 기반