first commit
0
blog_project/blog_app/__init__.py
Executable file
BIN
blog_project/blog_app/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
blog_project/blog_app/__pycache__/admin.cpython-312.pyc
Executable file
BIN
blog_project/blog_app/__pycache__/apps.cpython-312.pyc
Executable file
BIN
blog_project/blog_app/__pycache__/models.cpython-312.pyc
Executable file
BIN
blog_project/blog_app/__pycache__/urls.cpython-312.pyc
Executable file
BIN
blog_project/blog_app/__pycache__/views.cpython-312.pyc
Executable file
3
blog_project/blog_app/admin.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
blog_project/blog_app/apps.py
Executable file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogAppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog_app'
|
||||
0
blog_project/blog_app/migrations/__init__.py
Executable file
BIN
blog_project/blog_app/migrations/__pycache__/__init__.cpython-312.pyc
Executable file
3
blog_project/blog_app/models.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
BIN
blog_project/blog_app/static/assets/favicon.ico
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
blog_project/blog_app/static/assets/img/about-bg.jpg
Executable file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
blog_project/blog_app/static/assets/img/contact-bg.jpg
Executable file
|
After Width: | Height: | Size: 489 KiB |
BIN
blog_project/blog_app/static/assets/img/home-bg.jpg
Executable file
|
After Width: | Height: | Size: 984 KiB |
BIN
blog_project/blog_app/static/assets/img/post-bg.jpg
Executable file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
blog_project/blog_app/static/assets/img/post-sample-image.jpg
Executable file
|
After Width: | Height: | Size: 112 KiB |
12314
blog_project/blog_app/static/css/bootswatch_bootstrap.css
Executable file
10798
blog_project/blog_app/static/css/styles.css
Executable file
31
blog_project/blog_app/static/js/scripts.js
Executable file
@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* Start Bootstrap - Clean Blog v6.0.9 (https://startbootstrap.com/theme/clean-blog)
|
||||
* Copyright 2013-2023 Start Bootstrap
|
||||
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE)
|
||||
*/
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
let scrollPos = 0;
|
||||
const mainNav = document.getElementById('mainNav');
|
||||
const headerHeight = mainNav.clientHeight;
|
||||
window.addEventListener('scroll', function() {
|
||||
const currentTop = document.body.getBoundingClientRect().top * -1;
|
||||
if ( currentTop < scrollPos) {
|
||||
// Scrolling Up
|
||||
if (currentTop > 0 && mainNav.classList.contains('is-fixed')) {
|
||||
mainNav.classList.add('is-visible');
|
||||
} else {
|
||||
console.log(123);
|
||||
mainNav.classList.remove('is-visible', 'is-fixed');
|
||||
}
|
||||
} else {
|
||||
// Scrolling Down
|
||||
mainNav.classList.remove(['is-visible']);
|
||||
if (currentTop > headerHeight && !mainNav.classList.contains('is-fixed')) {
|
||||
mainNav.classList.add('is-fixed');
|
||||
}
|
||||
}
|
||||
scrollPos = currentTop;
|
||||
});
|
||||
})
|
||||
|
||||
/*테스트 www 폴더*/
|
||||
32
blog_project/blog_app/templates/blog_app/about.html
Executable file
@ -0,0 +1,32 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header-->
|
||||
<header class="masthead" style="background-image: url('{% static "assets/img/about-bg.jpg" %}')">
|
||||
<div class="container position-relative px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<div class="page-heading">
|
||||
<h1>About Me</h1>
|
||||
<span class="subheading">This is what I do.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content-->
|
||||
<main class="mb-4">
|
||||
<div class="container px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe nostrum ullam eveniet pariatur voluptates odit, fuga atque ea nobis sit soluta odio, adipisci quas excepturi maxime quae totam ducimus consectetur?</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius praesentium recusandae illo eaque architecto error, repellendus iusto reprehenderit, doloribus, minus sunt. Numquam at quae voluptatum in officia voluptas voluptatibus, minus!</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut consequuntur magnam, excepturi aliquid ex itaque esse est vero natus quae optio aperiam soluta voluptatibus corporis atque iste neque sit tempora!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
36
blog_project/blog_app/templates/blog_app/back.html
Executable file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>블로그 메인페이지</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="#">블로그</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">{{ user.username }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'logout' %}">로그아웃</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'login' %}">로그인</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'signup' %}">회원가입</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-4">
|
||||
<h1>블로그 메인페이지</h1>
|
||||
<p>여기에 블로그 포스트 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
124
blog_project/blog_app/templates/blog_app/base.html
Executable file
@ -0,0 +1,124 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="description" content="" />
|
||||
<meta name="author" content="" />
|
||||
<title>{% block title %}Blog{% endblock %}</title>
|
||||
<!--<link rel="icon" type="image/x-icon" href="{% static 'assets/favicon.ico' %}" />-->
|
||||
<!-- Font Awesome icons (free version)-->
|
||||
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
|
||||
<!-- Google fonts-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css" />
|
||||
<!-- Core theme CSS (includes Bootstrap)-->
|
||||
<link href="{% static 'css/styles.css' %}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation-->
|
||||
<!-- 네비게이션 영역 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
|
||||
<div class="container px-4 px-lg-5">
|
||||
<a class="navbar-brand" href="{% url 'index' %}">Start Bootstrap</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
|
||||
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||
Menu
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||
<ul class="navbar-nav ms-auto py-4 py-lg-0">
|
||||
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{% url 'index' %}">Home</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{% url 'about' %}">About</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{% url 'post' %}">Sample Post</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="{% url 'contact' %}">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- 🔽 날씨 정보 영역 (nav 바깥에 위치) -->
|
||||
<!--
|
||||
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
<div class="container" style="margin-top: 80px;">
|
||||
<div class="d-flex align-items-center justify-content-center gap-3 small py-2" id="weather-box">
|
||||
<img id="icon" src="" alt="날씨 아이콘" width="40">
|
||||
<span><strong id="city"></strong></span>
|
||||
<span id="description" class="text-muted"></span>
|
||||
<span id="temperature" class="ms-1"></span><span>℃</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer-->
|
||||
<footer class="border-top">
|
||||
<div class="container px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<ul class="list-inline text-center">
|
||||
<li class="list-inline-item">
|
||||
<a href="#!">
|
||||
<span class="fa-stack fa-lg">
|
||||
<i class="fas fa-circle fa-stack-2x"></i>
|
||||
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="#!">
|
||||
<span class="fa-stack fa-lg">
|
||||
<i class="fas fa-circle fa-stack-2x"></i>
|
||||
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="#!">
|
||||
<span class="fa-stack fa-lg">
|
||||
<i class="fas fa-circle fa-stack-2x"></i>
|
||||
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="small text-center text-muted fst-italic">Copyright © Your Website 2023</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Bootstrap core JS-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Core theme JS-->
|
||||
<script src="{% static 'js/scripts.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
function updateWeather() {
|
||||
fetch('/blog/weather/')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error("날씨 오류:", data.error);
|
||||
} else {
|
||||
document.getElementById('city').textContent = data.city;
|
||||
document.getElementById('description').textContent = data.description;
|
||||
document.getElementById('temperature').textContent = data.temperature;
|
||||
document.getElementById('icon').src = `https://openweathermap.org/img/wn/${data.icon}@2x.png`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("날씨 가져오기 실패:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// 최초 실행
|
||||
updateWeather();
|
||||
|
||||
// 5분마다 갱신 (300,000ms)
|
||||
setInterval(updateWeather, 300000);
|
||||
</script>
|
||||
82
blog_project/blog_app/templates/blog_app/contact.html
Executable file
@ -0,0 +1,82 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header-->
|
||||
<header class="masthead" style="background-image: url('{% static "assets/img/contact-bg.jpg" %}')">
|
||||
<div class="container position-relative px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<div class="page-heading">
|
||||
<h1>Contact Me</h1>
|
||||
<span class="subheading">Have questions? I have answers.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content-->
|
||||
<main class="mb-4">
|
||||
<div class="container px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<p>Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!</p>
|
||||
<div class="my-5">
|
||||
<!-- * * * * * * * * * * * * * * *-->
|
||||
<!-- * * SB Forms Contact Form * *-->
|
||||
<!-- * * * * * * * * * * * * * * *-->
|
||||
<!-- This form is pre-integrated with SB Forms.-->
|
||||
<!-- To make this form functional, sign up at-->
|
||||
<!-- https://startbootstrap.com/solution/contact-forms-->
|
||||
<!-- to get an API token!-->
|
||||
<form id="contactForm" data-sb-form-api-token="API_TOKEN">
|
||||
<div class="form-floating">
|
||||
<input class="form-control" id="name" type="text" placeholder="Enter your name..." data-sb-validations="required" />
|
||||
<label for="name">Name</label>
|
||||
<div class="invalid-feedback" data-sb-feedback="name:required">A name is required.</div>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input class="form-control" id="email" type="email" placeholder="Enter your email..." data-sb-validations="required,email" />
|
||||
<label for="email">Email address</label>
|
||||
<div class="invalid-feedback" data-sb-feedback="email:required">An email is required.</div>
|
||||
<div class="invalid-feedback" data-sb-feedback="email:email">Email is not valid.</div>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input class="form-control" id="phone" type="tel" placeholder="Enter your phone number..." data-sb-validations="required" />
|
||||
<label for="phone">Phone Number</label>
|
||||
<div class="invalid-feedback" data-sb-feedback="phone:required">A phone number is required.</div>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<textarea class="form-control" id="message" placeholder="Enter your message here..." style="height: 12rem" data-sb-validations="required"></textarea>
|
||||
<label for="message">Message</label>
|
||||
<div class="invalid-feedback" data-sb-feedback="message:required">A message is required.</div>
|
||||
</div>
|
||||
<br />
|
||||
<!-- Submit success message-->
|
||||
<!---->
|
||||
<!-- This is what your users will see when the form-->
|
||||
<!-- has successfully submitted-->
|
||||
<div class="d-none" id="submitSuccessMessage">
|
||||
<div class="text-center mb-3">
|
||||
<div class="fw-bolder">Form submission successful!</div>
|
||||
To activate this form, sign up at
|
||||
<br />
|
||||
<a href="https://startbootstrap.com/solution/contact-forms">https://startbootstrap.com/solution/contact-forms</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Submit error message-->
|
||||
<!---->
|
||||
<!-- This is what your users will see when there is-->
|
||||
<!-- an error submitting the form-->
|
||||
<div class="d-none" id="submitErrorMessage"><div class="text-center text-danger mb-3">Error sending message!</div></div>
|
||||
<!-- Submit Button-->
|
||||
<button class="btn btn-primary text-uppercase disabled" id="submitButton" type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
84
blog_project/blog_app/templates/blog_app/index.html
Executable file
@ -0,0 +1,84 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header-->
|
||||
<header class="masthead" style="background-image: url('{% static "assets/img/home-bg.jpg" %}')">
|
||||
<div class="container position-relative px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<div class="site-heading">
|
||||
<h1>Clean Blog</h1>
|
||||
<span class="subheading">A Blog Theme by Start Bootstrap</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content-->
|
||||
<div class="container px-4 px-lg-5">
|
||||
|
||||
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<!-- Post preview-->
|
||||
<div class="post-preview">
|
||||
<a href="post.html">
|
||||
<h2 class="post-title">Man must explore, and this is exploration at its greatest</h2>
|
||||
<h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3>
|
||||
</a>
|
||||
<p class="post-meta">
|
||||
Posted by
|
||||
<a href="#!">Start Bootstrap</a>
|
||||
on September 24, 2023
|
||||
</p>
|
||||
</div>
|
||||
<!-- Divider-->
|
||||
<hr class="my-4" />
|
||||
<!-- Post preview-->
|
||||
<div class="post-preview">
|
||||
<a href="post.html"><h2 class="post-title">I believe every human has a finite number of heartbeats. I don't intend to waste any of mine.</h2></a>
|
||||
<p class="post-meta">
|
||||
Posted by
|
||||
<a href="#!">Start Bootstrap</a>
|
||||
on September 18, 2023
|
||||
</p>
|
||||
</div>
|
||||
<!-- Divider-->
|
||||
<hr class="my-4" />
|
||||
<!-- Post preview-->
|
||||
<div class="post-preview">
|
||||
<a href="post.html">
|
||||
<h2 class="post-title">Science has not yet mastered prophecy</h2>
|
||||
<h3 class="post-subtitle">We predict too much for the next year and yet far too little for the next ten.</h3>
|
||||
</a>
|
||||
<p class="post-meta">
|
||||
Posted by
|
||||
<a href="#!">Start Bootstrap</a>
|
||||
on August 24, 2023
|
||||
</p>
|
||||
</div>
|
||||
<!-- Divider-->
|
||||
<hr class="my-4" />
|
||||
<!-- Post preview-->
|
||||
<div class="post-preview">
|
||||
<a href="post.html">
|
||||
<h2 class="post-title">Failure is not an option</h2>
|
||||
<h3 class="post-subtitle">Many say exploration is part of our destiny, but it’s actually our duty to future generations.</h3>
|
||||
</a>
|
||||
<p class="post-meta">
|
||||
Posted by
|
||||
<a href="#!">Start Bootstrap</a>
|
||||
on July 8, 2023
|
||||
</p>
|
||||
</div>
|
||||
<!-- Divider-->
|
||||
<hr class="my-4" />
|
||||
<!-- Pager-->
|
||||
<div class="d-flex justify-content-end mb-4"><a class="btn btn-primary text-uppercase" href="#!">Older Posts →</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
blog_project/blog_app/templates/blog_app/login.html
Executable file
@ -0,0 +1,59 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm rounded-4 border-0">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="text-center mb-4" style="font-size: 1.2rem;">🔐 로그인</h4>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger small">아이디 또는 비밀번호가 올바르지 않습니다.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.username.label_tag }}
|
||||
{{ form.username|add_class:"form-control form-control-sm" }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.password.label_tag }}
|
||||
{{ form.password|add_class:"form-control form-control-sm" }}
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-sm">로그인</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 🔗 회원가입 / 비밀번호 찾기 -->
|
||||
<div class="text-center mt-3 small">
|
||||
<a href="{% url 'signup' %}" class="me-2">회원가입</a> |
|
||||
<a href="{% url 'password_reset' %}" class="ms-2">비밀번호 재설정</a>
|
||||
</div>
|
||||
|
||||
<!-- ☁️ 소셜 로그인 (디자인용 버튼) -->
|
||||
<hr class="my-4" />
|
||||
<div class="text-center small text-muted">또는 소셜 계정으로 로그인</div>
|
||||
<div class="d-flex justify-content-center gap-2 mt-2">
|
||||
<a href="#" class="btn btn-outline-dark btn-sm">
|
||||
<i class="fab fa-google"></i> Google
|
||||
</a>
|
||||
<a href="#" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fab fa-facebook-f"></i> Facebook
|
||||
</a>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fab fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
35
blog_project/blog_app/templates/blog_app/password_reset.html
Executable file
@ -0,0 +1,35 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm rounded-4 border-0">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="text-center mb-4" style="font-size: 1.2rem;">🔐 비밀번호 재설정</h4>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.email.errors %}
|
||||
<div class="alert alert-danger small">입력한 이메일 주소를 다시 확인해주세요.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.email.label_tag }}
|
||||
{{ form.email|add_class:"form-control form-control-sm" }}
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-warning btn-sm">비밀번호 재설정 이메일 보내기</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3 small">
|
||||
<a href="{% url 'login' %}">← 로그인 페이지로 돌아가기</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
blog_project/blog_app/templates/blog_app/post.html
Executable file
@ -0,0 +1,56 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Header-->
|
||||
<header class="masthead" style="background-image: url('{% static "assets/img/post-bg.jpg" %}')">
|
||||
<div class="container position-relative px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<div class="post-heading">
|
||||
<h1>Man must explore, and this is exploration at its greatest</h1>
|
||||
<h2 class="subheading">Problems look mighty small from 150 miles up</h2>
|
||||
<span class="meta">
|
||||
Posted by
|
||||
<a href="#!">Start Bootstrap</a>
|
||||
on August 24, 2023
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Post Content-->
|
||||
<article class="mb-4">
|
||||
<div class="container px-4 px-lg-5">
|
||||
<div class="row gx-4 gx-lg-5 justify-content-center">
|
||||
<div class="col-md-10 col-lg-8 col-xl-7">
|
||||
<p>Never in all their history have men been able truly to conceive of the world as one: a single sphere, a globe, having the qualities of a globe, a round earth in which all the directions eventually meet, in which there is no center because every point, or none, is center — an equal earth which all men occupy as equals. The airman's earth, if free men make it, will be truly round: a globe in practice, not in theory.</p>
|
||||
<p>Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.</p>
|
||||
<p>What was most significant about the lunar voyage was not that man set foot on the Moon but that they set eye on the earth.</p>
|
||||
<p>A Chinese tale tells of some men sent to harm a young girl who, upon seeing her beauty, become her protectors rather than her violators. That's how I felt seeing the Earth for the first time. I could not help but love and cherish her.</p>
|
||||
<p>For those who have seen the Earth from space, and for the hundreds and perhaps thousands more who will, the experience most certainly changes your perspective. The things that we share in our world are far more valuable than those which divide us.</p>
|
||||
<h2 class="section-heading">The Final Frontier</h2>
|
||||
<p>There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.</p>
|
||||
<p>There can be no thought of finishing for ‘aiming for the stars.’ Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.</p>
|
||||
<blockquote class="blockquote">The dreams of yesterday are the hopes of today and the reality of tomorrow. Science has not yet mastered prophecy. We predict too much for the next year and yet far too little for the next ten.</blockquote>
|
||||
<p>Spaceflights cannot be stopped. This is not the work of any one man or even a group of men. It is a historical process which mankind is carrying out in accordance with the natural laws of human development.</p>
|
||||
<h2 class="section-heading">Reaching for the Stars</h2>
|
||||
<p>As we got further and further away, it [the Earth] diminished in size. Finally it shrank to the size of a marble, the most beautiful you can imagine. That beautiful, warm, living object looked so fragile, so delicate, that if you touched it with a finger it would crumble and fall apart. Seeing this has to change a man.</p>
|
||||
<a href="#!"><img class="img-fluid" src="{% static 'assets/img/post-sample-image.jpg' %}" alt="..." /></a>
|
||||
<span class="caption text-muted">To go places and do things that have never been done before – that’s what living is all about.</span>
|
||||
<p>Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.</p>
|
||||
<p>As I stand out here in the wonders of the unknown at Hadley, I sort of realize there’s a fundamental truth to our nature, Man must explore, and this is exploration at its greatest.</p>
|
||||
<p>
|
||||
Placeholder text by
|
||||
<a href="http://spaceipsum.com/">Space Ipsum</a>
|
||||
· Images by
|
||||
<a href="https://www.flickr.com/photos/nasacommons/">NASA on The Commons</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
40
blog_project/blog_app/templates/blog_app/signup.html
Executable file
@ -0,0 +1,40 @@
|
||||
{% extends 'blog_app/base.html' %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm rounded-4 border-0">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="text-center mb-4" style="font-size: 1.2rem;">📝 회원가입</h4>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger small">입력값을 확인해주세요.</div>
|
||||
{% endif %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
{{ field.label_tag }}
|
||||
{{ field|add_class:"form-control form-control-sm" }}
|
||||
{% if field.errors %}
|
||||
<small class="text-danger">{{ field.errors.0 }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success btn-sm">가입하기</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3 small">
|
||||
이미 계정이 있으신가요? <a href="{% url 'login' %}">로그인</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
3
blog_project/blog_app/tests.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
15
blog_project/blog_app/urls.py
Executable file
@ -0,0 +1,15 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('weather/', views.weather_data, name='weather_data'),
|
||||
path('signup/', views.signup, name='signup'),
|
||||
path('login/', auth_views.LoginView.as_view(template_name='blog_app/login.html'), name='login'),
|
||||
path('logout/', auth_views.LogoutView.as_view(next_page='index'), name='logout'),
|
||||
path('about/', views.about, name='about'),
|
||||
path('post/', views.post, name='post'),
|
||||
path('contact/', views.contact, name='contact'),
|
||||
path('password_reset/', auth_views.PasswordResetView.as_view(template_name='blog_app/password_reset.html'), name='password_reset'),
|
||||
]
|
||||
52
blog_project/blog_app/views.py
Executable file
@ -0,0 +1,52 @@
|
||||
import requests
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth import login
|
||||
from django.http import JsonResponse
|
||||
|
||||
def index(request):
|
||||
return render(request, 'blog_app/index.html')
|
||||
|
||||
def weather_data(request):
|
||||
api_key = "00eb2155e51742aa2856a69da0f6dc61" # OpenWeather에서 발급받은 API 키
|
||||
city = "Seoul"
|
||||
url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric&lang=kr"
|
||||
|
||||
weather = {}
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
data = response.json()
|
||||
|
||||
weather = {
|
||||
'city': city,
|
||||
'description': data['weather'][0]['description'],
|
||||
'icon': data['weather'][0]['icon'],
|
||||
'temperature': data['main']['temp'],
|
||||
}
|
||||
|
||||
return JsonResponse(weather)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
def signup(request):
|
||||
if request.method == 'POST':
|
||||
form = UserCreationForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect('index')
|
||||
else:
|
||||
form = UserCreationForm()
|
||||
return render(request, 'blog_app/signup.html', {'form': form})
|
||||
|
||||
|
||||
def about(request):
|
||||
return render(request, 'blog_app/about.html')
|
||||
|
||||
def post(request):
|
||||
return render(request, 'blog_app/post.html')
|
||||
|
||||
def contact(request):
|
||||
return render(request, 'blog_app/contact.html')
|
||||
0
blog_project/blog_project/__init__.py
Executable file
BIN
blog_project/blog_project/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
blog_project/blog_project/__pycache__/settings.cpython-312.pyc
Executable file
BIN
blog_project/blog_project/__pycache__/urls.cpython-312.pyc
Executable file
BIN
blog_project/blog_project/__pycache__/wsgi.cpython-312.pyc
Executable file
16
blog_project/blog_project/asgi.py
Executable file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for blog_project project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_project.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
143
blog_project/blog_project/settings.py
Executable file
@ -0,0 +1,143 @@
|
||||
"""
|
||||
Django settings for blog_project project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.1.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
#BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-#jy9i$x220ttsfa*8&_&7)w4izi@d*fi+-8=chq_$!4+9(xvw_'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['one.syye.net', 'localhost', '127.0.0.1']
|
||||
FORCE_SCRIPT_NAME = '/blog'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_REDIRECT_URL = '/'
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'blog_app',
|
||||
'crispy_forms',
|
||||
'widget_tweaks',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'blog_project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'blog_project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||
|
||||
# DATABASES = {
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||
# 'NAME': BASE_DIR / 'db.sqlite3',
|
||||
# }
|
||||
# }
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'blog_db',
|
||||
'USER': 'blog_user',
|
||||
'PASSWORD': 'blog_password',
|
||||
'HOST': 'localhost', # 또는 MariaDB 서버의 IP
|
||||
'PORT': '3306',
|
||||
'OPTIONS': {
|
||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'ko-kr'
|
||||
TIME_ZONE = 'Asia/Seoul'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/blog/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
13
blog_project/blog_project/urls.py
Executable file
@ -0,0 +1,13 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
import os
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('blog_app.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=os.path.join(settings.BASE_DIR, 'blog_app', 'static'))
|
||||
16
blog_project/blog_project/wsgi.py
Executable file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for blog_project project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_project.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
BIN
blog_project/db.sqlite3
Executable file
23
blog_project/manage.py
Executable file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_project.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
BIN
myworld_project/characters/logo_pig.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
myworld_project/diary_images/202504/1_20250424162919.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
myworld_project/diary_images/202504/1_20250424171322.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
myworld_project/diary_images/202504/4_20250424174346.jpeg
Normal file
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 71 KiB |
355
myworld_project/jtbc_debug.html
Executable file
22
myworld_project/manage.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks. """
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myworld_project.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
After Width: | Height: | Size: 161 KiB |
0
myworld_project/myworld_app/__init__.py
Executable file
BIN
myworld_project/myworld_app/__pycache__/__init__.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/admin.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/apps.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/forms.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/models.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/services_lotto.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/tests.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/urls.cpython-312.pyc
Normal file
BIN
myworld_project/myworld_app/__pycache__/utils.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/utils_lotto.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/__pycache__/views.cpython-312.pyc
Normal file
67
myworld_project/myworld_app/admin.py
Executable file
@ -0,0 +1,67 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import CustomUser, LottoDraw, LottoRecommendation, NewsArticle, UserProfile, Diary
|
||||
|
||||
# ✅ 사용자 정의 관리자
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
model = CustomUser
|
||||
|
||||
# 사용자 수정 페이지 필드
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
('추가 정보', {
|
||||
'fields': ('nickname', 'birthdate', 'email_verified', 'character_image', 'skin_image'),
|
||||
}),
|
||||
)
|
||||
|
||||
# 사용자 추가 페이지 필드
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('username', 'nickname', 'email', 'birthdate', 'password1', 'password2'),
|
||||
}),
|
||||
)
|
||||
|
||||
# 사용자 리스트 컬럼
|
||||
list_display = ['username', 'nickname', 'email', 'is_staff', 'is_superuser']
|
||||
|
||||
admin.site.register(CustomUser, CustomUserAdmin)
|
||||
|
||||
# ✅ 로또 당첨 정보 관리자
|
||||
@admin.register(LottoDraw)
|
||||
class LottoDrawAdmin(admin.ModelAdmin):
|
||||
list_display = ("draw_no", "number_1", "number_2", "number_3", "number_4", "number_5", "number_6", "bonus", "first_winners", "first_prize")
|
||||
search_fields = ("draw_no",)
|
||||
ordering = ("-draw_no",)
|
||||
|
||||
# ✅ 추천 번호 관리자
|
||||
@admin.register(LottoRecommendation)
|
||||
class LottoRecommendationAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "method", "numbers", "created_at")
|
||||
list_filter = ("method", "created_at")
|
||||
ordering = ("-created_at",)
|
||||
|
||||
# ✅ 뉴스 기사 관리자
|
||||
@admin.register(NewsArticle)
|
||||
class NewsArticleAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "link", "pub_date")
|
||||
search_fields = ("title", "link")
|
||||
ordering = ("-pub_date",)
|
||||
|
||||
@admin.register(UserProfile)
|
||||
class UserProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'mbti', 'nickname', 'mood_baseline', 'preferred_time')
|
||||
search_fields = ('user__username', 'mbti', 'nickname')
|
||||
list_filter = ('mbti', 'preferred_time')
|
||||
ordering = ('user__username',)
|
||||
|
||||
@admin.register(Diary)
|
||||
class DiaryAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'date', 'mood', 'weather', 'created_at')
|
||||
search_fields = ('user__username', 'keywords', 'diary_text')
|
||||
list_filter = ('mood', 'weather', 'date')
|
||||
ordering = ('-date',)
|
||||
|
||||
# ✅ 관리자 사이트 커스터마이징
|
||||
admin.site.site_header = "MyWorld 관리자"
|
||||
admin.site.site_title = "MyWorld Admin"
|
||||
admin.site.index_title = "관리자 패널"
|
||||
6
myworld_project/myworld_app/apps.py
Executable file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MyworldAppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'myworld_app'
|
||||
151
myworld_project/myworld_app/forms.py
Executable file
@ -0,0 +1,151 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from .models import CustomUser, UserProfile
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
email = forms.EmailField(required=True)
|
||||
birthdate = forms.DateField(required=True, widget=forms.DateInput(attrs={'type': 'date'}))
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ('username', 'nickname', 'email', 'birthdate', 'gender')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'password2' in self.fields:
|
||||
del self.fields['password2'] # ✅ password2 필드 제거
|
||||
|
||||
class UpdateInfoForm(forms.ModelForm):
|
||||
password = forms.CharField(required=False, widget=forms.PasswordInput())
|
||||
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['nickname', 'email', 'birthdate', 'gender', 'phone', 'address']
|
||||
|
||||
MBTI_CHOICES = [
|
||||
('', '선택하세요'),('INTJ', 'INTJ'), ('INTP', 'INTP'), ('ENTJ', 'ENTJ'), ('ENTP', 'ENTP'),
|
||||
('INFJ', 'INFJ'), ('INFP', 'INFP'), ('ENFJ', 'ENFJ'), ('ENFP', 'ENFP'),
|
||||
('ISTJ', 'ISTJ'), ('ISFJ', 'ISFJ'), ('ESTJ', 'ESTJ'), ('ESFJ', 'ESFJ'),
|
||||
('ISTP', 'ISTP'), ('ISFP', 'ISFP'), ('ESTP', 'ESTP'), ('ESFP', 'ESFP'),
|
||||
]
|
||||
|
||||
MOOD_BASELINE_CHOICES = [
|
||||
('', '선택하세요'),('차분함', '차분함'), ('활발함', '활발함'), ('감성적', '감성적'), ('사차원', '사차원'), ('화가 나있는', '화가 나있는'),
|
||||
('내향적', '내향적'), ('외향적', '외향적'), ('혼자 있는 걸 좋아함', '혼자 있는 걸 좋아함'),
|
||||
]
|
||||
|
||||
FAVORITE_KEYWORDS_CHOICES = [
|
||||
('', '선택하세요'),('자연', '자연'), ('계절', '계절'), ('추억', '추억'), ('재물', '재물'),('건강', '건강'),
|
||||
('사람', '사람'), ('시간', '시간'), ('풍경', '풍경'), ('사랑', '사랑'),
|
||||
('고요함', '고요함'), ('우주', '우주'), ('음악', '음악'),
|
||||
]
|
||||
|
||||
WRITING_STYLE_CHOICES = [
|
||||
('', '선택하세요'),('시적인', '시적인'), ('간결한', '간결한'), ('묘사적인', '묘사적인'),('MZ세대', 'MZ세대'),('X세대(올드)', 'X세대(올드)'),
|
||||
('철학적인', '철학적인'), ('일기장 스타일', '일기장 스타일'),
|
||||
]
|
||||
|
||||
TONE_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('조용하고 따뜻한 어조', '조용하고 따뜻한 어조'),
|
||||
('밝고 명랑한 어조', '밝고 명랑한 어조'),
|
||||
('서정적이고 차분한 어조', '서정적이고 차분한 어조'),
|
||||
('현실적이고 직설적인 어조', '현실적이고 직설적인 어조'),
|
||||
]
|
||||
|
||||
DREAM_TYPE_CHOICES = [
|
||||
('', '선택하세요'),('몽환적', '몽환적'), ('현실적', '현실적'), ('철학적', '철학적'),
|
||||
('서사적', '서사적'), ('감정 위주', '감정 위주'), ('상징적', '상징적'),
|
||||
]
|
||||
|
||||
PREFERRED_TIME_CHOICES = [
|
||||
('', '선택하세요'),('새벽', '새벽'), ('아침', '아침'), ('낮', '낮'),
|
||||
('저녁', '저녁'), ('밤', '밤'), ('시간에 구애받지 않음', '시간에 구애받지 않음'),
|
||||
]
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = [
|
||||
'nickname', 'mbti', 'mood_baseline', 'personality',
|
||||
'favorite_keywords', 'writing_style', 'tone',
|
||||
'dream_type', 'preferred_time'
|
||||
]
|
||||
widgets = {
|
||||
'nickname': forms.TextInput(),
|
||||
'mbti': forms.Select(choices=MBTI_CHOICES),
|
||||
'mood_baseline': forms.Select(choices=MOOD_BASELINE_CHOICES),
|
||||
'personality': forms.Textarea(attrs={'rows': 3, 'placeholder': '자신을 표현해주세요'}),
|
||||
'favorite_keywords': forms.Select(choices=FAVORITE_KEYWORDS_CHOICES),
|
||||
'writing_style': forms.Select(choices=WRITING_STYLE_CHOICES),
|
||||
'tone': forms.Select(choices=TONE_CHOICES),
|
||||
'dream_type': forms.Select(choices=DREAM_TYPE_CHOICES),
|
||||
'preferred_time': forms.Select(choices=PREFERRED_TIME_CHOICES),
|
||||
}
|
||||
|
||||
|
||||
MOOD_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('행복', '행복'), ('우울', '우울'), ('무기력', '무기력'),
|
||||
('불안', '불안'), ('평온', '평온'), ('설렘', '설렘')
|
||||
]
|
||||
|
||||
STATE_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('건강함', '건강함'), ('피곤함', '피곤함'), ('집중 안됨', '집중 안됨'),
|
||||
('졸림', '졸림'), ('불편함', '불편함'), ('무난함', '무난함')
|
||||
]
|
||||
|
||||
WEATHER_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('맑음', '맑음'), ('흐림', '흐림'), ('비', '비'), ('눈', '눈'), ('바람', '바람')
|
||||
]
|
||||
|
||||
TIME_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('아침', '아침'), ('오후', '오후'), ('저녁', '저녁'), ('새벽', '새벽')
|
||||
]
|
||||
|
||||
WHO_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('혼자', '혼자'), ('친구', '친구'), ('가족', '가족'), ('연인', '연인'),
|
||||
('직장동료', '직장동료'), ('모름', '모름')
|
||||
]
|
||||
|
||||
EVENT_CHOICES = [
|
||||
('', '선택하세요'),
|
||||
('없음', '없음'), ('기쁜 일', '기쁜 일'), ('속상한 일', '속상한 일'),
|
||||
('충격적인 일', '충격적인 일'), ('감동적인 일', '감동적인 일')
|
||||
]
|
||||
|
||||
|
||||
class AIDiaryForm(forms.Form):
|
||||
mood = forms.ChoiceField(label='오늘 기분',required=False, choices=MOOD_CHOICES, widget=forms.Select(attrs={'class': 'form-select'}))
|
||||
state = forms.ChoiceField(label='내 상태',required=False, choices=STATE_CHOICES, widget=forms.Select(attrs={'class': 'form-select'}))
|
||||
location = forms.CharField(label='현재 위치',required=False, widget=forms.TextInput(attrs={'class': 'form-control','placeholder': '입력 해 주세요'}))
|
||||
weather = forms.ChoiceField(label='날씨',required=False, choices=WEATHER_CHOICES,widget=forms.Select(attrs={'class': 'form-select'}))
|
||||
time_of_day = forms.ChoiceField(label='시간대',required=False, choices=TIME_CHOICES,widget=forms.Select(attrs={'class': 'form-select'}))
|
||||
with_whom = forms.ChoiceField(label='누구와 있었나요?',required=False, choices=WHO_CHOICES,widget=forms.Select(attrs={'class': 'form-select'}))
|
||||
event = forms.ChoiceField(label='특별한 사건이 있었나요?',required=False, choices=EVENT_CHOICES,widget=forms.Select(attrs={'class': 'form-select'}))
|
||||
one_word = forms.CharField(label='오늘을 한 단어로 표현한다면?',required=False, widget=forms.TextInput(attrs={'class': 'form-control','placeholder': '입력 해 주세요'}))
|
||||
most_important = forms.CharField(label='오늘 가장 중요한 순간은?',required=False, widget=forms.TextInput(attrs={'class': 'form-control','placeholder': '입력 해 주세요'}))
|
||||
image = forms.ImageField(label="이미지 업로드 (선택)", required=False)
|
||||
|
||||
keywords = forms.CharField(
|
||||
label='오늘의 키워드',
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 3,
|
||||
'placeholder': '예: 친구와 커피, 벚꽃, 조용한 시간 등 자유롭게 적어주세요.'
|
||||
})
|
||||
)
|
||||
summary = forms.CharField(
|
||||
label='오늘을 한 문장으로 표현해주세요 (선택)',
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 2,
|
||||
'placeholder': '예: 벚꽃잎처럼 내 마음도 흩날렸던 하루.'
|
||||
})
|
||||
)
|
||||
BIN
myworld_project/myworld_app/google-chrome-stable_current_amd64.deb
Executable file
BIN
myworld_project/myworld_app/google-chrome-stable_current_amd64.deb.1
Executable file
36
myworld_project/myworld_app/lotto_import.py
Executable file
@ -0,0 +1,36 @@
|
||||
# lotto_import.py (myworld_app 디렉토리에 두고 실행)
|
||||
|
||||
import csv
|
||||
import os
|
||||
import django
|
||||
import sys
|
||||
|
||||
# 현재 경로를 파이썬 모듈 경로에 추가
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myworld_project.settings")
|
||||
django.setup()
|
||||
|
||||
from myworld_app.models import LottoDraw
|
||||
|
||||
with open("./myworld_app/static/csv/lotto.csv", newline='', encoding='cp949') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
LottoDraw.objects.update_or_create(
|
||||
draw_no=int(row['회차']),
|
||||
defaults={
|
||||
'number_1': int(row['번호1']),
|
||||
'number_2': int(row['번호2']),
|
||||
'number_3': int(row['번호3']),
|
||||
'number_4': int(row['번호4']),
|
||||
'number_5': int(row['번호5']),
|
||||
'number_6': int(row['번호6']),
|
||||
'bonus': int(row['보너스']),
|
||||
'first_prize': int(row['1등 당첨금'].replace(',', '')),
|
||||
'first_winners': int(row['1등 당첨수']),
|
||||
'second_prize': int(row['2등 당첨금'].replace(',', '')),
|
||||
'second_winners': int(row['2등 당첨수']),
|
||||
}
|
||||
)
|
||||
|
||||
print("✅ 로또 데이터가 DB에 저장되었습니다. ")
|
||||
52
myworld_project/myworld_app/migrations/0001_initial.py
Executable file
@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-14 02:04
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('nickname', models.CharField(max_length=30, unique=True)),
|
||||
('birthdate', models.DateField(blank=True, null=True)),
|
||||
('email_verified', models.BooleanField(default=False)),
|
||||
('character_image', models.ImageField(blank=True, null=True, upload_to='characters/')),
|
||||
('skin_image', models.ImageField(blank=True, null=True, upload_to='skins/')),
|
||||
('gender', models.CharField(blank=True, choices=[('남', '남'), ('여', '여')], max_length=2)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('address', models.CharField(blank=True, max_length=255)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
25
myworld_project/myworld_app/migrations/0002_add_user_fields.py
Executable file
@ -0,0 +1,25 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='gender',
|
||||
field=models.CharField(max_length=2, choices=[('남', '남'), ('여', '여')], blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='phone',
|
||||
field=models.CharField(max_length=20, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='address',
|
||||
field=models.CharField(max_length=255, blank=True),
|
||||
),
|
||||
]
|
||||
31
myworld_project/myworld_app/migrations/0003_lottodraw.py
Executable file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-16 05:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0002_add_user_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LottoDraw',
|
||||
fields=[
|
||||
('draw_no', models.IntegerField(primary_key=True, serialize=False)),
|
||||
('number_1', models.PositiveSmallIntegerField()),
|
||||
('number_2', models.PositiveSmallIntegerField()),
|
||||
('number_3', models.PositiveSmallIntegerField()),
|
||||
('number_4', models.PositiveSmallIntegerField()),
|
||||
('number_5', models.PositiveSmallIntegerField()),
|
||||
('number_6', models.PositiveSmallIntegerField()),
|
||||
('bonus', models.PositiveSmallIntegerField()),
|
||||
('first_prize', models.BigIntegerField()),
|
||||
('first_winners', models.IntegerField()),
|
||||
('second_prize', models.BigIntegerField()),
|
||||
('second_winners', models.IntegerField()),
|
||||
('draw_date', models.DateField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
22
myworld_project/myworld_app/migrations/0004_lottorecommendation.py
Executable file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-16 07:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0003_lottodraw'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LottoRecommendation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('method', models.CharField(choices=[('freq', '빈도 기반'), ('gap', '간격 기반'), ('pattern', '패턴 기반'), ('cluster', '클러스터링 기반'), ('extra', '보조 추천')], max_length=10)),
|
||||
('numbers', models.JSONField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
23
myworld_project/myworld_app/migrations/0005_newsarticle.py
Executable file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-18 08:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0004_lottorecommendation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NewsArticle',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=300)),
|
||||
('link', models.URLField(unique=True)),
|
||||
('content_html', models.TextField()),
|
||||
('pub_date', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,58 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-22 05:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0005_newsarticle'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='author',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='content_image',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='content_txt',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='desc',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='isvod',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='main_image',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='section',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='newsarticle',
|
||||
name='link',
|
||||
field=models.CharField(max_length=200, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='newsarticle',
|
||||
name='pub_date',
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
]
|
||||
18
myworld_project/myworld_app/migrations/0007_newsarticle_aid.py
Executable file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-22 06:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0006_newsarticle_author_newsarticle_content_image_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='newsarticle',
|
||||
name='aid',
|
||||
field=models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
44
myworld_project/myworld_app/migrations/0008_diary_userprofile.py
Executable file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-23 02:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0007_newsarticle_aid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Diary',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('keywords', models.CharField(max_length=255)),
|
||||
('mood', models.CharField(max_length=100)),
|
||||
('weather', models.CharField(max_length=100)),
|
||||
('diary_text', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nickname', models.CharField(default='나', max_length=30)),
|
||||
('mbti', models.CharField(max_length=4)),
|
||||
('mood_baseline', models.CharField(max_length=100)),
|
||||
('personality', models.TextField()),
|
||||
('favorite_keywords', models.CharField(max_length=255)),
|
||||
('writing_style', models.TextField()),
|
||||
('tone', models.CharField(default='조용하고 따뜻한 어조', max_length=100)),
|
||||
('dream_type', models.CharField(blank=True, max_length=100)),
|
||||
('preferred_time', models.CharField(blank=True, max_length=50)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
36
myworld_project/myworld_app/migrations/0009_diaryinput.py
Executable file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-24 04:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0008_diary_userprofile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DiaryInput',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(auto_now_add=True)),
|
||||
('mood', models.CharField(max_length=50)),
|
||||
('state', models.CharField(max_length=50)),
|
||||
('location', models.CharField(max_length=100)),
|
||||
('weather', models.CharField(max_length=50)),
|
||||
('time_of_day', models.CharField(max_length=50)),
|
||||
('with_whom', models.CharField(max_length=50)),
|
||||
('event', models.CharField(max_length=50)),
|
||||
('one_word', models.CharField(max_length=50)),
|
||||
('most_important', models.CharField(max_length=255)),
|
||||
('keywords', models.TextField()),
|
||||
('summary', models.TextField(blank=True, null=True)),
|
||||
('prompt', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
myworld_project/myworld_app/migrations/0010_diaryinput_image.py
Executable file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-24 06:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0009_diaryinput'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='diaryinput',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='diary_images/'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-24 08:03
|
||||
|
||||
import django.db.models.deletion
|
||||
import myworld_app.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('myworld_app', '0010_diaryinput_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='diary',
|
||||
name='input',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='myworld_app.diaryinput'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='diaryinput',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to=myworld_app.models.diary_image_upload_path),
|
||||
),
|
||||
]
|
||||
0
myworld_project/myworld_app/migrations/__init__.py
Executable file
BIN
myworld_project/myworld_app/migrations/__pycache__/0001_initial.cpython-312.pyc
Executable file
BIN
myworld_project/myworld_app/migrations/__pycache__/__init__.cpython-312.pyc
Executable file
129
myworld_project/myworld_app/models.py
Executable file
@ -0,0 +1,129 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
nickname = models.CharField(max_length=30, unique=True)
|
||||
birthdate = models.DateField(null=True, blank=True)
|
||||
email_verified = models.BooleanField(default=False)
|
||||
character_image = models.ImageField(upload_to='characters/', null=True, blank=True)
|
||||
skin_image = models.ImageField(upload_to='skins/', null=True, blank=True)
|
||||
gender = models.CharField(max_length=2, choices=[('남', '남'), ('여', '여')], blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
address = models.CharField(max_length=255, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
class LottoDraw(models.Model):
|
||||
draw_no = models.IntegerField(primary_key=True)
|
||||
number_1 = models.PositiveSmallIntegerField()
|
||||
number_2 = models.PositiveSmallIntegerField()
|
||||
number_3 = models.PositiveSmallIntegerField()
|
||||
number_4 = models.PositiveSmallIntegerField()
|
||||
number_5 = models.PositiveSmallIntegerField()
|
||||
number_6 = models.PositiveSmallIntegerField()
|
||||
bonus = models.PositiveSmallIntegerField()
|
||||
first_prize = models.BigIntegerField()
|
||||
first_winners = models.IntegerField()
|
||||
second_prize = models.BigIntegerField()
|
||||
second_winners = models.IntegerField()
|
||||
draw_date = models.DateField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.draw_no}회"
|
||||
|
||||
class LottoRecommendation(models.Model):
|
||||
METHOD_CHOICES = [
|
||||
('freq', '빈도 기반'),
|
||||
('gap', '간격 기반'),
|
||||
('pattern', '패턴 기반'),
|
||||
('cluster', '클러스터링 기반'),
|
||||
('extra', '보조 추천'),
|
||||
]
|
||||
|
||||
method = models.CharField(max_length=10, choices=METHOD_CHOICES)
|
||||
numbers = models.JSONField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_method_display()} 추천 ({self.created_at.date()})"
|
||||
|
||||
class NewsArticle(models.Model):
|
||||
aid = models.CharField(max_length=20, blank=True, null=True)
|
||||
title = models.CharField(max_length=300)
|
||||
link = models.CharField(max_length=200, unique=True)
|
||||
content_html = models.TextField()
|
||||
content_txt = models.TextField(blank=True, null=True) # ✅ 본문 텍스트
|
||||
content_image = models.TextField(blank=True, null=True) # ✅ 본문 내 이미지 src 모음
|
||||
desc = models.TextField(blank=True, null=True) # ✅ 요약 설명
|
||||
author = models.CharField(max_length=100, blank=True, null=True) # ✅ 기자
|
||||
section = models.CharField(max_length=50, blank=True, null=True) # ✅ 정치/사회 등 섹션
|
||||
main_image = models.URLField(blank=True, null=True) # ✅ 대표 썸네일
|
||||
isvod = models.BooleanField(default=False) # ✅ 영상 포함 여부
|
||||
pub_date = models.DateTimeField()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
nickname = models.CharField(max_length=30, default='나') # 1인칭 호칭
|
||||
mbti = models.CharField(max_length=4) # 예: INFJ, ENFP 등
|
||||
mood_baseline = models.CharField(max_length=100) # 평소 기분 상태 (예: 차분함)
|
||||
personality = models.TextField() # 작성자 자가 기술
|
||||
favorite_keywords = models.CharField(max_length=255) # 선호 키워드 (예: 고요함, 계절, 추억)
|
||||
writing_style = models.TextField() # 시적인, 간결한, 철학적인 등
|
||||
tone = models.CharField(max_length=100, default='조용하고 따뜻한 어조') # 말투
|
||||
dream_type = models.CharField(max_length=100, blank=True) # 꿈의 성향 (몽환적, 현실적 등)
|
||||
preferred_time = models.CharField(max_length=50, blank=True) # 선호 시간대 (새벽, 밤, 낮)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}의 프로필"
|
||||
|
||||
class Diary(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
keywords = models.CharField(max_length=255)
|
||||
mood = models.CharField(max_length=100)
|
||||
weather = models.CharField(max_length=100)
|
||||
diary_text = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# 🔗 연결 관계 (작성된 프롬프트 기반 입력 정보)
|
||||
input = models.OneToOneField("DiaryInput", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.date}] {self.keywords}"
|
||||
|
||||
def diary_image_upload_path(instance, filename):
|
||||
now = datetime.now()
|
||||
folder = now.strftime("%Y%m")
|
||||
ext = os.path.splitext(filename)[1]
|
||||
user_id = instance.user.id if instance.user else "anonymous"
|
||||
fname = f"{user_id}_{now.strftime('%Y%m%d%H%M%S')}{ext}"
|
||||
return f"diary_images/{folder}/{fname}" # MEDIA_ROOT 기준 경로
|
||||
|
||||
class DiaryInput(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
date = models.DateField(auto_now_add=True)
|
||||
mood = models.CharField(max_length=50)
|
||||
state = models.CharField(max_length=50)
|
||||
location = models.CharField(max_length=100)
|
||||
weather = models.CharField(max_length=50)
|
||||
time_of_day = models.CharField(max_length=50)
|
||||
with_whom = models.CharField(max_length=50)
|
||||
event = models.CharField(max_length=50)
|
||||
one_word = models.CharField(max_length=50)
|
||||
most_important = models.CharField(max_length=255)
|
||||
keywords = models.TextField()
|
||||
summary = models.TextField(blank=True, null=True)
|
||||
prompt = models.TextField() # ✅ 프롬프트 저장 필드
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
image = models.ImageField(upload_to=diary_image_upload_path, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} - {self.date}"
|
||||
68
myworld_project/myworld_app/scripts/fetch_lotto.py
Executable file
@ -0,0 +1,68 @@
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# Django 설정 초기화
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.append(BASE_DIR)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myworld_project.settings")
|
||||
django.setup()
|
||||
|
||||
from myworld_app.models import LottoDraw
|
||||
|
||||
def fetch_latest_lotto():
|
||||
url = 'https://dhlottery.co.kr/gameResult.do?method=byWin'
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 회차
|
||||
draw_no = int(soup.select_one('.win_result h4 strong').text.replace('회', '').strip())
|
||||
|
||||
# 당첨 번호
|
||||
numbers = [int(el.text.strip()) for el in soup.select('.num.win span')[:6]]
|
||||
|
||||
# 보너스 번호
|
||||
bonus = int(soup.select_one('.bonus span').text.strip())
|
||||
|
||||
# ✅ 정확한 방식으로 1등/2등 정보 파싱
|
||||
prize_rows = soup.select('.tbl_data tbody tr')
|
||||
if len(prize_rows) >= 2:
|
||||
# 1등
|
||||
first_winners = int(prize_rows[0].select('td')[2].text.replace(',', '').replace('명', '').strip())
|
||||
first_prize = int(prize_rows[0].select('td')[3].text.replace(',', '').replace('원', '').strip())
|
||||
|
||||
# 2등
|
||||
second_winners = int(prize_rows[1].select('td')[2].text.replace(',', '').replace('명', '').strip())
|
||||
second_prize = int(prize_rows[1].select('td')[3].text.replace(',', '').replace('원', '').strip())
|
||||
else:
|
||||
first_winners = 0
|
||||
first_prize = 0
|
||||
second_winners = 0
|
||||
second_prize = 0
|
||||
|
||||
# 중복 체크 후 저장
|
||||
if LottoDraw.objects.filter(draw_no=draw_no).exists():
|
||||
print(f"✅ 이미 저장된 {draw_no}회차입니다.")
|
||||
print(f"🔢 번호: {numbers} + 보너스: {bonus}")
|
||||
print(f"🎉 {draw_no}회차 | 1등 {first_winners}명 (₩{first_prize:,}) | 2등 {second_winners}명 (₩{second_prize:,})")
|
||||
return
|
||||
|
||||
LottoDraw.objects.create(
|
||||
draw_no=draw_no,
|
||||
number_1=numbers[0], number_2=numbers[1], number_3=numbers[2],
|
||||
number_4=numbers[3], number_5=numbers[4], number_6=numbers[5],
|
||||
bonus=bonus,
|
||||
first_prize=first_prize,
|
||||
first_winners=first_winners,
|
||||
second_prize=second_prize,
|
||||
second_winners=second_winners,
|
||||
)
|
||||
|
||||
print(f"✅ 저장 완료: {draw_no}회차")
|
||||
print(f"🔢 번호: {numbers} + 보너스: {bonus}")
|
||||
print(f"🎉 1등 {first_winners}명 (₩{first_prize:,}) | 2등 {second_winners}명 (₩{second_prize:,})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
fetch_latest_lotto()
|
||||
118
myworld_project/myworld_app/scripts/fetch_mbc_news.py
Executable file
@ -0,0 +1,118 @@
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Django setup
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.append(BASE_DIR)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myworld_project.settings")
|
||||
django.setup()
|
||||
|
||||
from myworld_app.models import NewsArticle
|
||||
|
||||
def crawl_mbc_article(url):
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
|
||||
author_tag = soup.find("meta", attrs={"name": "author"})
|
||||
author = author_tag["content"].strip() if author_tag else ""
|
||||
|
||||
date_tag = soup.find("meta", attrs={"name": "nextweb:createDate"})
|
||||
pub_date = None
|
||||
if date_tag:
|
||||
try:
|
||||
pub_date = datetime.strptime(date_tag["content"], "%Y-%m-%d %H:%M")
|
||||
except:
|
||||
pub_date = timezone.now()
|
||||
|
||||
content_div = soup.find("div", class_="news_txt", itemprop="articleBody")
|
||||
content_html = content_div.decode_contents() if content_div else ""
|
||||
content_text = content_div.get_text(separator="\n", strip=True) if content_div else ""
|
||||
|
||||
image_srcs = []
|
||||
for img_tag in soup.select("div.news_img img"):
|
||||
src = img_tag.get("src")
|
||||
if src:
|
||||
if src.startswith("//"):
|
||||
src = "https:" + src
|
||||
image_srcs.append(src)
|
||||
|
||||
return {
|
||||
"author": author,
|
||||
"pub_date": pub_date,
|
||||
"content_html": content_html,
|
||||
"content_text": content_text,
|
||||
"content_image": "\n".join(image_srcs)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"❌ 크롤링 실패: {url} → {e}")
|
||||
return None
|
||||
|
||||
def fetch_and_update_mbc_articles():
|
||||
url = f"https://imnews.imbc.com/operate/common/main/topnews/headline_news.js?{datetime.now().strftime('%Y%m%d%H%M')}"
|
||||
response = requests.get(url)
|
||||
response.encoding = 'utf-8-sig'
|
||||
data = json.loads(response.text)
|
||||
|
||||
new_articles = []
|
||||
for item in data.get("Data", []):
|
||||
aid = item.get("AId", "").strip()
|
||||
if not aid:
|
||||
continue
|
||||
|
||||
# 중복 방지
|
||||
if NewsArticle.objects.filter(aid=aid).exists():
|
||||
continue
|
||||
|
||||
link = item.get("Link", "").strip()
|
||||
article = NewsArticle(
|
||||
title=item.get("Title", "").strip(),
|
||||
aid=aid,
|
||||
link=link,
|
||||
desc=item.get("Desc", "").strip(),
|
||||
author=item.get("Author", "").strip(),
|
||||
section=item.get("Section", "").strip(),
|
||||
main_image="https:" + item["Image"].strip() if item.get("Image", "").startswith("//") else item.get("Image"),
|
||||
isvod=(item.get("IsVod", "N") == "Y"),
|
||||
pub_date=timezone.now(), # 임시, 크롤링에서 수정됨
|
||||
content_html="",
|
||||
content_txt="",
|
||||
content_image=""
|
||||
)
|
||||
article.save()
|
||||
new_articles.append(article)
|
||||
print(f"✅ 새 기사 저장: {article.title}")
|
||||
|
||||
# 크롤링 및 업데이트
|
||||
for article in new_articles:
|
||||
time.sleep(2) # ⏱️ 페이지 로딩 딜레이
|
||||
detail = crawl_mbc_article(article.link)
|
||||
if detail:
|
||||
article.author = detail["author"] or article.author
|
||||
article.pub_date = detail["pub_date"] or article.pub_date
|
||||
article.content_html = detail["content_html"]
|
||||
article.content_txt = detail["content_text"]
|
||||
article.content_image = detail["content_image"]
|
||||
article.save()
|
||||
print(f"📝 크롤링 완료: {article.title}")
|
||||
|
||||
# 최대 30개 유지 (오래된 순으로 삭제)
|
||||
all_articles = NewsArticle.objects.all().order_by('-pub_date')
|
||||
if all_articles.count() > 30:
|
||||
to_delete = all_articles[30:]
|
||||
print(f"🧹 {len(to_delete)}개 기사 삭제")
|
||||
for a in to_delete:
|
||||
a.delete()
|
||||
|
||||
if __name__ == "__main__":
|
||||
fetch_and_update_mbc_articles()
|
||||
112
myworld_project/myworld_app/scripts/fetch_news.py
Executable file
@ -0,0 +1,112 @@
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
from datetime import datetime, timedelta
|
||||
import email.utils
|
||||
|
||||
# 🔧 Django 세팅
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.append(BASE_DIR)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myworld_project.settings")
|
||||
django.setup()
|
||||
|
||||
from myworld_app.models import NewsArticle
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
# 🔁 설정
|
||||
RECENT_HOURS = 2
|
||||
MAX_ARTICLE_COUNT = 30
|
||||
|
||||
def fetch_rss_links():
|
||||
rss_urls = [
|
||||
"https://news-ex.jtbc.co.kr/v1/get/rss/newsflesh",
|
||||
"https://news-ex.jtbc.co.kr/v1/get/rss/issue",
|
||||
]
|
||||
now = datetime.utcnow()
|
||||
links = []
|
||||
|
||||
for url in rss_urls:
|
||||
try:
|
||||
res = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
||||
soup = BeautifulSoup(res.content, "xml")
|
||||
for item in soup.find_all("item"):
|
||||
title = item.title.text.strip()
|
||||
link = item.link.text.strip()
|
||||
pub_raw = item.pubDate.text.strip()
|
||||
pub_date = email.utils.parsedate_to_datetime(pub_raw)
|
||||
|
||||
if (now - pub_date).total_seconds() > RECENT_HOURS * 3600:
|
||||
continue # 오래된 뉴스 스킵
|
||||
|
||||
links.append((title, link))
|
||||
except Exception as e:
|
||||
print(f"❌ RSS 파싱 실패: {url} → {e}")
|
||||
return links
|
||||
|
||||
def crawl_article(title, link):
|
||||
options = Options()
|
||||
options.add_argument("--headless")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
|
||||
driver = webdriver.Chrome(
|
||||
service=Service(ChromeDriverManager().install()),
|
||||
options=options
|
||||
)
|
||||
|
||||
try:
|
||||
driver.set_page_load_timeout(15)
|
||||
try:
|
||||
driver.get(link)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 페이지 로딩 실패: {link} → {e}")
|
||||
return
|
||||
|
||||
soup = BeautifulSoup(driver.page_source, "html.parser")
|
||||
content_div = soup.find("div", id="ijam_content")
|
||||
if not content_div:
|
||||
print(f"❌ 본문 미존재: {link}")
|
||||
return
|
||||
|
||||
# 불필요한 광고 div 제거
|
||||
for tag in content_div.select("#reo_0GqQ"):
|
||||
tag.decompose()
|
||||
|
||||
content_html = content_div.decode_contents()
|
||||
|
||||
if not NewsArticle.objects.filter(link=link).exists():
|
||||
NewsArticle.objects.create(title=title, link=link, content_html=content_html)
|
||||
print(f"✅ 저장됨: {title}")
|
||||
else:
|
||||
print(f"🔁 중복 기사 스킵: {title}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 크롤링 에러: {title} → {e}")
|
||||
finally:
|
||||
driver.quit()
|
||||
|
||||
def trim_old_articles(max_count=MAX_ARTICLE_COUNT):
|
||||
total = NewsArticle.objects.count()
|
||||
if total > max_count:
|
||||
excess = total - max_count
|
||||
old_articles = NewsArticle.objects.order_by("pub_date")[:excess]
|
||||
deleted_titles = [a.title for a in old_articles]
|
||||
count, _ = old_articles.delete()
|
||||
print(f"🧹 {count}개 오래된 뉴스 삭제됨: {deleted_titles}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📡 JTBC 뉴스 수집 시작...")
|
||||
articles = fetch_rss_links()
|
||||
print(f"📋 가져온 링크 수: {len(articles)}")
|
||||
for title, link in articles:
|
||||
crawl_article(title, link)
|
||||
trim_old_articles()
|
||||
print("✅ 뉴스 수집 완료.")
|
||||
67
myworld_project/myworld_app/scripts/ml_lotto.py
Executable file
@ -0,0 +1,67 @@
|
||||
# myworld_app/scripts/ml_lotto.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.append(BASE_DIR)
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myworld_project.settings")
|
||||
django.setup()
|
||||
|
||||
|
||||
import random
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.model_selection import train_test_split
|
||||
from myworld_app.models import LottoDraw
|
||||
|
||||
# 🔹 조합에서 특성 추출 함수
|
||||
def extract_features(numbers):
|
||||
odd = sum(1 for n in numbers if n % 2 == 1)
|
||||
total = sum(numbers)
|
||||
ranges = [0, 0, 0] # 1~15 / 16~30 / 31~45
|
||||
for n in numbers:
|
||||
if n <= 15:
|
||||
ranges[0] += 1
|
||||
elif n <= 30:
|
||||
ranges[1] += 1
|
||||
else:
|
||||
ranges[2] += 1
|
||||
ends = [n % 10 for n in numbers] # 끝수
|
||||
return [odd, total] + ranges + ends
|
||||
|
||||
# 🔹 데이터셋 구성
|
||||
draws = LottoDraw.objects.all().order_by('draw_no')
|
||||
X, y = [], []
|
||||
|
||||
for draw in draws:
|
||||
numbers = [draw.number_1, draw.number_2, draw.number_3,
|
||||
draw.number_4, draw.number_5, draw.number_6]
|
||||
X.append(extract_features(numbers))
|
||||
y.append(1) # 1등 당첨 조합
|
||||
|
||||
# 🔹 비당첨 가짜 조합 추가 (랜덤)
|
||||
for _ in range(len(X)):
|
||||
nums = sorted(random.sample(range(1, 46), 6))
|
||||
X.append(extract_features(nums))
|
||||
y.append(0)
|
||||
|
||||
# 🔹 학습
|
||||
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
|
||||
model = RandomForestClassifier(n_estimators=200, random_state=42)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# 🔹 무작위 1000개 조합 생성 → 예측
|
||||
candidates = [sorted(random.sample(range(1, 46), 6)) for _ in range(1000)]
|
||||
candidate_features = [extract_features(c) for c in candidates]
|
||||
probs = model.predict_proba(candidate_features)[:, 1] # 당첨 확률
|
||||
|
||||
# 🔹 확률이 높은 순으로 상위 5개 출력
|
||||
results = sorted(zip(candidates, probs), key=lambda x: x[1], reverse=True)[:5]
|
||||
|
||||
print("🎯 머신러닝 기반 추천 조합 (RandomForestClassifier)")
|
||||
for i, (combo, prob) in enumerate(results, 1):
|
||||
print(f"{i}. {combo} → 확률: {prob:.4f}")
|
||||