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 설정

  1. Google Cloud Console 에서 새 프로젝트 생성
  2. OAuth 동의 화면

    • 사용자 유형: 외부
    • 테스트 사용자에 내 구글 계정 추가
  3. OAuth 클라이언트 ID 생성

    • 애플리케이션 유형: 데스크톱 앱
    • JSON 다운로드 → client_secret.json 이름으로 프로젝트 루트에 저장
  4. 최초 실행 시 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. 중간에 만났던 주요 에러 & 해결 요약

  1. 가상환경 활성화 에러

    • 원인: 윈도우용 Scripts/activate 를 Mac에서 실행
    • 해결: source venv/bin/activate
  2. MySQL Access denied(1045)

    • 원인: 사용자/비밀번호/권한 설정 문제
    • 해결: mysql -u root 로 접속 확인 후, project_user 계정 권한 다시 설정
  3. googleapiclient 모듈 없음

    • 해결: pip install google-api-python-client ...
  4. OpenAI ascii UnicodeEncodeError

    • 원인: api_key에 한글 문자열 사용
    • 해결: .env + settings.OPENAI_API_KEY 구조로 변경
  5. invalid_grant / access_denied (autoppt)

    • 원인: 예전 프로젝트/클라이언트 사용
    • 해결: 새 GCP 프로젝트 + 새 OAuth 클라이언트 + test user 등록
  6. HttpError 404 (File not found 템플릿)

    • 원인: 내 계정에서 접근 불가능한 다른 계정 템플릿 ID 사용
    • 해결: 내 계정으로 새 구글 슬라이드 생성 후 ID 교체
  7. FileExistsError: 폴더 이미 존재

    • 해결: os.makedirs(dir_name, exist_ok=True) 로 변경
  8. IntegrityError: Column ‘ppt_url’ cannot be null

    • 원인: create_slides 에서 예외 발생 시 return 없이 끝나서 ppt_link=None
    • 해결:

      • create_slides 끝에 return presentation_link
      • except 에서도 return None
      • prompt에서 if not ppt_link: 체크 후 DB 저장 막기
  9. 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년이 다 되서 열어보니까 기억이 하나도 안 난다 일은 역시 그때그때 해야겠다. 쨋든 뭔가 전은 있었다 근데 문제는 최종적으로 내 서버에 올리고 다른 계정으로 접속했을 때도 작동이 되야 하는데 안 될 것 같다 일단 기능들만 다 돌아가게 만들어 놓고 고민좀 더 해봐야 할 것 같다.