관련 이슈
배경
하루 계획 마감 후 미완료 작업이 존재하면 finalize_daily_plan()이 같은 recovery_group_id를 가진 복구안을 생성한다.
현재 복구안 생성 및 실제 일정 적용 서비스는 구현돼 있지만, 사용자가 복구안을 조회하고 비교한 뒤 선택하여 적용할 수 있는 View와 URL은 아직 연결되지 않았다.
FE에서 구현하는 다음 템플릿에 실제 데이터를 연결한다.
planner/templates/planner/recovery_compare.html
planner/templates/planner/recovery_result.html
planner/templates/planner/includes/recovery_card.html
복구안은 생성 즉시 적용하지 않고, 사용자가 비교·확인 후 선택한 경우에만 apply_recovery_plan()을 호출해 실제 미래 일정에 반영한다.
작업 내용
다음 세 가지 View와 URL을 구현한다.
- 복구안 비교 화면
- 선택한 복구안 적용 전 미리보기 화면
- 선택한 복구안 실제 적용
URL
path(
"recovery/<uuid:group_id>/",
views.recovery_compare,
name="recovery_compare",
),
path(
"recovery/<int:plan_id>/preview/",
views.recovery_preview,
name="recovery_preview",
),
path(
"recovery/<int:plan_id>/apply/",
views.recovery_apply,
name="recovery_apply",
),
1. 복구안 비교 View
이름
요청
GET /planner/recovery/<group_id>/
처리
로그인한 사용자가 소유한 RecoveryPlan 중 다음 조건을 만족하는 복구안을 조회한다.
recovery_group_id == group_id
exam_period.user == request.user
status == PENDING
같은 그룹에 생성된 다음 복구안을 함께 조회한다.
- 분량 유지형:
maintain_volume
- 핵심 집중형:
core_focus
복구안 생성 과정에서 한 종류만 생성될 수도 있으므로, 두 복구안이 모두 존재한다고 가정하지 않는다.
다른 사용자의 복구안이거나 존재하지 않는 그룹이면 404를 반환한다.
Template
planner/recovery_compare.html
Context
{
"recovery_group_id": recovery_group_id,
"source_daily_plan": source_daily_plan,
"maintain_volume": {
"id": recovery_plan.id,
"recovery_type": recovery_plan.recovery_type,
"status": recovery_plan.status,
"reschedule_count": int,
"exclude_count": int,
"total_remaining_minutes": int,
"items": [
{
"study_task_id": int,
"subject_name": str,
"title": str,
"original_date": date | None,
"changed_date": date | None,
"action_type": str,
"remaining_minutes": int,
"reason": str | None,
}
],
"preview_url": str,
} | None,
"core_focus": {
"id": recovery_plan.id,
"recovery_type": recovery_plan.recovery_type,
"status": recovery_plan.status,
"reschedule_count": int,
"exclude_count": int,
"total_remaining_minutes": int,
"items": [...],
"preview_url": str,
} | None,
}
정렬
각 복구안의 항목은 다음 순서로 정렬한다.
action_type
→ changed_date
→ study_task.exam.exam_date
→ study_task.order
→ id
2. 복구안 미리보기 View
이름
요청
GET /planner/recovery/<plan_id>/preview/
처리
로그인한 사용자가 소유하고 있으며 PENDING 상태인 복구안 하나를 조회한다.
선택한 복구안을 적용했을 때 날짜별 공부량이 어떻게 변경되는지 계산한다.
RecoveryPlanItem의 RESCHEDULE 항목을 날짜별로 묶어 다음 값을 만든다.
기존 계획 시간
추가되는 복구 시간
적용 후 계획 시간
이동되는 작업 목록
EXCLUDE 항목은 별도 목록으로 전달한다.
이 단계에서는 DailyPlan이나 DailyPlanItem을 수정하지 않는다.
Template
planner/recovery_result.html
Context
{
"recovery_plan": recovery_plan,
"recovery_group_id": recovery_plan.recovery_group_id,
"source_daily_plan": recovery_plan.source_daily_plan,
"summary": {
"reschedule_count": int,
"exclude_count": int,
"total_remaining_minutes": int,
},
"date_changes": [
{
"date": date,
"before_minutes": int,
"added_minutes": int,
"after_minutes": int,
"items": [
{
"subject_name": str,
"title": str,
"remaining_minutes": int,
"original_date": date | None,
"changed_date": date,
}
],
}
],
"excluded_items": [
{
"subject_name": str,
"title": str,
"remaining_minutes": int,
"reason": str | None,
}
],
"apply_url": str,
"compare_url": str,
}
3. 복구안 적용 View
이름
요청
POST /planner/recovery/<plan_id>/apply/
처리
로그인한 사용자가 소유한 PENDING 복구안을 조회한 뒤 다음 서비스를 호출한다.
apply_recovery_plan(recovery_plan)
서비스 적용 결과:
RESCHEDULE 항목을 변경 날짜의 DailyPlanItem으로 생성
EXCLUDE 항목은 미래 일정에 생성하지 않음
- 선택한 복구안을
APPLIED로 변경
- 같은 그룹의 다른
PENDING 복구안을 DISCARDED로 변경
- 원본 과거
DailyPlanItem과 ProgressLog는 유지
성공 시 메시지를 추가하고 대시보드로 이동한다.
messages.success(request, "선택한 복구안이 일정에 적용되었습니다.")
return redirect("planner:dashboard")
예외 처리
존재하지 않거나 다른 사용자의 복구안
HTTP 404
소유권 필터에 다음 조건을 반드시 포함한다.
exam_period__user=request.user
이미 처리된 복구안
RecoveryPlanAlreadyProcessedError
messages.error(request, "이미 처리된 복구안입니다.")
복구안 비교 화면 또는 대시보드로 이동한다.
오래되어 적용할 수 없는 복구안
RecoveryPlanStaleError
messages.error(
request,
"일정이나 가능시간이 변경되어 이 복구안을 적용할 수 없습니다. 복구안을 다시 확인해주세요.",
)
복구안 비교 화면으로 이동한다.
유효하지 않은 복구안 데이터
RecoveryPlanInvalidDataError
messages.error(
request,
"복구안 데이터에 문제가 있어 적용할 수 없습니다.",
)
복구안 비교 화면으로 이동한다.
잘못된 요청 방식
복구안 적용 View는 POST만 허용한다.
@require_http_methods(["POST"])
GET 요청은 HTTP 405를 반환한다.
조회 최적화
복구안과 항목을 조회할 때 N+1 문제가 발생하지 않도록 다음 관계를 미리 조회한다.
RecoveryPlan.objects.prefetch_related(
"items__study_task__exam",
).select_related(
"exam_period",
"source_daily_plan",
)
주요 정책
- 복구안은 조회만으로 적용되지 않는다.
- 실제 일정 변경은
recovery_apply POST 요청에서만 수행한다.
- 다른 사용자의 복구안은 조회하거나 적용할 수 없다.
- 같은
recovery_group_id에서 하나의 복구안만 적용할 수 있다.
- 적용된 복구안과 폐기된 복구안은 다시 적용할 수 없다.
- 적용 전 미리보기에서는 DB 일정을 변경하지 않는다.
- 복구안 적용 로직을 View에서 재구현하지 않고 기존
apply_recovery_plan() 서비스를 호출한다.
- 분량 유지형과 핵심 집중형 중 한 종류만 생성된 경우에도 비교 화면이 정상적으로 표시되어야 한다.
이번 이슈 범위 밖
- 복구안 생성 로직 수정
apply_recovery_plan() 서비스 로직 수정
- 모델 또는 마이그레이션 변경
- 복구안 재생성 기능
- FE 템플릿, CSS 및 JavaScript 구현
- 하루 마감 버튼의
fetch 연결
- JSON API 구현
- 적용된 복구안을 다시 되돌리는 기능
이번 작업은 Django Template 기반 View와 URL 연결로 구현한다.
테스트
복구안 비교
- 로그인하지 않은 사용자 접근 시 로그인 화면 이동
- 다른 사용자의 복구안 그룹 접근 시
404
- 존재하지 않는
recovery_group_id 접근 시 404
- 분량 유지형과 핵심 집중형이 모두 존재하는 그룹 정상 조회
- 분량 유지형만 존재하는 그룹 정상 조회
- 핵심 집중형만 존재하는 그룹 정상 조회
APPLIED 또는 DISCARDED 복구안만 존재하는 그룹 접근 처리
- 이동 작업과 제외 작업 개수 및 합계 시간 확인
- 각 작업의 과목명, 제목, 날짜, 남은 시간 context 확인
복구안 미리보기
- 선택한
PENDING 복구안 정상 조회
- 다른 사용자 복구안 접근 시
404
- 적용 전
DailyPlanItem이 변경되지 않는지 확인
- 날짜별 기존 시간, 추가 시간, 적용 후 시간 계산 확인
- 제외 작업 목록 확인
- 분량 유지형의 제외 작업 수가 0인지 확인
- 핵심 집중형의 제외 작업 목록 확인
복구안 적용
- 분량 유지형 정상 적용
- 핵심 집중형 정상 적용
RESCHEDULE 항목이 미래 DailyPlanItem으로 생성되는지 확인
EXCLUDE 항목이 일정에 생성되지 않는지 확인
- 선택한 복구안이
APPLIED로 변경되는지 확인
- 같은 그룹의 다른 복구안이
DISCARDED로 변경되는지 확인
- 동일 그룹의 복구안을 두 번 적용하면 거부되는지 확인
- 이미 처리된 복구안 적용 시 오류 처리
- stale 복구안 적용 시 전체 변경 롤백
- 유효하지 않은
remaining_minutes 적용 시 전체 변경 롤백
- 다른 사용자 복구안 적용 시
404
- GET 요청으로 적용 시
405
- 원본 과거 계획과 진행 기록이 유지되는지 확인
완료 조건
- 하루 마감 API에서 받은
recovery_group_id로 복구안 비교 화면에 접근할 수 있다.
- 사용자가 두 복구안 중 하나를 선택해 적용 전 변경 내용을 확인할 수 있다.
- 사용자 승인 전에는 미래 일정이 변경되지 않는다.
- 적용 버튼을 누른 경우에만 선택한 복구안이 실제 미래 일정에 반영된다.
- 적용 후 같은 그룹의 다른 복구안은 다시 적용할 수 없다.
- 관련 테스트와 기존 전체 테스트가 통과한다.
python manage.py check
python manage.py test planner
python manage.py makemigrations --check
관련 이슈
배경
하루 계획 마감 후 미완료 작업이 존재하면
finalize_daily_plan()이 같은recovery_group_id를 가진 복구안을 생성한다.현재 복구안 생성 및 실제 일정 적용 서비스는 구현돼 있지만, 사용자가 복구안을 조회하고 비교한 뒤 선택하여 적용할 수 있는 View와 URL은 아직 연결되지 않았다.
FE에서 구현하는 다음 템플릿에 실제 데이터를 연결한다.
복구안은 생성 즉시 적용하지 않고, 사용자가 비교·확인 후 선택한 경우에만
apply_recovery_plan()을 호출해 실제 미래 일정에 반영한다.작업 내용
다음 세 가지 View와 URL을 구현한다.
URL
1. 복구안 비교 View
이름
요청
처리
로그인한 사용자가 소유한
RecoveryPlan중 다음 조건을 만족하는 복구안을 조회한다.같은 그룹에 생성된 다음 복구안을 함께 조회한다.
maintain_volumecore_focus복구안 생성 과정에서 한 종류만 생성될 수도 있으므로, 두 복구안이 모두 존재한다고 가정하지 않는다.
다른 사용자의 복구안이거나 존재하지 않는 그룹이면
404를 반환한다.Template
Context
{ "recovery_group_id": recovery_group_id, "source_daily_plan": source_daily_plan, "maintain_volume": { "id": recovery_plan.id, "recovery_type": recovery_plan.recovery_type, "status": recovery_plan.status, "reschedule_count": int, "exclude_count": int, "total_remaining_minutes": int, "items": [ { "study_task_id": int, "subject_name": str, "title": str, "original_date": date | None, "changed_date": date | None, "action_type": str, "remaining_minutes": int, "reason": str | None, } ], "preview_url": str, } | None, "core_focus": { "id": recovery_plan.id, "recovery_type": recovery_plan.recovery_type, "status": recovery_plan.status, "reschedule_count": int, "exclude_count": int, "total_remaining_minutes": int, "items": [...], "preview_url": str, } | None, }정렬
각 복구안의 항목은 다음 순서로 정렬한다.
2. 복구안 미리보기 View
이름
요청
처리
로그인한 사용자가 소유하고 있으며
PENDING상태인 복구안 하나를 조회한다.선택한 복구안을 적용했을 때 날짜별 공부량이 어떻게 변경되는지 계산한다.
RecoveryPlanItem의RESCHEDULE항목을 날짜별로 묶어 다음 값을 만든다.EXCLUDE항목은 별도 목록으로 전달한다.이 단계에서는
DailyPlan이나DailyPlanItem을 수정하지 않는다.Template
Context
{ "recovery_plan": recovery_plan, "recovery_group_id": recovery_plan.recovery_group_id, "source_daily_plan": recovery_plan.source_daily_plan, "summary": { "reschedule_count": int, "exclude_count": int, "total_remaining_minutes": int, }, "date_changes": [ { "date": date, "before_minutes": int, "added_minutes": int, "after_minutes": int, "items": [ { "subject_name": str, "title": str, "remaining_minutes": int, "original_date": date | None, "changed_date": date, } ], } ], "excluded_items": [ { "subject_name": str, "title": str, "remaining_minutes": int, "reason": str | None, } ], "apply_url": str, "compare_url": str, }3. 복구안 적용 View
이름
요청
처리
로그인한 사용자가 소유한
PENDING복구안을 조회한 뒤 다음 서비스를 호출한다.서비스 적용 결과:
RESCHEDULE항목을 변경 날짜의DailyPlanItem으로 생성EXCLUDE항목은 미래 일정에 생성하지 않음APPLIED로 변경PENDING복구안을DISCARDED로 변경DailyPlanItem과ProgressLog는 유지성공 시 메시지를 추가하고 대시보드로 이동한다.
예외 처리
존재하지 않거나 다른 사용자의 복구안
HTTP
404소유권 필터에 다음 조건을 반드시 포함한다.
이미 처리된 복구안
RecoveryPlanAlreadyProcessedError복구안 비교 화면 또는 대시보드로 이동한다.
오래되어 적용할 수 없는 복구안
RecoveryPlanStaleError복구안 비교 화면으로 이동한다.
유효하지 않은 복구안 데이터
RecoveryPlanInvalidDataError복구안 비교 화면으로 이동한다.
잘못된 요청 방식
복구안 적용 View는 POST만 허용한다.
@require_http_methods(["POST"])GET 요청은 HTTP
405를 반환한다.조회 최적화
복구안과 항목을 조회할 때 N+1 문제가 발생하지 않도록 다음 관계를 미리 조회한다.
주요 정책
recovery_applyPOST 요청에서만 수행한다.recovery_group_id에서 하나의 복구안만 적용할 수 있다.apply_recovery_plan()서비스를 호출한다.이번 이슈 범위 밖
apply_recovery_plan()서비스 로직 수정fetch연결이번 작업은 Django Template 기반 View와 URL 연결로 구현한다.
테스트
복구안 비교
404recovery_group_id접근 시404APPLIED또는DISCARDED복구안만 존재하는 그룹 접근 처리복구안 미리보기
PENDING복구안 정상 조회404DailyPlanItem이 변경되지 않는지 확인복구안 적용
RESCHEDULE항목이 미래DailyPlanItem으로 생성되는지 확인EXCLUDE항목이 일정에 생성되지 않는지 확인APPLIED로 변경되는지 확인DISCARDED로 변경되는지 확인remaining_minutes적용 시 전체 변경 롤백404405완료 조건
recovery_group_id로 복구안 비교 화면에 접근할 수 있다.python manage.py check python manage.py test planner python manage.py makemigrations --check