1. 개발 환경 세팅
1-1. 가상환경(venv) 문제
처음에는 윈도우용 경로로 활성화하려다가 계속 에러가 났다.
# 이렇게 해서 계속 에러
cd Scripts/activate
source Scripts/activate
# Mac / Linux 에서는 이렇게 해야 함
cd "/Users/bagjuwon/Downloads/project - API제거"
python3 -m venv venv
source venv/bin/activate
Scripts/activate는 윈도우용- Mac에서는
bin/activate를 사용해야 한다.
2. Django + MySQL 연동
2-1. settings.py 설정
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'project_db',
'USER': 'project_user',
'PASSWORD': '1234', # 실제로는 .env로 빼는 게 좋음
'HOST': 'localhost',
'PORT': '3306',
'OPTIONS': {
'charset': 'utf8mb4',
},
}
}
2-2. Access denied(1045) 에러
mysql -u root -p는 비밀번호 필요mysql -u root로는 그냥 접속되는 상황이었음- MySQL 쪽 사용자/권한 문제 확인 후
project_user계정으로 실제 접속 테스트
3. OpenAI API 연동 & .env 정리
3-1. 처음 에러: UnicodeEncodeError
뷰에서 API 키를 이렇게 박아 넣었을 때:
client = openai.Client(
api_key='API 제거됨!'
)
요청 헤더에 들어가는 문자열에 한글이 섞이면서
'ascii' codec can't encode characters …
같은 에러가 발생했다.
3-2. .env + settings.py 로 분리
.env 파일:
OPENAI_API_KEY=sk-프로젝트키...
settings.py:
from pathlib import Path
import os
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv() # .env 로드
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
views.py:
from django.conf import settings
import openai
client = openai.Client(
api_key=settings.OPENAI_API_KEY
)
이렇게 해서
- 코드에 키를 직접 안 넣고
- 인코딩 문제도 해결했다.
4. Google Slides / Drive API 연동
4-1. 필요한 패키지 설치
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
설치 전에는
ModuleNotFoundError: No module named 'googleapiclient'
에러가 났다.
4-2. OAuth 설정
- Google Cloud Console 에서 새 프로젝트 생성
-
OAuth 동의 화면
- 사용자 유형: 외부
- 테스트 사용자에 내 구글 계정 추가
-
OAuth 클라이언트 ID 생성
- 애플리케이션 유형: 데스크톱 앱
- JSON 다운로드 →
client_secret.json이름으로 프로젝트 루트에 저장
- 최초 실행 시
InstalledAppFlow로 브라우저에서 로그인 →token.json생성
5. create_slides 함수 구조
핵심 역할: 템플릿 슬라이드를 복사 → 텍스트 교체 → 권한 공개 → 링크 반환
def create_slides(original_file_id, SLIDE_TITLE_TEXT):
global presentation_id
SCOPES = [
'https://www.googleapis.com/auth/presentations',
'https://www.googleapis.com/auth/drive',
]
# 1) token 로드 & OAuth 인증
# (token.json 없거나 깨져 있으면 새로 인증)
...
service = build('slides', 'v1', credentials=creds)
drive_service = build('drive', 'v3', credentials=creds)
try:
# 2) 템플릿 복사
presentation = drive_service.files().copy(
fileId=original_file_id,
fields='id,name,webViewLink',
body={'name': SLIDE_TITLE_TEXT},
).execute()
presentation_id = presentation['id']
presentation_link = presentation['webViewLink']
# 3) 프레젠테이션 구조를 JSON 파일로 저장
presentation = service.presentations().get(
presentationId=presentation_id
).execute()
with open("presentation_data.json", "w", encoding="utf-8") as f:
json.dump(presentation, f, ensure_ascii=False, indent=4)
# 4) 텍스트 리스트 생성 & objectId 매핑
text_list = get_textlist_from_txt()
...
mapped_data = dict(zip(object_index, text_list))
requests_update = []
# 5) 슬라이드의 각 텍스트 요소를 순회하면서
# deleteText + insertText 요청을 쌓음
for slide in data["slides"]:
for element in slide.get("pageElements", []):
obj_id = element.get("objectId")
if obj_id in mapped_data:
...
requests_update.append({... deleteText ...})
requests_update.append({... insertText ...})
permission = {
"type": "anyone",
"role": "reader",
}
# 6) requests_update가 있을 때만 batchUpdate 호출
slides_service = build('slides', 'v1', credentials=creds)
if requests_update:
slides_service.presentations().batchUpdate(
presentationId=presentation_id,
body={'requests': requests_update},
).execute()
else:
print("requests_update가 비어 있어서 batchUpdate를 건너뜀")
# 7) 누구나 읽기 가능한 링크로 공유
drive_service.permissions().create(
fileId=presentation_id,
body=permission,
fields="id",
).execute()
# 8) 최종 링크 반환
return presentation_link
except Exception as e:
print("create_slides 에러:", e)
return None
여기서 original_file_id 는 템플릿 프레젠테이션 ID
(예: 1BD_IbF8x62MsUNlFGbWSmt4v7rpMR5us8BxIwmvMZ9I)
6. 템플릿 선택 (prompt.html)
prompt.html에서 체크박스 value로 템플릿 ID를 넘김:
<label class="label_check_box">
<input type="checkbox"
id="presentation_id"
class="single-checkbox"
name="presentation_id"
value="1BD_IbF8x62MsUNlFGbWSmt4v7rpMR5us8BxIwmvMZ9I">
<span class="custom-checkbox"></span>
</label>
예전에 쓰던 autoppt 템플릿 ID를 그대로 두어서
HttpError 404 … File not found ...
가 계속 났고, → 내 계정으로 만든 새 템플릿 ID로 교체해서 해결했다.
7. UserHistory 모델 & prompt 뷰
7-1. UserHistory 모델
class UserHistory(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
ppt_title = models.CharField(max_length=500)
ppt_url = models.CharField(max_length=500)
create_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.user.username} - {self.ppt_url}"
7-2. prompt 뷰 흐름
@login_required(login_url='/login/')
def prompt(request):
user_id = request.user.id
global SLIDE_TITLE_TEXT, filename, ppt_link
if request.method == "POST":
# 1) 템플릿 ID + 사용자 입력 받기
presentation_id = request.POST.get("presentation_id")
SLIDE_TITLE_TEXT = request.POST.get("user-input", "").strip()
# 2) 입력을 기반으로 파일명 생성 (OpenAI)
input_string = re.sub(r"[^\w\s.\-\(\)]", "", SLIDE_TITLE_TEXT).replace("\n", "")
filename_prompt = (
f'Generate a short, descriptive filename based on: "{input_string}". '
f'Answer just with the short filename, no explanation.'
)
filename_response = client.chat.completions.create(
model="gpt-3.5-turbo-1106",
messages=[{"role": "system", "content": filename_prompt}],
temperature=0.5,
max_tokens=30,
)
filename = filename_response.choices[0].message.content.strip().replace(" ", "_")
# 3) 폴더 생성 (이미 있으면 재사용)
dir_name = filename
os.makedirs(dir_name, exist_ok=True)
SLIDE_TITLE_TEXT = dir_name
# 4) PPT 내용 생성 및 슬라이드 분할
ppt_text = create_ppt_text(filename)
split_slides(ppt_text, index=0)
ppt_detail_text = create_ppt_detail_text()
split_slides(ppt_detail_text, index=2)
# 5) 구글 슬라이드 생성 + 링크 받기
ppt_link = create_slides(presentation_id, filename)
print("ppt_link =", ppt_link)
# 6) 링크 생성 실패시 DB 저장 막기
if not ppt_link:
return HttpResponse("슬라이드 링크 생성에 실패했습니다. 터미널 로그를 확인하세요.")
# 7) 히스토리 저장
UserHistory.objects.create(
user_id=user_id,
ppt_url=ppt_link,
ppt_title=filename,
)
return redirect('result')
else:
return render(request, 'blog/prompt.html')
8. 중간에 만났던 주요 에러 & 해결 요약
-
가상환경 활성화 에러
- 원인: 윈도우용
Scripts/activate를 Mac에서 실행 - 해결:
source venv/bin/activate
- 원인: 윈도우용
-
MySQL Access denied(1045)
- 원인: 사용자/비밀번호/권한 설정 문제
- 해결:
mysql -u root로 접속 확인 후,project_user계정 권한 다시 설정
-
googleapiclient 모듈 없음
- 해결:
pip install google-api-python-client ...
- 해결:
-
OpenAI ascii UnicodeEncodeError
- 원인: api_key에 한글 문자열 사용
- 해결:
.env+settings.OPENAI_API_KEY구조로 변경
-
invalid_grant / access_denied (autoppt)
- 원인: 예전 프로젝트/클라이언트 사용
- 해결: 새 GCP 프로젝트 + 새 OAuth 클라이언트 + test user 등록
-
HttpError 404 (File not found 템플릿)
- 원인: 내 계정에서 접근 불가능한 다른 계정 템플릿 ID 사용
- 해결: 내 계정으로 새 구글 슬라이드 생성 후 ID 교체
-
FileExistsError: 폴더 이미 존재
- 해결:
os.makedirs(dir_name, exist_ok=True)로 변경
- 해결:
-
IntegrityError: Column ‘ppt_url’ cannot be null
- 원인:
create_slides에서 예외 발생 시return없이 끝나서ppt_link=None -
해결:
create_slides끝에return presentation_linkexcept에서도return Noneprompt에서if not ppt_link:체크 후 DB 저장 막기
- 원인:
-
HttpError 400: Must specify at least one request
- 원인:
requests_update리스트가 비어 있는데batchUpdate호출 - 해결:
if requests_update:인 경우에만batchUpdate수행
- 원인:
9. 앞으로 할 TODO
템플릿별 텍스트 매핑(object_index, text_list) 로직 리팩터링
UserHistory 리스트 페이지 + 상세 페이지 구현
.env 에 DB 비밀번호, GOOGLE_PROJECT_ID 등도 분리
GitHub Actions / 배포 자동화 고민해보기
슬라이드 디자인 템플릿 여러 개 선택할 수 있게 확장
전에 만들었던 프로젝트인데 대충 만들어 놓고 나중에 다시 한번 보고 부족한 부분 있으면 좀 수정해야지 했다가 1년이 다 되서 열어보니까 기억이 하나도 안 난다 일은 역시 그때그때 해야겠다. 쨋든 뭔가 전은 있었다 근데 문제는 최종적으로 내 서버에 올리고 다른 계정으로 접속했을 때도 작동이 되야 하는데 안 될 것 같다 일단 기능들만 다 돌아가게 만들어 놓고 고민좀 더 해봐야 할 것 같다.