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
13 changes: 13 additions & 0 deletions accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ def __init__(self, *args, **kwargs):
'placeholder': '비밀번호를 한번 더 입력하세요',
})

def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError('이미 가입된 이메일입니다.')
return email

def save(self, commit=True):
user = super().save(commit=False)
user.username = self.cleaned_data['email']
if commit:
user.save()
return user


class CustomAuthenticationForm(AuthenticationForm):
"""
Expand Down
47 changes: 46 additions & 1 deletion accounts/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model

# Create your tests here.
User = get_user_model()


class SignupTests(TestCase):
def test_signup_with_different_emails_succeeds_each_time(self):
"""서로 다른 이메일로 회원가입을 두 번 해도 둘 다 정상 생성되어야 한다."""
response1 = self.client.post(reverse('accounts:signup'), {
'email': 'user1@example.com',
'nickname': '유저1',
'password1': 'StrongPass123!',
'password2': 'StrongPass123!',
})
response2 = self.client.post(reverse('accounts:signup'), {
'email': 'user2@example.com',
'nickname': '유저2',
'password1': 'StrongPass123!',
'password2': 'StrongPass123!',
})

self.assertEqual(response1.status_code, 302)
self.assertEqual(response2.status_code, 302)
self.assertEqual(User.objects.count(), 2)

user1 = User.objects.get(email='user1@example.com')
user2 = User.objects.get(email='user2@example.com')
# username에 email이 채워지고 서로 달라 unique 제약을 통과해야 함
self.assertEqual(user1.username, 'user1@example.com')
self.assertEqual(user2.username, 'user2@example.com')

def test_signup_with_duplicate_email_fails(self):
User.objects.create_user(
username='user1@example.com', email='user1@example.com', password='StrongPass123!'
)
response = self.client.post(reverse('accounts:signup'), {
'email': 'user1@example.com',
'nickname': '중복유저',
'password1': 'StrongPass123!',
'password2': 'StrongPass123!',
})
self.assertEqual(response.status_code, 200) # 에러와 함께 폼 재렌더링
self.assertEqual(User.objects.count(), 1)
self.assertFormError(
response.context['form'], 'email', '이미 가입된 이메일입니다.'
)
69 changes: 67 additions & 2 deletions accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from django.views.decorators.http import require_http_methods

# Create your views here.
from .forms import CustomUserCreationForm, CustomAuthenticationForm


# ==========================================
# 1. 회원가입 뷰 (Signup)
# ==========================================
@require_http_methods(["GET", "POST"])
def signup_view(request):
"""
URL: /accounts/signup/
템플릿: accounts/signup.html
"""
if request.user.is_authenticated:
return redirect('/')

if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
form.save()
return redirect('accounts:login')
# form.is_valid()가 False면 에러가 담긴 form을 내려보냄
else:
form = CustomUserCreationForm()

context = {'form': form}
return render(request, 'accounts/signup.html', context)


# ==========================================
# 2. 로그인 뷰 (Login)
# ==========================================
@require_http_methods(["GET", "POST"])
def login_view(request):
"""
URL: /accounts/login/
템플릿: accounts/login.html
"""
if request.user.is_authenticated:
return redirect('/')

if request.method == 'POST':
form = CustomAuthenticationForm(request, data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
next_url = request.POST.get('next') or request.GET.get('next') or '/'
return redirect(next_url)
else:
form = CustomAuthenticationForm()

context = {'form': form}
return render(request, 'accounts/login.html', context)


# ==========================================
# 3. 로그아웃 뷰 (Logout)
# ==========================================
@require_http_methods(["POST"])
def logout_view(request):
"""
URL: /accounts/logout/
"""
logout(request)
return redirect('accounts:login')
4 changes: 2 additions & 2 deletions exams/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ def save(self, commit=True):
class ExamForm(forms.ModelForm):
class Meta:
model = Exam
fields = ['name', 'exam_date', 'priority']
fields = ['subject_name', 'exam_date', 'priority']
widgets = {
'name': forms.TextInput(attrs={
'subject_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '예: 신호및시스템, 공학수학'
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.16 on 2026-08-01 06:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('exams', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='studymaterial',
name='analysis_error_message',
field=models.TextField(blank=True, null=True, verbose_name='AI 분석 실패 사유'),
),
migrations.AddField(
model_name='studymaterial',
name='analysis_retry_count',
field=models.PositiveSmallIntegerField(default=0, verbose_name='AI 분석 사용자 재시도 횟수'),
),
migrations.AddField(
model_name='studymaterial',
name='analysis_status',
field=models.CharField(choices=[('pending', '대기 중'), ('processing', '처리 중'), ('completed', '완료'), ('failed', '실패')], default='pending', max_length=20, verbose_name='AI 분석 상태'),
),
]
25 changes: 19 additions & 6 deletions exams/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __str__(self):

class AvailableTime(models.Model):
"""
시험기간 내 날짜별 전체 공부 가능시간
시험기간 내 날짜별 전체 공부 가능시간
"""
exam_period = models.ForeignKey(
ExamPeriod,
Expand All @@ -72,7 +72,7 @@ def __str__(self):

class Exam(models.Model):
"""
개별 시험 과목 모델
개별 시험 과목 모델
"""
exam_period = models.ForeignKey(
ExamPeriod,
Expand All @@ -98,7 +98,7 @@ class Meta:
ordering = ['exam_date']

def __str__(self):
return f"{self.name} ({self.exam_date})"
return f"{self.subject_name} ({self.exam_date})"


class StudyMaterial(models.Model):
Expand Down Expand Up @@ -127,19 +127,32 @@ class StudyMaterial(models.Model):
verbose_name="텍스트 추출 상태"
)
error_message = models.TextField(null=True, blank=True, verbose_name="추출/파싱 실패 원인")

# AI 분석(E-AI-01/02/03) 상태 - 위 status/error_message(텍스트 추출)와는 별개 필드.
# 리뷰 확정 사항: 텍스트 추출 성공 여부와 AI 분석 성공 여부는 서로 다른 단계라 분리한다.
analysis_status = models.CharField(
max_length=20,
choices=MaterialStatus.choices,
default=MaterialStatus.PENDING,
verbose_name="AI 분석 상태"
)
analysis_error_message = models.TextField(null=True, blank=True, verbose_name="AI 분석 실패 사유")
analysis_retry_count = models.PositiveSmallIntegerField(default=0, verbose_name="AI 분석 사용자 재시도 횟수")

created_at = models.DateTimeField(auto_now_add=True, verbose_name="생성일시")

class Meta:
db_table = 'study_materials'
verbose_name = '학습 자료'
verbose_name_plural = '학습 자료 목록'

def __str__(self):
return f"[{self.exam.name}] {self.title}"
return f"[{self.exam.subject_name}] {self.title}"


class StudyTask(models.Model):
"""
하루 학습 분량
하루 학습 분량
"""
exam = models.ForeignKey(
Exam,
Expand Down Expand Up @@ -211,4 +224,4 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)

def __str__(self):
return f"[{self.exam.name}] {self.title}"
return f"[{self.exam.subject_name}] {self.title}"
Empty file.
Loading