feat(sheets): 사용자 OAuth 리프레시 토큰 인증 지원
조직 정책(iam.disableServiceAccountKeyCreation)으로 서비스 계정 키를 만들 수 없는 환경 대응. 서비스 계정이 설정돼 있으면 그쪽을 먼저 쓰고, 없으면 GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN + client id/secret 으로 인증한다. (client id/secret 미지정 시 로그인용 GOOGLE_CLIENT_ID/SECRET 재사용) scripts/google_sheets_authorize.py: 데스크톱 앱 OAuth 클라이언트로 1회 동의해 refresh token 을 발급받는 스크립트. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,16 @@ CUSTOMER_ORDER_LIST_URL=/orderlist/
|
|||||||
# - 대상 문서는 반드시 "Google 스프레드시트" 형식(업로드한 .xlsx 는 파일>Google 스프레드시트로 저장)
|
# - 대상 문서는 반드시 "Google 스프레드시트" 형식(업로드한 .xlsx 는 파일>Google 스프레드시트로 저장)
|
||||||
# - 서비스 계정 이메일(client_email)에 해당 문서를 편집자로 공유해야 한다
|
# - 서비스 계정 이메일(client_email)에 해당 문서를 편집자로 공유해야 한다
|
||||||
# CUPANG_SHEET_ID=1J74op7lBZOgE27p4R28I3RWsv0EtXdii
|
# CUPANG_SHEET_ID=1J74op7lBZOgE27p4R28I3RWsv0EtXdii
|
||||||
|
#
|
||||||
|
# [방식 A] 사용자 OAuth 리프레시 토큰 — 조직 정책으로 서비스 계정 키를 못 만들 때
|
||||||
|
# 토큰 발급: python scripts/google_sheets_authorize.py --client-id .. --client-secret ..
|
||||||
|
# (Cloud Console 에서 "데스크톱 앱" 유형 OAuth 클라이언트 ID 발급 후 사용)
|
||||||
|
# client id/secret 을 생략하면 로그인용 GOOGLE_CLIENT_ID/SECRET 을 쓴다.
|
||||||
|
# GOOGLE_SHEETS_OAUTH_CLIENT_ID=xxx.apps.googleusercontent.com
|
||||||
|
# GOOGLE_SHEETS_OAUTH_CLIENT_SECRET=replace-me
|
||||||
|
# GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN=replace-me
|
||||||
|
#
|
||||||
|
# [방식 B] 서비스 계정 JSON (문서를 client_email 에 편집자로 공유해야 함)
|
||||||
# GOOGLE_SHEETS_CREDENTIALS=/opt/www/main/secrets/google-sheets-sa.json
|
# GOOGLE_SHEETS_CREDENTIALS=/opt/www/main/secrets/google-sheets-sa.json
|
||||||
# 또는 파일 대신 JSON 본문을 통째로 (한 줄):
|
# 또는 파일 대신 JSON 본문을 통째로 (한 줄):
|
||||||
# GOOGLE_SHEETS_CREDENTIALS_JSON={"type":"service_account", ...}
|
# GOOGLE_SHEETS_CREDENTIALS_JSON={"type":"service_account", ...}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Claude Code는 이 저장소에서 작업을 시작하기 전에 **반드시 아
|
|||||||
- 반품관리
|
- 반품관리
|
||||||
- 외부 쇼핑몰 API 연동 (카페24, 네이버 스마트스토어, 사방넷 등)
|
- 외부 쇼핑몰 API 연동 (카페24, 네이버 스마트스토어, 사방넷 등)
|
||||||
- 개인경비 (`app/modules/expense/`, `expense_db`)
|
- 개인경비 (`app/modules/expense/`, `expense_db`)
|
||||||
- 쿠팡 밀크런 (`app/modules/cupang/`, `cupang_db`) — 출고 달력/박스 계산/입고센터 관리, 상품은 `itemcode_db` 읽기 전용. 출고 묶음은 **박스 계산 화면의 [분배 확정] 으로만** 만든다(신규 등록 폼 없음, `/cupang/new` 는 박스 계산으로 리다이렉트). 확정 조건: 미배분 박스 0 + 담긴 센터마다 출고방식(택배/파렛트) 선택. 확정 시 센터마다 출고 묶음 1건(`status=출고준비`, 센터입고일=출고일+1일) + 출고리스트 엑셀 자동 다운로드(`GET /cupang/export.xlsx?date=`, 시트명 YYYYMMDD). 양식 생성은 `app/modules/cupang/export.py` 한 곳에서 만들어 xlsx·구글시트가 공유. `CUPANG_SHEET_ID` + 서비스 계정(`GOOGLE_SHEETS_CREDENTIALS[_JSON]`) 설정 시 확정과 동시에 Google 스프레드시트에 출고일 시트를 생성/덮어쓰기(`app/integrations/google_sheets.py`, 미설정이면 조용히 skip). 작업 중 상태는 `cupang_box_calc_drafts` 에 이름 붙여 임시 저장/불러오기. 쿠팡 발주 엑셀(xlsx) 다중 업로드 지원(`POST /cupang/api/box-calc/upload`, openpyxl) — F13 입고예정일의 하루 전 = 출고일, 22행부터 B=쿠팡상품코드·F=센터명·G=수량을 읽어 `cupang_products.coupang_item_code` 로 제품 매칭 후 센터별 합산 → 센터 단위로 박스 계산·자동 배분
|
- 쿠팡 밀크런 (`app/modules/cupang/`, `cupang_db`) — 출고 달력/박스 계산/입고센터 관리, 상품은 `itemcode_db` 읽기 전용. 출고 묶음은 **박스 계산 화면의 [분배 확정] 으로만** 만든다(신규 등록 폼 없음, `/cupang/new` 는 박스 계산으로 리다이렉트). 확정 조건: 미배분 박스 0 + 담긴 센터마다 출고방식(택배/파렛트) 선택. 확정 시 센터마다 출고 묶음 1건(`status=출고준비`, 센터입고일=출고일+1일) + 출고리스트 엑셀 자동 다운로드(`GET /cupang/export.xlsx?date=`, 시트명 YYYYMMDD). 양식 생성은 `app/modules/cupang/export.py` 한 곳에서 만들어 xlsx·구글시트가 공유. `CUPANG_SHEET_ID` + 인증(사용자 OAuth `GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN` 또는 서비스 계정 `GOOGLE_SHEETS_CREDENTIALS[_JSON]`) 설정 시 확정과 동시에 Google 스프레드시트에 출고일 시트를 생성/덮어쓰기(`app/integrations/google_sheets.py`, 미설정이면 조용히 skip). 작업 중 상태는 `cupang_box_calc_drafts` 에 이름 붙여 임시 저장/불러오기. 쿠팡 발주 엑셀(xlsx) 다중 업로드 지원(`POST /cupang/api/box-calc/upload`, openpyxl) — F13 입고예정일의 하루 전 = 출고일, 22행부터 B=쿠팡상품코드·F=센터명·G=수량을 읽어 `cupang_products.coupang_item_code` 로 제품 매칭 후 센터별 합산 → 센터 단위로 박스 계산·자동 배분
|
||||||
- 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver`
|
- 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver`
|
||||||
- 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-)은 재고 집계 제외 — 단, 창고 랙에는 위치 확인용으로 배치 가능(`store.LID_ITEMS`). 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia`
|
- 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-)은 재고 집계 제외 — 단, 창고 랙에는 위치 확인용으로 배치 가능(`store.LID_ITEMS`). 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia`
|
||||||
- 말레이시아 배송 (`app/modules/dispatch/`, `dispatch_db`) — TikTok·Shopee 출고관리. 플랫폼별 데이터 엑셀 업로드(TikTok=03_TikTok_Order_Export.xlsx, Shopee=Packing List.Doorstep Delivery.xlsx) → 1박스=1카드 출고 작업 리스트·SKU 피킹 요약·Kagayaku 전달표 자동 생성. 1박스 묶음 기준 Package ID > Tracking ID > Order ID, 같은 박스 같은 SKU 합산. 작업 상태 토글(`dispatch_logs` 기록). 받는 사람 이름/전화/주소는 박스 단위로 저장(작업 카드 표시 + 출고 엑셀 생성용 — 개인정보). 배치 다운로드 zip 에 업로드 원본 + 취합 출고 엑셀(`YYYY.MM.DD(Ddd)_tictoc|shopee.xlsx`) 포함. 엑셀은 openpyxl 파싱/생성. 권한키 `dispatch`. 상세는 `docs/DISPATCH_MODULE.md`
|
- 말레이시아 배송 (`app/modules/dispatch/`, `dispatch_db`) — TikTok·Shopee 출고관리. 플랫폼별 데이터 엑셀 업로드(TikTok=03_TikTok_Order_Export.xlsx, Shopee=Packing List.Doorstep Delivery.xlsx) → 1박스=1카드 출고 작업 리스트·SKU 피킹 요약·Kagayaku 전달표 자동 생성. 1박스 묶음 기준 Package ID > Tracking ID > Order ID, 같은 박스 같은 SKU 합산. 작업 상태 토글(`dispatch_logs` 기록). 받는 사람 이름/전화/주소는 박스 단위로 저장(작업 카드 표시 + 출고 엑셀 생성용 — 개인정보). 배치 다운로드 zip 에 업로드 원본 + 취합 출고 엑셀(`YYYY.MM.DD(Ddd)_tictoc|shopee.xlsx`) 포함. 엑셀은 openpyxl 파싱/생성. 권한키 `dispatch`. 상세는 `docs/DISPATCH_MODULE.md`
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
"""Google 스프레드시트 쓰기 공통 계층 (서비스 계정).
|
"""Google 스프레드시트 쓰기 공통 계층.
|
||||||
|
|
||||||
- 다른 모듈에서도 쓸 수 있게 `app/integrations/` 에 둔다.
|
인증 방식 두 가지를 지원한다(설정된 쪽을 자동 선택, 서비스 계정 우선).
|
||||||
- 인증: 구글 클라우드 **서비스 계정** JSON.
|
|
||||||
|
1) 사용자 OAuth 리프레시 토큰 — 조직 정책으로 서비스 계정 **키 발급이 막힌** 경우
|
||||||
|
· `GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN` 1회 동의로 받은 refresh token
|
||||||
|
· `GOOGLE_SHEETS_OAUTH_CLIENT_ID` / `..._SECRET`
|
||||||
|
(없으면 로그인용 `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` 사용)
|
||||||
|
토큰 발급: `python scripts/google_sheets_authorize.py`
|
||||||
|
이 방식은 토큰을 발급한 **사용자 권한**으로 동작하므로, 그 사용자가 이미
|
||||||
|
편집할 수 있는 문서면 별도 공유가 필요 없다.
|
||||||
|
|
||||||
|
2) 서비스 계정 JSON
|
||||||
· `GOOGLE_SHEETS_CREDENTIALS` 서비스 계정 JSON 파일 경로
|
· `GOOGLE_SHEETS_CREDENTIALS` 서비스 계정 JSON 파일 경로
|
||||||
· `GOOGLE_SHEETS_CREDENTIALS_JSON` JSON 본문(파일 대신 환경변수로 넣을 때)
|
· `GOOGLE_SHEETS_CREDENTIALS_JSON` JSON 본문(파일 대신 환경변수로)
|
||||||
대상 스프레드시트를 서비스 계정 이메일(client_email)에 **편집자로 공유**해야 한다.
|
대상 문서를 서비스 계정 이메일(client_email)에 **편집자로 공유**해야 한다.
|
||||||
- 비밀값(키)은 로그·화면에 절대 출력하지 않는다.
|
|
||||||
|
- 비밀값(키/토큰)은 로그·화면에 절대 출력하지 않는다.
|
||||||
- 라이브러리(google-api-python-client)가 없거나 설정이 비어 있으면
|
- 라이브러리(google-api-python-client)가 없거나 설정이 비어 있으면
|
||||||
`enabled = False` 로 조용히 비활성화되고, 호출부는 건너뛴다.
|
`enabled = False` 로 조용히 비활성화되고, 호출부는 건너뛴다.
|
||||||
"""
|
"""
|
||||||
@@ -17,6 +27,7 @@ import os
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
||||||
|
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
||||||
|
|
||||||
|
|
||||||
class GoogleSheetsWriter:
|
class GoogleSheetsWriter:
|
||||||
@@ -25,39 +36,91 @@ class GoogleSheetsWriter:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
self.reason = ""
|
self.reason = ""
|
||||||
|
self.auth_mode = "" # "service_account" | "oauth"
|
||||||
self._service: Any = None
|
self._service: Any = None
|
||||||
self._client_email = ""
|
self._client_email = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
except ImportError:
|
||||||
|
self.reason = "google-api-python-client 미설치"
|
||||||
|
return
|
||||||
|
|
||||||
|
creds = self._service_account_creds()
|
||||||
|
if creds is None:
|
||||||
|
creds = self._oauth_creds()
|
||||||
|
if creds is None:
|
||||||
|
if not self.reason:
|
||||||
|
self.reason = (
|
||||||
|
"GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN 또는 "
|
||||||
|
"GOOGLE_SHEETS_CREDENTIALS(_JSON) 미설정"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._service = build("sheets", "v4", credentials=creds, cache_discovery=False)
|
||||||
|
except Exception as exc: # noqa: BLE001 - 사유만 남긴다(비밀값 미출력)
|
||||||
|
self.reason = f"Sheets 클라이언트 생성 실패: {type(exc).__name__}"
|
||||||
|
return
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
# ── 인증 ─────────────────────────────────────────
|
||||||
|
def _service_account_creds(self) -> Any:
|
||||||
raw = (os.getenv("GOOGLE_SHEETS_CREDENTIALS_JSON") or "").strip()
|
raw = (os.getenv("GOOGLE_SHEETS_CREDENTIALS_JSON") or "").strip()
|
||||||
path = (os.getenv("GOOGLE_SHEETS_CREDENTIALS") or "").strip()
|
path = (os.getenv("GOOGLE_SHEETS_CREDENTIALS") or "").strip()
|
||||||
if not raw and not path:
|
if not raw and not path:
|
||||||
self.reason = "GOOGLE_SHEETS_CREDENTIALS(_JSON) 미설정"
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = json.loads(raw) if raw else json.loads(
|
info = json.loads(raw) if raw else json.loads(
|
||||||
open(path, encoding="utf-8").read()
|
open(path, encoding="utf-8").read()
|
||||||
)
|
)
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
self.reason = "서비스 계정 JSON 을 읽지 못했습니다."
|
self.reason = "서비스 계정 JSON 을 읽지 못했습니다."
|
||||||
return
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from google.oauth2.service_account import Credentials
|
from google.oauth2.service_account import Credentials
|
||||||
from googleapiclient.discovery import build
|
|
||||||
except ImportError:
|
|
||||||
self.reason = "google-api-python-client 미설치"
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
creds = Credentials.from_service_account_info(info, scopes=SCOPES)
|
creds = Credentials.from_service_account_info(info, scopes=SCOPES)
|
||||||
self._service = build("sheets", "v4", credentials=creds, cache_discovery=False)
|
except Exception as exc: # noqa: BLE001
|
||||||
except Exception as exc: # noqa: BLE001 - 인증 실패 사유만 남긴다
|
|
||||||
self.reason = f"서비스 계정 인증 실패: {type(exc).__name__}"
|
self.reason = f"서비스 계정 인증 실패: {type(exc).__name__}"
|
||||||
return
|
return None
|
||||||
|
|
||||||
self._client_email = str(info.get("client_email") or "")
|
self._client_email = str(info.get("client_email") or "")
|
||||||
self.enabled = True
|
self.auth_mode = "service_account"
|
||||||
|
return creds
|
||||||
|
|
||||||
|
def _oauth_creds(self) -> Any:
|
||||||
|
refresh_token = (os.getenv("GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN") or "").strip()
|
||||||
|
if not refresh_token:
|
||||||
|
return None
|
||||||
|
client_id = (
|
||||||
|
os.getenv("GOOGLE_SHEETS_OAUTH_CLIENT_ID")
|
||||||
|
or os.getenv("GOOGLE_CLIENT_ID")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
client_secret = (
|
||||||
|
os.getenv("GOOGLE_SHEETS_OAUTH_CLIENT_SECRET")
|
||||||
|
or os.getenv("GOOGLE_CLIENT_SECRET")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
self.reason = "GOOGLE_SHEETS_OAUTH_CLIENT_ID/SECRET 미설정"
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
|
||||||
|
creds = Credentials(
|
||||||
|
token=None,
|
||||||
|
refresh_token=refresh_token,
|
||||||
|
token_uri=TOKEN_URI,
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
scopes=SCOPES,
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self.reason = f"OAuth 자격 생성 실패: {type(exc).__name__}"
|
||||||
|
return None
|
||||||
|
self.auth_mode = "oauth"
|
||||||
|
return creds
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client_email(self) -> str:
|
def client_email(self) -> str:
|
||||||
|
|||||||
@@ -14,3 +14,5 @@ pillow>=10.0
|
|||||||
cryptography>=42.0
|
cryptography>=42.0
|
||||||
google-api-python-client>=2.100
|
google-api-python-client>=2.100
|
||||||
google-auth>=2.30
|
google-auth>=2.30
|
||||||
|
# refresh token 발급 스크립트(scripts/google_sheets_authorize.py) 전용
|
||||||
|
google-auth-oauthlib>=1.2
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Google 스프레드시트 쓰기용 refresh token 1회 발급 스크립트.
|
||||||
|
|
||||||
|
조직 정책(`iam.disableServiceAccountKeyCreation`)으로 서비스 계정 키를 만들 수 없을 때
|
||||||
|
사용자 계정 OAuth 로 서버가 시트에 기록하도록 하기 위한 토큰을 받는다.
|
||||||
|
|
||||||
|
사전 준비 (Google Cloud Console)
|
||||||
|
1. API 및 서비스 → 라이브러리 → "Google Sheets API" 사용
|
||||||
|
2. API 및 서비스 → OAuth 동의 화면 구성(내부 또는 외부)
|
||||||
|
3. 사용자 인증 정보 → 사용자 인증 정보 만들기 → **OAuth 클라이언트 ID**
|
||||||
|
→ 애플리케이션 유형 **데스크톱 앱** → 클라이언트 ID / 보안 비밀 확인
|
||||||
|
|
||||||
|
실행 (개발 PC, 브라우저 있는 환경에서)
|
||||||
|
python scripts/google_sheets_authorize.py --client-id <ID> --client-secret <SECRET>
|
||||||
|
또는 환경변수로:
|
||||||
|
GOOGLE_SHEETS_OAUTH_CLIENT_ID / GOOGLE_SHEETS_OAUTH_CLIENT_SECRET
|
||||||
|
|
||||||
|
브라우저가 열리면 **시트를 편집할 수 있는 계정**으로 로그인·동의한다.
|
||||||
|
마지막에 출력되는 refresh token 을 서버 `.env` 의
|
||||||
|
`GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN` 에 넣는다.
|
||||||
|
|
||||||
|
주의
|
||||||
|
- 출력된 refresh token 은 비밀값이다. Git·메신저에 남기지 말 것.
|
||||||
|
- 토큰은 발급한 사용자 권한으로 동작한다. 그 사용자가 대상 문서를 편집할 수
|
||||||
|
있어야 하며, 계정 비밀번호 변경·토큰 폐기 시 다시 발급해야 한다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Google Sheets refresh token 발급")
|
||||||
|
parser.add_argument("--client-id", default=os.getenv("GOOGLE_SHEETS_OAUTH_CLIENT_ID", ""))
|
||||||
|
parser.add_argument(
|
||||||
|
"--client-secret", default=os.getenv("GOOGLE_SHEETS_OAUTH_CLIENT_SECRET", "")
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port", type=int, default=0, help="로컬 콜백 포트(기본: 임의 포트)"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client_id = (args.client_id or "").strip()
|
||||||
|
client_secret = (args.client_secret or "").strip()
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
print("client id / secret 이 필요합니다. --client-id / --client-secret 또는 환경변수로 넣으세요.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
except ImportError:
|
||||||
|
print("google-auth-oauthlib 가 필요합니다: pip install google-auth-oauthlib")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
flow = InstalledAppFlow.from_client_config(
|
||||||
|
{
|
||||||
|
"installed": {
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"redirect_uris": ["http://localhost"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scopes=SCOPES,
|
||||||
|
)
|
||||||
|
# prompt=consent 를 줘야 refresh token 이 항상 내려온다.
|
||||||
|
creds = flow.run_local_server(port=args.port, prompt="consent", access_type="offline")
|
||||||
|
|
||||||
|
if not creds.refresh_token:
|
||||||
|
print("refresh token 을 받지 못했습니다. 계정의 기존 권한을 해제하고 다시 시도하세요.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("서버 .env 에 아래 값을 넣으세요 (비밀값 — 공유 금지)")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"GOOGLE_SHEETS_OAUTH_CLIENT_ID={client_id}")
|
||||||
|
print("GOOGLE_SHEETS_OAUTH_CLIENT_SECRET=<위에서 쓴 보안 비밀>")
|
||||||
|
print(f"GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN={creds.refresh_token}")
|
||||||
|
print("=" * 70)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user