www 패키지 페이지·Google 로그인·이메일 인증·언어 선택 개선 기획
목적: (1) 패키지 선택 페이지에서 IP 기반 국가 기본값, (2) Google sign-in 미설정 에러 원인·개선, (3) 이메일 인증코드 미수신 원인·해결, (4) 이메일 인증코드 인라인 입력(페이지 이동 없음), (5) 언어 선택 확장 및 브라우저 감지 기본값.
범위: 기획·요구사항·원인 분석·개선안만 정의. 코드 생성은 별도 단계.
1. 패키지 선택 페이지 — IP 기반 국가 기본값
1.1 요구사항
- 패키지 선택(Step 1) 페이지 진입 시 사용자 IP를 바탕으로 접속 국가를 인식하여, Country 선택기의 기본값으로 설정한다.
1.2 현행
locale.js fetchGeoCountry(): Cloudflare Pages functions/api/country.js → GET /api/country → CF-IPCountry 헤더 기반 국가 코드 반환.
getEffectiveCountry(): stored > geoCountry > null.
init(): renderLocaleSelector() 후 !getStoredCountry() 이면 fetchGeoCountry() 호출.
- 이슈:
fetchGeoCountry()가 비동기이므로, 초기 렌더 시 Country 드롭다운이 “Select country”로 표시되었다가 나중에 geo 값으로 갱신됨. 패키지 페이지에 진입할 때 아직 geo가 로드되지 않았을 수 있음.
1.3 개선안
| 항목 | 내용 |
|---|
| 로드 시점 | index.html 로드 시(또는 DOMContentLoaded) fetchGeoCountry()를 먼저 호출하고, 응답 수신 후 renderLocaleSelector() 실행. 또는 renderLocaleSelector() 내부에서 geo가 없으면 로딩 표시 후 fetch 완료 시 재렌더. |
| 패키지 페이지 특화 | Step 1이 보일 때 Country 선택기에 geo 기반 기본값이 반영되어 있도록, fetchGeoCountry() 완료 후 setCountry(geoCountry)로 저장(사용자가 아직 선택하지 않은 경우). |
| 배포 환경 | /api/country는 Cloudflare Pages Functions에서만 동작. 로컬 개발 시 CF-IPCountry 없으면 빈 응답. 로컬용 fallback(예: 외부 IP→국가 API 또는 기본값 null)은 선택. |
1.4 구현 포인트
locale.js init(): fetchGeoCountry()를 await/순차화하여, geo 수신 후 renderLocaleSelector() 호출.
- 또는
fetchGeoCountry() 완료 시 geoCountry를 setCountry(geoCountry)로 저장(저장값이 없는 경우만).
- Country→Region 매핑(예: KR→sg, US→us)은 기존 region 선택 로직과 연동 시 검토.
2.1 에러 발생 위치
signup.html (line 131–136): Google 버튼 클릭 시 api.getGoogleAuthUrl(returnUrl) 반환값이 없거나 '#' 이면 showError('Google sign-in is not configured.') 호출.
2.2 원인
| 원인 | 설명 |
|---|
| PREGO_AUTH_URL 미설정 | api.js getGoogleAuthUrl() (line 246–249): PREGO_AUTH_URL이 비어 있으면 '#' 반환. |
| 환경 변수 주입 누락 | www는 정적 HTML. PREGO_AUTH_URL은 배포 시 <script>window.PREGO_AUTH_URL='...';</script> 형태로 주입해야 함. Cloudflare Pages 등에서는 빌드 시점 또는 Workers/Pages 환경변수를 HTML에 주입하는 설정 필요. |
| Auth 서비스 미배포 | PREGO_AUTH_URL이 가리키는 Auth Worker/Gateway가 Google OAuth를 지원하지 않거나, /auth/google 경로가 없음. |
2.3 확인 절차
- 브라우저 콘솔에서
window.PREGO_AUTH_URL 확인 — 비어 있으면 환경 주입 문제.
- Auth URL이 설정되어 있으면
GET {PREGO_AUTH_URL}/auth/google?return_url=... 호출 가능 여부 확인.
- Auth 서비스(Zuplo/Worker)에 Google Client ID, Client Secret, redirect_uri 설정 여부 확인.
2.4 개선안
| 항목 | 내용 |
|---|
| 즉시 | 배포 환경에서 PREGO_AUTH_URL을 Auth Gateway URL(예: Zuplo /auth/google 노출 URL)로 설정. 빌드/배포 스크립트 또는 Pages 환경변수 → HTML injection. |
| UI | PREGO_AUTH_URL이 없을 때 Google 버튼을 비활성화 또는 숨김하고, “Google sign-in is not available” 툴팁/문구 표시. (에러 클릭 후 표시보다 예방적.) |
| 대체 진입 | PREGO_CONTROL_PLANE_URL 등 다른 URL에 /auth/google이 있다면, Auth 전용 URL을 별도로 두거나 프록시 경로를 PREGO_AUTH_URL로 통일. |
| 문서화 | apps/www/README.md, docs/runbook/tenant-onboarding-flow.md에 PREGO_AUTH_URL 설정 절차 및 Google OAuth 연동 요건 명시. |
3. 이메일 인증코드 미수신 — 원인 분석 및 해결안
3.0 증상
- 이메일 인증 버튼(“Send verification code”) 클릭 시 UI에서는 성공으로 보이나, 실제 이메일이 수신함에 도착하지 않음.
3.0.1 플로우 요약
- www →
api.sendEmailOtp(email) → PREGO_AUTH_URL/auth/email/send-otp (실제 호출) 또는 mock 모드 (PREGO_AUTH_URL 미설정 시)
- Zuplo auth-handler (
authEmailSendOtpHandler): Control Plane D1에 OTP 저장 → Resend API로 이메일 발송
3.0.2 원인 분석
| # | 원인 | 환경 | 설명 |
|---|
| 1 | PREGO_AUTH_URL 미설정 | 로컬 | api.js 254–278행: base가 비어 있으면 실제 fetch 없이 resolve({ success: true, message: 'OTP sent (mock)' }) 반환. 이메일 발송 요청이 절대 나가지 않음. 로컬에서 index.html/signup.html을 그대로 열고 PREGO_AUTH_URL을 주입하지 않으면 이 상황에 해당. |
| 2 | RESEND_API_KEY 미설정 | 배포 | Zuplo auth-handler.ts 324–353행: RESEND_API_KEY가 없으면 Resend API를 호출하지 않고 context.log.warn("RESEND_API_KEY not configured...") 후 200 + “OTP sent” 반환. OTP는 D1에 저장되지만 이메일은 발송되지 않음. |
| 3 | CONTROL_PLANE_URL / INTERNAL_API_KEY 미설정 | 배포 | auth-handler가 D1 저장 전에 500 "Control Plane not configured" 반환. 이 경우 사용자에게 에러가 표시되므로, 이메일 미수신만 보인다면 1·2번 가능성이 큼. |
| 4 | Resend API 실패 | 배포 | emailResponse.ok가 false여도 auth-handler는 200을 유지. Resend API 에러(할당량 초과, 도메인 미인증 등) 시 이메일 미발송. Zuplo 로그 확인 필요. |
| 5 | CORS / 네트워크 | 로컬→배포 | www를 localhost에서 실행하고 PREGO_AUTH_URL이 배포 URL이면, CORS가 localhost를 허용해야 함. 미허용 시 브라우저에서 fetch 실패 → 사용자에게 에러 표시됨. |
3.0.3 로컬 환경인 경우
- 로컬에서 www만 실행하고
PREGO_AUTH_URL을 설정하지 않으면 → 원인 1에 해당.
- 로컬에서 이메일까지 수신하려면:
- 배포된 Zuplo Gateway URL을
PREGO_AUTH_URL로 설정 (예: index.html 또는 빌드 시 <script>window.PREGO_AUTH_URL='https://xxx.zuplo.app';</script>)
- Zuplo에
RESEND_API_KEY, CONTROL_PLANE_URL, INTERNAL_API_KEY 설정
- CORS에서
http://localhost:* 허용
3.0.4 해결안
| 대상 | 조치 |
|---|
| 로컬 개발 | (1) 로컬용 PREGO_AUTH_URL을 배포된 Auth Gateway로 설정하거나, (2) mock 모드일 때 “OTP sent (mock)” 대신 “로컬 개발 모드: 실제 이메일은 발송되지 않습니다. 테스트 코드 123456 사용” 등 명시적 안내 표시. |
| 배포 | Zuplo 환경변수에 RESEND_API_KEY 설정. Resend 대시보드에서 도메인 인증(noreply@pregoi.com 등) 확인. |
| 디버깅 | 브라우저 Network 탭에서 POST .../auth/email/send-otp 호출 여부 확인. 호출이 없으면 PREGO_AUTH_URL 미설정. 200인데 메일이 없으면 Zuplo/Resend 설정 확인. |
| 문서화 | apps/www/README.md, docs/runbook/에 PREGO_AUTH_URL 및 Zuplo 환경변수(RESEND_API_KEY 등) 설정 절차 명시. |
4. 이메일 인증코드 — 인라인 입력(페이지 이동 없음)
4.1 요구사항
- 이메일 인증코드를 다른 페이지로 이동하지 않고, 이메일 주소 입력 필드 바로 아래에 일반 input으로 숫자를 받음.
- Verify 버튼 클릭 시 해당 페이지에서 인증 처리.
4.2 현행
- index.html Step 2: 이미 인라인 패턴 있음 — 이메일 폼 +
#otp-section(hidden) + #otp-code input + #btn-verify-otp. OTP 발송 후 섹션 표시, Verify 클릭 시 api.verifyEmailOtp() 호출.
- signup.html: OTP 발송 후
signup-verify.html로 리다이렉트. signup-verify.html에서 6칸 OTP 박스로 입력.
4.3 개선안
| 페이지 | 변경 |
|---|
| signup.html | OTP 발송 후 리다이렉트하지 않고, 이메일 입력 필드 아래에 (1) 인증코드 input 필드(6자리 숫자), (2) Verify 버튼을 추가. Verify 성공 시 index.html?signin=success 등으로 이동. |
| signup-verify.html | 유지 — 직접 URL 접근(예: 북마크, 이메일 링크) 시 fallback. 또는 signup.html로 통합 후 삭제 가능(선택). |
| input 형태 | 단일 <input type="text" inputmode="numeric" maxlength="6" pattern="[0-9]*"> 또는 기존 6칸 박스. 요구사항 “일반 input field”에 따라 단일 input 권장. |
4.4 구현 포인트
- signup.html:
#otp-section 블록 추가(초기 hidden). Send verification code 성공 시 (1) otp-section 표시, (2) 리다이렉트 제거.
- Verify 버튼:
api.verifyEmailOtp(email, code) 호출. 성공 시 세션 저장 후 index.html 이동.
- Resend 버튼: 기존과 동일.
5. 언어 선택 — 확장 및 브라우저 감지 기본값
5.1 요구사항
- 기본 언어: English.
- 옵션 추가: Korean, Japanese, Chinese, Vietnamese, Arabic.
- 기본값 결정: 브라우저의 기본 사용 언어(
navigator.language)를 감지하여, 지원 언어에 포함되면 해당 언어를 기본값으로 설정.
- 번역: 현재 번역본 없음. 모든 언어 선택 시 일단 영어 콘텐츠만 표시.
5.2 현행
locale.js SUPPORTED_LANGS: [{ code: 'en', label: 'English' }] 만 존재.
getEffectiveLocale(): stored > browser > ‘en’.
getBrowserLang(): navigator.language 또는 navigator.userLanguage 의 첫 부분(예: ‘ko-KR’ → ‘ko’).
5.3 개선안
| 항목 | 내용 |
|---|
| SUPPORTED_LANGS | en, ko, ja, zh, vi, ar 추가. label: English, 한국어, 日本語, 中文, Tiếng Việt, العربية. |
| 기본값 | getEffectiveLocale(): 1) localStorage 저장값 2) navigator.language/navigator.languages[0] 에서 지원 코드 매칭 3) ‘en’. |
| 매핑 | zh-CN, zh-TW → zh. ar-SA, ar-EG 등 → ar. |
| 콘텐츠 | 언어 코드별 번역 없음. 모든 lang 값에 대해 동일한 영어 텍스트 사용. getEffectiveLocale() 반환값만 저장하고, UI 문자열은 영어 고정. |
| 선택기 | 상단 header의 Language 드롭다운에 6개 옵션 노출. |
5.4 구현 포인트
SUPPORTED_LANGS 배열 확장.
getBrowserLang(): navigator.languages 순회하며 지원 코드와 첫 번째 매칭 반환.
- 번역: Phase 1에서는 적용하지 않음. 향후 i18n 파일 또는 스크립트 도입 시 확장.
6. 요약 및 적용 순서
| # | 항목 | 변경 대상 | 우선순위 |
|---|
| 1 | IP 기반 국가 기본값 | locale.js (fetchGeoCountry 순서, setCountry 적용) | P1 |
| 2 | Google sign-in 원인·개선 | PREGO_AUTH_URL 설정, signup.html 버튼 비활성화 조건 | P1 |
| 3 | 이메일 인증코드 미수신 | PREGO_AUTH_URL 로컬 설정, Zuplo RESEND_API_KEY, mock 모드 안내 | P1 |
| 4 | 이메일 인증 인라인 | signup.html (otp-section, Verify 버튼, 리다이렉트 제거) | P1 |
| 5 | 언어 옵션·브라우저 감지 | locale.js (SUPPORTED_LANGS, getBrowserLang) | P2 |
7. 참조