Skip to content

[planner] 복구안 비교·미리보기·적용 View/URL 연결 #81

Description

@wngjs8114

관련 이슈


배경

하루 계획 마감 후 미완료 작업이 존재하면 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을 구현한다.

  1. 복구안 비교 화면
  2. 선택한 복구안 적용 전 미리보기 화면
  3. 선택한 복구안 실제 적용

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

이름

planner:recovery_compare

요청

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

이름

planner:recovery_preview

요청

GET /planner/recovery/<plan_id>/preview/

처리

로그인한 사용자가 소유하고 있으며 PENDING 상태인 복구안 하나를 조회한다.

선택한 복구안을 적용했을 때 날짜별 공부량이 어떻게 변경되는지 계산한다.

RecoveryPlanItemRESCHEDULE 항목을 날짜별로 묶어 다음 값을 만든다.

기존 계획 시간
추가되는 복구 시간
적용 후 계획 시간
이동되는 작업 목록

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

이름

planner:recovery_apply

요청

POST /planner/recovery/<plan_id>/apply/

처리

로그인한 사용자가 소유한 PENDING 복구안을 조회한 뒤 다음 서비스를 호출한다.

apply_recovery_plan(recovery_plan)

서비스 적용 결과:

  • RESCHEDULE 항목을 변경 날짜의 DailyPlanItem으로 생성
  • EXCLUDE 항목은 미래 일정에 생성하지 않음
  • 선택한 복구안을 APPLIED로 변경
  • 같은 그룹의 다른 PENDING 복구안을 DISCARDED로 변경
  • 원본 과거 DailyPlanItemProgressLog는 유지

성공 시 메시지를 추가하고 대시보드로 이동한다.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions