diff --git a/backend/app.py b/backend/app.py index 2ce3c45..8ea3bb9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -745,6 +745,40 @@ def claim_kakao_start(): return redirect(auth_url) +def _handle_mypage_kakao_callback(code, kakao_client): + """마이페이지 카카오 콜백 처리 - 카카오 ID로 유저 조회 후 마이페이지 이동""" + success, token_data = kakao_client.get_access_token(code) + if not success: + return render_template('error.html', message="카카오 인증에 실패했습니다.") + + access_token = token_data.get('access_token') + success, user_info = kakao_client.get_user_info(access_token) + if not success: + return render_template('error.html', message="카카오 사용자 정보를 가져올 수 없습니다.") + + kakao_id = user_info.get('kakao_id') + kakao_phone_raw = user_info.get('phone_number') + kakao_phone = normalize_kakao_phone(kakao_phone_raw) + + # 1) 카카오 ID로 기존 유저 조회 + existing_user_id = find_user_by_kakao_id(kakao_id) + if existing_user_id: + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + cursor.execute("SELECT phone FROM users WHERE id = ?", (existing_user_id,)) + row = cursor.fetchone() + if row: + return redirect(f"/my-page?phone={row['phone']}") + + # 2) 카카오에서 전화번호를 받은 경우 + if kakao_phone: + return redirect(f"/my-page?phone={kakao_phone}") + + # 3) 둘 다 없으면 전화번호 입력 안내 + return render_template('error.html', + message="카카오 계정에 연결된 적립 정보가 없습니다. 전화번호로 조회해주세요.") + + @app.route('/claim/kakao/callback') def claim_kakao_callback(): """카카오 OAuth 콜백 - 토큰 교환 → 사용자 정보 → 적립 처리""" @@ -772,6 +806,10 @@ def claim_kakao_callback(): if csrf_token != session.pop('kakao_csrf', None): return render_template('error.html', message="보안 검증에 실패했습니다. 다시 시도해주세요.") + # 2.5 마이페이지 조회 목적이면 별도 처리 + if state_data.get('purpose') == 'mypage': + return _handle_mypage_kakao_callback(code, get_kakao_client()) + # 3. claim 컨텍스트 복원 token_param = state_data.get('t', '') parts = token_param.split(':') @@ -894,6 +932,28 @@ def api_claim_kakao(): }) +@app.route('/my-page/kakao/start') +def mypage_kakao_start(): + """마이페이지 카카오 로그인 조회""" + from services.kakao_client import get_kakao_client + + csrf_token = secrets.token_hex(16) + state_data = { + 'purpose': 'mypage', + 'csrf': csrf_token + } + state_encoded = base64.urlsafe_b64encode( + json.dumps(state_data).encode() + ).decode() + + session['kakao_csrf'] = csrf_token + + kakao_client = get_kakao_client() + auth_url = kakao_client.get_authorization_url(state=state_encoded) + + return redirect(auth_url) + + @app.route('/my-page') def my_page(): """마이페이지 (전화번호로 조회)""" diff --git a/backend/services/kakao_client.py b/backend/services/kakao_client.py index f3f7457..43db429 100644 --- a/backend/services/kakao_client.py +++ b/backend/services/kakao_client.py @@ -39,7 +39,7 @@ class KakaoAPIClient: 'client_id': self.client_id, 'redirect_uri': self.redirect_uri, 'response_type': 'code', - 'scope': 'profile_nickname,profile_image,account_email,name,phone_number' + 'scope': 'profile_nickname,profile_image,account_email' } if state: diff --git a/backend/templates/my_page.html b/backend/templates/my_page.html index 3d1ff56..545a954 100644 --- a/backend/templates/my_page.html +++ b/backend/templates/my_page.html @@ -272,6 +272,10 @@
마이페이지
다른 번호로 조회 + + + 카카오 조회 +
diff --git a/backend/templates/my_page_login.html b/backend/templates/my_page_login.html index e9304b5..46e4faf 100644 --- a/backend/templates/my_page_login.html +++ b/backend/templates/my_page_login.html @@ -167,6 +167,20 @@ + +
+ 또는 +
+
+ + + + + + + 카카오로 조회하기 + + ← 홈으로
diff --git a/docs/kakao-troubleshooting.md b/docs/kakao-troubleshooting.md new file mode 100644 index 0000000..161f975 --- /dev/null +++ b/docs/kakao-troubleshooting.md @@ -0,0 +1,168 @@ +# 카카오 OAuth 트러블슈팅 가이드 + +## 에러 코드 목록 + +### KOE101 - 앱 관리자 설정 오류 / 플랫폼키 에러 + +**원인**: `KAKAO_CLIENT_ID`(REST API 키)가 비어있거나 잘못된 값 + +**해결**: +1. `backend/.env` 파일이 존재하는지 확인 +2. `KAKAO_CLIENT_ID` 값이 올바른지 확인 +3. 카카오 개발자 콘솔 > 앱 > 플랫폼 키 > REST API 키에서 확인 + +```bash +# backend/.env +KAKAO_CLIENT_ID=caad27ac4bc92d8dc83bdd6aae744811 +``` + +### KOE205 - Invalid scope + +**원인**: 요청한 OAuth scope가 앱에서 허용되지 않음 + +**해결**: +- `name`, `phone_number` scope는 **비즈앱 심사** 후 동의항목에서 별도 활성화 필요 +- 기본 scope만 사용: `profile_nickname,profile_image,account_email` +- 파일: `backend/services/kakao_client.py` > `get_authorization_url()` > `scope` 파라미터 + +**동의항목 활성화 경로**: +``` +카카오 개발자 콘솔 > 카카오 로그인 > 동의항목 +``` + +| scope | 비즈앱 필요 | 현재 상태 | +|-------|-----------|----------| +| profile_nickname | X | 사용 중 | +| profile_image | X | 사용 중 | +| account_email | X | 사용 중 | +| name | O | 미사용 (비즈앱 심사 필요) | +| phone_number | O | 미사용 (비즈앱 심사 필요) | + +### KOE320 - authorization code not found + +**원인**: 카카오 authorization code가 만료되었거나 이미 사용됨 + +**해결**: +- code는 **1회용**이며 발급 후 수 분 내 사용해야 함 +- 브라우저 새로고침으로 같은 code를 재사용하면 발생 +- 사용자에게 다시 카카오 로그인하도록 안내 + +### UnboundLocalError: kakao_client + +**원인**: 콜백 핸들러에서 `kakao_client` 변수가 초기화되기 전에 접근 + +**상황**: 마이페이지 카카오 조회 시, 콜백 함수 상단의 `purpose == 'mypage'` 분기에서 +`kakao_client` 변수가 아직 할당되지 않은 상태에서 사용 + +**해결**: `get_kakao_client()` 함수를 직접 호출 +```python +# 잘못된 코드 +if state_data.get('purpose') == 'mypage': + return _handle_mypage_kakao_callback(code, kakao_client) # UnboundLocalError + +# 올바른 코드 +if state_data.get('purpose') == 'mypage': + return _handle_mypage_kakao_callback(code, get_kakao_client()) +``` + +### ModuleNotFoundError: No module named 'requests' + +**원인**: `requests` 라이브러리 미설치 + +**해결**: +```bash +pip install requests +``` + +## 카카오 개발자 콘솔 주의사항 + +### Redirect URI 위치 (2025년 12월 개편) + +**현재 경로** (2025.12~): +``` +앱 > 플랫폼 키 > REST API 키 클릭 > 리다이렉트 URI +``` + +**혼동하기 쉬운 위치**: +- `카카오 로그인 > 일반` - 여기에는 더 이상 Redirect URI 없음 (Webhook만 있음) +- `카카오 로그인 > 고급 > 로그아웃 리다이렉트 URI` - 로그아웃용이므로 혼동 주의 + +자세한 설정 가이드: [kakao-oauth-setup.md](./kakao-oauth-setup.md) + +### Client Secret 위치 + +``` +앱 > 플랫폼 키 > REST API 키 클릭 > 클라이언트 시크릿 +``` + +활성화 상태 확인 필수. 비활성화 시 토큰 교환 실패. + +## 환경변수 체크리스트 + +```bash +# backend/.env (git에 포함되지 않음) +KAKAO_CLIENT_ID= +KAKAO_CLIENT_SECRET=<클라이언트 시크릿 코드> +KAKAO_REDIRECT_URI=https://mile.0bin.in/claim/kakao/callback +``` + +## 로컬 개발 환경 주의사항 + +로컬(192.168.0.14:7001)에서 테스트 시: +1. 카카오 버튼 클릭 → 카카오 로그인 화면 (정상) +2. 로그인 후 `https://mile.0bin.in/claim/kakao/callback`로 리다이렉트됨 +3. 로컬이 아닌 **운영 서버**로 돌아감 + +로컬에서 완전한 테스트를 하려면: +- `KAKAO_REDIRECT_URI`를 `http://192.168.0.14:7001/claim/kakao/callback`로 변경 +- 카카오 콘솔에도 해당 URI 등록 필요 +- 테스트 후 반드시 원래 값으로 복원 + +## 데이터 흐름 + +### 카카오 적립 (QR 스캔 → 카카오 로그인) + +``` +QR 스캔 → /claim?t=txn:nonce + → "카카오로 적립하기" 클릭 + → /claim/kakao/start?t=txn:nonce + → state={t, csrf} 인코딩 → 카카오 OAuth + → /claim/kakao/callback + → phone_number 없음 → claim_kakao_phone.html (전화번호 입력) + → POST /api/claim/kakao (세션의 kakao_data + 폼의 phone) + → get_or_create_user → link_kakao_identity → claim_mileage + → 성공 화면 +``` + +### 카카오 마이페이지 조회 + +``` +/my-page → "카카오로 조회하기" 클릭 + → /my-page/kakao/start + → state={purpose:"mypage", csrf} 인코딩 → 카카오 OAuth + → /claim/kakao/callback (동일 콜백 URL 재사용) + → purpose=="mypage" 감지 → _handle_mypage_kakao_callback() + → find_user_by_kakao_id(kakao_id) + → 유저 발견 → /my-page?phone=xxx 리다이렉트 + → 유저 없음 → "카카오 계정에 연결된 적립 정보가 없습니다" 에러 +``` + +### customer_identities 테이블 매핑 + +``` +카카오 적립 시: + kakao_id="12345678" + phone="01021307390" + → users 테이블: user_id=7 + → customer_identities: {user_id=7, provider="kakao", provider_id="12345678"} + +카카오 조회 시: + kakao_id="12345678" + → customer_identities에서 user_id=7 조회 + → users 테이블에서 phone="01021307390" 조회 + → /my-page?phone=01021307390 +``` + +--- + +**작성일**: 2026-02-25 +**관련 문서**: [kakao-oauth-setup.md](./kakao-oauth-setup.md)