Skip to content

[Feat/#110] 스플래시·로그인·온보딩 화면 구현 - #112

Open
jjunh33 wants to merge 17 commits into
mainfrom
feat/#110-auth-onboarding
Open

jjunh33 wants to merge 17 commits into
mainfrom
feat/#110-auth-onboarding

Conversation

@jjunh33

@jjunh33 jjunh33 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

앱을 처음 열었을 때의 진입 흐름(로그인, 온보딩)이 없어서, 모든 화면이 로그인 없이 바로 열립니다. Figma의 스플래시 & 로그인(3658:112096)과 온보딩(3738:73115) 화면을 화면설계서(스플래시/로그인 3747:74283, 온보딩 3747:74298, 화면 ID FLW_HOM_001)에 맞춰 구현하고, 로그인은 K-CONNECT(국민대 OIDC) 인증과 @billilge/oauth-kconnect 패키지로 연결합니다. 스플래시는 웹에서 쓰지 않기로 해서 이 PR에서 제외했습니다.

❓ 왜 해결해야 하나요?

신규 사용자는 K-CONNECT 인증 후 연락처와 약관 동의를 받아야 서비스를 쓸 수 있습니다. 이 입구가 없으면 행사 신청 같은 기능에 필요한 연락처를 받을 방법이 없고, 앱을 처음 여는 사용자가 어디로 시작해야 하는지도 정해지지 않습니다.

⭐ 어떻게 해결했나요?

1. 로그인·온보딩 퍼널 (AuthFunnelScreen)

@use-funnel/react-router-dom의 useFunnel 하나로 로그인과 온보딩을 한 화면에 묶었습니다. 라우트는 /auth 하나이고, 단계는 ?auth.step=으로 나뉩니다.

단계 내용
login 로그인 소개 캐러셀 3장(자동 전환 없음, 스와이프·닷으로 이동) + K-CONNECT로 시작하기
phone 1/2 연락처. 인삿말에 이름, 숫자 키패드, 자동 하이픈, 11자리일 때만 다음 활성화
terms 2/2 약관 동의. 전체 동의와 개별 항목 연동, 필수 2개 동의 시 다음 활성화(선택 약관은 미동의여도 진행)
termsDetail 약관 상세 3종. 확인했어요·뒤로가기로 복귀하고 동의 상태는 바뀌지 않음
complete 가입 완료. 체크 로띠, stream 시작하기·닫기는 홈으로 이동
  • 단계 사이에 넘기는 값(이름, 연락처, 약관 동의)은 퍼널 context가 들고 있어서 약관 상세를 다녀와도 유지됩니다. 체크·연락처 입력은 history.replace로 context만 갈아 끼워서 히스토리가 늘지 않습니다.
  • 앞 단계 값 없이 중간 단계로 들어오면(새로고침·주소 직접 입력) 각 단계의 guard가 걸러서 로그인 단계를 보여 줍니다.
  • 단계가 바뀌는 이동에는 기존 화면 이동과 같은 슬라이드(View Transition)를 켭니다. 라이브러리 기본 어댑터는 React Router 7.18.3과 옵션 이름이 달라 슬라이드가 무시되어서, src/lib/useFunnel.ts에 같은 어댑터를 createUseFunnel로 직접 만들고 옵션 이름만 viewTransition으로 바꿨습니다.

2. K-CONNECT 로그인 (@billilge/oauth-kconnect)

[로그인 단계] K-CONNECT로 시작하기
  → useKConnectLogin().login()  →  K-CONNECT 로그인 페이지 (PKCE, state는 패키지가 sessionStorage에 보관)
  → 인증 후 /oauth/kconnect 로 되돌아옴 (KConnectCallbackScreen)
  → useKConnectCallback() 으로 { code, codeVerifier, redirectUri } 수신
  → 서버로 전송 (exchangeKConnectCode, 지금은 목)
  → 신규 사용자: /auth 연락처 단계에서 시작 / 기존 사용자: 홈 / 취소·오류: 로그인 화면 + 실패 토스트
  • /oauth/kconnect는 K-CONNECT에 등록하는 redirect URI이자 패키지가 정한 경로라 바꾸지 않았습니다. 어느 경우든 주소창에 code가 남지 않도록 replace로 이동합니다.
  • 신규 사용자는 콜백이 /auth로 이동하면서 이름을 navigate state로 넘기고, 퍼널의 initial(주소에 단계가 없을 때만 쓰임)이 연락처 단계에서 시작합니다.
  • 서버 POST /auth/kconnect(요청 { code, codeVerifier, redirectUri })의 주소와 응답 형식이 아직 없어서 exchangeKConnectCode는 응답만 흉내 냅니다. 실 API가 붙으면 이 함수만 교체합니다.
  • client_id는 코드에 넣지 않고 환경 변수 VITE_KCONNECT_CLIENT_ID로 받습니다(.env.example 참고).
  • 패키지가 Expo용 expo-auth-session을 optional peer로 선언하는데 pnpm autoInstallPeers가 설치까지 해서 웹에 필요 없는 Expo 의존성 수백 개(lockfile 약 4,450줄)가 딸려 와서, pnpm-workspace.yaml의 overrides로 뺐습니다. lockfile 변경은 15줄입니다. Docker 빌드의 --frozen-lockfile이 이 설정을 보도록 Dockerfile의 COPY에 pnpm-workspace.yaml을 추가했습니다.

3. 진입 흐름

  • 앱 첫 진입점 /에 AuthEntryGate를 두었습니다. 이 탭에서 로그인한 기록이 없으면 /auth(로그인)로 보내고(replace), 있으면 홈을 그대로 보여 줍니다. 다른 화면 경로(/events 등)에는 가드를 걸지 않았습니다.
  • 로그인 표시는 sessionStorage에 stream:auth:logged-in = "1"만 저장합니다(이름 등 로그인 정보는 저장하지 않음). 같은 탭에서는 새로고침해도 홈이 유지되고, 탭·브라우저를 새로 열면 로그인부터 시작합니다.

🧪 목데이터로 테스트하는 방법

K-CONNECT 계정과 client_id 없이도 로그인 이후 흐름을 확인할 수 있습니다. 개발 서버(pnpm dev)에서 아래 주소를 열고 K-CONNECT로 시작하기를 누릅니다. 로그인 상태는 탭 단위라서 이미 로그인된 탭이면 홈으로 이동하니, 새 탭(또는 시크릿 창)으로 여세요.

확인할 것 주소 결과
신규 사용자 흐름 /auth?mockKConnect=new 인증 사이트에 가지 않고 연락처 → 약관 동의 → 약관 상세 → 가입 완료 → 홈
기존 사용자 흐름 /auth?mockKConnect=existing 바로 홈
첫 진입 가드 새 탭에서 / 로그인 화면으로 이동
K-CONNECT 취소·오류 /oauth/kconnect?error=access_denied 로그인 화면 + 실패 토스트("로그인에 실패했어요. 다시 시도해 주세요.")
콜백 정상 응답(신규) 아래 "콜백 직접 시뮬레이션" 참고 연락처 단계에서 시작
서버 실패 콜백 시뮬레이션 주소 끝에 &mockLogin=fail 로그인 화면 + 실패 토스트
홈만 바로 보기 콘솔에서 sessionStorage.setItem('stream:auth:logged-in','1') 후 / 가드 통과
  • mockKConnect는 import.meta.env.DEV일 때만 읽어서 배포 빌드에는 들어가지 않습니다(pnpm build 결과물에 문자열이 없음을 확인했습니다).
  • 콜백 직접 시뮬레이션: 개발자 도구 콘솔에서 아래를 실행하면 K-CONNECT가 code를 돌려준 상황이 됩니다(패키지가 로그인 시작 때 저장하는 state를 흉내 내는 값이고, 주소의 state와 같아야 통과합니다). 붙여넣기가 막히면 allow pasting을 먼저 입력하세요. 기존 사용자는 주소 끝에 &mockLogin=existing을 붙입니다.
    sessionStorage.setItem('oauth-kconnect', JSON.stringify({ state: 'S', codeVerifier: 'V'.repeat(43), redirectUri: location.origin + '/oauth/kconnect' })); location.href = '/oauth/kconnect?code=test&state=S';
  • state 불일치(state=WRONG)나 pending 값이 없는 콜백은 오류로 처리되어 로그인 화면과 실패 토스트가 뜹니다.
  • 실제 K-CONNECT 로그인 확인은 .env에 VITE_KCONNECT_CLIENT_ID를 채우고, K-CONNECT의 허가된 Redirect URI에 http://localhost:5173/oauth/kconnect를 등록한 뒤 /auth에서 K-CONNECT로 시작하기를 누르면 됩니다. 폰에서 192.168.x.x:5173으로 접속하면 등록되지 않은 주소라 거부됩니다. 이 환경에서는 K-CONNECT 계정으로 실제 로그인은 확인하지 못했고, 인증 요청 주소(client_id, redirect_uri, scope, state, PKCE S256)가 올바르게 만들어져 K-CONNECT 로그인 페이지로 이동하는 것까지 확인했습니다.

커밋 구성: 문서 → 목 로그인 API·로그인 표시 → 로그인 화면 → 온보딩 화면 → 라우트·첫 진입 가드 → use-funnel 의존성 → View Transition 어댑터 → 퍼널 통합(스플래시 제거) → 퍼널 문서 → oauth-kconnect 의존성·설정 → K-CONNECT 연동·인증 우회 → K-CONNECT 문서

🧩 이 PR의 한계 & 트레이드오프

  • 서버 연동은 목입니다. POST /auth/kconnect의 주소와 응답 형식(서비스 토큰, 신규 사용자 여부)이 정해지지 않아서 exchangeKConnectCode가 지연 후 결과만 돌려줍니다. 서버가 발급한 서비스 토큰은 앱·웹이 직접 저장해야 한다고 패키지가 안내하는데(README), 지금은 토큰이 없어서 로그인 표시만 있고 저장 방식은 서버 연동 때 정해야 합니다.
  • client_id가 필요합니다. K-CONNECT 관리자에게 받은 값을 VITE_KCONNECT_CLIENT_ID로 넣어야 실제 로그인이 시작됩니다. 없으면 버튼이 경고와 실패 토스트만 띄웁니다.
  • 로그인 유지는 탭 단위입니다. 로그인 정보를 저장하지 않으려고 sessionStorage에 표시만 남겼습니다. 실제 인증이 붙으면 서버 세션(쿠키·토큰)으로 바뀌어야 하고, 설계서의 "유효한 세션이 있으면 로그인 화면을 건너뛴다"는 그때 서버 기준으로 다시 맞춰야 합니다.
  • 스플래시는 웹에서 쓰지 않아 구현하지 않았습니다. 앱 WebView에서 스플래시가 필요해지면 그때 다시 만들어야 합니다.
  • 앱 WebView에서는 이 웹 로그인을 쓰면 안 됩니다. 패키지 안내(RFC 8252)대로 앱은 @billilge/oauth-kconnect/react-native로 시스템 브라우저에서 로그인해야 합니다. 이 PR은 웹(브라우저) 로그인만 다룹니다.
  • View Transition 어댑터를 직접 두었습니다. 라이브러리가 옵션 이름을 viewTransition으로 고치면 src/lib/useFunnel.ts를 지우고 기본 useFunnel로 돌아가면 됩니다.
  • 약관 본문은 Figma 문구를 정적 상수로 넣었습니다. 실제 약관 게시 방식(서버·정적 문서)이 정해지면 교체해야 합니다.
  • 용어: 로그인·온보딩이 용어 사전에 따로 없어서 공통 auth 아래에서 처리했습니다. 백엔드 엔드포인트 네이밍과 맞는지 확인이 필요합니다.
  • 이미지 용량: 로그인 일러스트 3장이 960x720 PNG라 장당 약 500KB, 합쳐서 약 1.5MB입니다. 투명 배경이 필요해서 PNG로 두었고, WebP 변환 도구는 도입하지 않았습니다.
  • 고정 px 일부 유지: 하단 안전 영역 여백 sm:h-[14px](2곳)은 행사 신청·사물함 등 저장소 전체가 같은 표기라 그대로 두었습니다.

⛓️ 기존 기능에 미치는 영향

  • /(홈) 진입이 바뀝니다. 이 PR이 머지되면 브라우저(탭)에 로그인 표시가 없을 때 /가 /auth(로그인)로 갑니다. 개발·QA에서 홈을 바로 보려면 한 번 로그인해야 합니다(/auth?mockKConnect=existing이 가장 빠름).
  • 다른 경로(/events, /notices 등)는 그대로 열립니다.
  • 딥링크로 들어온 화면의 뒤로가기 폴백이 /로 가는 경우, 로그인 표시가 없으면 로그인으로 한 번 더 넘어갑니다.
  • 의존성이 2개 추가됩니다. @use-funnel/react-router-dom(react-router-dom 6 이상, React 16.8 이상 필요, 현재 환경과 호환)과 GitHub Packages 비공개 패키지 @billilge/oauth-kconnect입니다. 후자는 설치에 read:packages 권한 토큰이 필요합니다.
  • .npmrc에 @billilge:registry를 추가했고, pnpm-workspace.yaml(새 파일)과 .env.example(새 파일)이 생깁니다.
  • index.css에 토큰 1개(--color-k-connect)를 추가했고, 기존 화면 스타일은 바뀌지 않습니다.
  • 공용 ResultScreen과 CompleteCheck는 수정 없이 재사용했습니다.

🔀 Edge Case & 실패 시나리오

  • 로그인 취소·K-CONNECT 오류(국민대 학생이 아니어서 거부된 경우 포함), state 불일치, 서버 실패는 모두 /auth로 돌아와 실패 토스트를 띄우고 버튼은 다시 눌러집니다.
  • 일회용 인증 code가 StrictMode 이중 실행으로 서버에 두 번 가지 않도록 콜백에서 한 번만 처리합니다.
  • 로그인 요청 중에는 버튼이 비활성화되어(aria-busy) 중복 요청을 막습니다.
  • VITE_KCONNECT_CLIENT_ID가 없으면 인증 사이트로 이동하지 않고 경고와 실패 토스트만 띄웁니다.
  • 연락처에 숫자 아닌 문자나 11자리를 넘는 입력을 넣어도 숫자 11자리까지만 남고 하이픈이 붙습니다.
  • 약관 동의에서 뒤로 가도 입력한 연락처가 남고, 약관 상세에서 돌아와도 체크 상태가 남습니다.
  • 앞 단계 값 없이 ?auth.step=terms 같은 주소로 들어오면 로그인 단계가 보입니다.
  • 가입 완료 단계는 약관 동의 항목을 대체(replace)하므로 약관 동의로 돌아오는 경로가 없습니다.
  • 모션 줄이기를 켠 사용자는 로그인 닷 이동이 즉시 이동하고, 화면 전환 슬라이드도 기존 화면과 같이 꺼집니다.
  • sessionStorage 접근이 막힌 환경에서는 로그인 표시를 저장하지 못해 새로고침마다 로그인부터 시작합니다(경고만 남김).

📋 검토한 대안과 선택 이유

  • K-CONNECT 인증을 직접 구현: PKCE, state 검증, 콜백 처리가 보안에 민감해서 팀의 공용 패키지(@billilge/oauth-kconnect)를 쓰고 앱과 웹이 같은 결과 형태를 공유하도록 했습니다.
  • Expo 의존성을 그대로 설치: 웹에 필요 없는 수백 개 패키지가 딸려 와서 overrides로 제외했습니다.
  • mockLogin 쿼리로 로그인 흐름 시뮬레이션(이전 구현): 실제 로그인은 외부 사이트를 거쳐 돌아와서 쿼리가 사라집니다. 로그인 시작 전 우회(mockKConnect)와 콜백 쿼리(mockLogin)로 나눴습니다.
  • 화면마다 별도 라우트를 둔 이전 구현: 이름·연락처는 navigate state로, 약관 동의는 모듈 저장소로 따로 넘겨야 했습니다. 퍼널의 context로 한 번에 풀려서 퍼널 하나로 합쳤습니다.
  • 라이브러리 기본 useFunnel 그대로 사용: 단계 전환 슬라이드가 무시되어서 어댑터를 직접 만들었습니다.
  • WDS PaginationDots 사용: Figma 닷은 지름 6px이고 중심 간격이 12px인데, WDS는 small이 8px, 간격 16px이고 크기를 CSS 변수로만 바꿀 수 있어서 로컬 AuthPaginationDots로 만들었습니다.
  • K-CONNECT 버튼에 WDS ActionAreaButton 사용: 브랜드색 #004F9F와 높이 57px이 WDS 버튼 스케일에 없어서 로컬 버튼으로 만들었습니다.
  • 약관 체크박스 크기: 행사 신청 폼의 small(20px)이 아니라 Figma Control이 24px이라 medium을 썼습니다.
  • 모든 화면 로그인 가드: 개발·QA 방식이 바뀌는 부담이 커서 / 하나에만 걸었습니다.

💬 리뷰 포인트

  • [r] 배포(CI) 확인이 필요합니다. deploy-dev.yml은 WDS_NPM_TOKEN으로 @wanteddev 패키지를 설치하는데, @billilge/oauth-kconnect도 같은 레지스트리(npm.pkg.github.com)의 비공개 패키지라 이 토큰이 billilge 조직 패키지 읽기 권한도 가져야 합니다. 권한이 없으면 Docker 빌드의 pnpm install이 실패합니다. 같은 이유로 Dockerfile 빌드에는 VITE_KCONNECT_CLIENT_ID를 넘기는 ARG와 워크플로우의 build-args가 아직 없어서, 배포 빌드에서는 client_id가 비어 로그인이 시작되지 않습니다. 이 PR에서는 인프라 설정은 건드리지 않았고 Dockerfile의 COPY에 pnpm-workspace.yaml만 추가했습니다(없으면 --frozen-lockfile이 ERR_PNPM_LOCKFILE_CONFIG_MISMATCH로 실패하는 것을 확인했습니다). 이 부분은 후속 작업이나 이 PR에서 같이 처리할지 알려주세요.
  • [r] 앱 WebView와 웹 브라우저의 위치 처리가 다릅니다. 로그인 본문은 Figma 프레임에서 상태 표시줄(54px)을 뺀 앱 WebView 영역 기준 값입니다. 브라우저는 창 전체가 웹 영역이라 같은 값이면 위로 치우쳐 보여서, isInAppShell()(앱 연동에 이미 쓰는 window.ReactNativeWebView 판별)일 때와 아닐 때를 나눴습니다.
    • 앱: 본문이 위에서 72px에 시작하고 버튼을 바닥에 붙임
    • 웹: 본문이 버튼 위 영역의 세로 가운데
    • 폰 브라우저도 앱이 아니므로 웹 기준(가운데)으로 보입니다. 앱 WebView에서 ReactNativeWebView가 주입되는지는 실기기에서 확인해 주세요.
  • [r] 용어 auth 아래에서 로그인·온보딩을 처리하는 것이 백엔드 엔드포인트 네이밍과 맞는지 확인해 주세요.
  • [c] pnpm-workspace.yaml의 overrides: expo-auth-session: "-"로 Expo 의존성을 제외하는 방식이 괜찮은지 봐주세요. 패키지가 expo-auth-session을 optional peer로만 선언해서 웹에는 필요 없습니다.
  • [c] useFunnel 도입이 컨벤션에 없는 새 패턴입니다. 다른 다단계 화면(행사 신청 등)에도 같은 방식을 쓸지, 이 PR에서는 인증에만 쓰는 것으로 둘지 의견 부탁드립니다.
  • [c] src/lib/useFunnel.ts(View Transition 어댑터)를 직접 두는 것이 괜찮은지, 아니면 라이브러리에 이슈를 올리고 기다릴지 봐주세요.
  • [c] /에서 로그인 표시가 없을 때 로그인으로 보내는 진입 가드가 의도에 맞는지 봐주세요.
  • [c] [Feat/#96] 설정 화면과 전화번호 변경·회원 탈퇴 화면 구현 #97(설정·전화번호 변경·회원 탈퇴)과 전화번호 입력·포맷(formatPhoneNumber)이 겹칠 수 있습니다. 머지 순서에 따라 한쪽에서 공통화가 필요합니다.
  • [a] 이 환경에서는 Pretendard가 로드되지 않아 대체 폰트 기준으로 줄바꿈을 확인했습니다. 실기기에서 로그인 문구와 약관 본문 줄바꿈이 Figma와 같은지 봐주세요.
  • [a] Figma 대조는 측정 기준으로 했고, 🟢 수준의 차이가 남아 있습니다. 연락처 입력칸에 Figma의 약한 그림자가 없는 것(WDS TextField 기본값), 로그인 배경 빛의 가로 위치가 4.5px 다를 수 있는 것입니다.

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c28b562e-b271-4257-850e-668262abab0a
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tnals0924

Copy link
Copy Markdown
Member

온보딩은 퍼널 구조를 적용하면 좋을 거 같고, 로그인 기능같은 경우에는 제가 개발해둔 k-connect 로그인을 위한 private npm 패키지를 사용하도록 수정하는 건 어떨까요?!

월요일이나 화요일 중 관련해서 자세한 설명드리도록 하겠습니다.

퍼널 구조 참고용 발표 영상
https://www.youtube.com/watch?v=NwLWX2RNVcw&t=289s

@jjunh33
jjunh33 force-pushed the feat/#110-auth-onboarding branch from 5afef1c to c304302 Compare October 12, 2026 07:46
@jjunh33

jjunh33 commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

온보딩은 퍼널 구조를 적용하면 좋을 거 같고, 로그인 기능같은 경우에는 제가 개발해둔 k-connect 로그인을 위한 private npm 패키지를 사용하도록 수정하는 건 어떨까요?!

월요일이나 화요일 중 관련해서 자세한 설명드리도록 하겠습니다.

퍼널 구조 참고용 발표 영상 https://www.youtube.com/watch?v=NwLWX2RNVcw&t=289s

해당 내용 확인해서 수정했습니다. useFunnel()을 이용한 퍼널 구조 적용했고, k-connect 로그인 레포 확인해서 반영했습니다!

@xeoxxn

xeoxxn commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

#97(설정·전화번호 변경·회원 탈퇴) 쪽을 정리하면서 겹치는 부분을 확인해서 공유드립니다.

1. 전화번호 포맷 — 로직은 같고 통과 기준이 다릅니다

리뷰 포인트에 적어주신 그 건이 맞습니다. 하이픈을 넣는 로직은 두 구현이 완전히 같습니다.

다만 버튼이 열리는 기준이 다릅니다.

조건 결과
#112 digits.length === PHONE_DIGIT_COUNT(11) 앞자리 제한 없음
#97 /^010-\d{4}-\d{4}$/ 010으로 시작해야 함

그래서 지금 상태로 둘 다 머지되면 011·016 같은 번호가 가입은 되는데 나중에 설정에서 같은 번호로 바꾸려고 하면 막힙니다. 공통화할 때 함수뿐 아니라 기준도 하나로 맞춰야 할 것 같은데, 구 번호를 받을지 정해진 게 있을까요? 없으면 010 고정(=#97 기준)이 안전해 보입니다.

머지 순서는 말씀하신 대로 나중에 들어가는 쪽이 공통 모듈로 빼는 걸로 하면 될 것 같습니다.

2. (리뷰 포인트에 없던 건) 탈퇴·로그아웃이 AuthEntryGate를 그냥 통과합니다

#112가 /에 AuthEntryGate를 두고 isLoggedIn()으로 가드하는데, src/entities/auth/session.ts에는 표시를 지우는 함수가 없습니다(isLoggedIn·markLoggedIn 둘뿐이고, 저장소 어디에도 removeItem 호출이 없습니다).

#97에는 계정을 떠나는 길이 두 개 있고 둘 다 /로 나갑니다.

  • 설정 → 로그아웃 확인 모달 → 확정
  • 설정 → 회원 탈퇴 → 탈퇴 완료 화면 → "확인"

두 PR이 다 머지되면 stream:auth:logged-in이 남아 있어서 가드를 그대로 통과합니다. 방금 탈퇴한 계정으로 홈이 그대로 보이고, 로그아웃도 같은 탭에서는 아무 효과가 없게 됩니다.

#97 쪽은 나중에 바꿀 자리를 한 곳으로 모아뒀습니다 — src/features/settings/constants/settings.ts의 ACCOUNT_EXIT_PATH입니다. 로그인 화면이 들어오면 이 상수를 /auth로 바꾸고, 호출부 두 곳(SettingsScreen의 로그아웃 확인 모달, WithdrawalCompleteScreen)에서 세션 표시를 지우면 됩니다.

필요한 건 session.ts에 지우는 함수 하나(clearLoggedIn() 정도)뿐이라, #112에서 같이 export 해주시면 #97 또는 후속 PR에서 그대로 가져다 쓰겠습니다. 어느 쪽에서 붙이는 게 편하실까요?

참고 — #97에서 바뀐 것

@jjunh33
jjunh33 force-pushed the feat/#110-auth-onboarding branch from c304302 to e3417a6 Compare October 13, 2026 09:38
@jjunh33

jjunh33 commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

#97(설정·전화번호 변경·회원 탈퇴) 쪽을 정리하면서 겹치는 부분을 확인해서 공유드립니다.

1. 전화번호 포맷 — 로직은 같고 통과 기준이 다릅니다

리뷰 포인트에 적어주신 그 건이 맞습니다. 하이픈을 넣는 로직은 두 구현이 완전히 같습니다.

다만 버튼이 열리는 기준이 다릅니다.

조건 결과
#112 digits.length === PHONE_DIGIT_COUNT(11) 앞자리 제한 없음
#97 /^010-\d{4}-\d{4}$/ 010으로 시작해야 함
그래서 지금 상태로 둘 다 머지되면 011·016 같은 번호가 가입은 되는데 나중에 설정에서 같은 번호로 바꾸려고 하면 막힙니다. 공통화할 때 함수뿐 아니라 기준도 하나로 맞춰야 할 것 같은데, 구 번호를 받을지 정해진 게 있을까요? 없으면 010 고정(=#97 기준)이 안전해 보입니다.

머지 순서는 말씀하신 대로 나중에 들어가는 쪽이 공통 모듈로 빼는 걸로 하면 될 것 같습니다.

2. (리뷰 포인트에 없던 건) 탈퇴·로그아웃이 AuthEntryGate를 그냥 통과합니다

#112가 /에 AuthEntryGate를 두고 isLoggedIn()으로 가드하는데, src/entities/auth/session.ts에는 표시를 지우는 함수가 없습니다(isLoggedIn·markLoggedIn 둘뿐이고, 저장소 어디에도 removeItem 호출이 없습니다).

#97에는 계정을 떠나는 길이 두 개 있고 둘 다 /로 나갑니다.

  • 설정 → 로그아웃 확인 모달 → 확정
  • 설정 → 회원 탈퇴 → 탈퇴 완료 화면 → "확인"

두 PR이 다 머지되면 stream:auth:logged-in이 남아 있어서 가드를 그대로 통과합니다. 방금 탈퇴한 계정으로 홈이 그대로 보이고, 로그아웃도 같은 탭에서는 아무 효과가 없게 됩니다.

#97 쪽은 나중에 바꿀 자리를 한 곳으로 모아뒀습니다 — src/features/settings/constants/settings.ts의 ACCOUNT_EXIT_PATH입니다. 로그인 화면이 들어오면 이 상수를 /auth로 바꾸고, 호출부 두 곳(SettingsScreen의 로그아웃 확인 모달, WithdrawalCompleteScreen)에서 세션 표시를 지우면 됩니다.

필요한 건 session.ts에 지우는 함수 하나(clearLoggedIn() 정도)뿐이라, #112에서 같이 export 해주시면 #97 또는 후속 PR에서 그대로 가져다 쓰겠습니다. 어느 쪽에서 붙이는 게 편하실까요?

참고 — #97에서 바뀐 것

확인해서 두가지 모두 반영했습니다!

@tnals0924 tnals0924 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인·온보딩 흐름이 퍼널 하나로 정리돼 있어서 단계별로 따라가기 편했어요. 테스트 방법이랑 트레이드오프도 자세히 적어 주셔서 리뷰하기 수월했습니다.

코멘트는 템플릿 기준으로 [r] 꼭 반영 / [c] 웬만하면 반영 / [a] 사소한 의견으로 달았어요. [r]로 단 네 건(배포 후 홈 진입, 연락처 단계 뒤로가기, 가입 완료 후 뒤로가기, 앱 WebView 동작)은 머지 전에 같이 얘기해 보면 좋겠습니다.

PR 본문도 하나 봐 주세요. 리팩토링하면서 화면 이름이 AuthSignupScreen으로 바뀌었는데 본문에는 아직 AuthFunnelScreen으로 남아 있어요. 제목에도 '스플래시'가 들어가 있는데, 이번 PR에서 빠졌으니 같이 맞춰 주시면 좋을 것 같아요.

걱정하셨던 부분 중에 확인해 본 것도 남겨요.

  • 콜백에서 인증 code가 두 번 나가는 문제: 패키지의 useKConnectCallback이 결과를 동기로 돌려주고 completeLoginOnce로 저장값도 한 번만 꺼내 써요. 콜백 화면의 hasHandled까지 있어서 괜찮아 보여요.
  • history.replace("complete", {}): use-funnel이 이전 context와 합쳐 줘서 name이 유지되고 guard도 통과해요.
  • src/lib/useFunnel.ts: 0.0.16 원본 어댑터랑 비교해 보니 옵션 이름만 바뀐 게 맞았어요.

Comment thread src/app/router.tsx
element: <HomeScreen />,
// 앱 첫 진입점 — 로그인한 기록이 없으면 스플래시(→ 로그인 화면)로 보낸다
element: (
<AuthEntryGate>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[r] 이대로 머지되면 dev 배포에서 홈에 들어갈 방법이 없어질 것 같아요.

main에 push되면 dev가 자동 배포되는데, 지금 상태에서는 이렇게 돼요.

  • /에 들어가면 AuthEntryGate가 /auth로 보내요.
  • Docker 빌드에 VITE_KCONNECT_CLIENT_ID가 안 넘어가서 K-CONNECT로 시작하기를 누르면 실패 토스트만 떠요.
  • 우회용 ?mockKConnect=는 import.meta.env.DEV에서만 동작해서 배포 빌드에서는 막혀 있어요.

결국 홈을 보려면 콘솔에서 sessionStorage를 직접 넣는 방법밖에 없어요. 그리고 WDS_NPM_TOKEN에 billilge 패키지 읽기 권한이 없으면 pnpm install 단계에서 배포가 아예 실패하고요. 리뷰 포인트에 남겨 주신 CI 건은 후속으로 미루기보다 이 PR에서 같이 처리하거나, 처리 전까지는 가드를 잠시 꺼 두는 게 좋을 것 같은데 어떻게 생각하세요?

<AuthPhoneStep
initialPhone={context.phone ?? ""}
name={context.name}
onBack={() => history.back()}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[r] 실제 K-CONNECT로 로그인한 신규 사용자가 연락처 단계에서 뒤로가기를 누르면 K-CONNECT 쪽 페이지로 나갈 것 같아요.

콜백 화면이 /auth로 replace하면서 initial로 연락처 단계부터 시작하면, 퍼널 history에는 연락처 단계 하나만 있어요. 이 상태에서 history.back()은 navigate(-1)이라 브라우저 history의 바로 앞, 즉 K-CONNECT 페이지로 가게 돼요.

?mockKConnect=new로 테스트하면 앞에 로그인 단계가 있어서 재현되지 않고 실제 로그인에서만 생겨서 놓치기 쉬울 것 같아요. 연락처 단계가 퍼널의 첫 기록일 때는 로그인 단계로 replace하는 식으로 처리하면 어떨까요?

steps: AUTH_SIGNUP_STEP_OPTIONS,
});

const goHome = () => navigate("/", { replace: true });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[r] 가입을 마치고 홈에서 뒤로가기를 누르면 연락처 단계로 다시 들어가져요.

약관 동의 → 가입 완료 → 홈이 모두 replace라서, 홈 바로 앞 history가 /auth?auth.step=phone(이름·연락처가 채워진 상태)으로 남아요. 거기서 다시 진행하면 submitOnboarding이 한 번 더 호출되고요.

/auth에서 이미 로그인한 상태면 홈으로 보내는 처리를 두면, 이 경우와 로그인한 사용자가 /auth로 직접 들어오는 경우를 같이 막을 수 있을 것 같아요. 다만 가입 완료 단계 직전에 markLoggedIn()을 부르고 있어서, 가입 완료 단계는 예외로 둬야 할 것 같습니다.

// 로그인 표시는 sessionStorage에만 있어서, 같은 탭의 새로고침은 홈이 유지되고 탭·브라우저를 새로 열 때만
// 이 가드가 로그인 화면으로 보낸다.
function AuthEntryGate({ children }: AuthEntryGateProps) {
if (!isLoggedIn()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[r] 앱 WebView에서는 어떻게 동작하는 게 맞을지 확인 부탁드려요.

PR 본문에는 "앱 WebView에서는 이 웹 로그인을 쓰면 안 된다(RFC 8252)"고 되어 있는데, 이 가드는 WebView에서도 똑같이 /auth로 보내고 있어요. AuthLoginStep도 isInAppShell()로 앱 레이아웃을 따로 잡아 두셔서, 앱에서 버튼을 누르면 WebView 안에서 그대로 K-CONNECT로 이동하게 될 것 같아요.

앱에서는 가드를 건너뛸지, 브리지로 네이티브 로그인을 띄울지 정해 두고 머지하면 좋을 것 같습니다.

}) {
const { login } = useKConnectLogin({ clientId: KCONNECT_CLIENT_ID });
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [toastOpen, setToastOpen] = useState(hasFailedLogin);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] 로그인 실패 토스트가 실패하지 않았는데도 다시 뜨는 경우가 있어요.

loginFailed가 location.state에 남아 있고, 퍼널이 이동할 때도 ...location.state로 같이 넘기고 있어요. 그래서 아래 경우에 AuthLoginStep이 다시 마운트되면서 토스트가 또 떠요.

  • 실패 후 /auth에서 새로고침할 때
  • 목 흐름에서 연락처 단계로 갔다가 뒤로 돌아올 때

한 번 읽고 나면 state에서 loginFailed를 지워 주면 좋을 것 같아요.

}) {
const [isSubmitting, setIsSubmitting] = useState(false);
const isAllAgreed = TERMS_IDS.every((id) => agreements[id]);
const isRequiredAgreed = agreements.service && agreements.privacy;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] 필수 여부를 service && privacy로 직접 적고 있는데, TERMS[id].isRequired가 이미 있으니 거기서 꺼내 쓰면 약관이 바뀌어도 한 곳만 고치면 될 것 같아요.

const isRequiredAgreed = TERMS_IDS.filter((id) => TERMS[id].isRequired).every(
  (id) => agreements[id],
);

@@ -0,0 +1,65 @@
import type { KConnectUser, OnboardingSubmission } from "@/entities/auth/types";

const MOCK_DELAY_MS = 700;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] coding-style.md에서 목데이터는 <도메인>Mock.ts에 두기로 해서, MOCK_NEW_USER·MOCK_EXISTING_USER는 authMock.ts로 옮기면 다른 도메인과 맞을 것 같아요. 로그인에 mockupApi를 안 쓰신 이유(요청마다 결과가 달라짐)는 주석 보고 이해했어요. 그래도 지연 시간은 mockupApi처럼 배포 빌드에서 0으로 두면 어떨까요? 지금은 배포에서도 700ms씩 기다려요.

return;
}

exchangeKConnectCode(result)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] result를 그대로 넘기면 type: "success"도 같이 들어가요. 지금은 목이라 상관없지만, 실 API에서 이 값을 그대로 요청 body로 보내면 type까지 전송될 수 있어요. 필요한 값만 꺼내서 넘기면 좋을 것 같아요.

const { code, codeVerifier, redirectUri } = result;
exchangeKConnectCode({ code, codeVerifier, redirectUri })

Comment thread src/app/router.tsx
children: [
{
element: <HomeScreen />,
// 앱 첫 진입점 — 로그인한 기록이 없으면 스플래시(→ 로그인 화면)로 보낸다

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 스플래시가 빠졌는데 주석에는 "스플래시(→ 로그인 화면)로 보낸다"가 남아 있어요. src/entities/auth/session.ts 2번째 줄 주석도 같이 고쳐 주시면 좋을 것 같아요.

@@ -0,0 +1,161 @@
import eventsIllustration from "@/assets/images/auth/login-events.png";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 로그인 일러스트 PNG가 장당 500KB 정도라 첫 화면에서 1.5MB를 받아요. 투명 배경 때문에 PNG로 두셨다고 했는데, WebP나 AVIF도 투명 배경을 지원해서 용량을 꽤 줄일 수 있을 것 같아요. 변환 도구를 새로 들이는 게 부담스러우면 이미지만 미리 변환해서 넣는 것도 방법일 것 같습니다.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webp로 일단 변환해보면 좋을 거 같습니다~

xeoxxn added a commit that referenced this pull request Oct 14, 2026
- 계정 정보와 앱 버전을 `entities/users/`로 옮기고 화면은 `use()`로 읽는다. 다른 기능과 같은
  `<도메인>Api` + `mockupApi` + Suspense 구조다. 카드 안쪽만 데이터 컴포넌트로 빼서
  껍데기·라벨은 그대로 그려지고, 받는 동안은 전용 스켈레톤이 행 자리를 채운다
- 앱 버전을 `{ version, isLatest }`로 나눠 받는다 — "26.0.1 · 최신버전" 문구 조합은 화면이 한다
- 전화번호 하이픈 포맷·유효성을 `lib/phone.ts`로 뺀다. #110(PR #112)의 연락처 입력과 같은
  로직이라 그쪽이 import만 하면 되도록 먼저 공용 위치에 둔다. 통과 기준은 `010` 고정으로
  맞췄다 — 01X는 2G 종료로 전부 010 번호이동이 끝나서 새로 입력될 일이 없다
- `SettingsRow` props를 variant별로 나눠 `variant="account"` + `tone` 조합이 타입에서 걸리게 한다
- 로그아웃 확정과 탈퇴 완료의 "계정에서 나가기"를 `useAccountExit` 훅 하나로 묶는다 —
  세션 정리를 붙일 때 한쪽을 빠뜨리지 않게 한다
- 탈퇴 완료 화면을 주소로 바로 열면 홈으로 보낸다(사물함 신청 완료와 같은 처리). navigate
  state는 꾸밀 수 있는 값이라 실제 본인 확인은 서버 몫이라는 걸 주석으로 남겼다
- 탈퇴 확인 모달도 로그아웃처럼 닫고 이동하도록 순서를 맞춘다
- 경로를 `/settings/withdrawal`로 바꿔 컴포넌트 이름(`Withdrawal*`)과 형태를 맞춘다
- `settings`가 용어 사전에 없는 이름이라는 점과, 백엔드 엔드포인트가 나오면 맞춰야 한다는 걸
  경로 상수와 `usersApi`에 주석으로 남겼다
@xeoxxn

xeoxxn commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

앞서 공유드린 전화번호 포맷 건, #97 리뷰에서 "나중에 머지되는 쪽이 정리하는 것보다 먼저 공용 위치에 빼두는 게 낫다"는 의견을 받아서 #97에서 먼저 빼놨습니다. #112 쪽은 import만 해주시면 됩니다!

// src/lib/phone.ts
export const PHONE_NUMBER_MAX_LENGTH = 13; // 010-1234-5678
export function toPhoneDigits(value: string): string;
export function formatPhoneNumber(value: string): string;
export function isValidPhoneNumber(value: string): boolean;

features/auth/utils/formatPhoneNumber.ts의 toPhoneDigits·formatPhoneNumber와 하이픈 로직이 같습니다. 다만 두 가지만 봐주세요.

1. formatPhoneNumber가 받는 값이 다릅니다. #112 것은 이미 숫자만 추려진 digits를 받지만, 공용 쪽은 원본 입력값을 받아 안에서 숫자만 추립니다. 그래서 formatPhoneNumber(toPhoneDigits(x))처럼 겹쳐 부르지 않아도 되고, 지금 코드의 value={formatPhoneNumber(digits)}는 그대로 두셔도 동작합니다(이미 숫자뿐이라 추리는 단계가 그냥 통과합니다).

2. 통과 기준을 010 고정으로 맞췄습니다. isValidPhoneNumber가 /^010-\d{4}-\d{4}$/입니다. #112의 digits.length !== PHONE_DIGIT_COUNT보다 좁습니다.

01X(011·016~019)는 2G 종료와 함께 전부 010으로 번호이동이 끝나서 지금 새로 입력될 일이 없다고 보고 좁은 쪽으로 정했습니다. 같은 앱에서 가입은 되는데 설정에서 같은 번호로 못 바꾸는 상황을 막는 게 더 중요하다고 판단했고요. 혹시 온보딩에서는 넓게 받아야 할 이유가 있으면 알려주세요 — 그러면 공용 쪽 정규식을 넓히고 #97도 같이 맞추겠습니다.

PHONE_DIGIT_COUNT는 공용 모듈 안으로 들어가서(하이픈 뺀 자릿수) features/auth/constants/auth.ts에서는 빼셔도 됩니다. 버튼 활성 조건은 isValidPhoneNumber(formatPhoneNumber(digits))로 바꾸시면 됩니다.

#97은 이 변경까지 올려뒀습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

스플래시·로그인·온보딩 화면 구현

3 participants