feat: 카카오 JS SDK 전환 - 앱 직접 실행으로 로그인 UX 개선
- claim_form.html, my_page_login.html 카카오 버튼을 JS SDK Kakao.Auth.authorize()로 전환 - 카카오톡 앱 설치 시 앱으로 직접 전환 (원탭 로그인), 미설치 시 웹 폴백 - JS SDK 로드 실패 시 기존 서버 리다이렉트(/claim/kakao/start) 폴백 유지 - app.py: /claim, /my-page 라우트에서 kakao_state 생성하여 템플릿에 전달 - kakao_client.py: birthyear 스코프 제거 (미승인 → KOE205 에러 방지) - docs/kakao-oauth-setup.md: 플랫폼 키, JS SDK 비교, 다른 계정 적립 안내, 콘솔 설정 문서화 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f969756caa
commit
d868a494c2
@ -649,7 +649,15 @@ def claim():
|
||||
except Exception as e:
|
||||
logging.warning(f"품목 조회 실패 (transaction_id={transaction_id}): {e}")
|
||||
|
||||
return render_template('claim_form.html', token_info=token_info, sale_items=sale_items)
|
||||
# JS SDK용 카카오 state 생성 (CSRF 보호)
|
||||
csrf_token = secrets.token_hex(16)
|
||||
state_data = {'t': token_param, 'csrf': csrf_token}
|
||||
kakao_state = base64.urlsafe_b64encode(
|
||||
json.dumps(state_data).encode()
|
||||
).decode()
|
||||
session['kakao_csrf'] = csrf_token
|
||||
|
||||
return render_template('claim_form.html', token_info=token_info, sale_items=sale_items, kakao_state=kakao_state)
|
||||
|
||||
|
||||
@app.route('/api/claim', methods=['POST'])
|
||||
@ -876,6 +884,11 @@ def claim_kakao_callback():
|
||||
kakao_phone_raw = user_info.get('phone_number')
|
||||
kakao_phone = normalize_kakao_phone(kakao_phone_raw)
|
||||
|
||||
# 카카오에서 받은 생년월일 조합 (YYYY-MMDD)
|
||||
kakao_birthday = None
|
||||
if user_info.get('birthyear') and user_info.get('birthday'):
|
||||
kakao_birthday = f"{user_info['birthyear']}-{user_info['birthday'][:2]}-{user_info['birthday'][2:]}"
|
||||
|
||||
# 7. 분기: 전화번호가 있으면 자동 적립, 없으면 폰 입력 폼
|
||||
if kakao_phone:
|
||||
# 자동 적립
|
||||
@ -883,8 +896,13 @@ def claim_kakao_callback():
|
||||
if existing_user_id:
|
||||
user_id = existing_user_id
|
||||
is_new = False
|
||||
# 생년월일이 있으면 업데이트
|
||||
if kakao_birthday:
|
||||
conn = db_manager.get_sqlite_connection()
|
||||
conn.cursor().execute("UPDATE users SET birthday = ? WHERE id = ? AND birthday IS NULL", (kakao_birthday, user_id))
|
||||
conn.commit()
|
||||
else:
|
||||
user_id, is_new = get_or_create_user(kakao_phone, kakao_name)
|
||||
user_id, is_new = get_or_create_user(kakao_phone, kakao_name, birthday=kakao_birthday)
|
||||
|
||||
link_kakao_identity(user_id, kakao_id, user_info)
|
||||
|
||||
@ -1006,7 +1024,14 @@ def my_page():
|
||||
phone = request.args.get('phone', '')
|
||||
|
||||
if not phone:
|
||||
return render_template('my_page_login.html')
|
||||
# JS SDK용 카카오 state 생성
|
||||
csrf_token = secrets.token_hex(16)
|
||||
state_data = {'purpose': 'mypage', 'csrf': csrf_token}
|
||||
kakao_state = base64.urlsafe_b64encode(
|
||||
json.dumps(state_data).encode()
|
||||
).decode()
|
||||
session['kakao_csrf'] = csrf_token
|
||||
return render_template('my_page_login.html', kakao_state=kakao_state)
|
||||
|
||||
# 전화번호로 사용자 조회
|
||||
phone = phone.replace('-', '').replace(' ', '')
|
||||
|
||||
@ -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,birthday,birthyear'
|
||||
'scope': 'profile_nickname,profile_image,account_email,name,phone_number,birthday'
|
||||
}
|
||||
|
||||
if state:
|
||||
|
||||
@ -569,17 +569,17 @@
|
||||
<div style="position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #e9ecef; z-index: 0;"></div>
|
||||
</div>
|
||||
|
||||
<a href="/claim/kakao/start?t={{ request.args.get('t') }}"
|
||||
<button type="button" onclick="kakaoLogin()"
|
||||
style="display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
width: 100%; padding: 16px; background: #FEE500; color: #191919;
|
||||
border: none; border-radius: 14px; font-size: 16px; font-weight: 600;
|
||||
text-decoration: none; letter-spacing: -0.3px; transition: all 0.2s ease;
|
||||
letter-spacing: -0.3px; transition: all 0.2s ease; cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 1C4.477 1 0 4.477 0 8.5c0 2.58 1.693 4.847 4.243 6.134l-1.084 3.97a.3.3 0 00.457.338L7.7 16.392c.75.112 1.52.17 2.3.17 5.523 0 10-3.477 10-7.562C20 4.477 15.523 1 10 1z" fill="#191919"/>
|
||||
</svg>
|
||||
카카오로 적립하기
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<div class="alert error" id="alertMsg"></div>
|
||||
|
||||
@ -732,6 +732,28 @@
|
||||
successScreen.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
<script src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.4/kakao.min.js"
|
||||
integrity="sha384-DKYJZ8NLiK8MN4/C5P2dtSmLQ4KwPaoqAfyA/DfmOGfnSNqoARCbb2xl4Kh1v6Q"
|
||||
crossorigin="anonymous"></script>
|
||||
<script>
|
||||
// 카카오 JS SDK 초기화
|
||||
if (typeof Kakao !== 'undefined') {
|
||||
Kakao.init('3d1e098107157c5021b73bd5ab48600f');
|
||||
}
|
||||
|
||||
function kakaoLogin() {
|
||||
if (typeof Kakao !== 'undefined' && Kakao.isInitialized()) {
|
||||
Kakao.Auth.authorize({
|
||||
redirectUri: 'https://mile.0bin.in/claim/kakao/callback',
|
||||
scope: 'profile_nickname,profile_image,account_email,name,phone_number,birthday',
|
||||
state: '{{ kakao_state }}'
|
||||
});
|
||||
} else {
|
||||
// JS SDK 로드 실패 시 서버 리다이렉트 폴백
|
||||
window.location.href = '/claim/kakao/start?t={{ request.args.get("t") }}';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(()=>{});}
|
||||
|
||||
|
||||
@ -180,13 +180,13 @@
|
||||
<div style="position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #e9ecef; z-index: 0;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 카카오 로그인 버튼 -->
|
||||
<a href="/my-page/kakao/start" style="display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 18px; background: #FEE500; color: #191919; border: none; border-radius: 14px; font-size: 17px; font-weight: 700; cursor: pointer; letter-spacing: -0.3px; text-decoration: none; transition: all 0.2s ease;">
|
||||
<!-- 카카오 로그인 버튼 (JS SDK) -->
|
||||
<button type="button" onclick="kakaoLogin()" style="display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 18px; background: #FEE500; color: #191919; border: none; border-radius: 14px; font-size: 17px; font-weight: 700; cursor: pointer; letter-spacing: -0.3px; transition: all 0.2s ease;">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 1C4.477 1 0 4.477 0 8.5c0 2.58 1.693 4.847 4.243 6.134l-1.084 3.97a.3.3 0 00.457.338L7.7 16.392c.75.112 1.52.17 2.3.17 5.523 0 10-3.477 10-7.562C20 4.477 15.523 1 10 1z" fill="#191919"/>
|
||||
</svg>
|
||||
카카오로 조회하기
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<a href="/" class="btn-back">← 홈으로</a>
|
||||
</div>
|
||||
@ -213,6 +213,24 @@
|
||||
|
||||
phoneInput.focus();
|
||||
</script>
|
||||
<script src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.4/kakao.min.js"
|
||||
integrity="sha384-DKYJZ8NLiK8MN4/C5P2dtSmLQ4KwPaoqAfyA/DfmOGfnSNqoARCbb2xl4Kh1v6Q"
|
||||
crossorigin="anonymous"></script>
|
||||
<script>
|
||||
if (typeof Kakao !== 'undefined') Kakao.init('3d1e098107157c5021b73bd5ab48600f');
|
||||
|
||||
function kakaoLogin() {
|
||||
if (typeof Kakao !== 'undefined' && Kakao.isInitialized()) {
|
||||
Kakao.Auth.authorize({
|
||||
redirectUri: 'https://mile.0bin.in/claim/kakao/callback',
|
||||
scope: 'profile_nickname,profile_image,account_email,name,phone_number,birthday',
|
||||
state: '{{ kakao_state }}'
|
||||
});
|
||||
} else {
|
||||
window.location.href = '/my-page/kakao/start';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(()=>{});}</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -7,6 +7,118 @@
|
||||
- **앱 유형**: 비즈 앱
|
||||
- **개발자 콘솔**: https://developers.kakao.com/console/app/1165131
|
||||
|
||||
---
|
||||
|
||||
## 플랫폼 키 (앱 > 플랫폼 키)
|
||||
|
||||
| 키 종류 | 값 | 용도 |
|
||||
|---------|---|------|
|
||||
| **Native App Key** | `346b84c4e018e20f0f8` | Android/iOS 네이티브 앱 (현재 미사용) |
|
||||
| **JavaScript Key** | `3d1e098107157c5021b73bd5ab48600f` | 카카오 JS SDK (프론트엔드) |
|
||||
| **REST API Key** | `caad27ac4bc92d8dc83bdd6aae744811` | 서버 간 API 호출 (현재 사용 중) |
|
||||
| **Admin Key** | (콘솔에서 확인) | 서버 관리 기능 (사용 주의) |
|
||||
|
||||
### 키 사용 구분
|
||||
|
||||
```
|
||||
[현재 구현] REST API 방식
|
||||
프론트엔드 → 302 리다이렉트 → 카카오 웹 로그인 페이지 → 콜백
|
||||
사용 키: REST API Key (서버 환경변수 KAKAO_CLIENT_ID)
|
||||
|
||||
[향후 전환] JS SDK 방식
|
||||
프론트엔드 → Kakao.Auth.authorize() → 카카오톡 앱 직접 실행 → 콜백
|
||||
사용 키: JavaScript Key (프론트엔드 HTML에 노출)
|
||||
```
|
||||
|
||||
### Client Secret
|
||||
|
||||
```
|
||||
앱 > 보안 > Client Secret 코드
|
||||
```
|
||||
- 환경변수: `KAKAO_CLIENT_SECRET`
|
||||
- REST API 토큰 교환 시 필수
|
||||
|
||||
---
|
||||
|
||||
## REST API vs JS SDK 비교
|
||||
|
||||
| 항목 | REST API (폴백) | JS SDK (현재 적용) |
|
||||
|------|----------------|-------------------|
|
||||
| **인증 키** | REST API Key | JavaScript Key |
|
||||
| **로그인 UX** | 웹 브라우저에서 카카오 로그인 페이지 표시 (매번 동의 확인) | 카카오톡 앱이 직접 열림 → 원탭 동의 → 즉시 복귀 |
|
||||
| **모바일 경험** | 웹뷰 로그인 (느림) | 앱 ↔ 앱 전환 (빠름) |
|
||||
| **앱 미설치 시** | 웹 로그인 표시 | 자동으로 웹 로그인 폴백 |
|
||||
| **백엔드** | `kakao_client.get_authorization_url()` | 변경 없음 (콜백 동일) |
|
||||
| **보안** | 키가 서버에만 존재 | JavaScript Key는 공개 가능 (도메인 제한으로 보호) |
|
||||
|
||||
### 현재 적용 상태 (JS SDK)
|
||||
|
||||
JS SDK가 적용된 페이지:
|
||||
- `claim_form.html` — QR 적립 시 "카카오로 적립하기" 버튼
|
||||
- `my_page_login.html` — 마이페이지 "카카오로 조회하기" 버튼
|
||||
|
||||
서버 리다이렉트 유지 페이지 (보조 진입점):
|
||||
- `index.html`, `my_page.html`, `signup.html`, `error.html` → `/my-page/kakao/start`
|
||||
|
||||
### JS SDK 동작 방식
|
||||
|
||||
```
|
||||
모바일:
|
||||
카카오톡 앱 설치됨 → 앱으로 전환 (원탭 로그인) → 콜백
|
||||
카카오톡 앱 미설치 → 웹 로그인 페이지로 자동 폴백 → 콜백
|
||||
|
||||
PC:
|
||||
항상 웹 로그인 페이지 표시 → 콜백
|
||||
|
||||
JS SDK 로드 실패 시:
|
||||
서버 리다이렉트 폴백 (/claim/kakao/start 또는 /my-page/kakao/start)
|
||||
```
|
||||
|
||||
### 다른 카카오 계정으로 적립 (향후 구현)
|
||||
|
||||
폰이 2대이거나 다른 계정으로 적립하고 싶은 경우:
|
||||
|
||||
```javascript
|
||||
// 기본: 카카오톡 앱 계정으로 바로 로그인
|
||||
Kakao.Auth.authorize({
|
||||
redirectUri: '...',
|
||||
state: '...'
|
||||
});
|
||||
|
||||
// 다른 계정으로: 기존 세션 무시, 계정 입력 강제
|
||||
Kakao.Auth.authorize({
|
||||
redirectUri: '...',
|
||||
state: '...',
|
||||
prompt: 'login' // ← 핵심 파라미터
|
||||
});
|
||||
```
|
||||
|
||||
UI 구성안:
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ [카카오로 적립하기] │ ← 기본 (앱 → 원탭)
|
||||
│ │
|
||||
│ 다른 카카오 계정으로 적립 → │ ← prompt:'login'
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 카카오 개발자 콘솔 필수 설정
|
||||
|
||||
> **중요**: JS SDK 사용 시 JavaScript 키에도 Redirect URI 등록 필요
|
||||
|
||||
```
|
||||
앱 > 플랫폼 키 > JavaScript 키 클릭 > 리다이렉트 URI
|
||||
→ https://mile.0bin.in/claim/kakao/callback 추가
|
||||
```
|
||||
|
||||
Web 플랫폼 도메인도 등록 확인:
|
||||
```
|
||||
앱 > 플랫폼 > Web > 사이트 도메인
|
||||
→ https://mile.0bin.in 포함 확인
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redirect URI 등록 (2025년 12월 개편 후)
|
||||
|
||||
> **주의**: 2025년 12월 카카오 콘솔 UI가 개편되면서 Redirect URI 위치가 변경됨.
|
||||
@ -18,12 +130,6 @@
|
||||
앱 > 플랫폼 키 > REST API 키 클릭 > 리다이렉트 URI
|
||||
```
|
||||
|
||||
### 이전 경로 (~ 2025.11, 더 이상 사용 안 함)
|
||||
|
||||
```
|
||||
카카오 로그인 > 일반 > Redirect URI ← 여기 더 이상 없음
|
||||
```
|
||||
|
||||
### 등록된 Redirect URI 목록
|
||||
|
||||
| 서비스 | Redirect URI |
|
||||
@ -41,6 +147,8 @@
|
||||
|
||||
로그인용 Redirect URI와 혼동하지 않도록 주의.
|
||||
|
||||
---
|
||||
|
||||
## 웹 도메인 등록
|
||||
|
||||
```
|
||||
@ -59,51 +167,73 @@
|
||||
- `https://ka.0bin.in`
|
||||
- `https://mile.0bin.in`
|
||||
|
||||
---
|
||||
|
||||
## 동의항목 설정
|
||||
|
||||
```
|
||||
카카오 로그인 > 동의항목
|
||||
```
|
||||
|
||||
| 항목 | ID | 용도 | 비즈앱 필요 |
|
||||
|------|-----|------|------------|
|
||||
| 닉네임 | profile_nickname | 사용자 이름 | X |
|
||||
| 프로필 사진 | profile_image | 아바타 | X |
|
||||
| 이메일 | account_email | 계정 연동 | X |
|
||||
| 이름 (실명) | name | 마일리지 적립자명 | O |
|
||||
| 전화번호 | phone_number | 마일리지 유저 매칭 | O |
|
||||
| 항목 | ID | 동의 목적 | 상태 | 비즈앱 필요 |
|
||||
|------|-----|----------|------|------------|
|
||||
| 닉네임 | profile_nickname | 사용자 식별 | 승인 | X |
|
||||
| 프로필 사진 | profile_image | 아바타 표시 | 승인 | X |
|
||||
| 이메일 | account_email | 계정 연동 | 승인 | X |
|
||||
| 이름 (실명) | name | 마일리지 적립자명 | 승인 | O |
|
||||
| 전화번호 | phone_number | 마일리지 적립 계정 식별 및 포인트 조회 | 승인 | O |
|
||||
| 생일 | birthday | 생일 기념 포인트 2배 적립 이벤트 제공 | 승인 | O |
|
||||
| 출생연도 | birthyear | 생일 기념 포인트 2배 적립 이벤트 제공 | 권한 없음 (미승인) | O |
|
||||
|
||||
### 현재 사용 중인 스코프
|
||||
|
||||
```
|
||||
profile_nickname,profile_image,account_email,name,phone_number,birthday
|
||||
```
|
||||
|
||||
> ⚠️ `birthyear`는 아직 권한 미승인 상태. 스코프에 포함하면 **KOE205 에러** 발생.
|
||||
> 승인되면 스코프에 추가하고, `kakao_client.py`의 scope 문자열 수정 필요.
|
||||
|
||||
---
|
||||
|
||||
## 환경변수
|
||||
|
||||
```bash
|
||||
KAKAO_CLIENT_ID=<REST API 키>
|
||||
# 카카오 OAuth (REST API 방식)
|
||||
KAKAO_CLIENT_ID=caad27ac4bc92d8dc83bdd6aae744811 # REST API Key
|
||||
KAKAO_CLIENT_SECRET=<카카오 개발자 콘솔 > 앱 > 보안에서 확인>
|
||||
KAKAO_REDIRECT_URI=https://mile.0bin.in/claim/kakao/callback
|
||||
|
||||
# JS SDK 전환 시 추가 (프론트엔드 전용, 서버 환경변수 불필요)
|
||||
# JavaScript Key: 3d1e098107157c5021b73bd5ab48600f
|
||||
```
|
||||
|
||||
### Client ID 확인 위치
|
||||
|
||||
```
|
||||
앱 > 플랫폼 키 > REST API 키 > 키 값
|
||||
```
|
||||
|
||||
### Client Secret 확인 위치
|
||||
|
||||
```
|
||||
앱 > 보안 > Client Secret 코드
|
||||
```
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 프로젝트 | 파일 | 설명 |
|
||||
|---------|------|------|
|
||||
| pharmacy-pos-qr-system | `backend/services/kakao_client.py` | 카카오 API 클라이언트 |
|
||||
| pharmacy-pos-qr-system | `backend/services/kakao_client.py` | 카카오 API 클라이언트 (REST API 방식) |
|
||||
| pharmacy-pos-qr-system | `backend/app.py` | OAuth 라우트 (`/claim/kakao/*`) |
|
||||
| board-system-project | `backend/services/kakao_client.py` | 카카오 API 클라이언트 (원본) |
|
||||
| board-system-project | `backend/routes/auth.py` | OAuth 라우트 (`/auth/kakao/*`) |
|
||||
|
||||
---
|
||||
|
||||
## 카카오 데이터 포맷 참고
|
||||
|
||||
| 필드 | 포맷 | 예시 | DB 저장 |
|
||||
|------|------|------|---------|
|
||||
| birthday | MMDD | `0315` | `YYYY-MM-DD`로 변환 |
|
||||
| birthyear | YYYY | `1990` | birthday와 결합 |
|
||||
| phone_number | +82 10-XXXX-XXXX | `+82 10-2130-7390` | 하이픈/국가코드 제거 후 저장 |
|
||||
|
||||
---
|
||||
|
||||
## 참고 링크
|
||||
|
||||
- [카카오 로그인 REST API 문서](https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api)
|
||||
- [카카오 JS SDK 문서](https://developers.kakao.com/docs/latest/ko/javascript/getting-started)
|
||||
- [카카오 로그인 설정하기](https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite)
|
||||
- [카카오 앱 키 구조 개편 공지 (2025.12)](https://devtalk.kakao.com/t/upcoming-kakao-developers-app-key-update/147295)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user