Skip to content
Merged
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
21 changes: 21 additions & 0 deletions planner/templates/planner/feasibility.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}계획 가능성 판정 - PLAN B{% endblock %}
{% block content %}
<h1>계획 가능성 판정</h1>
<p>{{ exam_period.title }}</p>

<p>판정: {{ result.status }}</p>
<p>확정된 작업 수: {{ task_count }}</p>
<p>최소 필요시간: {{ total_min_minutes }}분</p>
<p>최대 필요시간: {{ total_max_minutes }}분</p>
<p>가용시간: {{ total_available_minutes }}분</p>

{% if can_generate %}
<form method="post" action="{% url 'planner:plan_generate' period_id=exam_period.id %}">
{% csrf_token %}
<button type="submit">계획 생성하기</button>
</form>
{% else %}
<p>{{ readiness_error|default:"가능시간 또는 학습작업을 조정해주세요." }}</p>
{% endif %}
{% endblock %}
13 changes: 13 additions & 0 deletions planner/templates/planner/plan_complete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}계획 생성 완료 - PLAN B{% endblock %}
{% block content %}
<h1>계획 생성 완료</h1>
<p>{{ exam_period.title }}</p>
<p>총 {{ daily_plan_count }}일, {{ total_planned_minutes }}분</p>

<ul>
{% for dp in daily_plans %}
<li>{{ dp.date }} - {{ dp.planned_minutes }}분 ({{ dp.items.count }}개 작업)</li>
{% endfor %}
</ul>
{% endblock %}
170 changes: 170 additions & 0 deletions planner/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.utils import timezone as django_timezone
from unittest.mock import patch
from planner.services.recovery import RecoveryPlanInvalidDataError
from django.urls import reverse

from planner.services.progress_recorder import (
finalize_daily_plan,
Expand Down Expand Up @@ -1680,3 +1681,172 @@ def test_apply_raises_invalid_data_when_remaining_minutes_not_positive(self):

with self.assertRaises(RecoveryPlanInvalidDataError):
apply_recovery_plan(self.maintain_volume)

class PlanGenerateFlowTests(TestCase):
def setUp(self):
from django.contrib.auth import get_user_model
from exams.models import Exam, ExamPeriod, StudyTask

User = get_user_model()
self.user = User.objects.create_user(
username="plan_gen_tester", email="plangen@example.com", password="pass1234"
)
self.other_user = User.objects.create_user(
username="other_tester", email="other@example.com", password="pass1234"
)
self.today = django_timezone.localdate()
self.exam_period = ExamPeriod.objects.create(
user=self.user,
title="계획 생성 테스트 시험기간",
start_date=self.today,
end_date=self.today + timedelta(days=10),
status="active",
)
self.exam = Exam.objects.create(
exam_period=self.exam_period,
subject_name="테스트 과목",
exam_date=self.today + timedelta(days=5),
)
self.client.login(username="plangen@example.com", password="pass1234")

def _make_confirmed_task(self, exam=None, order=1, min_m=20, max_m=40):
from exams.models import StudyTask
return StudyTask.objects.create(
exam=exam or self.exam, title=f"작업 {order}", importance="high", depth="core",
task_type="concept", difficulty="normal", order=order,
estimated_min_minutes=min_m, estimated_max_minutes=max_m, is_confirmed=True,
)

def test_feasibility_blocks_other_user(self):
response = self.client.get(
reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.client.login(username="other@example.com", password="pass1234")
response = self.client.get(
reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(response.status_code, 404)

def test_plan_generate_rejects_no_confirmed_tasks(self):
response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(DailyPlan.objects.filter(exam_period=self.exam_period).count(), 0)

def test_plan_generate_rejects_unconfirmed_task_present(self):
from exams.models import StudyTask
self._make_confirmed_task(order=1)
StudyTask.objects.create(
exam=self.exam, title="미확정 작업", importance="high", depth="core",
task_type="concept", difficulty="normal", order=2,
estimated_min_minutes=20, estimated_max_minutes=40, is_confirmed=False,
)
AvailableTime.objects.create(exam_period=self.exam_period, date=self.today, available_minutes=100)

response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(DailyPlan.objects.filter(exam_period=self.exam_period).count(), 0)

def test_plan_generate_rejects_subject_without_confirmed_task(self):
from exams.models import Exam
self._make_confirmed_task(order=1)
Exam.objects.create(
exam_period=self.exam_period, subject_name="작업 없는 과목",
exam_date=self.today + timedelta(days=6),
)
AvailableTime.objects.create(exam_period=self.exam_period, date=self.today, available_minutes=100)

response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(DailyPlan.objects.filter(exam_period=self.exam_period).count(), 0)

def test_plan_generate_rejects_zero_estimated_time_task(self):
from exams.models import StudyTask
StudyTask.objects.create(
exam=self.exam, title="예상시간 0", importance="high", depth="core",
task_type="concept", difficulty="normal", order=1,
estimated_min_minutes=0, estimated_max_minutes=0, is_confirmed=True,
)
AvailableTime.objects.create(exam_period=self.exam_period, date=self.today, available_minutes=100)

response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(DailyPlan.objects.filter(exam_period=self.exam_period).count(), 0)

def test_plan_generate_ignores_past_available_time(self):
self._make_confirmed_task(order=1, min_m=20, max_m=40)
AvailableTime.objects.create(
exam_period=self.exam_period, date=self.today - timedelta(days=1), available_minutes=100
)
response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(DailyPlan.objects.filter(exam_period=self.exam_period).count(), 0)

def test_plan_generate_rejects_risky_feasibility(self):
self._make_confirmed_task(order=1, min_m=100, max_m=200)
AvailableTime.objects.create(exam_period=self.exam_period, date=self.today, available_minutes=150)

response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(DailyPlan.objects.filter(exam_period=self.exam_period).count(), 0)

def test_plan_generate_success_allocates_all_tasks(self):
self._make_confirmed_task(order=1, min_m=20, max_m=40)
self._make_confirmed_task(order=2, min_m=20, max_m=40)
AvailableTime.objects.create(exam_period=self.exam_period, date=self.today, available_minutes=100)

response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:plan_complete', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(
DailyPlanItem.objects.filter(daily_plan__exam_period=self.exam_period).count(), 2
)

def test_plan_generate_twice_redirects_to_complete(self):
self._make_confirmed_task(order=1, min_m=20, max_m=40)
AvailableTime.objects.create(exam_period=self.exam_period, date=self.today, available_minutes=100)

self.client.post(reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id}))
response = self.client.post(
reverse('planner:plan_generate', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:plan_complete', kwargs={'period_id': self.exam_period.id})
)
self.assertEqual(
DailyPlanItem.objects.filter(daily_plan__exam_period=self.exam_period).count(), 1
)

def test_plan_complete_redirects_when_no_plan_yet(self):
response = self.client.get(
reverse('planner:plan_complete', kwargs={'period_id': self.exam_period.id})
)
self.assertRedirects(
response, reverse('planner:feasibility', kwargs={'period_id': self.exam_period.id})
)
6 changes: 5 additions & 1 deletion planner/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django.urls import path
from . import views

app_name = 'planner'

urlpatterns = [
]
path('feasibility/<int:period_id>/', views.feasibility, name='feasibility'),
path('plan/generate/<int:period_id>/', views.plan_generate, name='plan_generate'),
path('plan/complete/<int:period_id>/', views.plan_complete, name='plan_complete'),
]
Loading