diff --git a/backend/app.py b/backend/app.py index 2bf3a6f..bfd28d1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -791,7 +791,15 @@ def claim_kakao_start(): def _handle_mypage_kakao_callback(code, kakao_client): - """마이페이지 카카오 콜백 처리 - 카카오 ID로 유저 조회 후 마이페이지 이동""" + """ + 마이페이지 카카오 콜백 처리 - 카카오 연동(머지) + 마이페이지 이동 + + 케이스별 동작: + A) 카카오 ID가 이미 연결된 유저 → 그 유저의 마이페이지로 이동 + B) 미연결 + 카카오 전화번호로 기존 유저 발견 → 카카오 연동 후 이동 + C) 미연결 + 기존 유저 없음 → 신규 생성 + 카카오 연동 + D) 전화번호 없음 → 에러 안내 + """ success, token_data = kakao_client.get_access_token(code) if not success: return render_template('error.html', message="카카오 인증에 실패했습니다.") @@ -804,24 +812,51 @@ def _handle_mypage_kakao_callback(code, kakao_client): kakao_id = user_info.get('kakao_id') kakao_phone_raw = user_info.get('phone_number') kakao_phone = normalize_kakao_phone(kakao_phone_raw) + kakao_name = user_info.get('name') or user_info.get('nickname', '고객') - # 1) 카카오 ID로 기존 유저 조회 + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # Case A: 카카오 ID로 이미 연결된 유저 있음 existing_user_id = find_user_by_kakao_id(kakao_id) if existing_user_id: - conn = db_manager.get_sqlite_connection() - cursor = conn.cursor() + # "고객" 이름이면 카카오 실명으로 업데이트 + if kakao_name and kakao_name != '고객': + cursor.execute( + "UPDATE users SET nickname = ? WHERE id = ? AND nickname = '고객'", + (kakao_name, existing_user_id)) + conn.commit() cursor.execute("SELECT phone FROM users WHERE id = ?", (existing_user_id,)) row = cursor.fetchone() - if row: + if row and row['phone']: return redirect(f"/my-page?phone={row['phone']}") - # 2) 카카오에서 전화번호를 받은 경우 - if kakao_phone: - return redirect(f"/my-page?phone={kakao_phone}") + # 전화번호 없으면 연동 불가 + if not kakao_phone: + return render_template('error.html', + message="카카오 계정에 전화번호 정보가 없습니다. 카카오 설정에서 전화번호를 등록해주세요.") - # 3) 둘 다 없으면 전화번호 입력 안내 - return render_template('error.html', - message="카카오 계정에 연결된 적립 정보가 없습니다. 전화번호로 조회해주세요.") + # Case B/C: 전화번호로 기존 유저 조회 + cursor.execute("SELECT id, nickname FROM users WHERE phone = ?", (kakao_phone,)) + phone_user = cursor.fetchone() + + if phone_user: + # Case B: 기존 전화번호 유저 → 카카오 연동 (머지) + user_id = phone_user['id'] + link_kakao_identity(user_id, kakao_id, user_info) + # "고객" 이름이면 카카오 실명으로 업데이트 + if phone_user['nickname'] == '고객' and kakao_name and kakao_name != '고객': + cursor.execute("UPDATE users SET nickname = ? WHERE id = ?", + (kakao_name, user_id)) + conn.commit() + logging.info(f"마이페이지 카카오 머지: user_id={user_id}, kakao_id={kakao_id}") + else: + # Case C: 신규 유저 생성 + 카카오 연동 + user_id, _ = get_or_create_user(kakao_phone, kakao_name) + link_kakao_identity(user_id, kakao_id, user_info) + logging.info(f"마이페이지 카카오 신규: user_id={user_id}, kakao_id={kakao_id}") + + return redirect(f"/my-page?phone={kakao_phone}") @app.route('/claim/kakao/callback') diff --git a/docs/user-identity-merge.md b/docs/user-identity-merge.md new file mode 100644 index 0000000..a90ac0f --- /dev/null +++ b/docs/user-identity-merge.md @@ -0,0 +1,168 @@ +# 사용자 계정 연동(머지) 시나리오 + +## 개요 + +청춘약국 마일리지 시스템은 두 가지 경로로 사용자가 생성됩니다: +1. **전화번호 경로** — 키오스크에서 번호 입력, 회원가입 폼 +2. **카카오 경로** — QR 스캔 후 카카오 로그인 + +한 고객이 두 경로를 모두 사용할 수 있으므로, **전화번호 기반 유저에 카카오 계정을 연동(머지)** 하는 로직이 필요합니다. + +--- + +## 데이터 구조 + +``` +users 테이블 +├── id (PK) +├── nickname ← 키오스크 신규: "고객" / 카카오 연동 후: 실명 +├── phone ← 전화번호 (유니크 키) +├── mileage_balance +└── ... + +customer_identities 테이블 +├── user_id (FK → users.id) +├── provider = 'kakao' +└── provider_user_id = 카카오 ID +``` + +**핵심 원칙**: `phone`이 유저의 기본 식별자. 카카오는 부가 연동. + +--- + +## 시나리오별 동작 + +### 시나리오 1: 키오스크만 사용하는 고객 (가장 흔함) + +``` +1회차: 키오스크 → 010-1234-5678 입력 + → get_or_create_user("01012345678", "고객") + → users: {id: 100, nickname: "고객", phone: "01012345678", balance: 0} + → 500P 적립 → balance: 500 + +2회차: 키오스크 → 같은 번호 + → get_or_create_user → 기존 user_id=100 반환 + → 300P 적립 → balance: 800 + +5회차까지: 전부 user_id=100에 누적 +``` + +**결과**: 한 유저에 전부 쌓임. 문제없음. + +--- + +### 시나리오 2: 키오스크 N회 → 알림톡 → 카카오 연동 (핵심 플로우) + +``` +[키오스크 5회 적립] → user_id=100, balance=2,000P, nickname="고객" + +[알림톡 수신] → "적립 내역 확인" 버튼 탭 + → https://mile.0bin.in/my-page?phone=01012345678 + → 마이페이지 표시 (잔액 2,000P, 거래 5건) + +[카카오 로그인 버튼 클릭] + → _handle_mypage_kakao_callback() 실행 + → find_user_by_kakao_id(kakao_id) → 없음 + → 카카오에서 전화번호 "01012345678" 수신 + → phone으로 users 조회 → user_id=100 발견 + → link_kakao_identity(100, kakao_id, ...) ← 여기서 머지! + → nickname "고객" → 카카오 실명 "김철수"로 업데이트 + → /my-page?phone=01012345678 리다이렉트 +``` + +**결과**: +- user_id=100에 카카오 계정 연결됨 +- 기존 5건의 적립 내역 그대로 유지 +- 이름 "고객" → "김철수"로 업데이트 +- 다음부터 QR 스캔으로도 같은 계정으로 적립 + +--- + +### 시나리오 3: QR 스캔(카카오) 먼저 → 키오스크 사용 + +``` +[QR 스캔 + 카카오 로그인] + → 카카오에서 phone "01012345678" + kakao_id 수신 + → get_or_create_user("01012345678", "김철수") + → user_id=100 생성 (phone + kakao 동시에 연결) + → 500P 적립 + +[이후 키오스크 사용] → 010-1234-5678 입력 + → get_or_create_user → 같은 phone → user_id=100 반환 + → 300P 적립 → balance: 800 +``` + +**결과**: 처음부터 phone + kakao가 연결되어 있으므로 머지 불필요. + +--- + +### 시나리오 4: 이미 카카오 연동된 유저가 다시 카카오 로그인 + +``` +[카카오 로그인 (마이페이지)] + → find_user_by_kakao_id(kakao_id) → user_id=100 발견 + → 이미 연결됨 → 마이페이지로 리다이렉트 +``` + +**결과**: 중복 연동 방지. `link_kakao_identity()`도 내부적으로 중복 INSERT 방지. + +--- + +### 시나리오 5: 카카오에 전화번호가 없는 경우 + +``` +[카카오 로그인 (마이페이지)] + → find_user_by_kakao_id → 없음 + → kakao_phone = None (카카오 설정에서 전화번호 미등록) + → 에러: "카카오 계정에 전화번호 정보가 없습니다" +``` + +**결과**: 연동 불가. 카카오 설정에서 전화번호 등록 안내. + +--- + +### 시나리오 6: 알림톡에서 바로 my-page 접근 (카카오 로그인 없이) + +``` +[알림톡 수신] → "적립 내역 확인" 버튼 + → https://mile.0bin.in/my-page?phone=01012345678 + → 전화번호로 직접 조회 → 마이페이지 표시 + → 카카오 연동 없이 내역만 확인 +``` + +**결과**: 열람만 가능. 카카오 연동은 카카오 로그인 버튼을 눌러야 진행. + +--- + +## 연동 시점 정리 + +| 진입 경로 | link_kakao_identity 호출 | 비고 | +|-----------|-------------------------|------| +| QR 스캔 → 카카오 로그인 → 적립 | O | claim 플로우에서 자동 연동 | +| 키오스크 → 번호 입력 → 적립 | X | 전화번호만 있음 | +| 마이페이지 → 카카오 로그인 | O (신규!) | `_handle_mypage_kakao_callback`에서 머지 | +| 알림톡 → 마이페이지 (번호 직접) | X | 카카오 로그인 안 함 | + +--- + +## 이름 업데이트 규칙 + +| 현재 이름 | 카카오 이름 | 결과 | +|-----------|-------------|------| +| "고객" | "김철수" | → "김철수" (업데이트) | +| "고객" | "고객" or 없음 | → "고객" (유지) | +| "김철수" (기존 실명) | "김영희" | → "김철수" (기존 유지, 덮어쓰지 않음) | + +**원칙**: "고객"(키오스크 기본값)인 경우에만 카카오 실명으로 업데이트. 이미 실명이 있으면 유지. + +--- + +## 관련 코드 + +| 함수 | 파일:라인 | 역할 | +|------|-----------|------| +| `get_or_create_user()` | app.py:372 | 전화번호로 유저 조회/생성 | +| `link_kakao_identity()` | app.py:485 | 카카오 계정을 유저에 연결 | +| `find_user_by_kakao_id()` | app.py:519 | 카카오 ID로 유저 조회 | +| `_handle_mypage_kakao_callback()` | app.py:793 | 마이페이지 카카오 콜백 (머지 포함) | +| `api_kiosk_claim()` | app.py:1986 | 키오스크 적립 + 알림톡 발송 |