diff --git a/planner/templates/planner/dashboard.html b/planner/templates/planner/dashboard.html index e69de29..f895c96 100644 --- a/planner/templates/planner/dashboard.html +++ b/planner/templates/planner/dashboard.html @@ -0,0 +1,180 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}대시보드 · PLAN B{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block body %} +
+ + {% include "planner/includes/sidebar.html" with nav="dashboard" %} + +
+
+ +

대시보드

+
+ +
+ + {% if not exam_period %} + {# ===== 1단계 : 시험기간 없음 ===== #} +
+ {% include "planner/includes/empty_state.html" with icon="i-calendar" title="아직 등록된 시험기간이 없습니다" desc="시험기간을 만들고 과목과 공부 가능시간을 입력하면, PLAN B가 날짜별 계획을 대신 계산해 드립니다." btn_label="시험기간 만들기" show_steps=True %} +
+ + {% elif not has_plan %} + {# ===== 2단계 : 시험기간은 있지만 계획이 아직 없음 ===== #} +
+
+

{{ exam_period.title }}

+
{{ exam_period.start_date|date:"n월 j일" }} ~ {{ exam_period.end_date|date:"n월 j일" }}
+
+
+
+ {% include "planner/includes/empty_state.html" with icon="i-calendar" title="아직 계획이 만들어지지 않았습니다" desc="과목과 시험 범위를 입력하고 학습 작업을 확정하면 계획을 생성할 수 있습니다." btn_label=next_step_label btn_url=next_step_url %} +
+ + {% else %} + {# ===== 3단계 : 평소 대시보드 ===== #} +
+
+

{{ today|date:"n월 j일 l" }}

+
+ 공부할 수 있는 날 {{ remaining_days }}일 남음 + · 오늘 배정 {{ today_minutes }}분 +
+
+ {# TODO: {% url 'planner:today' %} 로 교체 #} + + 오늘의 공부 시작 + +
+ + {% if pending_recovery %} + + {% endif %} + + {# ---- 전체 판정 ---- #} +
+
+
+
+ 시험기간 전체 판정 + + {{ overall.status_label }} + +
+ + {{ overall.viewed_at|date:"n.j H:i" }} 조회 기준 + +
+ + {% include "planner/includes/fit_bar.html" with status=overall.status min_pct=overall.min_pct band_pct=overall.band_pct mark_pct=overall.mark_pct need_label_pct=overall.need_label_pct min_minutes=overall.min_minutes max_minutes=overall.max_minutes available_minutes=overall.available_minutes axis_max=overall.axis_max %} + +
+
+
남은 가능시간
+
{{ overall.available_minutes }}
+
+
+
최소 필요시간
+
{{ overall.min_minutes }}
+
+
+
최대 필요시간
+
{{ overall.max_minutes }}
+
+
+
가능까지 부족
+
+ {% if overall.max_shortage_minutes %}-{{ overall.max_shortage_minutes }}{% else %}0{% endif %} +
+ {% if overall.daily_extra_minutes %} +
남은 {{ remaining_days }}일 동안 하루 +{{ overall.daily_extra_minutes }}분이면 가능
+ {% endif %} +
+
+
+
+ + + +
+ {% for s in subject_summary %} +
+ D-{{ s.d_day }} +
+
{{ s.subject_name }}
+
시험 {{ s.exam_date|date:"n월 j일" }}
+
+
+
{{ s.remaining_minutes }}분
+
남은 예상시간
+
+
+
{{ s.remaining_task_count }}개
+
남은 작업
+
+
+ {% endfor %} +
+ +
+

진행 상황

+ 완료 환산 기준 +
+ +
+
+
오늘 완료율
+
{{ progress.today_percent }}%
+
+
{{ progress.today_done_minutes }} / {{ progress.today_total_minutes }}분
+
+ +
+
전체 진행률
+
{{ progress.total_percent }}%
+
+
{{ progress.total_done_minutes }} / {{ progress.total_minutes }}분
+
+ +
+
남은 CORE 작업
+
{{ progress.core_left }}
+
필수 학습만 남긴 기준
+
+
+ {% endif %} + +
+
+
+ +{% include "includes/icons.html" %} +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/planner/templates/planner/includes/empty_state.html b/planner/templates/planner/includes/empty_state.html new file mode 100644 index 0000000..9358742 --- /dev/null +++ b/planner/templates/planner/includes/empty_state.html @@ -0,0 +1,27 @@ +
+
+ +

{{ title }}

+ {% if desc %}

{{ desc }}

{% endif %} + + {% if btn_label %} + {{ btn_label }} + {% endif %} + + {% if show_steps %} +
+
+
STEP 1
시험기간과 과목 등록 + 기간, 과목명, 시험일을 입력합니다 +
+
+
STEP 2
공부 가능시간 입력 + 날짜별 전체 시간만 넣으면 됩니다 +
+
+
STEP 3
시험 범위 넣기 + PDF 또는 직접 입력 +
+
+ {% endif %} +
\ No newline at end of file diff --git a/planner/templates/planner/includes/fit_bar.html b/planner/templates/planner/includes/fit_bar.html new file mode 100644 index 0000000..0ffae53 --- /dev/null +++ b/planner/templates/planner/includes/fit_bar.html @@ -0,0 +1,15 @@ +
+
+
+
+
+
+ 가능 {{ available_minutes }}분 +
+
+ +
+ 0분 + 필요 {{ min_minutes }} ~ {{ max_minutes }}분 + {{ axis_max }}분 +
\ No newline at end of file diff --git a/planner/templates/planner/includes/sidebar.html b/planner/templates/planner/includes/sidebar.html new file mode 100644 index 0000000..45c2d24 --- /dev/null +++ b/planner/templates/planner/includes/sidebar.html @@ -0,0 +1,47 @@ + \ No newline at end of file diff --git a/static/css/planner.css b/static/css/planner.css new file mode 100644 index 0000000..6398cf7 --- /dev/null +++ b/static/css/planner.css @@ -0,0 +1,227 @@ +.ico{width:18px;height:18px;flex:none;display:block; + stroke:currentColor;fill:none;stroke-width:1.8; + stroke-linecap:round;stroke-linejoin:round;overflow:visible} + +.app{display:grid;grid-template-columns:220px 1fr;min-height:100vh} +.app.collapsed{grid-template-columns:0 1fr} + +.side{ + background:var(--neutral1);border-right:1px solid var(--neutral4); + display:flex;flex-direction:column;position:sticky;top:0;height:100vh; + padding:0 12px 18px;overflow:hidden;transition:.2s; +} +.app.collapsed .side{padding:0;border-right:0} +.app.collapsed .side>*{display:none} + +.brand{ + height:60px;display:flex;align-items:center; + margin:0 -12px 12px;padding:0 22px; + border-bottom:1px solid var(--neutral4); + font-size:17px;font-weight:800;color:var(--neutral11);letter-spacing:-0.035em; +} +.brand em{font-style:normal;color:var(--brand4);margin-left:5px} + +.nav-i{ + display:flex;align-items:center;gap:11px;height:38px;padding:0 10px; + border-radius:9px;font-size:14px;font-weight:550;color:var(--neutral9);transition:.16s; +} +.nav-i .ico{width:17px;height:17px;color:var(--neutral7)} +.nav-i:hover{background:var(--neutral3)} +.nav-i.on{background:var(--neutral11);color:var(--neutral1)} +.nav-i.on .ico{color:var(--neutral1)} +.nav-i.off{color:var(--neutral6);pointer-events:none} +.nav-i .count{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--neutral7)} + +.side-label{font-size:11px;font-weight:700;color:var(--neutral7); + letter-spacing:.08em;padding:20px 10px 9px} +.side-box{padding:14px;border:1px solid var(--neutral5);border-radius:12px;background:var(--neutral2)} +.side-box.dashed{border-style:dashed;text-align:center} +.side-box h4{font-size:13px;font-weight:650;color:var(--neutral10)} +.side-box p{font-size:12px;color:var(--neutral7);line-height:1.6} +.side-box.alert{background:var(--judge2-bg);border-color:var(--judge2-line);border-style:solid} +.side-box.alert h4{color:var(--judge2);display:flex;align-items:center;gap:7px} +.side-box.alert p{color:var(--judge2);margin-top:6px} +.side-foot{margin-top:auto;padding-top:16px;border-top:1px solid var(--neutral4)} + +.main{min-width:0;display:flex;flex-direction:column} +.topbar{ + height:60px;background:var(--neutral1);border-bottom:1px solid var(--neutral4); + display:flex;align-items:center;gap:12px;padding:0 28px;position:sticky;top:0;z-index:20; +} +.topbar h2{font-size:15.5px;font-weight:700;color:var(--neutral11)} +.content{padding:34px 28px 44px;flex:1} + +.page-head{display:flex;align-items:flex-end;margin-bottom:26px} +.page-head h1{font-size:26px;font-weight:800;color:var(--neutral11);letter-spacing:-0.038em} +.page-head .sub{font-size:13.5px;color:var(--neutral8);margin-top:7px} +.page-head .sub b{color:var(--neutral9)} + +.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px} +.section-head h3{font-size:15px;font-weight:700;color:var(--neutral10)} +.section-head .note{font-family:var(--mono);font-size:11.5px;color:var(--neutral7); + padding:3px 8px;background:var(--neutral3);border-radius:6px} + + +.btn{ + display:inline-flex;align-items:center;justify-content:center;gap:7px; + height:40px;padding:0 16px;border-radius:var(--r-sm); + font-size:14px;font-weight:650;border:1px solid transparent;transition:.16s; +} +.btn .ico{width:16px;height:16px} +.btn-primary{background:var(--neutral11);color:var(--neutral1);box-shadow:var(--shadow)} +.btn-primary:hover{background:var(--neutral10)} +.btn-secondary{background:var(--neutral1);color:var(--neutral10);border-color:var(--neutral5)} +.btn-secondary:hover{background:var(--neutral2)} +.btn-ghost{color:var(--neutral8)} +.btn-ghost:hover{background:var(--neutral3)} +.btn-accent{background:var(--brand4);color:var(--neutral1)} +.btn-accent:hover{background:var(--brand5)} +.btn-lg{height:44px;padding:0 20px} +.btn-sm{height:32px;padding:0 12px;font-size:13px;border-radius:8px} +.btn-sm .ico{width:14px;height:14px} + +.icon-btn{width:34px;height:34px;border-radius:9px;color:var(--neutral8);position:relative; + display:inline-flex;align-items:center;justify-content:center} +.icon-btn:hover{background:var(--neutral3)} + + +/* ===== 카드 · 태그 · 배지 ===== */ +.card{background:var(--neutral1);border:1px solid var(--neutral5); + border-radius:var(--r-md);box-shadow:var(--shadow)} +.card-body{padding:24px} +.card-pad{padding:20px} + +.tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:6px; + font-size:11.5px;font-weight:650;background:var(--neutral11);color:var(--neutral1)} + +.badge{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 11px; + border-radius:999px;font-size:12px;font-weight:700;border:1px solid; + white-space:nowrap;flex:none} +.badge-ok{background:var(--judge1-bg);color:var(--judge1);border-color:var(--judge1-line)} +.badge-warn{background:var(--judge2-bg);color:var(--judge2);border-color:var(--judge2-line)} +.badge-bad{background:var(--judge3-bg);color:var(--judge3);border-color:var(--judge3-line)} +.dot{width:6px;height:6px;border-radius:50%} +.dot-ok{background:var(--judge1)} +.dot-warn{background:var(--judge2)} +.dot-bad{background:var(--judge3)} + + +.banner{display:flex;gap:14px;padding:18px 20px;border-radius:var(--r-md); + border:1px solid var(--judge2-line);background:var(--judge2-bg);color:var(--judge2)} +.banner .ico{width:19px;height:19px;margin-top:1px} +.banner .body{flex:1} +.banner h4{font-size:14.5px;font-weight:700;margin-bottom:5px} +.banner p{font-size:13.5px;line-height:1.65} +.banner .actions{display:flex;gap:8px;align-self:center} + + +.kpi-label{font-size:12px;color:var(--neutral8)} +.kpi-value{font-family:var(--mono);font-size:24px;font-weight:600; + color:var(--neutral11);letter-spacing:-0.035em;margin-top:7px} +.kpi-value small{font-family:var(--ui);font-size:14px;color:var(--neutral7);margin-left:3px} +.kpi-value.warn{color:var(--judge2)} +.kpi-sub{font-size:11.5px;color:var(--neutral7);margin-top:6px} + +.meter{height:6px;background:var(--neutral4);border-radius:999px;overflow:hidden} +.meter i{display:block;height:100%;background:var(--neutral11)} + + +.fit{position:relative;padding-top:24px} +.fit-scale{position:relative;height:44px;border-radius:var(--r-sm);overflow:hidden; + background:var(--neutral3);border:1px solid var(--neutral4)} + +.fit-band{position:absolute;top:0;bottom:0} +.fit-min{position:absolute;top:0;bottom:0;left:0;opacity:.5} +.fit-band.ok{background:var(--judge1-line)} .fit-min.ok{background:var(--judge1)} +.fit-band.warn{background:var(--judge2-line)} .fit-min.warn{background:var(--judge2)} +.fit-band.bad{background:var(--judge3-line)} .fit-min.bad{background:var(--judge3)} + +.fit-mark{position:absolute;top:20px;bottom:-4px;width:2px;background:var(--neutral11);z-index:2} +.fit-mark span{position:absolute;left:50%;transform:translateX(-50%);top:-21px; + font-family:var(--mono);font-size:10.5px;font-weight:600; + color:var(--neutral11);white-space:nowrap} + +.fit-caption{position:relative;height:15px;margin-top:9px; + font-family:var(--mono);font-size:11px;color:var(--neutral7)} +.fit-caption .start{position:absolute;left:0} +.fit-caption .end{position:absolute;right:0} +.fit-caption .need{position:absolute;transform:translateX(-50%);color:var(--neutral8)} + + +.subject-card{display:flex;align-items:center;gap:16px;padding:16px 20px; + border-bottom:1px solid var(--neutral4)} +.subject-card:last-child{border-bottom:0} +.subject-card .info{flex:1;min-width:0} +.subject-card .name{font-size:15px;font-weight:650;color:var(--neutral10)} +.subject-card .date{font-family:var(--mono);font-size:11.5px;color:var(--neutral7);margin-top:3px} +.subject-card .dday{font-family:var(--mono);font-size:13px;font-weight:600; + color:var(--neutral9);padding:4px 9px; + background:var(--neutral3);border-radius:6px} +.subject-card .dday.near{background:var(--judge2-bg);color:var(--judge2)} +.subject-card .left{text-align:right;flex:none;min-width:88px} +.subject-card .left .v{font-family:var(--mono);font-size:15px;font-weight:600;color:var(--neutral10)} +.subject-card .left .l{font-size:11.5px;color:var(--neutral7);margin-top:2px} + + +.empty{display:flex;flex-direction:column;align-items:center;text-align:center;padding:64px 40px} +.empty-icon{width:64px;height:64px;border-radius:20px;margin-bottom:24px; + background:var(--neutral3);border:1px solid var(--neutral5); + display:flex;align-items:center;justify-content:center} +.empty-icon .ico{width:28px;height:28px;color:var(--neutral7)} +.empty h3{font-size:19px;font-weight:750;color:var(--neutral11);margin-bottom:10px} +.empty p{font-size:14px;color:var(--neutral8);line-height:1.7;max-width:420px;margin-bottom:26px} + +.steps{display:flex;margin-top:36px;width:100%;max-width:720px} +.step{flex:1;text-align:left;padding:0 18px;border-right:1px solid var(--neutral4)} +.step:last-child{border-right:0} +.step .no{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--brand4);margin-bottom:7px} +.step b{display:block;font-size:13.5px;color:var(--neutral9);margin-bottom:4px} +.step span{font-size:12px;color:var(--neutral7);line-height:1.55} + + +.row{display:flex;align-items:center;gap:12px} +.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px} +.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:22px} +.ml-auto{margin-left:auto} + + +/* ===== 반응형 ===== */ +@media (max-width:1024px){ + .grid-3,.grid-4{grid-template-columns:repeat(2,1fr)} + .content{padding:28px 20px 40px} +} + +@media (max-width:767px){ + .app,.app.collapsed{grid-template-columns:1fr} + .side,.app.collapsed .side{ + position:fixed;left:0;right:0;bottom:0;top:auto;height:auto; + flex-direction:row;border:0;border-top:1px solid var(--neutral4); + padding:8px 6px 12px;z-index:50; + } + .app.collapsed .side>*{display:flex} + .brand,.side-label,.side-box,.side-foot{display:none!important} + .side nav{display:flex;flex:1} + .nav-i{flex:1;flex-direction:column;gap:4px;height:auto;padding:6px 0; + font-size:10.5px;background:transparent!important;color:var(--neutral7)} + .nav-i.on,.nav-i.on .ico{color:var(--neutral11)} + .nav-i .count{display:none} + + .content{padding:20px 16px 96px} + .grid-3,.grid-4{grid-template-columns:1fr} + .page-head{flex-direction:column;align-items:flex-start;gap:16px} + .page-head h1{font-size:23px} + .page-head .ml-auto{margin-left:0;width:100%} + .page-head .btn{width:100%} + .topbar{padding:0 16px} + .steps{flex-direction:column} + .step{border-right:0} + + .subject-card{flex-wrap:wrap;gap:12px;padding:16px} + .subject-card .info{flex:1 1 calc(100% - 62px);min-width:0} + .subject-card .name{font-size:14.5px} + .subject-card .left{ + flex:1 1 calc(50% - 6px);min-width:0;text-align:left; + padding-top:12px;border-top:1px solid var(--neutral4); + } + .subject-card .left .v{font-size:14px} +} \ No newline at end of file diff --git a/static/js/planner.js b/static/js/planner.js new file mode 100644 index 0000000..bbf3e0b --- /dev/null +++ b/static/js/planner.js @@ -0,0 +1,15 @@ +document.addEventListener('DOMContentLoaded', function () { + const app = document.querySelector('.app'); + const btn = document.getElementById('toggleSide'); + const open = document.getElementById('openSide'); + + if (sessionStorage.getItem('sideCollapsed') === '1') app.classList.add('collapsed'); + + function toggle() { + app.classList.toggle('collapsed'); + sessionStorage.setItem('sideCollapsed', app.classList.contains('collapsed') ? '1' : '0'); + } + + if (btn) btn.addEventListener('click', toggle); + if (open) open.addEventListener('click', toggle); +}); \ No newline at end of file diff --git a/templates/includes/icons.html b/templates/includes/icons.html new file mode 100644 index 0000000..34b4259 --- /dev/null +++ b/templates/includes/icons.html @@ -0,0 +1,32 @@ +{# 아이콘 (Lucide). 사용법: #} + + \ No newline at end of file