Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ venv/
db.sqlite3
# 미디어 및 정적 파일 폴더
media/
static/
# static/
# VS Code 설정
.vscode/
# 보안 및 환경 설정 파일 (추가)
Expand Down
18 changes: 18 additions & 0 deletions accounts/templates/accounts/mypage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends 'base.html' %}

{% block content %}
<h2>👤 마이페이지</h2>
<div class="user-info" style="border: 1px solid #ddd; padding: 20px; border-radius: 10px;">
<p><strong>이름:</strong> {{ user.username }}</p>
<p><strong>이메일:</strong> {{ user.email }}</p>
<p><strong>가입일:</strong> {{ user.date_joined|date:"Y-m-d" }}</p>

<hr>

<a href="{% url 'accounts:logout' %}"
onclick="return confirm('로그아웃 하시겠습니까?')"
style="color: red; text-decoration: none;">
🚪 로그아웃
</a>
</div>
{% endblock %}
9 changes: 9 additions & 0 deletions accounts/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
path('mypage/', views.mypage, name='mypage'),
path('logout/', views.logout_view, name='logout'),
]
14 changes: 12 additions & 2 deletions accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.decorators import login_required

# Create your views here.
@login_required
def mypage(request):
# 내 정보 조회
return render(request, 'accounts/mypage.html', {'user': request.user})

def logout_view(request):
# 로그아웃 처리
auth_logout(request)
return redirect('gallery:photo_list')
30 changes: 30 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,25 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',

'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',

'accounts',
'photos',
'classification',
'gallery',
]

SITE_ID = 1

# 로그인 후 리다이렉트 경로
LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
Expand All @@ -51,6 +64,7 @@
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
]

ROOT_URLCONF = 'config.urls'
Expand All @@ -66,6 +80,7 @@
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'gallery.context_processors.notification_context',
],
},
},
Expand Down Expand Up @@ -125,3 +140,18 @@
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)

import os
from dotenv import load_dotenv
load_dotenv()

GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
GOOGLE_SECRET = os.getenv('GOOGLE_SECRET')

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
10 changes: 9 additions & 1 deletion config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('accounts/', include('allauth.urls')),
path('gallery/', include('gallery.urls')),
]

if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
18 changes: 17 additions & 1 deletion gallery/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
from django.contrib import admin
from .models import Photo, Category, Notification

# Register your models here.
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'category_key', 'parent', 'user']
list_filter = ['category_key', 'user']

@admin.register(Photo)
class PhotoAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'category', 'is_bookmarked', 'is_trashed', 'created_at']
list_filter = ['user', 'category', 'is_bookmarked', 'is_trashed']
search_fields = ['memo']
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "category":
kwargs["queryset"] = Category.objects.filter(user=request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

admin.site.register(Notification)
3 changes: 3 additions & 0 deletions gallery/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class GalleryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gallery'

def ready(self):
import gallery.signals
9 changes: 9 additions & 0 deletions gallery/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .models import Notification

def notification_context(request):
if request.user.is_authenticated:
return {
'latest_notifications': Notification.objects.filter(user=request.user)[:5],
'unread_notifications_count': Notification.objects.filter(user=request.user, is_read=False).count()
}
return {}
19 changes: 19 additions & 0 deletions gallery/management/commands/cleanup_photos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from gallery.models import Photo, Notification

class Command(BaseCommand):
help = '7일 이상 미확인된 사진을 휴지통으로 이동합니다.'

def handle(self, *args, **options):
# 미확인 상태의 사진들만 가져오기
photos = Photo.objects.filter(is_confirmed=False, is_trashed=False)
count = 0
for photo in photos:
if photo.check_auto_trash(days=0): # 7일 기준
count += 1
Notification.objects.create(
user=photo.user,
message=f"미확인 사진이 휴지통으로 이동되었습니다: {photo.created_at.strftime('%Y-%m-%d')}"
)

self.stdout.write(self.style.SUCCESS(f'총 {count}장의 사진이 휴지통으로 이동되었습니다.'))
47 changes: 47 additions & 0 deletions gallery/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.17 on 2026-01-29 16:49

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Photo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='photos/%Y/%m/%d/')),
('image_hash', models.CharField(max_length=64, unique=True)),
('source', models.CharField(choices=[('direct', 'Direct'), ('google', 'Google Drive')], max_length=20)),
('category_main', models.CharField(default='기타', max_length=50)),
('category_sub', models.CharField(blank=True, max_length=50, null=True)),
('is_confirmed', models.BooleanField(default=False)),
('memo', models.TextField(blank=True, null=True)),
('is_bookmarked', models.BooleanField(default=False)),
('is_trashed', models.BooleanField(default=False)),
('trashed_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.CharField(max_length=255)),
('notif_type', models.CharField(choices=[('reminder', '리마인드'), ('trash', '휴지통이동')], max_length=20)),
('is_read', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.17 on 2026-01-30 06:48

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gallery', '0001_initial'),
]

operations = [
migrations.RemoveField(
model_name='photo',
name='category_main',
),
migrations.RemoveField(
model_name='photo',
name='category_sub',
),
migrations.AddField(
model_name='notification',
name='photo',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reminders', to='gallery.photo'),
),
migrations.AddField(
model_name='notification',
name='remind_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('category_key', models.CharField(blank=True, choices=[('finance', '결제/금융'), ('study_note', '학습/노트'), ('shopping', '쇼핑 정보'), ('schedule', '일정/예약'), ('document', '문서/정보'), ('others', '기타정보(비정보)')], max_length=20, null=True)),
('is_bookmarked', models.BooleanField(default=False)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='gallery.category')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'name', 'parent')},
},
),
migrations.AddField(
model_name='photo',
name='category',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='photos', to='gallery.category'),
),
]
84 changes: 83 additions & 1 deletion gallery/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta

# Create your models here.
class Category(models.Model):
# 6가지 고정 대분류 정의
BASIC_CATEGORIES = [
('finance', '결제/금융'),
('study_note', '학습/노트'),
('shopping', '쇼핑 정보'),
('schedule', '일정/예약'),
('document', '문서/정보'),
('others', '기타정보(비정보)'),
]

user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='categories')
name = models.CharField(max_length=50) # 사용자가 보는 이름 (예: 예적금)

# 1차 대분류인 경우에만 선택, 2차 커스텀 폴더일 경우 부모를 따라가거나 비워둠
category_key = models.CharField(
max_length=20,
choices=BASIC_CATEGORIES,
null=True, blank=True
)

# 자기 참조: 이 필드가 있으면 2차 분류, 없으면(None) 1차 분류가 됨
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='subcategories')
is_bookmarked = models.BooleanField(default=False)

class Meta:
# 같은 유저 내에서, 같은 부모 아래에 동일한 이름의 폴더를 만들 수 없도록 제한
unique_together = ('user', 'name', 'parent')

def __str__(self):
return f"[{self.get_category_key_display()}] {self.name}" if self.category_key else self.name

class Photo(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='photos')
image = models.ImageField(upload_to='photos/%Y/%m/%d/')
image_hash = models.CharField(max_length=64, unique=True)
source = models.CharField(max_length=20, choices=[('direct', 'Direct'), ('google', 'Google Drive')])

# 새로 정의한 Category 모델 연결
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='photos')

# 기존 문자열 필드는 데이터 마이그레이션 후 삭제해도 무방합니다.
is_confirmed = models.BooleanField(default=False)
memo = models.TextField(blank=True, null=True)
is_bookmarked = models.BooleanField(default=False)

is_trashed = models.BooleanField(default=False)
trashed_at = models.DateTimeField(blank=True, null=True)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return f"[{self.user.username}] {self.id} ({self.category.name if self.category else '미분류'})"

@property
def expires_at(self):
if self.trashed_at:
return self.trashed_at + timedelta(days=30)
return None

def check_auto_trash(self, days=7):
if not self.is_confirmed and self.created_at <= timezone.now() - timedelta(days=days):
self.is_trashed = True
self.trashed_at = timezone.now()
self.save()
return True
return False

class Notification(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
photo = models.ForeignKey(Photo, on_delete=models.CASCADE, null=True, blank=True, related_name='reminders')
message = models.CharField(max_length=255)
notif_type = models.CharField(max_length=20, choices=[('reminder', '리마인드'), ('trash', '휴지통이동')])
remind_at = models.DateTimeField(null=True, blank=True)
is_read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"[{self.user.username}] {self.message}"
Loading