Skip to content

style: #49 대시보드 화면 구현 - #51

Merged
borissal1207 merged 4 commits into
devfrom
feature/#49-dashboard
Aug 5, 2026
Merged

style: #49 대시보드 화면 구현#51
borissal1207 merged 4 commits into
devfrom
feature/#49-dashboard

Conversation

@borissal1207

@borissal1207 borissal1207 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

작업 내용

공통 토큰(#40) 위에 FE2 첫 화면인 대시보드를 구현했습니다.
planner 전용 CSS와 재사용 컴포넌트도 함께 만들었습니다.

추가한 파일

  • static/css/planner.css — 레이아웃, 버튼, 카드, 태그, 배지, Fit Bar, 빈 화면
  • static/js/planner.js — 사이드바 접기
  • templates/includes/icons.html — Lucide 아이콘 17개
  • planner/templates/planner/dashboard.html — 기본 / 시험기간 없음 / 복구안 대기
  • planner/templates/planner/includes/ — sidebar, fit_bar, empty_state

주요 결정 사항

  1. Fit Bar — 필요시간과 가능시간 비교를 막대 하나로 표현했습니다.
    검은 세로선(가능시간)이 띠(필요시간)보다 오른쪽이면 가능, 띠 안이면 위험, 왼쪽이면 불가능입니다.
    전체 판정과 과목 카드에서 같은 컴포넌트를 재사용합니다.
  2. 과목 카드 3개는 같은 눈금을 사용합니다. 카드마다 축이 다르면 비교가 성립하지 않습니다.
  3. 빈 화면은 별도 템플릿을 만들지 않고 empty_state.html 하나를 문구만 바꿔 재사용합니다.
  4. 로고 클릭 시 사이드바가 접힙니다. 접힌 상태는 sessionStorage에 저장됩니다.
  5. {% url %} 태그는 아직 URL이 없어 href="#" + TODO 주석으로 두었습니다.

이번 PR 범위 밖

  • View/URL 연결 (BE 담당) — 더미 데이터 기준으로 마크업만 진행했습니다
  • task_row.html — 대시보드에서 쓰지 않아 오늘의 공부 이슈로 넘겼습니다

View에서 넘겨주셔야 하는 context

{
  # 없으면 온보딩 화면이 뜹니다
  'exam_period': {
      'title': str, 'start_date': date, 'end_date': date,
      'progress_percent': int,        # 사이드바 진행 막대
  },

  'today': date,                      # "10월 14일 수요일"
  'remaining_days': int,
  'today_minutes': int,
  'today_count': int,                 # 사이드바 메뉴 옆 숫자

  'pending_recovery': {               # 없으면 배너 미표시
      'created_at': date,
  },

  'overall': {
      'status': 'ok|warn|bad',
      'status_label': '가능|위험|불가능',
      'min_pct': int, 'band_pct': int, 'mark_pct': int,   # Fit Bar 좌표(%)
      'min_minutes': int, 'max_minutes': int,
      'available_minutes': int, 'axis_max': int,
      'shortage_minutes': int,        # 0이면 부족 없음으로 표시
      'daily_extra_minutes': int,     # 없으면 안내문 생략
      'calculated_at': datetime,
  },

  'subject_status': [{
      'subject_name': str, 'exam_date': date, 'd_day': int,
      'status': 'ok|warn|bad', 'status_label': str,
      'min_pct': int, 'band_pct': int, 'mark_pct': int,
      'min_minutes': int, 'max_minutes': int, 'available_minutes': int,
  }],
  'subject_axis_max': int,            # 과목 카드 공통 축

  'progress': {
      'today_percent': int, 'today_done_minutes': int, 'today_total_minutes': int,
      'total_percent': int, 'total_done_minutes': int, 'total_minutes': int,
      'core_left': int,
      'core_by_subject': [{'name': str, 'count': int}],
  },
}

Fit Bar 좌표 계산 (참고)

axis = max(max_minutes, available_minutes) * 1.15
min_pct  = min_minutes / axis * 100
band_pct = (max_minutes - min_minutes) / axis * 100
mark_pct = available_minutes / axis * 100

과목 카드는 세 과목 중 가장 큰 값으로 axis를 하나 잡고,
세 카드의 _pct를 전부 그 값으로 계산해 주셔야 비교가 됩니다.


화면 확인 방법

planner/urls.py에 아래를 임시로 추가하면 /planner/dashboard/ 에서 볼 수 있습니다.
(이번 PR에는 포함하지 않았습니다)

from django.views.generic import TemplateView
path('dashboard/', TemplateView.as_view(template_name='planner/dashboard.html'), name='dashboard'),

context가 없으므로 "시험기간 없음" 화면이 표시됩니다.
전체 화면은 view가 나온 뒤 확인 가능합니다.

확인 부탁드립니다

화면

  • 로고와 상단바 "대시보드"가 같은 높이에 있는지
  • 로고를 누르면 사이드바가 접히고 다시 펴지는지
  • 아이콘이 잘리거나 커지지 않는지
  • 시험기간 없을 때 온보딩 안내가 뜨는지

반응형

  • 1024 이하에서 카드 2열, 767 이하에서 1열
  • 767 이하에서 사이드바가 하단 탭바로 바뀌는지

코드

  • 색을 하드코딩하지 않고 var(--neutral11) 형태로만 썼는지
  • planner.css가 다른 앱 화면에 영향을 주지 않는지

context

  • 위 구조대로 view에서 만들 수 있는지, 어려운 값이 있는지

Closes #49

@borissal1207 borissal1207 changed the title [planner/FE] 대시보드 화면 구현 (#49) style: #49 대시보드 화면 구현 Aug 4, 2026
@wngjs8114

Copy link
Copy Markdown
Collaborator

전체적인 화면 구성과 sidebar, fit_bar, empty_state 컴포넌트 분리는 잘 되어 있습니다. 반응형 구조와 과목 카드의 공통 축을 사용하려는 방향도 확인했습니다.

다만 현재 context 구조 중 백엔드에서 계산 기준이 불명확하거나, 현재 데이터 모델로는 정확히 만들기 어려운 값이 있어 머지 전에 아래 사항을 먼저 정리해야 할 것 같습니다.

1. 과목별 available_minutes와 판정 기준 확인이 필요합니다

현재 AvailableTime은 과목별이 아니라 ExamPeriod 전체에 속하고, 기존 calculate_feasibility()도 시험기간 전체 작업과 전체 가용시간을 비교합니다.

따라서 과목별 카드마다 available_minutes를 어떻게 배분할지 현재 정책이 없습니다. 전체 가용시간을 각 과목에 동일하게 넣으면 같은 시간이 과목마다 중복 계산되고, 임의로 나누려면 별도의 배분 정책이 필요합니다.

MVP에서는 아래 중 하나로 정리하면 좋을 것 같습니다.

  • 과목별 Fit Bar는 제외하고 시험일과 남은 작업량만 표시
  • 실제 스케줄 배정 결과를 기반으로 별도의 과목별 지표를 정의

현재 구조에서는 첫 번째 방향이 더 적절해 보입니다.

2. 진행률 계산 기준을 명확히 해주세요

아래 값들이 작업 개수 기준인지, 계획시간 기준인지, 실제 공부시간 기준인지 정의가 필요합니다.

today_percent
today_done_minutes
today_total_minutes
total_percent
total_done_minutes
total_minutes

actual_minutes / planned_minutes로 계산하면 사용자가 예상보다 오래 공부한 경우 100%를 초과할 수 있습니다.

계획시간과 completion_percent를 기준으로 계산한다면 다음처럼 정의할 수 있을 것 같습니다.

completed_equivalent_minutes = sum(
    item.planned_minutes * progress_log.completion_percent / 100
)

progress_percent = (
    completed_equivalent_minutes / total_planned_minutes * 100
)

이 경우 done_minutes는 실제 공부시간이 아니라 완료 환산 분량이라는 의미가 됩니다.

3. 시험기간은 있지만 계획이 아직 없는 상태가 필요합니다

현재 템플릿은 exam_period 존재 여부로만 분기하고 있습니다.

시험기간은 생성됐지만 아직 과목 등록, 범위 입력, 작업 확정 또는 계획 생성이 완료되지 않은 경우에는 overall, subject_status, progress가 존재하지 않을 수 있습니다.

has_plan과 같은 분기를 추가하거나, View에서 미완성 단계로 리다이렉트하는 정책을 정하면 좋을 것 같습니다.

4. Fit Bar 필요시간 문구 위치를 수정해주세요

현재 fit_bar.html에서 필요시간 문구가 가능시간 표시선인 mark_pct에 배치되어 있습니다.

<span class="need" style="left:{{ mark_pct }}%">

필요시간 범위 중앙에 표시하려면 별도 좌표가 필요합니다.

need_label_pct = min_pct + band_pct / 2
<span class="need" style="left:{{ need_label_pct }}%">

5. 복구안 context에 그룹 식별자가 필요합니다

복구안 보기 화면에서 같은 시점에 생성된 분량 유지형·핵심 집중형을 조회하려면 recovery_group_id가 필요합니다.

'pending_recovery': {
    'group_id': UUID,
    'created_at': datetime,
}

형태로 전달받는 것이 좋을 것 같습니다.

6. ‘나중에’ 버튼 동작을 확인해주세요

현재 복구안 배너의 나중에 버튼은 링크나 JS 이벤트가 없어 눌러도 동작하지 않습니다.

배너를 현재 화면에서만 닫는 용도라면 JS 처리가 필요하고, 별도 동작이 없다면 버튼을 제거해도 될 것 같습니다.

7. 색상 하드코딩이 일부 남아 있습니다

planner.css에 아래와 같이 직접 입력된 색상이 있습니다.

color: #fff;
background: #1D2338;

색상 토큰 사용을 팀 기준으로 적용한다면 tokens.css에 정의한 뒤 var(...)로 교체해주세요.

추가로 아래 context 값도 의미를 간단히 명시해주시면 View 구현 시 혼선이 줄어들 것 같습니다.

  • exam_period.progress_percent: 날짜 경과율인지 전체 학습 진행률인지
  • remaining_days: 달력상 남은 날짜인지 공부 가능시간이 존재하는 날짜 수인지
  • calculated_at: 데이터 변경 시각인지 페이지 조회 시각인지
  • daily_extra_minutes: 부족시간을 어떤 날짜 수로 나눈 값인지

위 계산 기준과 상태 분기만 확정되면 View 연결 자체는 가능합니다. 다만 현재 범위와 일정상 과목별 Fit Bar와 복잡한 통계를 모두 구현하기보다는, 시험기간 정보·오늘 계획·전체 가능성 판정·복구안 배너 중심으로 대시보드를 먼저 단순화하는 방향을 권장합니다.

@borissal1207

Copy link
Copy Markdown
Collaborator Author

꼼꼼한 리뷰 감사합니다. 지적하신 내용 전부 반영해서 커밋 올렸습니다.

1. 과목별 Fit Bar 제거했습니다
말씀대로 AvailableTime이 ExamPeriod 단위라 배분 정책 없이는 만들 수 없는 값이었습니다.
과목 카드는 남기되 Fit Bar와 판정 배지를 빼고
시험일 / D-day / 남은 예상시간 / 남은 작업 수만 표시하도록 바꿨습니다.
subject_statussubject_summary로 이름도 바꿨습니다.

2. 진행률은 완료 환산 기준으로 가겠습니다
제안하신 planned_minutes × completion_percent 방식이 맞다고 생각합니다.
100%를 넘지 않고 일부완료도 자연스럽게 반영되니까요.
화면에도 "완료 환산 기준"이라고 표시했습니다.

3. has_plan 분기 추가했습니다

  • exam_period 없음 → 온보딩
  • exam_period 있음 + has_plan False → "아직 계획이 없습니다" + 다음 단계 버튼
  • 계획 있음 → 대시보드

버튼 문구와 링크는 next_step_label, next_step_url로 받도록 했습니다.
어느 단계인지에 따라 다르게 보내주시면 됩니다.

4. need_label_pct 분리했습니다
min_pct + band_pct / 2로 계산해서 넘겨주시면 됩니다.

5. group_id 추가했습니다
pending_recovery.group_id로 받겠습니다.

6. '나중에' 버튼 제거했습니다
배너를 닫아도 사이드바 표시가 남아야 해서, 버튼 없이 '복구안 보기'만 뒀습니다.

7. 색상 토큰화했습니다
#fffvar(--neutral1), #1D2338var(--neutral10)

값 정의

  • exam_period.progress_percent: 날짜 경과율 (진행률 카드가 따로 있어서 중복 피했습니다)
  • remaining_days: 공부 가능시간이 있는 날짜 수 (시험 당일 제외)
  • calculated_at: 판정을 계산한 시각 (하루 종료 시 갱신)
  • daily_extra_minutes: shortage_minutes / remaining_days

마지막에 말씀하신 단순화 방향 반영해서 전체적으로 가볍게 정리했습니다.
다시 확인 부탁드립니다!

@borissal1207

borissal1207 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

바뀐 context

'has_plan': bool,
'next_step_label': '과목 등록하기',
'next_step_url': '...',

'overall': {
...,
'need_label_pct': int, # min_pct + band_pct / 2
},

'subject_summary': [{
'subject_name': str,
'exam_date': date,
'd_day': int,
'is_near': bool, # D-3 이하면 배지 강조
'remaining_minutes': int,
'remaining_task_count': int,
}],

'pending_recovery': {'group_id': ..., 'created_at': date},

@wngjs8114

Copy link
Copy Markdown
Collaborator

수정사항 확인했습니다. 기존 리뷰에서 요청드린 아래 항목은 실제 코드에 반영된 것을 확인했습니다.

  • 과목별 Fit Bar와 과목별 판정 제거
  • 진행률에 완료 환산 기준 표시
  • has_plan 상태 분기 추가
  • Fit Bar의 need_label_pct 분리
  • pending_recovery.group_id 추가
  • 복구안 배너의 나중에 버튼 제거
  • 하드코딩 색상 토큰화

전체적으로 처음보다 context 구조가 훨씬 단순해졌고, View 연결도 가능한 방향으로 정리됐습니다.

다만 아래 사항은 추가 수정이 필요합니다.

1. shortage_minutes 표시 기준을 수정해주세요

현재 화면에서는 shortage_minutes를 다음과 같이 표시합니다.

<div class="kpi-label">최대 기준 부족</div>

하지만 기존 calculate_feasibility()shortage_minutes는 상태에 따라 계산 기준이 다릅니다.

# 위험
required_max_minutes - available_minutes

# 불가능
required_min_minutes - available_minutes

따라서 불가능 상태에서 반환되는 값은 최대 기준 부족분이 아닙니다.

아래 중 하나로 정리하면 좋을 것 같습니다.

  • 화면 문구를 판정 기준 부족으로 변경
  • 최대 필요시간 기준 부족분을 별도로 계산
max_shortage_minutes = max(
    0,
    max_minutes - available_minutes,
)

daily_extra_minutes도 최소 기준 부족분과 최대 기준 부족분 중 무엇을 나누는 값인지 함께 확정해주세요.

또한 공부 가능일이 0일 수 있으므로 View에서는 0으로 나누지 않도록 방어가 필요합니다.

daily_extra_minutes = (
    math.ceil(shortage_minutes / remaining_days)
    if shortage_minutes > 0 and remaining_days > 0
    else None
)

2. calculated_at의 데이터 근거를 다시 정해주세요

현재 가능성 판정은 DB에 결과나 계산 시각을 저장하지 않고 요청 시 계산하는 순수 함수입니다.

따라서 하루 종료 시 갱신되는 판정 시각을 현재 모델에서 직접 가져올 수 없습니다.

MVP에서는 아래 중 하나가 적절해 보입니다.

  • calculated_at 표시 제거
  • 페이지 조회 시각을 표시하고 문구를 조회 기준으로 변경
  • 최근 DailyPlan.finalized_at을 사용하되 의미를 최근 하루 마감 기준으로 변경

별도 저장 필드 없이 하루 종료 시 갱신된 계산 시각으로 정의하면 View에서 정확한 값을 만들 수 없습니다.

3. 모바일 과목 카드 레이아웃을 확인해주세요

현재 과목 카드 한 줄에 아래 요소가 모두 들어갑니다.

  • D-day
  • 과목명과 시험일
  • 남은 예상시간
  • 남은 작업 수

숫자 영역 두 개가 각각 min-width: 96px이고, 모바일에서도 세로 배치나 줄바꿈 처리가 없습니다.

320px와 375px 화면에서는 과목명이 매우 좁아지거나 카드가 가로로 넘칠 가능성이 높아 보입니다.

767px 이하에서는 과목 카드를 줄바꿈하거나 숫자 영역을 다음 줄로 내리는 반응형 처리를 추가하고, 320px·375px 화면에서 확인 부탁드립니다.

4. task_row.html의 못함 문구를 수정하거나 이번 PR에서 제외해주세요

이번 수정 커밋에서 task_row.html이 새로 추가됐는데, 못함 상태의 문구가 다음과 같습니다.

<div class="sub">못함 · 내일로 이동</div>

현재 정책에서는 못한 작업이 즉시 내일로 이동하지 않습니다.

하루 마감 시 복구 대상에 포함되고, 사용자가 복구안을 선택한 뒤 실제 일정이 변경됩니다.

따라서 문구는 아래처럼 수정하는 것이 맞습니다.

<div class="sub">못함 · 하루 마감 시 복구 대상</div>

기존 PR 설명대로 task_row.html이 오늘의 공부 이슈 범위라면, 이번 대시보드 PR에서는 파일 자체를 제외해도 될 것 같습니다.

위 네 가지 중 1번, 3번, 4번은 화면에 잘못된 정보가 표시되거나 모바일 레이아웃이 깨질 수 있는 문제라 머지 전에 반영 부탁드립니다.

@borissal1207

Copy link
Copy Markdown
Collaborator Author

2/3/4번, 그리고 1번 라벨 변경은 확인했습니다. 다만 1번에 새로 추가된 부제 문구에서 하나만 짚고 넘어가면 좋을 것 같아요 !!

1. daily_extra_minutes "해소" 문구가 불가능 상태에서 부정확합니다

kpi-sub에 추가하신 남은 N일 동안 하루 +N분이면 해소가 여전히 상태별로 기준이 달라지는 shortage_minutes를 그대로 나눈 값이라, "위험" 상태에서는 맞지만 "불가능" 상태에서는 실제와 다른 값이 됩니다.

예: required_min=900, required_max=1200, available=500 (불가능, remaining_days=5)

  • shortage_minutes = 900 - 500 = 400daily_extra_minutes = ceil(400/5) = 80
  • 화면엔 "하루 +80분이면 해소"로 뜨지만, 실제로 80분씩 5일을 더 채워도 available은 900이 되어 required_min에 딱 도달할 뿐입니다. 상태는 "위험"으로 바뀌는 거지 "가능"(해소)이 아니고, 진짜 해소하려면 1200-500=700분이 더 필요합니다.

daily_extra_minutes는 상태와 무관하게 max_minutes - available_minutes(= "가능" 상태 도달까지 필요한 값) 기준으로 계산하는 쪽으로 정리하겠습니다.

나머지는 모두 반영 확인했습니다.

  • calculated_atviewed_at + 조회 기준 문구 변경 확인
  • 모바일 과목 카드: 767px 이하에서 D-day+과목명 첫 줄, 숫자 두 개 아랫줄로 wrap 처리 확인
  • task_row.html PR에서 제외 확인

@wngjs8114

Copy link
Copy Markdown
Collaborator

2차 리뷰에서 요청드린 아래 사항은 반영된 것을 확인했습니다.

  • calculated_atviewed_at으로 변경
  • 모바일 과목 카드 줄바꿈 처리
  • task_row.html을 이번 PR에서 제외
  • 부족시간 라벨 수정

다만 daily_extra_minutes 계산 기준은 아직 최종 반영이 필요합니다.

현재 calculate_feasibility()shortage_minutes는 상태에 따라 기준이 다릅니다.

# 위험
required_max_minutes - available_minutes

# 불가능
required_min_minutes - available_minutes

따라서 불가능 상태의 shortage_minutesremaining_days로 나누고 “하루 +N분이면 해소”라고 표시하면, 실제로는 최소 필요시간에 도달해 불가능에서 위험으로 바뀔 뿐 가능 상태가 되지는 않습니다.

대시보드에서는 가능 상태 도달 기준을 별도 값으로 전달하는 것이 명확해 보입니다.

max_shortage_minutes = max(
    0,
    max_minutes - available_minutes,
)

daily_extra_minutes = (
    math.ceil(max_shortage_minutes / remaining_days)
    if max_shortage_minutes > 0 and remaining_days > 0
    else None
)

context도 기존 shortage_minutes와 혼동되지 않도록 다음처럼 명시해주세요.

'max_shortage_minutes': int,
'daily_extra_minutes': int | None,

화면 문구 역시 같은 기준으로 아래처럼 통일하는 편이 좋겠습니다.

  • 가능까지 부족
  • 남은 N일 동안 하루 +N분이면 가능

이 계산 기준과 context 이름까지 반영되면 PR #51은 머지해도 될 것 같습니다.

@borissal1207
borissal1207 force-pushed the feature/#49-dashboard branch from fe18de9 to 3307aee Compare August 5, 2026 09:20
@borissal1207

Copy link
Copy Markdown
Collaborator Author

#49 dashboard.html이 기대하는 context 구조 (3차 리뷰 반영 후 최신)

최상위

  • exam_period: 객체 | None (None이면 1단계 empty state)
    • .title, .start_date, .end_date
  • has_plan: bool (True 아니면 2단계 empty state)
  • next_step_label, next_step_url: str (has_plan=False일 때만 사용)
  • today: date
  • remaining_days: int (공부 가능일 수, 0일 수 있음)
  • today_minutes: int
  • pending_recovery: 객체 | None
    • .created_at: datetime
    • (recovery_group_id도 필요 - 모델 필드명은 group_id 아니라 recovery_group_id니 이름 맞춰주세요)

overall (dict)

  • status: "possible" | "risky" | "impossible"
  • status_label: str
  • min_pct, band_pct, mark_pct, need_label_pct, axis_max: fit_bar 렌더링용
  • min_minutes, max_minutes, available_minutes: int
  • max_shortage_minutes: int # NEW: max(0, max_minutes - available_minutes)
  • daily_extra_minutes: int | None # NEW: max_shortage_minutes/remaining_days 올림, 조건 안 맞으면 None
  • viewed_at: datetime # timezone.now(), 저장값 아님

subject_summary (list of dict)

  • is_near, d_day, subject_name, exam_date, remaining_minutes, remaining_task_count

progress (dict)

  • today_percent, today_done_minutes, today_total_minutes
  • total_percent, total_done_minutes, total_minutes
  • core_left

참고: shortage_minutes(상태별 기준, calculate_feasibility() 원본 값)와
max_shortage_minutes(가능 도달 기준, View에서 별도 계산)는 다른 값이고, 화면엔 max_shortage_minutes만 씁니다.

@borissal1207
borissal1207 merged commit fa5516d into dev Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[planner/FE] 대시보드 화면 구현

2 participants