484 lines
18 KiB
Python
Executable File
484 lines
18 KiB
Python
Executable File
import os
|
|
import requests
|
|
from PIL import Image
|
|
from io import BytesIO
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import render, redirect
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth import authenticate, login, logout
|
|
from django.views.decorators.http import require_POST
|
|
from django.utils.timezone import now
|
|
from django.core.files.base import ContentFile
|
|
from django.db import transaction
|
|
from datetime import datetime, date
|
|
from bs4 import BeautifulSoup
|
|
from .forms import SignUpForm ,UserProfileForm, UpdateInfoForm, AIDiaryForm
|
|
from .models import CustomUser, NewsArticle, Diary, UserProfile, DiaryInput
|
|
from .services_lotto import get_latest_recommendations, generate_recommendations
|
|
from .scripts.ml_lotto_recommend import get_ml_recommendations
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
|
|
# 유틸: 로그인 + 스킨 설정 여부 체크
|
|
def redirect_logged_user(user):
|
|
if not user.skin_image:
|
|
return redirect('/skin-setup/')
|
|
return redirect('/main/')
|
|
|
|
@login_required
|
|
def main_view(request):
|
|
if not request.user.skin_image:
|
|
return redirect('/skin-setup/') # 스킨 설정 안 되어 있으면 차단
|
|
return render(request, 'myworld_app/main.html')
|
|
|
|
def skin_setup_view(request):
|
|
if not request.user.is_authenticated:
|
|
return redirect('/login')
|
|
return render(request, 'myworld_app/skinsetup.html', {
|
|
'icon_range': range(1, 25),
|
|
'skin_images': ['bg_00.webp', 'bg_11.webp', 'bg_22.webp', 'bg_33.webp', 'bg_44.webp'],
|
|
'is_main': False
|
|
})
|
|
|
|
def index(request):
|
|
if request.user.is_authenticated:
|
|
return redirect_logged_user(request.user)
|
|
return render(request, 'myworld_app/index.html', {'is_main': True})
|
|
|
|
def login_view(request):
|
|
if request.user.is_authenticated:
|
|
return redirect_logged_user(request.user)
|
|
|
|
if request.method == 'POST':
|
|
username = request.POST.get('username')
|
|
password = request.POST.get('password')
|
|
|
|
user = authenticate(request, username=username, password=password)
|
|
if user is not None:
|
|
login(request, user)
|
|
|
|
request.session['skin_image'] = os.path.basename(user.skin_image.name) if user.skin_image else ''
|
|
request.session['character_image'] = os.path.basename(user.character_image.name) if user.character_image else ''
|
|
request.session['birthdate'] = str(user.birthdate) if user.birthdate else ''
|
|
request.session['nickname'] = user.nickname or ''
|
|
request.session['username'] = user.username or ''
|
|
request.session['is_staff'] = user.is_staff # Boolean 값 그대로 저장
|
|
request.session['email'] = user.email or ''
|
|
request.session['gender'] = user.gender or ''
|
|
request.session['phone'] = user.phone or ''
|
|
request.session['address'] = user.address or ''
|
|
request.session['oid'] = user.id or ''
|
|
|
|
# ✅ 스킨 설정 여부 확인
|
|
if not user.skin_image:
|
|
return redirect('/skin-setup/') # 스킨 설정 페이지로 이동
|
|
|
|
return redirect('/main/') # 스킨 설정 완료 시 메인 페이지로 이동
|
|
else:
|
|
messages.error(request, '아이디 또는 비밀번호가 올바르지 않습니다.')
|
|
return redirect('/login')
|
|
|
|
return render(request, 'myworld_app/login.html')
|
|
|
|
|
|
def logout_view(request):
|
|
logout(request)
|
|
request.session.flush()
|
|
# messages.success(request, "로그아웃되었습니다.")
|
|
return redirect('/')
|
|
|
|
|
|
def signup_view(request):
|
|
if request.user.is_authenticated:
|
|
return redirect_logged_user(request.user)
|
|
|
|
if request.method == 'POST':
|
|
form = SignUpForm(request.POST)
|
|
password = request.POST.get('password1')
|
|
|
|
print("비밀번호 값:", password)
|
|
|
|
if form.is_valid() and password and len(password) == 6:
|
|
try:
|
|
user = form.save(commit=False)
|
|
user.set_password(password)
|
|
user.save()
|
|
messages.success(request, "회원가입이 완료되었습니다. 로그인해주세요.")
|
|
return redirect('/login')
|
|
except Exception as e:
|
|
print("회원가입 오류:", e)
|
|
messages.error(request, "회원가입 중 오류가 발생했습니다. 다시 시도해주세요.")
|
|
return redirect('/signup')
|
|
else:
|
|
print("폼 유효성 실패:", form.errors)
|
|
messages.error(request, "입력값이 올바르지 않거나 비밀번호가 6자리가 아닙니다.")
|
|
return redirect('/signup')
|
|
else:
|
|
form = SignUpForm()
|
|
return render(request, 'myworld_app/signup.html', {'form': form})
|
|
|
|
@login_required
|
|
def update_info_view(request):
|
|
user = request.user
|
|
if request.method == 'POST':
|
|
form = UpdateInfoForm(request.POST, instance=user)
|
|
if form.is_valid():
|
|
user = form.save(commit=False)
|
|
|
|
password = request.POST.get('password')
|
|
if password:
|
|
user.set_password(password)
|
|
update_session_auth_hash(request, user) # 세션 유지
|
|
|
|
user.save()
|
|
|
|
request.session['nickname'] = user.nickname or ''
|
|
request.session['email'] = user.email or ''
|
|
request.session['birthdate'] = str(user.birthdate) if user.birthdate else ''
|
|
request.session['gender'] = user.gender or ''
|
|
request.session['phone'] = user.phone or ''
|
|
request.session['address'] = user.address or ''
|
|
|
|
messages.success(request, "정보가 저장되었습니다.")
|
|
return redirect('/main/')
|
|
else:
|
|
messages.error(request, "입력값을 확인해주세요.")
|
|
else:
|
|
form = UpdateInfoForm(instance=user)
|
|
|
|
return render(request, 'myworld_app/update_info.html', {'user': request.user})
|
|
|
|
@require_POST
|
|
@login_required
|
|
def save_skin_icon(request):
|
|
skin = request.POST.get('skin_image') # ex: bg_11.webp
|
|
icon = request.POST.get('character_icon') # ex: pig-17
|
|
|
|
if not skin or not icon:
|
|
messages.error(request, '스킨 또는 캐릭터를 선택하지 않았습니다.')
|
|
return redirect('/skin-setup/')
|
|
|
|
user = request.user
|
|
user.skin_image.name = f'{skin}'
|
|
user.character_image.name = f'{icon}' # 확장자 맞춰줘야 함
|
|
user.save()
|
|
|
|
request.session['skin_image'] = os.path.basename(user.skin_image.name)
|
|
request.session['character_image'] = os.path.basename(user.character_image.name)
|
|
|
|
# messages.success(request, '내 월드 설정이 저장되었습니다.')
|
|
return redirect('/main/')
|
|
|
|
|
|
def check_duplicate(request):
|
|
field = request.GET.get('field')
|
|
value = request.GET.get('value')
|
|
mode = request.GET.get('mode', 'signup') # 기본은 회원가입
|
|
user = request.user if request.user.is_authenticated else None
|
|
|
|
exists = False
|
|
queryset = CustomUser.objects.none()
|
|
|
|
if field == 'username':
|
|
queryset = CustomUser.objects.filter(username=value)
|
|
elif field == 'nickname':
|
|
queryset = CustomUser.objects.filter(nickname=value)
|
|
elif field == 'email':
|
|
queryset = CustomUser.objects.filter(email=value)
|
|
|
|
if mode == 'update' and user:
|
|
queryset = queryset.exclude(id=user.id)
|
|
|
|
exists = queryset.exists()
|
|
|
|
return JsonResponse({'exists': exists})
|
|
|
|
@login_required
|
|
def lotto_view(request):
|
|
existing = get_latest_recommendations()
|
|
if existing.exists():
|
|
recommendations = existing.order_by('created_at')
|
|
else:
|
|
recommendations = generate_recommendations()
|
|
return render(request, 'myworld_app/lotto.html', {'recommendations': recommendations})
|
|
|
|
@login_required
|
|
def lotto_ml_view(request):
|
|
ml_recommendations = get_ml_recommendations() # 머신러닝 기반 추천 조합 5개
|
|
return render(request, 'myworld_app/lotto_ml.html', {
|
|
'ml_recommendations': ml_recommendations
|
|
})
|
|
|
|
@login_required
|
|
def news_list_view(request):
|
|
articles = NewsArticle.objects.order_by('-pub_date')[:30]
|
|
return render(request, 'myworld_app/news.html', {'news_list': articles})
|
|
|
|
#뉴스 상세 페이지 뷰
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def news_view(request):
|
|
Aid = request.GET.get("Aid")
|
|
article = NewsArticle.objects.filter(aid=Aid).first()
|
|
if not article:
|
|
return render(request, 'myworld_app/news_view.html', {'error': '뉴스를 찾을 수 없습니다.'})
|
|
return render(request, 'myworld_app/news_view.html', {
|
|
'title': article.title,
|
|
'content_html': article.content_html,
|
|
'content_txt': force_paragraphs(article.content_txt),
|
|
'content_image': article.content_image,
|
|
'author': article.author,
|
|
'section': article.section,
|
|
'main_image': article.main_image,
|
|
'pub_date': article.pub_date,
|
|
})
|
|
|
|
def force_paragraphs(text):
|
|
"""
|
|
모든 줄바꿈(\n)을 두 줄바꿈(\n\n)으로 바꿔서 Django의 linebreaks 필터가
|
|
<p> 단락을 제대로 생성하게 유도하는 전처리 함수
|
|
"""
|
|
if not text:
|
|
return ""
|
|
|
|
# 중복 줄바꿈은 유지하고, 단일 줄바꿈만 \n\n으로
|
|
lines = text.strip().splitlines()
|
|
return "\n\n".join(line.strip() for line in lines if line.strip())
|
|
|
|
|
|
@login_required
|
|
def diary_list_view(request):
|
|
user = request.user
|
|
month_str = request.GET.get("month")
|
|
|
|
if not month_str:
|
|
# 💡 기본값: 현재 연-월
|
|
today = now().date()
|
|
month_str = today.strftime("%Y-%m")
|
|
|
|
try:
|
|
year, month = map(int, month_str.split('-'))
|
|
diaries = Diary.objects.filter(
|
|
user=user,
|
|
date__year=year,
|
|
date__month=month
|
|
).order_by('-date')
|
|
except:
|
|
diaries = Diary.objects.filter(user=user).order_by('-date')
|
|
|
|
return render(request, 'myworld_app/diary_list.html', {
|
|
'diaries': diaries,
|
|
'selected_month': month_str,
|
|
})
|
|
|
|
@login_required
|
|
def user_profile_view(request):
|
|
user = request.user
|
|
profile, created = UserProfile.objects.get_or_create(user=user)
|
|
|
|
if request.method == 'POST':
|
|
form = UserProfileForm(request.POST, instance=profile)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, "프로필이 저장되었습니다.")
|
|
return redirect('/diary/list/') # 또는 일기 작성 페이지
|
|
else:
|
|
messages.error(request, "입력값을 확인해주세요.")
|
|
else:
|
|
form = UserProfileForm(instance=profile)
|
|
|
|
return render(request, 'myworld_app/user_profile.html', {
|
|
'form': form,
|
|
'is_main': False
|
|
})
|
|
|
|
def calculate_age(birthdate):
|
|
today = date.today()
|
|
return today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day))
|
|
|
|
def build_prompt(request, form_data):
|
|
user = request.user # 🔥 여기서 바로 user 객체
|
|
profile, created = UserProfile.objects.get_or_create(user=user)
|
|
|
|
def calculate_age(birthdate):
|
|
if isinstance(birthdate, str):
|
|
birthdate = datetime.strptime(birthdate, "%Y-%m-%d").date() # 문자열 → 날짜로 변환
|
|
today = date.today()
|
|
return today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day))
|
|
|
|
name = request.session.get("nickname") or "이름없음"
|
|
gender = request.session.get("gender") or "비공개"
|
|
age = calculate_age(request.session.get("birthdate")) or "?"
|
|
mbti = profile.mbti or "알 수 없음"
|
|
mood_baseline = profile.mood_baseline or "기본 기분 없음"
|
|
keywords = profile.favorite_keywords or "없음"
|
|
writing_style = profile.writing_style or "없음"
|
|
tone = profile.tone or "없음"
|
|
dream_type = profile.dream_type or "없음"
|
|
preferred_time = profile.preferred_time or "없음"
|
|
personality = profile.personality or "성향 설명 없음"
|
|
|
|
# ✅ 작성자 소개 텍스트 만들기
|
|
author_description = f"""
|
|
[작성자 정보]
|
|
|
|
이름은 {name}이며, {gender}이고 {age}세입니다.
|
|
MBTI는 {mbti}로, 평소 {mood_baseline}한 성향을 가지고 있습니다.
|
|
선호하는 키워드는 '{keywords}'이며, 글을 쓸 때는 '{writing_style}' 스타일을 선호하고, '{tone}'를 자주 사용합니다.
|
|
꿈은 '{dream_type}' 분위기를 좋아하고, 주로 '{preferred_time}'에 글을 쓰는 것을 즐깁니다.
|
|
이 사용자의 성향을 한마디로 표현하자면: "{personality}"입니다.
|
|
|
|
아래에 오늘 하루에 대한 정보가 있습니다.
|
|
이를 바탕으로 감성적이고 1인칭 시점의 AI 일기를 작성해주세요.
|
|
"""
|
|
today_context = f"""
|
|
[오늘의 정보]
|
|
- 기분: {form_data['mood']}
|
|
- 내상태: {form_data['state']}
|
|
- 위치: {form_data['location']}
|
|
- 날씨: {form_data['weather']}
|
|
- 시간대: {form_data['time_of_day']}
|
|
- 함께한 사람: {form_data['with_whom']}
|
|
- 특별한 사건: {form_data['event']}
|
|
- 오늘을 한 단어로 표현: {form_data['one_word']}
|
|
- 오늘의 키워드: {form_data['keywords']}
|
|
- 가장 중요한 순간: {form_data['most_important']}
|
|
- 요약 문장: {form_data.get('summary') or '(없음)'}
|
|
|
|
위의 정보를 바탕으로 1인칭 감성적인 일기를 작성해 주세요.
|
|
전체 분량은 약 500자 이내로 해주세요.
|
|
"""
|
|
|
|
return (author_description.strip() + "\n\n" + today_context.strip())
|
|
|
|
def save_diary_input(user, form_data, prompt):
|
|
DiaryInput.objects.create(
|
|
user=user,
|
|
mood=form_data['mood'],
|
|
state=form_data['state'],
|
|
location=form_data['location'],
|
|
weather=form_data['weather'],
|
|
time_of_day=form_data['time_of_day'],
|
|
with_whom=form_data['with_whom'],
|
|
event=form_data['event'],
|
|
one_word=form_data['one_word'],
|
|
most_important=form_data['most_important'],
|
|
keywords=form_data['keywords'],
|
|
summary=form_data.get('summary', ''),
|
|
prompt=prompt
|
|
)
|
|
|
|
@login_required
|
|
def diary_write_view(request):
|
|
if not request.session.get("birthdate") or not request.session.get("gender") or not request.session.get("nickname"):
|
|
messages.warning(request, "생년월일, 성별이 있어야 AI 일기 작성이 가능합니다. 생년월일, 성별을 선택하세요.")
|
|
return redirect("/update-info/")
|
|
|
|
if request.method == "POST":
|
|
form = AIDiaryForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
form_data = form.cleaned_data
|
|
prompt = build_prompt(request, form_data)
|
|
|
|
image_file = form.cleaned_data.get("image")
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
diary_input = DiaryInput(
|
|
user=request.user,
|
|
mood=form_data['mood'],
|
|
state=form_data['state'],
|
|
location=form_data['location'],
|
|
weather=form_data['weather'],
|
|
time_of_day=form_data['time_of_day'],
|
|
with_whom=form_data['with_whom'],
|
|
event=form_data['event'],
|
|
one_word=form_data['one_word'],
|
|
most_important=form_data['most_important'],
|
|
keywords=form_data['keywords'],
|
|
summary=form_data.get('summary', ''),
|
|
prompt=prompt
|
|
)
|
|
if image_file:
|
|
resized = resize_image(image_file)
|
|
diary_input.image.save(image_file.name, resized, save=False)
|
|
diary_input.save()
|
|
|
|
diary = generate_diary_from_openai(prompt, diary_input)
|
|
|
|
return redirect("diary_view", pk=diary.id)
|
|
|
|
except Exception as e:
|
|
print("❌ GPT 오류:", e)
|
|
form = AIDiaryForm(request.POST, request.FILES)
|
|
messages.error(request, "AI 일기 생성 중 오류가 발생했습니다. 다시 시도해 주세요.")
|
|
return render(request, "myworld_app/diary_write.html", {
|
|
"form": form,
|
|
"prompt": prompt
|
|
})
|
|
|
|
else:
|
|
form = AIDiaryForm()
|
|
|
|
return render(request, "myworld_app/diary_write.html", {"form": form})
|
|
|
|
@login_required
|
|
def diary_view(request, pk):
|
|
diary = get_object_or_404(Diary, pk=pk, user=request.user)
|
|
return render(request, "myworld_app/diary_preview.html", {
|
|
"diary": diary
|
|
})
|
|
|
|
|
|
def resize_image(image_file, max_size=(800, 800)):
|
|
image = Image.open(image_file)
|
|
|
|
if image.mode == "RGBA":
|
|
image = image.convert("RGB") # ✅ RGBA → RGB 변환
|
|
elif image.mode == "P": # 팔레트 기반 이미지도 변환 필요할 수 있음
|
|
image = image.convert("RGB")
|
|
|
|
image.thumbnail(max_size) # 비율 유지하며 리사이즈
|
|
|
|
buffer = BytesIO()
|
|
image.save(buffer, format="JPEG", quality=85)
|
|
return ContentFile(buffer.getvalue())
|
|
|
|
|
|
from openai import OpenAI
|
|
client = OpenAI(api_key="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")
|
|
from datetime import date
|
|
from .models import Diary
|
|
|
|
def generate_diary_from_openai(prompt, diary_input):
|
|
# 🔹 OpenAI 호출
|
|
response = client.chat.completions.create(
|
|
model="gpt-4",
|
|
messages=[
|
|
{"role": "user", "content": prompt}
|
|
],
|
|
temperature=0.9,
|
|
max_tokens=2048,
|
|
)
|
|
|
|
ai_text = response.choices[0].message.content.strip()
|
|
|
|
# 🔹 키워드 추출: 첫 줄 또는 적당한 길이
|
|
keywords = ai_text.splitlines()[0][:40] if ai_text else "일기"
|
|
|
|
# 🔹 Diary 저장 및 반환
|
|
diary = Diary.objects.create(
|
|
user=diary_input.user,
|
|
input=diary_input, # ForeignKey로 연결되어 있어야 함
|
|
date=date.today(),
|
|
mood=diary_input.mood,
|
|
weather=diary_input.weather,
|
|
keywords=keywords,
|
|
diary_text=ai_text
|
|
)
|
|
|
|
return diary # ✅ Diary 인스턴스를 그대로 리턴
|
|
|