▶️ JavaScript
[▶️Front-end] 사용자 요청 결과의 Prompt를 보여주기 1차
▲ 본 게시물은 지난번에 JSON으로 받아지지 않았던 오류를 수정하는 내용을 담았어요.
해골 병사를 생성하는 프롬프트를 작성해 줘. 키는 160cm 정도고, 성별은 남자야.
오류를 잡아가는 과정
백엔드에서 오류 확인
def get(self, request):
# 250325 : API 상태 확인 대신 prompt.html 렌더링
print(self.__class__.__name__ + "request: ", request) # request가 찍히는지 확인 250325
return render(request, "prompt.html")
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
optimized_prompt = loop.run_until_complete(generate_3d_prompt(user_request))
loop.close()
print("optimized_prompt: ", optimized_prompt) # optimized_prompt가 출력되는지 확인 250325
우선 백엔드에서 잘 작동되고 있는지를 확인했어요.
# bash
[25/Mar/2025 07:08:04] "GET /article/ HTTP/1.1" 200 32084
GeneratePromptAPIrequest: <rest_framework.request.Request: GET '/api/prompts/'>
[25/Mar/2025 07:08:14] "GET /api/prompts/ HTTP/1.1" 200 2807
2025-03-25 07:08:45,116 - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
optimized_prompt: 해골 병사 3D 모델을 위한 프롬프트: 해골 병사는 키 160cm 남성의 체형에 기반하며, 중세 판타지 테마에 어울리도록 디자인합니다. 해골의 뼈는 사실적이고 자연스러우며, 싸움의 경험을 반영해 흠집과 마모가 가미됩니다. 머리에는 철제 헬멧을 착용하고, 가슴 부분엔 오래된 녹슨 갑옷을, 팔과 다리엔 검은 가죽으로 된 팔 보호대와 레깅스를 착용합니다. 오른손에는 녹슨 검을, 왼손에는 방패를 들고 있습니다. 이 모델은 액션 포즈로 설정되며, 뒤틀림 없이 안정적인 자세를 유지합니다. 적절한 조명을 통해 어두운 분위기를 강조합니다.
[25/Mar/2025 07:08:45] "POST /api/prompts/ HTTP/1.1" 200 1353
백엔드에서는 문제없이 잘 작동되고 있네요.
그렇다면 프런트에서 문제가 있음을 의미해요.
JavaScript_console.log로 확인하기
// 콘솔에서 CSRF 토큰이 잘 가져와졌는지 확인 (디버깅용) 250324
console.log("csrfToken: ", csrfToken);
// 버튼 클릭 시 실행되는 함수
submitBtn.addEventListener('click', function() {
console.log("generate button click"); // click을 눌렀을 때 콘솔 확인 250325
.then(response => {
// 요청이 끝났으므로 로딩 표시 숨기기
console.log("response : ", response); // response가 받아지는지 확인 250325
// 서버 응답이 정상적이지 않을 경우 처리
if (!response.ok) {
....
}
console.log("response.json() : ", response.json()); // response가 json로 변환되는지 확인 250325
.then(data => {
console.log("data : ", data); // data가 받아지는지 확인 250325
// 서버 응답이 정상적일 경우 결과 출력
if (data.Miravelle) {
console.log("data.Miravelle : ", data.Miravelle) // Miravelle data가 잘 넘어오는지 확인 250325
JS에서 console.log는 python에서 print 함수 역할과 똑같아요.
python에서는 print()로 하면 끝나지만, JS에서 console.log를 쓰실 때는
console.log();
이 형태로 작성하셔야 작동이 돼요.
Front-end 영역이기 때문에 오류를 개발자 모드(Resource와 Console)에서 확인하며(F10) 내려갔어요.
물론 튜터님의 지시 아래에서요,,
F10을 누르면서 설정해 놓은 브레이크 부분을 하나하나 내려가면 오류를 발견할 수 있어요.
근데
작동이 잘 되다가 갑자기 새로고침이 돼버리는 현상이 발견됐어요. 🤔
prompt.html
{% extends "base.html" %}
{% load static %}
{% block content %}
...
<form id="promptForm">
{% csrf_token %}
<input type="text" id="userInput" class="form-control" placeholder="생성하고 싶은 모델을 설명해주세요." maxlength="500"> {# maxlength 추가 #}
<button id="submitBtn" class="btn btn-dark mt-3">Generate</button>
</form>
...
</div>
...
{% endblock %}
prompt.js 파일에서 말고 prompt.html에서 문제가 있는지 확인해 보았어요.
버튼이 두 번 클릭되어서 새로고침이 되는 건가 싶은 생각이 드셨나 봐요. (튜터님 지니어스,,)
<button id="submitBtn" class="btn btn-dark mt-3">Generate</button>
버튼의 type이 설정되어 있지 않더라고요.
그래서 type을 추가해 주었어요.
수정된 prompt.html
{% extends "base.html" %}
{% load static %}
{% block content %}
...
<form id="promptForm">
{% csrf_token %}
<input type="text" id="userInput" class="form-control" placeholder="생성하고 싶은 모델을 설명해주세요." maxlength="500"> {# maxlength 추가 #}
<button id="submitBtn" class="btn btn-dark mt-3" type="button">Generate</button>
</form>
...
</div>
...
{% endblock %}
<button id="submitBtn" class="btn btn-dark mt-3" type="button">Generate</button>
이렇게 수정을 완료하고, 서버로 돌아가서 작동 확인을 했어요.
Genius,,
작동이 되네요.
그렇다면 왜, type이 명시돼 있지 않다는 이유로 이런 오류가 발생했을까요? 🤔
<button id="submitBtn" class="btn btn-dark mt-3">Generate</button>
이 상태로 둔다면,
중복 호출이 될 수 있어요. 그렇기 때문에 새로고침 현상이 발생하게 된 것이에요.<button id="submitBtn" class="btn btn-dark mt-3" type="button">Generate</button>
하지만 이렇게 type을 명시해 둔다면,
버튼을 직접 눌러야지만, 호출이 발생하게 돼서 새로고침 현상이 발생하지 않게 돼요. 😳👏🏻
이렇게 해서, 오류를 잡아낼 수 있었어요.
고생하신 튜터님께 압도적 감사 인사와 박수를,,
생성된 모델
최종 코드
prompts/views.py
더보기
from rest_framework.views import APIView
# from rest_framework.response import Response
from rest_framework import status
from openai import AsyncOpenAI
import asyncio
import openai
from drf_yasg.utils import swagger_auto_schema
from prompts.serializers import GeneratePromptSerializer
from django.utils.decorators import method_decorator # 250324 추가
from django.contrib.auth.decorators import login_required # 250324 추가
from django.http import JsonResponse # 250324 추가
from django.shortcuts import render # 250325 추가
from utils.azure_key_manager import AzureKeyManager
# OpenAI 프롬프트 생성 함수
async def generate_3d_prompt(user_input):
azure_keys = AzureKeyManager.get_instance()
api_key = azure_keys.openai_api_key
if not api_key:
raise ValueError("Missing OpenAI API Key")
aclient = AsyncOpenAI(api_key=api_key)
system_prompt = (
"너는 3D 모델을 생성하기 위한 최적의 프롬프트를 만드는 AI야."
"사용자의 요청을 분석하여 디테일한 프롬프트를 제공해야 해."
"사용자가 제공한 정보를 존중하며, 부족한 경우 일반적인 추천을 추가해."
"3D 모델링에 적합한 키워드를 선정하고, 문장은 직관적이며 이해하기 쉽게 작성해."
"프롬프트 내의 개행을 삭제하고, 자연스럽게 이어지도록 구성해."
"필요한 정보를 압축해 500자 이내로 한국어로 작성해."
)
try:
response = await aclient.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input}
]
)
return response.choices[0].message.content
except openai.APIConnectionError as e:
print(f"OpenAI 서버 연결 오류: {e}")
return "OpenAI 서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요."
except openai.RateLimitError as e:
print(f"OpenAI API Rate Limit 초과: {e}")
return "OpenAI API Rate Limit이 초과되었습니다. 잠시 후 다시 시도해주세요."
except Exception as e:
print(f"OpenAI API 호출 중 오류 발생: {e}")
return "OpenAI API 호출 중 오류가 발생했습니다."
# Django REST Framework API 엔드포인트
@method_decorator(login_required(login_url='users:login'), name='dispatch')
class GeneratePromptAPI(APIView):
def get(self, request):
# 250325 : API 상태 확인 대신 prompt.html 렌더링
print(self.__class__.__name__ + "request: ", request) # request가 찍히는지 확인 250325
return render(request, "prompt.html")
# message = "API가 사용 가능한 상태입니다."
# return JsonResponse({"status": message}, status=status.HTTP_200_OK)
@swagger_auto_schema(
request_body=GeneratePromptSerializer,
operation_description="3D 모델 생성을 위한 프롬프트 생성 API",
)
def post(self, request):
serializer = GeneratePromptSerializer(data=request.data)
if serializer.is_valid():
user_input = serializer.validated_data['user_input']
user_request = "Create a 3D model based on 3D Model Prompt: {}".format(user_input)
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
optimized_prompt = loop.run_until_complete(generate_3d_prompt(user_request))
loop.close()
print("optimized_prompt: ", optimized_prompt) # optimized_prompt가 출력되는지 확인 250325
return JsonResponse({"Miravelle": optimized_prompt}, status=status.HTTP_200_OK) # 20250324
except Exception as e:
return JsonResponse(
{"error": f"API 호출 중 오류 발생: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
else:
return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
prompts/templates/prompt.html
더보기
{% extends "base.html" %}
{% load static %}
{% block content %}
{% if user.is_authenticated %}
<div class="container mt-5">
<h2>Generate 3D Model Prompt</h2>
<form id="promptForm">
{% csrf_token %}
<input type="text" id="userInput" class="form-control" placeholder="생성하고 싶은 모델을 설명해주세요." maxlength="500"> <!--maxlength 추가-->
<button id="submitBtn" class="btn btn-dark mt-3" type="button">Generate</button> <!-- 250325 버튼 타입 추가-->
</form>
<div id="output" class="mt-4"></div>
<div id="loading" class="mt-3" style="display:none;">loading...</div> <!--로딩 표시-->
<div id="error" class="mt-3 text-danger" style="display:none;"></div> <!--오류 표시-->
</div>
{% else %}
<div class="container mt-5">
<h2>Login required</h2>
<p>Please <a href="{% url 'users:login' %}">log in</a> to use this page.</p>
</div>
{% endif %}
<script src="{% static 'prompt_js/prompt.js' %}"></script>
{% endblock %}
core/static/prompt_js/prompt.js
더보기
// 특정 이름의 쿠키 값을 가져오는 함수
function getCookie(name) {
let cookieValue = null; // 쿠키 값을 저장할 변수 (초기값: null)
if (document.cookie && document.cookie !== '') { // 쿠키가 존재하는지 확인
const cookies = document.cookie.split(';'); // 쿠키 문자열을 ';' 기준으로 분리하여 배열로 저장
for (let i = 0; i < cookies.length; i++) { // 배열을 순회하며 원하는 쿠키 찾기
const cookie = cookies[i].trim(); // 공백 제거
// 쿠키 이름이 우리가 찾는 값(name)으로 시작하는지 확인
if (cookie.startsWith(name + '=')) {
// '=' 이후의 값이 실제 쿠키 값이므로 이를 가져와 디코딩하여 저장
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break; // 원하는 쿠키를 찾았으니 반복문 종료
}
}
}
return cookieValue; // 최종적으로 찾은 쿠키 값을 반환 (없다면 null)
}
// 페이지가 로드되면 실행되는 이벤트 리스너
document.addEventListener('DOMContentLoaded', function() {
// HTML에서 사용할 요소들 가져오기 (버튼, 입력창, 결과 출력 영역 등)
const submitBtn = document.getElementById('submitBtn'); // "Generate" 버튼
const userInput = document.getElementById('userInput'); // 사용자 입력 필드
const output = document.getElementById('output'); // 결과 출력할 div
const loading = document.getElementById('loading'); // 로딩 메시지
const error = document.getElementById('error'); // 오류 메시지
const csrfToken = getCookie('csrftoken'); // CSRF 토큰 가져오기
const API_URL = '/api/prompts/'; // 백엔드 API의 URL
// 콘솔에서 CSRF 토큰이 잘 가져와졌는지 확인 (디버깅용) 250324
console.log("csrfToken: ", csrfToken);
// 필수 요소가 없을 경우 콘솔에 오류 메시지를 출력하고 코드 실행을 중단
if (!submitBtn || !userInput || !output || !loading || !error) {
console.error("필수 요소가 존재하지 않습니다. HTML 구조를 확인하세요.");
return;
}
// CSRF 토큰이 없을 경우 보안 문제로 요청을 차단
if (!csrfToken) {
console.error("CSRF 토큰을 가져올 수 없습니다. 요청이 차단됩니다.");
error.textContent = "보안 오류: CSRF 토큰이 없습니다.";
error.style.display = "block"; // 오류 메시지를 화면에 표시
return; // 코드 실행 중단
}
// 버튼 클릭 시 실행되는 함수
submitBtn.addEventListener('click', function() {
console.log("generate button click"); // click을 눌렀을 때 콘솔 확인 250325
const inputText = userInput.value; // 사용자가 입력한 텍스트 가져오기
// 입력값 검증 (빈 입력 방지)
if (!inputText) {
error.textContent = "프롬프트를 입력해주세요."; // 사용자에게 알림
error.style.display = "block"; // 오류 메시지 표시
return; // 요청 중단
}
// 글자 수 제한 (500자 초과 시 오류 메시지 출력)
if (inputText.length > 500) {
error.textContent = "입력은 500자를 초과할 수 없습니다.";
error.style.display = "block";
return;
}
// 로딩 표시 활성화 (사용자가 기다리는 동안 로딩 메시지 표시)
loading.style.display = "block";
output.style.display = "none"; // 기존 출력 숨김
error.style.display = "none"; // 기존 오류 메시지 숨김
// 백엔드로 AJAX 요청 보내기 (fetch API 사용)
fetch(API_URL, {
method: 'POST', // POST 요청 (새 데이터를 서버로 보낼 때 사용)
headers: {
'Content-Type': 'application/json', // 데이터 타입을 JSON으로 설정
'X-CSRFToken': csrfToken // CSRF 보안 토큰 추가 (Django에서 요구함)
},
body: JSON.stringify({ user_input: inputText }) // 사용자의 입력을 JSON 형식으로 변환하여 전송
})
.then(response => {
// 요청이 끝났으므로 로딩 표시 숨기기
console.log("response : ", response); // response가 받아지는지 확인 250325
loading.style.display = "none";
output.style.display = "block"; // 결과 표시 활성화
// // 서버 응답이 정상적이지 않을 경우 처리
// if (!response.ok) {
// return response.text().then(text => { // 응답 본문을 가져옴
// try {
// const err = JSON.parse(text); // JSON으로 변환 시도
// throw new Error(err.error || "API 요청 실패"); // 서버에서 보낸 오류 메시지 출력
// } catch {
// throw new Error("서버 오류: 예상치 못한 응답을 받았습니다."); // JSON 변환 실패 시 일반 오류 처리
// }
// });
// }
// return response.json(); // 응답을 JSON 형식으로 변환
// 250325 추가된 코드
return response.json().then(data =>
{
if(!response.ok){ //문제가 있으면
throw new Error(data.error || "API 요청 실패!");
}
else{ //문제가 없으면
console.log("data :", data); // AI가 뱉어낸 프롬프트 data 확인 250325
return data; //정상적인 응답 반환
}
});})
.then(data => {
console.log("data : ", data); // data가 받아지는지 확인 250325
// 서버 응답이 정상적일 경우 결과 출력
if (data.Miravelle) {
console.log("data.Miravelle : ", data.Miravelle) // Miravelle data가 잘 넘어오는지 확인 250325
output.textContent = data.Miravelle; // 서버에서 받은 결과 출력
} else {
throw new Error(data.error || "서버 응답 오류: 결과를 가져올 수 없습니다."); // 예상과 다른 응답 처리
}
})
.catch(err => {
// 오류 발생 시 사용자에게 표시
error.textContent = err.message || "서버 오류가 발생했습니다.";
error.style.display = "block";
});
});
});
프런트 작업도 이제 끝!!
'👥 최종 팀 프로젝트(250227~250331) > 구현 과정 ▶️' 카테고리의 다른 글
[▶️중간 점검] 4주 동안의 구현된 현황 (0) | 2025.03.26 |
---|---|
[▶️Back-end] 메인 페이지의 페이지네이션(pagination) 구현 (0) | 2025.03.26 |
[▶️Front-end] 사용자 요청 결과의 Prompt를 보여주기 1차 (1) | 2025.03.24 |
[▶️Back-end] 사용자 입력의 Prompt를 다듬는 기능 구현 5차_LLM 完 (0) | 2025.03.21 |
[▶️Back-end] 사용자 입력의 Prompt를 다듬는 기능 구현 4차_LLM (5) | 2025.03.20 |