Some checks failed
ci / build (push) Failing after 5s
- 핵심 규칙 6: 완료의 정의는 게이트 통과. 사용자를 QA로 쓰지 않는다 - 5단계 기계 검사 4번째: design-gate 실행 의무화 - references/audit-gate.md: 사고-검사 매핑, SEO/meta 체크리스트, OS/브라우저 특성, 하니스 규칙 - tools/design-gate.mjs: 범용 게이트(메타/SEO·대비·수축·리듬·트랙·스케일×폭 + 옵션 L0/L3/L4, checks 깊은 병합) - 리듬·트랙 불변식: 숫자 라벨 등폭·빈 셀 트랙 균일·등간격 — 시간표 자동배치 결함 재현 픽스처 검출, 앱 15뷰 통과(오탐 0) feat(site): 관리 앱 2종 프로덕션 콘솔(v6→v18) - 가온 학적부 9뷰·두레 수강신청 6뷰: shadcn 문법, Pretendard/Noto Serif/IBM Plex 폰트 전략, 볼드 금지(400/500/600), WCAG AA 대비 전면 교정, 한글 keep-all 조판 - LMS 필수 요소(알림 센터·공지·진도·평가 유형·출결 사유·학점 경고), 12명 기준 데이터 정합, SEO 구조(h1 유일·OG/twitter·og.png) - 시간표 자동배치 결함 수리(명시적 격자 좌표) - QA 게이트: L0 stylelint/html-validate · L1 단위 30 · L2 감사 61+불변식 · L3 시각회귀 30화면 · L4 WebKit · L5 키보드 탐색 — npm run verify 실패 시 배포 금지 체인
1316 lines
76 KiB
HTML
1316 lines
76 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>가온 학적부 · 학습관리 대시보드</title>
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:title" content="가온 학적부 — 학습관리 콘솔" />
|
||
<meta property="og:url" content="https://designpaca.chanpaca.net/work/gaon-lms/" />
|
||
<meta property="og:image" content="https://designpaca.chanpaca.net/work/gaon-lms/og.png" />
|
||
<meta name="twitter:card" content="summary_large_image" />
|
||
<meta name="twitter:title" content="가온 학적부 — 학습관리 콘솔" />
|
||
<meta name="twitter:image" content="https://designpaca.chanpaca.net/work/gaon-lms/og.png" />
|
||
<meta
|
||
name="description"
|
||
content="학원 강사를 위한 학습관리 대시보드. 출석부, 수강생 기록, 과제 채점, 학사 일정까지 여덟 화면. 데모 데이터로 실제로 작동합니다."
|
||
/>
|
||
<meta property="og:title" content="가온 학적부 · 학습관리 대시보드" />
|
||
<meta property="og:description" content="출석부에서 출발한 학습관리 대시보드." />
|
||
<meta property="og:type" content="website" />
|
||
<meta name="theme-color" content="#F7F5EF" />
|
||
|
||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link
|
||
rel="stylesheet"
|
||
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
|
||
/>
|
||
<link
|
||
rel="stylesheet"
|
||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Noto+Serif+KR:wght@500;600&display=swap"
|
||
/>
|
||
<link rel="stylesheet" href="./styles/main.css?v=17" />
|
||
<link
|
||
rel="icon"
|
||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F7F5EF'/%3E%3Crect x='7' y='6' width='18' height='2.4' fill='%2324272B'/%3E%3Crect x='7' y='12' width='18' height='2.4' fill='%23F5D662'/%3E%3Crect x='7' y='18' width='12' height='2.4' fill='%2324272B'/%3E%3Crect x='7' y='24' width='18' height='2.4' fill='%23F5D662'/%3E%3C/svg%3E"
|
||
/>
|
||
</head>
|
||
|
||
<body>
|
||
<a class="skip" href="#main">본문으로 건너뛰기</a>
|
||
|
||
<h1 class="sr-only">가온 학적부</h1>
|
||
<div class="app">
|
||
<!-- 사이드 내비 — 여덟 화면을 세 묶음으로. 그룹 헤더가 묶음의 이름을 말한다 -->
|
||
<aside class="side">
|
||
<a class="brand" href="#main">
|
||
<span class="brand-mark" aria-hidden="true"></span>
|
||
가온 학적부
|
||
</a>
|
||
<nav class="side-nav" aria-label="화면">
|
||
<p class="side-group" aria-hidden="true">학적</p>
|
||
<button type="button" class="side-link is-current" data-view="dashboard" aria-current="page">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
|
||
대시보드
|
||
</button>
|
||
<button type="button" class="side-link" data-view="students">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||
수강생</button>
|
||
<button type="button" class="side-link" data-view="attendance">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 2v4"/><path d="M16 2v4"/><rect x="3" y="4" width="18" height="18" rx="2"/><path d="m9 14 2 2 4-4"/></svg>
|
||
출석부</button>
|
||
|
||
<p class="side-group" aria-hidden="true">수업</p>
|
||
<button type="button" class="side-link" data-view="grades">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M9 14h6"/><path d="M9 10h6"/></svg>
|
||
과제 · 성적</button>
|
||
<button type="button" class="side-link" data-view="files">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
|
||
자료실</button>
|
||
|
||
<p class="side-group" aria-hidden="true">학사</p>
|
||
<button type="button" class="side-link" data-view="calendar">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 2v4"/><path d="M16 2v4"/><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M3 10h18"/></svg>
|
||
학사 일정</button>
|
||
<button type="button" class="side-link" data-view="counsel">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||
상담 기록</button>
|
||
<button type="button" class="side-link" data-view="report">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>
|
||
학기 리포트</button>
|
||
<button type="button" class="side-link" data-view="notice">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>
|
||
공지 · 알림</button>
|
||
</nav>
|
||
<p class="side-foot">
|
||
강사 김가온 · 3월 2주차<br />
|
||
<span class="mono">2026학년도 1학기</span>
|
||
</p>
|
||
</aside>
|
||
|
||
<main id="main" class="main">
|
||
<div class="topbar">
|
||
<p class="crumb">가온 학적부 <span aria-hidden="true">/</span> <strong id="crumb-view">대시보드</strong></p>
|
||
<div class="topbar-right">
|
||
<span class="term-badge mono">2026학년도 1학기 · 3월 2주차</span>
|
||
<div class="bell-wrap">
|
||
<button type="button" class="bell" id="bell" aria-label="읽지 않은 알림 3건" aria-expanded="false" aria-haspopup="true">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
|
||
<span class="bell-dot" id="bell-dot" aria-hidden="true"></span>
|
||
</button>
|
||
<div class="bell-menu" id="bell-menu">
|
||
<div class="bell-head">
|
||
알림
|
||
<button type="button" id="bell-all">모두 읽음 처리</button>
|
||
</div>
|
||
<div class="ntc is-unread">
|
||
<span class="n-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M9 14h6"/><path d="M9 10h6"/></svg></span>
|
||
<p>자기소개 논술 초고 2명 제출 · 1명 미제출 — 채점 대기 <time>10분 전</time></p>
|
||
</div>
|
||
<div class="ntc is-unread">
|
||
<span class="n-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect x="3" y="4" width="18" height="18" rx="2"/><path d="m9 14 2 2 4-4"/></svg></span>
|
||
<p>국어 논술 A 미응시 1명 — 학부모 알림톡 발송 완료 <time>1시간 전</time></p>
|
||
</div>
|
||
<div class="ntc is-unread">
|
||
<span class="n-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg></span>
|
||
<p>새 공지 “3월 단원 평가 일정 안내” 등록 <time>어제</time></p>
|
||
</div>
|
||
<div class="ntc is-read">
|
||
<span class="n-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M3 10h18"/></svg></span>
|
||
<p>강이안 학생 보강 신청 — 3/12(목) 17:00 코딩 입문 <time>어제</time></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span class="avatar">김</span>
|
||
</div>
|
||
</div>
|
||
<!-- ── 대시보드 ─────────────────────────────────────────────── -->
|
||
<section class="view is-on" id="view-dashboard" aria-labelledby="dash-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="dash-h">이번 주, 열두 명 중 열 명</h2>
|
||
<p class="view-sub">3월 9일 월요일 오전 기준. 명단 12명 중 재학 10명, 주간 출석은 지난 주(3/2–3/6) 결산입니다.</p>
|
||
</div>
|
||
<p class="stamp" aria-hidden="true">3월 2주</p>
|
||
</header>
|
||
|
||
<dl class="headline">
|
||
<div><dt>명단</dt><dd>12명</dd><dd class="stat-sub">재학 10 · 휴학 2 · 이번 달 전입 1</dd></div>
|
||
<div><dt>주간 출석률</dt><dd>92%</dd><dd class="stat-sub">출석 46 · 지각 2 · 결석 2 (지난 주 · 재학 10명)</dd></div>
|
||
<div><dt>미채점 과제</dt><dd>2건</dd><dd class="stat-sub">세트 4 · 워크시트 채점 대기 — 3/11 마감</dd></div>
|
||
</dl>
|
||
|
||
<div class="dash-grid">
|
||
<!-- 오늘의 수업: 카드가 아니라 장부의 행 -->
|
||
<section class="panel ledger" aria-labelledby="today-h">
|
||
<h2 id="today-h">오늘의 수업</h2>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<caption class="sr-only">오늘의 수업 목록</caption>
|
||
<thead>
|
||
<tr><th scope="col">시간</th><th scope="col">과목</th><th scope="col">강의실</th><th scope="col">상태</th><th scope="col" class="num">출석</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td class="mono">10:00–11:30</td>
|
||
<td>수학 심화 B</td><td>2실</td>
|
||
<td><span class="state state-run">진행 중</span></td>
|
||
<td class="num"><span class="hl">3 / 3</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="mono">14:00–15:30</td>
|
||
<td>국어 논술 A</td><td>1실</td>
|
||
<td><span class="state state-off">예정</span></td>
|
||
<td class="num">2 / 3 · <span class="t-warn">미응시 1</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="mono">16:30–18:00</td>
|
||
<td>코딩 입문</td><td>3실</td>
|
||
<td><span class="state state-off">예정</span></td>
|
||
<td class="num">2 / 2</td>
|
||
</tr>
|
||
</tbody>
|
||
<tfoot>
|
||
<tr><td colspan="5" class="tsum">금일 3교시 · 남은 수업 2교시 — 국어 논술 A 미응시 1명(한지우) 사유 확인 필요</td></tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 주간 출석: 쌓인 막대. 상태색 3종은 상태 예외 -->
|
||
<section class="panel week-panel" aria-labelledby="week-h">
|
||
<h2 id="week-h">주간 출석</h2>
|
||
<figure class="week-chart">
|
||
<svg viewBox="0 0 320 150" role="img" aria-label="주간 출석 막대 — 월 10 전원, 화 9 출석 1 지각, 수 10, 목 9 출석 1 결석, 금 8 출석 1 지각 1 결석">
|
||
<g class="axis">
|
||
<line x1="28" y1="8" x2="28" y2="122" />
|
||
<line x1="28" y1="122" x2="308" y2="122" />
|
||
</g>
|
||
<g class="bars">
|
||
<g transform="translate(44 0)"><rect y="22" width="26" height="100" class="b-att" /></g>
|
||
<g transform="translate(101 0)"><rect y="32" width="26" height="90" class="b-att" /><rect y="25" width="26" height="7" class="b-late" /></g>
|
||
<g transform="translate(158 0)"><rect y="22" width="26" height="100" class="b-att" /></g>
|
||
<g transform="translate(215 0)"><rect y="32" width="26" height="90" class="b-att" /><rect y="25" width="26" height="7" class="b-abs" /></g>
|
||
<g transform="translate(272 0)"><rect y="42" width="26" height="80" class="b-att" /><rect y="35" width="26" height="7" class="b-late" /><rect y="28" width="26" height="7" class="b-abs" /></g>
|
||
</g>
|
||
<g class="axis-labels">
|
||
<text x="57" y="138" text-anchor="middle">월</text>
|
||
<text x="114" y="138" text-anchor="middle">화</text>
|
||
<text x="171" y="138" text-anchor="middle">수</text>
|
||
<text x="228" y="138" text-anchor="middle">목</text>
|
||
<text x="285" y="138" text-anchor="middle">금</text>
|
||
</g>
|
||
</svg>
|
||
<figcaption>
|
||
<span class="key"><i class="k-att"></i>출석</span>
|
||
<span class="key"><i class="k-late"></i>지각</span>
|
||
<span class="key"><i class="k-abs"></i>결석</span>
|
||
</figcaption>
|
||
</figure>
|
||
<table class="wk-sum">
|
||
<caption class="sr-only">요일별 출석 결산</caption>
|
||
<thead><tr><th scope="col">요일</th><th scope="col" class="num">출석</th><th scope="col" class="num">지각</th><th scope="col" class="num">결석</th><th scope="col" class="num">출석률</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>월 3/2</td><td class="num">10</td><td class="num">0</td><td class="num">0</td><td class="num">100%</td></tr>
|
||
<tr><td>화 3/3</td><td class="num">9</td><td class="num">1</td><td class="num">0</td><td class="num">90%</td></tr>
|
||
<tr><td>수 3/4</td><td class="num">10</td><td class="num">0</td><td class="num">0</td><td class="num">100%</td></tr>
|
||
<tr><td>목 3/5</td><td class="num">9</td><td class="num">0</td><td class="num">1</td><td class="num">90%</td></tr>
|
||
<tr><td>금 3/6</td><td class="num">8</td><td class="num">1</td><td class="num">1</td><td class="num">80%</td></tr>
|
||
</tbody>
|
||
<tfoot><tr><td>합계</td><td class="num">46</td><td class="num">2</td><td class="num">2</td><td class="num"><span class="hl">92%</span></td></tr></tfoot>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- 마감 임박 -->
|
||
<section class="panel" aria-labelledby="due-h">
|
||
<h2 id="due-h">마감 임박 과제</h2>
|
||
<ul class="due-list">
|
||
<li><button type="button" class="due-row" data-assign="a1">
|
||
<span>미분 응용 세트 4</span>
|
||
<span class="mono muted">수학 심화 B · 3/11 마감 · <strong class="hl">1명 미제출</strong></span>
|
||
</button></li>
|
||
<li><button type="button" class="due-row" data-assign="a2">
|
||
<span>자기소개 논술 초고</span>
|
||
<span class="mono muted">국어 논술 A · 3/12 마감 · 1명 미제출</span>
|
||
</button></li>
|
||
<li><button type="button" class="due-row" data-assign="a3">
|
||
<span>변수와 조건 워크시트</span>
|
||
<span class="mono muted">코딩 입문 · 3/13 마감 · 1명 미제출</span>
|
||
</button></li>
|
||
</ul>
|
||
<p class="panel-note muted">행을 누르면 채점 화면이 열립니다.</p>
|
||
</section>
|
||
</div>
|
||
|
||
<section class="panel" aria-labelledby="prog-h">
|
||
<h2 id="prog-h">수업 진행 <span class="h-note">3월 2주차 기준 · 개설 과목 4개</span></h2>
|
||
<div class="prog-grid">
|
||
<div class="prog">
|
||
<div class="prog-head"><strong>수학 심화 B</strong><span class="mono muted">12/16</span></div>
|
||
<div class="prog-bar" role="img" aria-label="수학 심화 B 진행률 75퍼센트"><i style="width: 75%"></i></div>
|
||
<p class="prog-meta">75% · 이번 주 2차시 완료 · 단원 평가 D-5</p>
|
||
</div>
|
||
<div class="prog">
|
||
<div class="prog-head"><strong>국어 논술 A</strong><span class="mono muted">8/12</span></div>
|
||
<div class="prog-bar" role="img" aria-label="국어 논술 A 진행률 67퍼센트"><i style="width: 67%"></i></div>
|
||
<p class="prog-meta">67% · 이번 주 2차시 완료 · 초고 채점 중</p>
|
||
</div>
|
||
<div class="prog">
|
||
<div class="prog-head"><strong>코딩 입문</strong><span class="mono muted">5/10</span></div>
|
||
<div class="prog-bar" role="img" aria-label="코딩 입문 진행률 50퍼센트"><i style="width: 50%"></i></div>
|
||
<p class="prog-meta">50% · 이번 주 1차시 완료 · 워크시트 2명 미제출</p>
|
||
</div>
|
||
<div class="prog">
|
||
<div class="prog-head"><strong>과학 실험</strong><span class="mono muted">9/12</span></div>
|
||
<div class="prog-bar" role="img" aria-label="수학 심화 B 진행률 75퍼센트"><i style="width: 75%"></i></div>
|
||
<p class="prog-meta">75% · 이번 주 2차시 완료 · 리포트 채점 완료</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<!-- ── 공지 · 알림 ──────────────────────────────────────────── -->
|
||
<section class="view" id="view-notice" aria-labelledby="no-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="no-h">공지 · 알림</h2>
|
||
<p class="view-sub">반에 전달한 안내입니다. 행을 누르면 본문이 열리고 읽음 처리됩니다.</p>
|
||
</div>
|
||
</header>
|
||
<div class="toolbar">
|
||
<div class="chips" role="group" id="nt-chips">
|
||
<button type="button" class="chip is-on" data-nt="전체" aria-pressed="true">전체</button>
|
||
<button type="button" class="chip" data-nt="시험" aria-pressed="false">시험</button>
|
||
<button type="button" class="chip" data-nt="수업" aria-pressed="false">수업</button>
|
||
<button type="button" class="chip" data-nt="소통" aria-pressed="false">소통</button>
|
||
<button type="button" class="chip" data-nt="행정" aria-pressed="false">행정</button>
|
||
</div>
|
||
</div>
|
||
<div class="panel">
|
||
<ul class="notice-list">
|
||
<li><button type="button" class="notice-row" aria-expanded="false">
|
||
<span class="n-t"><svg class="pin" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>3월 단원 평가 일정 안내</span>
|
||
<span class="tag tag-exam">시험</span>
|
||
<span class="n-meta"><span>3/14 16:00 · 김가온</span><span>읽은 사람 11 / 12</span></span>
|
||
<span class="n-body">3/17(화) 19:00 수학 심화 B 단원 평가를 실시합니다. 범위는 미분 응용 1~3단원이며 지참물은 필기구와 계산기입니다. 당일 결석자는 3/19(목) 보강 평가로 대체하며, 평가 결과는 3/20 자정 전 학부모 알림톡으로 발송됩니다.</span>
|
||
</button></li>
|
||
<li><button type="button" class="notice-row is-unread" aria-expanded="false">
|
||
<span class="n-t">미분 응용 세트 4 마감 연장</span>
|
||
<span class="tag">수업</span>
|
||
<span class="n-meta"><span>3/11 08:30 · 김가온</span><span>읽은 사람 8 / 12</span></span>
|
||
<span class="n-body">제출 시스템 오류 문의가 이어져 마감을 하루 연장합니다. 새 마감은 3/12(목) 23:59이며, 기존 제출분은 그대로 유효합니다.</span>
|
||
</button></li>
|
||
<li><button type="button" class="notice-row is-unread" aria-expanded="false">
|
||
<span class="n-t">강이안 학생 보강 수업 배정</span>
|
||
<span class="tag">소통</span>
|
||
<span class="n-meta"><span>3/10 14:00 · 김가온</span><span>읽은 사람 3 / 3</span></span>
|
||
<span class="n-body">3/12(목) 17:00 코딩 입문 5차시 보강이 배정되었습니다. 보호자에게 알림톡을 발송했으니 시간을 확인해 주세요.</span>
|
||
</button></li>
|
||
<li><button type="button" class="notice-row" aria-expanded="false">
|
||
<span class="n-t">이번 주 학부모 알림톡 발송 내역</span>
|
||
<span class="tag">소통</span>
|
||
<span class="n-meta"><span>3/9 19:00 · 시스템</span><span>읽은 사람 10 / 12</span></span>
|
||
<span class="n-body">이번 주 결석 2건 모두 발송이 완료되었습니다. 목요일 병결 1건은 답변을 받아 기록에 남겼습니다.</span>
|
||
</button></li>
|
||
<li><button type="button" class="notice-row" aria-expanded="false">
|
||
<span class="n-t">4월 학사 일정 초안 의견을 받습니다</span>
|
||
<span class="tag">행정</span>
|
||
<span class="n-meta"><span>3/8 10:00 · 김가온</span><span>읽은 사람 9 / 12</span></span>
|
||
<span class="n-body">4월 중간고사 기간(4/14~4/16)과 그에 맞춘 수업 커리큘럼 조정안을 자료실에 올렸습니다. 3/20까지 의견을 주세요.</span>
|
||
</button></li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── 수강생 ───────────────────────────────────────────────── -->
|
||
<section class="view" id="view-students" aria-labelledby="st-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="st-h">수강생 기록</h2>
|
||
<p class="view-sub">행을 누르면 상담 기록까지 볼 수 있습니다.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="statline" id="stu-stats"></div>
|
||
|
||
<div class="toolbar" role="search">
|
||
<label class="sr-only" for="q">수강생 검색</label>
|
||
<input id="q" type="search" placeholder="이름, 과목으로 검색" autocomplete="off" />
|
||
<div class="chips" role="group">
|
||
<button type="button" class="chip is-on" data-filter="all" aria-pressed="true">전체 12</button>
|
||
<button type="button" class="chip" data-filter="재학" aria-pressed="false">재학 10</button>
|
||
<button type="button" class="chip" data-filter="휴학" aria-pressed="false">휴학 2</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel table-wrap">
|
||
<table id="stu-table">
|
||
<caption class="sr-only">수강생 목록 — 이름, 학년, 과목, 보호자, 출석률, 과제 제출, 상태</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">이름</th><th scope="col">학년</th><th scope="col">주 과목</th>
|
||
<th scope="col">보호자</th>
|
||
<th scope="col" class="num">출석률</th>
|
||
<th scope="col" class="num">과제 제출</th><th scope="col">상태</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><!-- JS 가 그린다 --></tbody>
|
||
</table>
|
||
<p class="panel-note muted" id="stu-empty" hidden>조건에 맞는 수강생이 없습니다 — 검색어를 줄이거나 필터를 ‘전체’로 바꿔 보세요.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── 출석부 ───────────────────────────────────────────────── -->
|
||
<section class="view" id="view-attendance" aria-labelledby="att-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="att-h">출석부</h2>
|
||
<p class="view-sub">칸을 누르면 출석 · 지각 · 결석이 순환합니다. 반과 주를 바꿔가며 적습니다.</p>
|
||
</div>
|
||
<p class="stamp" aria-hidden="true">3월</p>
|
||
</header>
|
||
|
||
<div class="toolbar">
|
||
<div class="chips" role="group" id="att-class"></div>
|
||
<div class="week-nav">
|
||
<button type="button" class="btn-line" id="att-prev">이전 주</button>
|
||
<span class="mono week-label" id="att-week" aria-live="polite">3월 2주</span>
|
||
<button type="button" class="btn-line" id="att-next">다음 주</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="statline" id="att-stats" role="status"></div>
|
||
|
||
<div class="panel table-wrap">
|
||
<table id="att-table">
|
||
<caption class="sr-only">주간 출석부 — 학생별 월요일부터 금요일까지 출석 상태</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">이름</th>
|
||
<th scope="col" class="att-day">월</th><th scope="col" class="att-day">화</th>
|
||
<th scope="col" class="att-day">수</th><th scope="col" class="att-day">목</th>
|
||
<th scope="col" class="att-day">금</th>
|
||
<th scope="col" class="num">이번 주</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><!-- JS 가 그린다 --></tbody>
|
||
<tfoot id="att-foot"><!-- JS: 일별 합계 --></tfoot>
|
||
</table>
|
||
<p class="panel-note muted" id="att-status" role="status"></p>
|
||
<div class="att-chips">
|
||
<span class="tag">병결 1</span>
|
||
<span class="tag">조퇴 1</span>
|
||
<span class="tag tag-warn">사유 검토 1</span>
|
||
<span class="tag">보강 배정 1</span>
|
||
<span class="tag tag-ok">학부모 알림톡 2/2 발송</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── 과제 · 성적 ──────────────────────────────────────────── -->
|
||
<section class="view" id="view-grades" aria-labelledby="gr-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="gr-h">과제 · 성적</h2>
|
||
<p class="view-sub">채점 대기 행을 눌러 점수를 남깁니다.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="statline">
|
||
<div>채점 완료 <strong>3</strong></div>
|
||
<div>채점 대기 <strong class="warn-ink">2</strong></div>
|
||
<div>자동 채점 <strong>1</strong></div>
|
||
<div>예정 <strong class="muted-ink">1</strong></div>
|
||
<div>평균 제출률 <strong>83%</strong></div>
|
||
</div>
|
||
|
||
<div class="toolbar">
|
||
<div class="chips" role="group" id="as-chips">
|
||
<button type="button" class="chip is-on" data-as="all" aria-pressed="true">전체</button>
|
||
<button type="button" class="chip" data-as="수학 심화 B" aria-pressed="false">수학 심화 B</button>
|
||
<button type="button" class="chip" data-as="국어 논술 A" aria-pressed="false">국어 논술 A</button>
|
||
<button type="button" class="chip" data-as="코딩 입문" aria-pressed="false">코딩 입문</button>
|
||
<button type="button" class="chip" data-as="과학 실험" aria-pressed="false">과학 실험</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel table-wrap">
|
||
<table id="as-table">
|
||
<caption class="sr-only">과제 목록</caption>
|
||
<thead>
|
||
<tr><th scope="col">과제</th><th scope="col">유형</th><th scope="col">과목</th><th scope="col">마감</th><th scope="col" class="num">제출</th><th scope="col">상태</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<th scope="row"><button type="button" class="row-open" data-assign="a1">미분 응용 세트 3</button></th>
|
||
<td><span class="tag">과제</span></td>
|
||
<td>수학 심화 B</td><td class="mono">03-04</td>
|
||
<td class="num mono">3/3</td><td><span class="state">채점 완료</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><button type="button" class="row-open" data-assign="a2">자기소개 논술 초고</button></th>
|
||
<td><span class="tag">과제</span></td>
|
||
<td>국어 논술 A</td><td class="mono">03-12</td>
|
||
<td class="num mono">2/3</td><td><span class="state state-wait">채점 대기</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><button type="button" class="row-open" data-assign="a3">변수와 조건 워크시트</button></th>
|
||
<td><span class="tag">과제</span></td>
|
||
<td>코딩 입문</td><td class="mono">03-13</td>
|
||
<td class="num mono">1/2</td><td><span class="state state-wait">채점 대기</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><button type="button" class="row-open" data-assign="a4">측정과 그래프 리포트</button></th>
|
||
<td><span class="tag">과제</span></td>
|
||
<td>과학 실험</td><td class="mono">03-06</td>
|
||
<td class="num mono">3/3</td><td><span class="state">채점 완료</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">단원 평가 퀴즈 — 미분 응용</th>
|
||
<td><span class="tag tag-quiz">퀴즈</span></td>
|
||
<td>수학 심화 B</td><td class="mono">03-08</td>
|
||
<td class="num mono">3/3</td><td><span class="state">자동 채점</span></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">3월 단원 평가</th>
|
||
<td><span class="tag tag-exam">시험</span></td>
|
||
<td>수학 심화 B</td><td class="mono">03-17</td>
|
||
<td class="num mono">—</td><td><span class="state state-off">예정</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<section class="panel dist" aria-labelledby="dist-h">
|
||
<h2 id="dist-h">수학 심화 B · 세트 3 점수 분포</h2>
|
||
<div class="dist-body">
|
||
<figure class="dist-chart">
|
||
<svg viewBox="0 0 340 150" role="img" aria-label="점수 분포 — 70점 1명, 80점 3명, 90점 4명">
|
||
<g class="axis">
|
||
<line x1="34" y1="8" x2="34" y2="122" /><line x1="34" y1="122" x2="326" y2="122" />
|
||
</g>
|
||
<g class="bars">
|
||
<g transform="translate(58 0)"><rect y="97" width="44" height="25" class="b-att" /><text x="22" y="88" text-anchor="middle">1명</text></g>
|
||
<g transform="translate(148 0)"><rect y="47" width="44" height="75" class="b-att" /><text x="22" y="38" text-anchor="middle">3명</text></g>
|
||
<g transform="translate(238 0)"><rect y="22" width="44" height="100" class="b-hl" /><text x="22" y="13" text-anchor="middle">4명</text></g>
|
||
</g>
|
||
<g class="axis-labels">
|
||
<text x="80" y="138" text-anchor="middle">70점</text>
|
||
<text x="170" y="138" text-anchor="middle">80점</text>
|
||
<text x="260" y="138" text-anchor="middle">90점</text>
|
||
</g>
|
||
</svg>
|
||
<figcaption>점수 구간별 인원 · 채점 인원 8명</figcaption>
|
||
</figure>
|
||
<dl class="dist-stats">
|
||
<div><dt>평균</dt><dd>84.4점</dd></div>
|
||
<div><dt>최고 · 최저</dt><dd>96 · 70점</dd></div>
|
||
<div><dt>90점 이상</dt><dd>4명 <span class="muted">(50%)</span></dd></div>
|
||
<div><dt>재응시 대상</dt><dd>70점대 1명</dd></div>
|
||
<div><dt>미제출</dt><dd>0명</dd></div>
|
||
</dl>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<!-- ── 자료실 ───────────────────────────────────────────────── -->
|
||
<section class="view" id="view-files" aria-labelledby="fi-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="fi-h">자료실</h2>
|
||
<p class="view-sub">배부 자료의 원장. 배부를 눌렀는지에 따라 도장이 남습니다.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="statline" id="file-stats"></div>
|
||
|
||
<div class="toolbar">
|
||
<div class="chips" role="group" id="file-chips">
|
||
<button type="button" class="chip is-on" data-kind="all" aria-pressed="true">전체</button>
|
||
<button type="button" class="chip" data-kind="인쇄물" aria-pressed="false">인쇄물</button>
|
||
<button type="button" class="chip" data-kind="PDF" aria-pressed="false">PDF</button>
|
||
<button type="button" class="chip" data-kind="양식" aria-pressed="false">양식</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel table-wrap">
|
||
<table>
|
||
<caption class="sr-only">배부 자료 목록</caption>
|
||
<thead>
|
||
<tr><th scope="col">자료</th><th scope="col">과목</th><th scope="col">종류</th><th scope="col" class="num">크기</th><th scope="col" class="num">조회</th><th scope="col" class="num">날인일</th><th scope="col">배부</th></tr>
|
||
</thead>
|
||
<tbody id="file-body"><!-- JS --></tbody>
|
||
</table>
|
||
<p class="panel-note muted">다운로드 링크를 만들지 않는다 — 이 데모에는 파일이 없다. 배부 상태만 기록한다.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── 학사 일정 ─────────────────────────────────────────────── -->
|
||
<section class="view" id="view-calendar" aria-labelledby="cal-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="cal-h">학사 일정 · 2026년 3월</h2>
|
||
<p class="view-sub">마감과 휴원을 먼저 보는 달력. 오른쪽은 3월 9일 이후의 일정입니다.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="statline" id="cal-stats"></div>
|
||
|
||
<div class="cal-layout">
|
||
<div class="panel cal-panel" aria-hidden="false">
|
||
<table class="cal" id="cal-table">
|
||
<caption class="sr-only">2026년 3월 달력 — 월요일 시작, 일정이 있는 날은 제목이 적힙니다</caption>
|
||
<thead>
|
||
<tr><th scope="col">월</th><th scope="col">화</th><th scope="col">수</th><th scope="col">목</th><th scope="col">금</th><th scope="col">토</th><th scope="col">일</th></tr>
|
||
</thead>
|
||
<tbody><!-- JS --></tbody>
|
||
</table>
|
||
<div class="cal-legend">
|
||
<span class="key"><i class="lg-exam"></i>평가 · 시험</span>
|
||
<span class="key"><i class="lg-due"></i>과제 마감</span>
|
||
<span class="key"><i class="lg-admin"></i>행정 · 상담</span>
|
||
<span class="key"><i class="lg-holi"></i>휴원</span>
|
||
</div>
|
||
</div>
|
||
<section class="panel" aria-labelledby="up-h">
|
||
<h2 id="up-h">다가오는 일정</h2>
|
||
<ul class="due-list" id="up-list"><!-- JS --></ul>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── 상담 기록 ─────────────────────────────────────────────── -->
|
||
<section class="view" id="view-counsel" aria-labelledby="co-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="co-h">상담 기록</h2>
|
||
<p class="view-sub">수강생 화면에 남긴 기록을 모아 봅니다. 카드를 누르면 그 학생의 기록이 열립니다.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="toolbar">
|
||
<div class="chips" role="group" id="co-chips">
|
||
<button type="button" class="chip is-on" data-sub="all" aria-pressed="true">전 과목</button>
|
||
<button type="button" class="chip" data-sub="수학 심화 B" aria-pressed="false">수학 심화 B</button>
|
||
<button type="button" class="chip" data-sub="국어 논술 A" aria-pressed="false">국어 논술 A</button>
|
||
<button type="button" class="chip" data-sub="코딩 입문" aria-pressed="false">코딩 입문</button>
|
||
<button type="button" class="chip" data-sub="과학 실험" aria-pressed="false">과학 실험</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="statline" id="co-stats"></div>
|
||
|
||
<div class="counsel-grid" id="counsel-grid"><!-- JS --></div>
|
||
<p class="empty-note muted" id="counsel-empty" hidden>조건에 맞는 상담 기록이 없습니다.</p>
|
||
|
||
<section class="panel" aria-labelledby="co-next-h" style="margin-top: var(--space-4);">
|
||
<h2 id="co-next-h">예정된 상담 <span class="h-note">3월 9일 기준</span></h2>
|
||
<ul class="due-list">
|
||
<li><p class="due-row is-static">
|
||
<span>오세림 학부모 상담 — 추가 과외 일정 조율</span>
|
||
<span class="mono muted">3/12(목) 18:00 <span class="tag tag-ok">알림톡 발송</span></span>
|
||
</p></li>
|
||
<li><p class="due-row is-static">
|
||
<span>박서연 진로 상담 — 영재 교육원 지원 검토</span>
|
||
<span class="mono muted">3/13(금) 17:30 <span class="tag">발송 대기</span></span>
|
||
</p></li>
|
||
<li><p class="due-row is-static">
|
||
<span>한지우 학습 상담 — 등교 시간 재조정 후속</span>
|
||
<span class="mono muted">3/16(월) 19:00 <span class="tag">발송 대기</span></span>
|
||
</p></li>
|
||
</ul>
|
||
</section>
|
||
</section>
|
||
|
||
<!-- ── 학기 리포트 ───────────────────────────────────────────── -->
|
||
<section class="view" id="view-report" aria-labelledby="re-h" tabindex="-1">
|
||
<header class="view-head">
|
||
<div>
|
||
<h2 class="v-h" id="re-h">학기 리포트</h2>
|
||
<p class="view-sub">아래 표는 명단에서 직접 계산합니다. 채점이 끝나지 않은 과목의 점수는 비워 둡니다.</p>
|
||
</div>
|
||
<p class="stamp" aria-hidden="true">1학기 중간</p>
|
||
</header>
|
||
|
||
<div class="headline headline-4" id="rep-stats"><!-- JS --></div>
|
||
|
||
<div class="panel table-wrap">
|
||
<table id="report-table">
|
||
<caption class="sr-only">과목별 학기 요약 — 재원, 평균 출석률, 과제 제출률, 채점 평균</caption>
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">과목</th><th scope="col" class="num">재원</th>
|
||
<th scope="col" class="num">평균 출석률</th><th scope="col" class="num">과제 제출률</th>
|
||
<th scope="col" class="num">채점 평균</th><th scope="col" class="num">진행 차시</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody><!-- JS 가 그린다 --></tbody>
|
||
</table>
|
||
<p class="panel-note muted">휴학생은 재원과 계산에서 제외합니다. 채점 평균은 완료된 과제만 반영합니다. 학부모 리포트는 3/20에 알림톡으로 일괄 발송됩니다 — 대상 재학생 보호자 전원.</p>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ── 수강생 상세 모달: 학적 카드 ── -->
|
||
<!-- ── 수강생 상세: 펼침 장부. 왼쪽 표지판 + 오른쪽 내용판 ── -->
|
||
<dialog id="stu-dialog" class="modal modal-split" aria-labelledby="stu-dlg-h">
|
||
<div class="mspread">
|
||
<div class="mcover">
|
||
<span class="mcover-mark" id="stu-mark" aria-hidden="true">가</span>
|
||
<span class="mcover-seal" id="stu-seal" aria-hidden="true">가</span>
|
||
<h2 class="mcover-name" id="stu-dlg-h">수강생</h2>
|
||
<p class="mcover-meta" id="stu-meta">주 과목 · 상태</p>
|
||
<dl class="mcover-stats">
|
||
<div><dt>출석률</dt><dd class="mono" id="stu-att">—</dd></div>
|
||
<div><dt>과제 제출</dt><dd class="mono" id="stu-hw">—</dd></div>
|
||
<div><dt>상담 기록</dt><dd class="mono" id="stu-memo-n">—</dd></div>
|
||
</dl>
|
||
<p class="mcover-foot mono">가온 학적부 · 2026-1</p>
|
||
</div>
|
||
<div class="msheet">
|
||
<button type="button" class="modal-close" data-close aria-label="닫기">
|
||
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
|
||
</button>
|
||
|
||
<section class="msec">
|
||
<h3>보호자 · 알림</h3>
|
||
<p class="guard-line" id="stu-guardian">—</p>
|
||
<ul class="guard-log mono" id="stu-alklog">
|
||
<li>3/05 출결 알림톡 발송 완료</li>
|
||
<li>2/28 월간 학습 리포트 발송 완료</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="msec">
|
||
<h3>3월 2주까지의 출석</h3>
|
||
<div class="att-strip" id="stu-att-strip"></div>
|
||
</section>
|
||
|
||
<section class="msec">
|
||
<h3>상담 기록</h3>
|
||
<ul id="stu-note-list"></ul>
|
||
<form class="note-form" id="note-form">
|
||
<label class="sr-only" for="note-input">새 상담 기록</label>
|
||
<input id="note-input" type="text" placeholder="3/10 학부모 상담 — 심야 스터디 조율" required />
|
||
<button type="submit" class="btn-line">기록 추가</button>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- ── 채점 모달: 점수를 넣으면 평균이 따라온다 ── -->
|
||
<dialog id="as-dialog" class="modal" aria-labelledby="as-dlg-h">
|
||
<div class="modal-inner">
|
||
<button type="button" class="modal-close" data-close aria-label="닫기">
|
||
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
|
||
</button>
|
||
<h2 class="modal-title" id="as-dlg-h">채점</h2>
|
||
<p class="modal-meta mono" id="as-dlg-meta">과제 · 과목</p>
|
||
<div class="table-wrap">
|
||
<table class="grade-table">
|
||
<caption class="sr-only">제출자 목록과 점수 입력</caption>
|
||
<thead><tr><th scope="col">제출자</th><th scope="col" class="num">점수</th></tr></thead>
|
||
<tbody>
|
||
<tr><th scope="row">박서연</th><td class="num"><label class="sr-only" for="g1">박서연 점수</label><input id="g1" class="score" type="number" min="0" max="100" value="92" /></td></tr>
|
||
<tr><th scope="row">김도현</th><td class="num"><label class="sr-only" for="g2">김도현 점수</label><input id="g2" class="score" type="number" min="0" max="100" value="88" /></td></tr>
|
||
<tr><th scope="row">이수아</th><td class="num"><label class="sr-only" for="g3">이수아 점수</label><input id="g3" class="score" type="number" min="0" max="100" value="95" /></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="as-summary mono" aria-live="polite">
|
||
제출 3명 · 평균 <strong class="hl" id="as-avg">91.7</strong>점
|
||
</p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn-solid" id="as-save">채점 저장</button>
|
||
<span class="modal-note muted" id="as-saved" hidden>저장되었습니다. 상태가 채점 완료로 바뀝니다.</span>
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<script src="calc.js"></script>
|
||
<script>
|
||
/*
|
||
앱 상태는 전부 이 안에 있다. 서버도 localStorage 도 없다 — 데모 원칙.
|
||
뷰 전환 · 검색/필터 · 출석 순환 · 배부 도장 · 리포트 계산까지
|
||
네이티브 기능 위에 얹은 얇은 JS.
|
||
*/
|
||
const STUDENTS = [
|
||
{ name: "박서연", grade: "중3", sub: "수학 심화 B", att: 100, hw: "8/8", status: "재학", guard: "모 · 박지현", phone: "010-****-4821", memo: ["3/04 세트 3 만점. 다음 세트 조기 완료."] },
|
||
{ name: "김도현", grade: "중3", sub: "수학 심화 B", att: 96, hw: "8/8", status: "재학", guard: "부 · 김성호", phone: "010-****-7310", memo: [] },
|
||
{ name: "이수아", grade: "중2", sub: "국어 논술 A", att: 92, hw: "8/9", status: "재학", guard: "모 · 이경민", phone: "010-****-2245", memo: ["2/26 논술 주제 선정 상담."] },
|
||
{ name: "최민준", grade: "중2", sub: "코딩 입문", att: 89, hw: "7/9", status: "재학", guard: "모 · 최은정", phone: "010-****-9012", memo: [] },
|
||
{ name: "정하율", grade: "중3", sub: "과학 실험", att: 100, hw: "6/6", status: "재학", guard: "부 · 정민수", phone: "010-****-3378", memo: ["3/06 리포트 우수 사례로 게시."] },
|
||
{ name: "한지우", grade: "중2", sub: "국어 논술 A", att: 85, hw: "8/9", status: "재학", guard: "모 · 한송이", phone: "010-****-6654", memo: ["3/08 지각 2회 — 등교 시간 재조정 약속."] },
|
||
{ name: "윤채원", grade: "고1", sub: "코딩 입문", att: 93, hw: "7/9", status: "재학", guard: "모 · 윤정아", phone: "010-****-1189", memo: [] },
|
||
{ name: "오세림", grade: "중3", sub: "수학 심화 B", att: 78, hw: "7/8", status: "재학", guard: "모 · 오현주", phone: "010-****-5503", memo: ["3/05 학부모 상담 — 주 2회 추가 과외 조율."] },
|
||
{ name: "남유진", grade: "고1", sub: "과학 실험", att: 100, hw: "6/6", status: "재학", guard: "부 · 남기훈", phone: "010-****-7742", memo: [] },
|
||
{ name: "서다인", grade: "중2", sub: "국어 논술 A", att: 90, hw: "8/9", status: "재학", guard: "모 · 서지원", phone: "010-****-2960", memo: [] },
|
||
{ name: "강이안", grade: "중2", sub: "코딩 입문", att: 0, hw: "0/9", status: "휴학", guard: "모 · 강은심", phone: "010-****-8031", memo: ["2/28 개인 사정으로 3월 말 복학 예정."] },
|
||
{ name: "조은채", grade: "고1", sub: "과학 실험", att: 0, hw: "0/6", status: "휴학", guard: "모 · 조민경", phone: "010-****-4417", memo: [] },
|
||
];
|
||
const SUBJECTS = ["수학 심화 B", "국어 논술 A", "코딩 입문", "과학 실험"];
|
||
|
||
// ── 뷰 전환 ──
|
||
const views = document.querySelectorAll(".view");
|
||
document.querySelectorAll(".side-link").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
document.querySelectorAll(".side-link").forEach((b) => {
|
||
b.classList.toggle("is-current", b === btn);
|
||
if (b === btn) b.setAttribute("aria-current", "page"); else b.removeAttribute("aria-current");
|
||
});
|
||
views.forEach((v) => v.classList.toggle("is-on", v.id === "view-" + btn.dataset.view));
|
||
document.getElementById("crumb-view").textContent = btn.textContent.trim();
|
||
document.getElementById("view-" + btn.dataset.view).focus({ preventScroll: true });
|
||
});
|
||
});
|
||
|
||
// ── 수강생 테이블: 검색 + 상태 칩 ──
|
||
const tbody = document.querySelector("#stu-table tbody");
|
||
let filter = "all";
|
||
const renderStudents = () => {
|
||
const q = document.getElementById("q").value.trim();
|
||
const rows = STUDENTS
|
||
.map((s, i) => ({ ...s, i }))
|
||
.filter((s) => (filter === "all" || s.status === filter))
|
||
.filter((s) => !q || s.name.includes(q) || s.sub.includes(q));
|
||
tbody.replaceChildren(
|
||
...rows.map((s) => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML =
|
||
`<th scope="row"><button type="button" class="row-open" data-stu="${s.i}">${s.name}</button></th>` +
|
||
`<td>${s.grade}</td><td>${s.sub}</td><td class="muted">${s.guard} · ${s.phone}</td>` +
|
||
`<td class="num mono">${s.att}%</td><td class="num mono">${s.hw}</td>` +
|
||
`<td><span class="state ${s.status === "휴학" ? "state-off" : ""}">${s.status}</span></td>`;
|
||
return tr;
|
||
})
|
||
);
|
||
document.getElementById("stu-empty").hidden = rows.length > 0;
|
||
};
|
||
const renderStuStats = () => {
|
||
const active = STUDENTS.filter((s) => s.status === "재학");
|
||
const avg = Math.round(active.reduce((a, s) => a + s.att, 0) / active.length);
|
||
document.getElementById("stu-stats").innerHTML =
|
||
`<div>전체 <strong>${STUDENTS.length}명</strong></div>` +
|
||
`<div>재학 <strong>${active.length}명</strong></div>` +
|
||
`<div>휴학 <strong>${STUDENTS.length - active.length}명</strong></div>` +
|
||
`<div>재학 평균 출석률 <strong>${avg}%</strong></div>` +
|
||
`<div>학부모 알림톡 수신 <strong>${active.length}/${active.length}</strong></div>`;
|
||
};
|
||
renderStuStats();
|
||
document.getElementById("q").addEventListener("input", renderStudents);
|
||
document.querySelectorAll("#view-students .chip").forEach((chip) => {
|
||
chip.addEventListener("click", () => {
|
||
document.querySelectorAll("#view-students .chip").forEach((c) => {
|
||
c.classList.toggle("is-on", c === chip);
|
||
c.setAttribute("aria-pressed", c === chip ? "true" : "false");
|
||
});
|
||
filter = chip.dataset.filter;
|
||
renderStudents();
|
||
});
|
||
});
|
||
renderStudents();
|
||
|
||
// ── 출석부: 상태 2=출석 1=지각 0=결석. 휴학은 null ──
|
||
const WEEKS = [
|
||
{ label: "3월 1주", days: ["03-02", "03-03", "03-04", "03-05", "03-06"] },
|
||
{ label: "3월 2주", days: ["03-09", "03-10", "03-11", "03-12", "03-13"] },
|
||
];
|
||
const mk = (a) => a.split("").map(Number);
|
||
// 명단 순서(STUDENTS 인덱스)와 1:1. 대시보드 차트(전원 기준)와 반 단위라 정합한다
|
||
const ATT = [
|
||
mk("22222"), mk("22222"), mk("22122"), mk("22221"), mk("22222"),
|
||
mk("21222"), mk("22222"), mk("22022"), mk("22222"), mk("22222"),
|
||
null, null,
|
||
];
|
||
const ATT_W2 = [
|
||
mk("22222"), mk("22222"), mk("21222"), mk("22222"), mk("22222"),
|
||
mk("20222"), mk("22220"), mk("21222"), mk("22222"), mk("22122"),
|
||
null, null,
|
||
];
|
||
const ATT_ALL = [ATT, ATT_W2];
|
||
const ATT_MARK = { 2: "○", 1: "△", 0: "✕" };
|
||
const ATT_NAME = { 2: "출석", 1: "지각", 0: "결석" };
|
||
let attWeek = 1; // 3월 2주 기준
|
||
let attClass = SUBJECTS[0];
|
||
|
||
const attData = (i) => ATT_ALL[attWeek][i];
|
||
const attSummary = GAON.attSummary;
|
||
|
||
const renderAttClass = () => {
|
||
document.getElementById("att-class").replaceChildren(
|
||
...SUBJECTS.map((sub) => {
|
||
const b = document.createElement("button");
|
||
b.type = "button";
|
||
b.className = "chip" + (sub === attClass ? " is-on" : "");
|
||
b.dataset.sub = sub;
|
||
b.setAttribute("aria-pressed", String(sub === attClass));
|
||
b.textContent = sub;
|
||
return b;
|
||
})
|
||
);
|
||
};
|
||
|
||
const renderAttendance = () => {
|
||
document.getElementById("att-week").textContent = WEEKS[attWeek].label;
|
||
document.getElementById("att-prev").disabled = attWeek === 0;
|
||
document.getElementById("att-next").disabled = attWeek === WEEKS.length - 1;
|
||
const body = document.querySelector("#att-table tbody");
|
||
body.replaceChildren(
|
||
...STUDENTS.map((s, i) => ({ s, i }))
|
||
.filter(({ s }) => s.sub === attClass)
|
||
.map(({ s, i }) => {
|
||
const tr = document.createElement("tr");
|
||
const row = attData(i);
|
||
let cells = `<th scope="row">${s.name}${s.status === "휴학" ? ' <span class="state state-off">휴학</span>' : ""}</th>`;
|
||
for (let d = 0; d < 5; d++) {
|
||
if (!row) {
|
||
cells += `<td class="att-cell is-na">·</td>`;
|
||
} else {
|
||
cells += `<td class="att-cell"><button type="button" class="att-btn ${row[d] === 2 ? "v-att" : row[d] === 1 ? "v-late" : "v-abs"}" data-stu="${i}" data-day="${d}" aria-label="${s.name} ${WEEKS[attWeek].days[d]} ${ATT_NAME[row[d]]}">${ATT_MARK[row[d]]}</button></td>`;
|
||
}
|
||
}
|
||
cells += `<td class="num mono">${row ? attSummary(row) : "—"}</td>`;
|
||
tr.innerHTML = cells;
|
||
return tr;
|
||
})
|
||
);
|
||
renderAttClass();
|
||
// 요약 스트립 + 일별 합계 — 현재 반·주 기준 실계산
|
||
const roster = STUDENTS.map((s, i) => ({ s, i })).filter(({ s }) => s.sub === attClass);
|
||
const marks = roster.map(({ i }) => attData(i)).filter(Boolean);
|
||
const C = GAON.counts(marks); const rate = C.rate;
|
||
document.getElementById("att-stats").innerHTML =
|
||
`<div>출석 <strong>${C.att}</strong></div>` +
|
||
`<div>지각 <strong>${C.late}</strong></div>` +
|
||
`<div>결석 <strong>${C.abs}</strong></div>` +
|
||
`<div>출석률 <strong>${rate}%</strong></div>` +
|
||
`<div>대상 <strong>${marks.length}명</strong></div>`;
|
||
document.getElementById("att-foot").innerHTML =
|
||
`<tr><td class="tsum">일별 출석</td>` +
|
||
[0, 1, 2, 3, 4].map((d) => `<td class="num mono">${GAON.dayTotal(marks, d) ? GAON.dayPresent(marks, d) + "/" + GAON.dayTotal(marks, d) : "—"}</td>`)
|
||
.join("") +
|
||
`<td class="num mono">${rate}%</td></tr>`;
|
||
};
|
||
|
||
document.getElementById("att-class").addEventListener("click", (e) => {
|
||
const chip = e.target.closest("[data-sub]");
|
||
if (!chip) return;
|
||
attClass = chip.dataset.sub;
|
||
renderAttendance();
|
||
});
|
||
document.getElementById("att-prev").addEventListener("click", () => { if (attWeek > 0) { attWeek -= 1; renderAttendance(); } });
|
||
document.getElementById("att-next").addEventListener("click", () => { if (attWeek < WEEKS.length - 1) { attWeek += 1; renderAttendance(); } });
|
||
|
||
// 칸 순환: 출석 → 지각 → 결석 → 출석. 합계와 상태줄을 함께 갱신
|
||
document.querySelector("#att-table tbody").addEventListener("click", (e) => {
|
||
const btn = e.target.closest(".att-btn");
|
||
if (!btn) return;
|
||
const i = +btn.dataset.stu;
|
||
const d = +btn.dataset.day;
|
||
const row = attData(i);
|
||
row[d] = (row[d] + 2) % 3; // 2→1→0→2
|
||
const s = STUDENTS[i];
|
||
const status = document.getElementById("att-status");
|
||
status.textContent = `${s.name} · ${WEEKS[attWeek].days[d]} → ${ATT_NAME[row[d]]}`;
|
||
renderAttendance();
|
||
});
|
||
renderAttendance();
|
||
|
||
// ── 과제·성적: 과목 필터 ──
|
||
document.getElementById("as-chips").addEventListener("click", (e) => {
|
||
const chip = e.target.closest("[data-as]");
|
||
if (!chip) return;
|
||
document.querySelectorAll("#as-chips .chip").forEach((c) => {
|
||
c.classList.toggle("is-on", c === chip);
|
||
c.setAttribute("aria-pressed", String(c === chip));
|
||
});
|
||
const sub = chip.dataset.as;
|
||
document.querySelectorAll("#as-table tbody tr").forEach((tr) => {
|
||
tr.hidden = sub !== "all" && tr.children[2].textContent.trim() !== sub;
|
||
});
|
||
});
|
||
|
||
// ── 자료실 ──
|
||
const FILES = [
|
||
{ name: "미분 응용 세트 3 해설지", sub: "수학 심화 B", kind: "인쇄물", size: "2.4MB", views: 64, date: "03-05", given: true },
|
||
{ name: "논술 첨삭 기준표", sub: "국어 논술 A", kind: "PDF", size: "860KB", views: 51, date: "03-03", given: true },
|
||
{ name: "변수와 조건 워크시트 예시", sub: "코딩 입문", kind: "PDF", size: "1.1MB", views: 37, date: "03-08", given: false },
|
||
{ name: "측정과 그래프 리포트 양식", sub: "과학 실험", kind: "양식", size: "540KB", views: 29, date: "03-02", given: true },
|
||
{ name: "4월 학사일정 안내장", sub: "공통", kind: "인쇄물", size: "3.0MB", views: 22, date: "03-10", given: false },
|
||
{ name: "오답 정리 노트 양식", sub: "공통", kind: "양식", size: "420KB", views: 83, date: "02-26", given: true },
|
||
];
|
||
let fileKind = "all";
|
||
const renderFiles = () => {
|
||
document.querySelector("#file-body").replaceChildren(
|
||
...FILES.map((f, i) => ({ f, i }))
|
||
.filter(({ f }) => fileKind === "all" || f.kind === fileKind)
|
||
.map(({ f, i }) => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML =
|
||
`<th scope="row">${f.name}</th><td>${f.sub}</td>` +
|
||
`<td><span class="state state-off">${f.kind}</span></td>` +
|
||
`<td class="num mono">${f.size}</td><td class="num mono">${f.views}</td>` +
|
||
`<td class="num mono">${f.date}</td>` +
|
||
`<td><button type="button" class="give-btn ${f.given ? "is-given" : ""}" data-file="${i}" aria-pressed="${f.given}">${f.given ? "배부됨" : "배부"}</button></td>`;
|
||
return tr;
|
||
})
|
||
);
|
||
};
|
||
document.getElementById("file-chips").addEventListener("click", (e) => {
|
||
const chip = e.target.closest("[data-kind]");
|
||
if (!chip) return;
|
||
document.querySelectorAll("#file-chips .chip").forEach((c) => {
|
||
c.classList.toggle("is-on", c === chip);
|
||
c.setAttribute("aria-pressed", String(c === chip));
|
||
});
|
||
fileKind = chip.dataset.kind;
|
||
renderFiles();
|
||
});
|
||
document.querySelector("#file-body").addEventListener("click", (e) => {
|
||
const btn = e.target.closest("[data-file]");
|
||
if (!btn) return;
|
||
const f = FILES[+btn.dataset.file];
|
||
f.given = !f.given;
|
||
renderFiles();
|
||
renderFileStats();
|
||
});
|
||
const renderFileStats = () => {
|
||
const given = FILES.filter((f) => f.given).length;
|
||
document.getElementById("file-stats").innerHTML =
|
||
`<div>자료 <strong>${FILES.length}건</strong></div>` +
|
||
`<div>배부 완료 <strong>${given}건</strong></div>` +
|
||
`<div>미배부 <strong>${FILES.length - given}건</strong></div>` +
|
||
`<div>총 조회 <strong>${FILES.reduce((a, f) => a + f.views, 0)}회</strong></div>`;
|
||
};
|
||
renderFiles();
|
||
renderFileStats();
|
||
|
||
// ── 학사 일정: 2026년 3월, 월요일 시작 ──
|
||
const EVENTS = {
|
||
6: { name: "리포트 마감", type: "due" },
|
||
11: { name: "모의고사", type: "exam" },
|
||
12: { name: "논술 초고 마감", type: "due" },
|
||
13: { name: "워크시트 마감", type: "due" },
|
||
16: { name: "학부모 상담 주간", type: "admin" },
|
||
19: { name: "휴원", type: "holi" },
|
||
25: { name: "4월 시간표 확정", type: "admin" },
|
||
31: { name: "1차 평가 마감", type: "exam" },
|
||
};
|
||
const EV_TODAY = 9;
|
||
const renderCalendar = () => {
|
||
// 2026-03-01 은 일요일. 월요일 시작 격자: 첫 주 앞 6칸은 비고 1일이 마지막 칸
|
||
const body = document.querySelector("#cal-table tbody");
|
||
const grid = new Array(6).fill(null);
|
||
grid.push(1);
|
||
for (let d = 2; d <= 31; d++) grid.push(d); // 3/2 부터 순서대로 이어붙인다
|
||
while (grid.length % 7 !== 0) grid.push(null);
|
||
const rows = [];
|
||
for (let r = 0; r < grid.length; r += 7) rows.push(grid.slice(r, r + 7));
|
||
body.replaceChildren(
|
||
...rows.map((weekRow) => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = weekRow
|
||
.map((d) => {
|
||
if (!d) return `<td class="cal-day is-empty"></td>`;
|
||
const ev = EVENTS[d] ? `<span class="cal-ev is-${EVENTS[d].type}">${EVENTS[d].name}</span>` : "";
|
||
const today = d === EV_TODAY ? " is-today" : "";
|
||
return `<td class="cal-day${today}"><span class="cal-num mono">${d}</span>${ev}</td>`;
|
||
})
|
||
.join("");
|
||
return tr;
|
||
})
|
||
);
|
||
// 다가오는 일정 (3월 9일 이후)
|
||
document.getElementById("up-list").replaceChildren(
|
||
...Object.entries(EVENTS)
|
||
.map(([d, ev]) => ({ d: +d, ...ev }))
|
||
.filter(({ d }) => d >= EV_TODAY)
|
||
.sort((a, b) => a.d - b.d)
|
||
.map(({ d, name, type }) => {
|
||
const li = document.createElement("li");
|
||
const item = document.createElement("p");
|
||
item.className = "due-row is-static";
|
||
const dd = d === EV_TODAY ? "오늘" : "D-" + (d - EV_TODAY);
|
||
item.innerHTML = `<span><i class="ev-dot is-${type}" aria-hidden="true"></i>${name}</span><span class="mono muted">${dd} · 3월 ${d}일</span>`;
|
||
li.append(item);
|
||
return li;
|
||
})
|
||
);
|
||
const byType = (t) => Object.values(EVENTS).filter((e) => e.type === t).length;
|
||
document.getElementById("cal-stats").innerHTML =
|
||
`<div>3월 일정 <strong>${Object.keys(EVENTS).length}건</strong></div>` +
|
||
`<div>평가 · 시험 <strong>${byType("exam")}건</strong></div>` +
|
||
`<div>과제 마감 <strong>${byType("due")}건</strong></div>` +
|
||
`<div>행정 · 상담 <strong>${byType("admin")}건</strong></div>` +
|
||
`<div>휴원 <strong>${byType("holi")}일</strong></div>`;
|
||
};
|
||
renderCalendar();
|
||
|
||
// ── 상담 기록: memo 에서 카드로 ──
|
||
let coSub = "all";
|
||
const renderCounsel = () => {
|
||
const cards = STUDENTS
|
||
.map((s, i) => ({ s, i }))
|
||
.filter(({ s }) => (coSub === "all" || s.sub === coSub))
|
||
.flatMap(({ s, i }) =>
|
||
s.memo.map((m) => {
|
||
const date = m.match(/^(\d+\/\d+)/);
|
||
return { s, i, date: date ? date[1] : "날짜 미기재", text: m };
|
||
})
|
||
)
|
||
.sort((a, b) => (a.date < b.date ? 1 : -1));
|
||
document.getElementById("counsel-grid").replaceChildren(
|
||
...cards.map((c) => {
|
||
const card = document.createElement("button");
|
||
card.type = "button";
|
||
card.className = "counsel-card";
|
||
card.dataset.stu = c.i;
|
||
card.innerHTML =
|
||
`<span class="co-head"><strong>${c.s.name}</strong><span class="mono muted">${c.date}</span></span>` +
|
||
`<span class="co-sub muted">${c.s.sub} · ${c.s.status}</span>` +
|
||
`<span class="co-text">${c.text}</span>`;
|
||
return card;
|
||
})
|
||
);
|
||
document.getElementById("counsel-empty").hidden = cards.length > 0;
|
||
};
|
||
const renderCoStats = () => {
|
||
const all = STUDENTS.flatMap((s) => s.memo);
|
||
const march = all.filter((m) => m.startsWith("3/"));
|
||
const parent = all.filter((m) => m.includes("학부모"));
|
||
document.getElementById("co-stats").innerHTML =
|
||
`<div>기록 총 <strong>${all.length}건</strong></div>` +
|
||
`<div>3월 <strong>${march.length}건</strong></div>` +
|
||
`<div>학부모 상담 <strong>${parent.length}건</strong></div>` +
|
||
`<div>예정 <strong>3건</strong></div>`;
|
||
};
|
||
renderCoStats();
|
||
document.getElementById("co-chips").addEventListener("click", (e) => {
|
||
const chip = e.target.closest("[data-sub]");
|
||
if (!chip) return;
|
||
document.querySelectorAll("#co-chips .chip").forEach((c) => {
|
||
c.classList.toggle("is-on", c === chip);
|
||
c.setAttribute("aria-pressed", String(c === chip));
|
||
});
|
||
coSub = chip.dataset.sub;
|
||
renderCounsel();
|
||
});
|
||
renderCounsel();
|
||
|
||
// ── 학기 리포트: 명단에서 직접 계산 ──
|
||
const renderReport = () => {
|
||
const body = document.querySelector("#report-table tbody");
|
||
body.replaceChildren(
|
||
...SUBJECTS.map((sub) => {
|
||
const roster = STUDENTS.filter((s) => s.sub === sub && s.status === "재학");
|
||
const avgAtt = roster.length
|
||
? Math.round(roster.reduce((a, s) => a + s.att, 0) / roster.length)
|
||
: 0;
|
||
const hw = roster.reduce(
|
||
(a, s) => {
|
||
const [done, total] = s.hw.split("/").map(Number);
|
||
return { done: a.done + done, total: a.total + total };
|
||
},
|
||
{ done: 0, total: 0 }
|
||
);
|
||
const hwRate = hw.total ? Math.round((hw.done / hw.total) * 100) : 0;
|
||
// 채점이 끝난 과목만 평균을 쓴다 — 나머지는 비워 둔다
|
||
const graded = sub === "수학 심화 B";
|
||
const PROG = { "수학 심화 B": [12, 16], "국어 논술 A": [8, 12], "코딩 입문": [5, 10], "과학 실험": [9, 12] };
|
||
const [pDone, pTotal] = PROG[sub];
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML =
|
||
`<th scope="row">${sub}</th><td class="num mono">${roster.length}명</td>` +
|
||
`<td class="num mono">${avgAtt}%</td>` +
|
||
`<td class="num mono">${hw.done}/${hw.total} · ${hwRate}%</td>` +
|
||
`<td class="num mono">${graded ? '<span class="hl">84.4점</span>' : "채점 중"}</td>` +
|
||
`<td class="num mono">${pDone}/${pTotal} · ${Math.round((pDone / pTotal) * 100)}%</td>`;
|
||
return tr;
|
||
})
|
||
);
|
||
};
|
||
renderReport();
|
||
{
|
||
const active = STUDENTS.filter((s) => s.status === "재학");
|
||
const avgAtt = Math.round(active.reduce((a, s) => a + s.att, 0) / active.length);
|
||
const hw = active.reduce(
|
||
(a, s) => {
|
||
const [done, total] = s.hw.split("/").map(Number);
|
||
return { done: a.done + done, total: a.total + total };
|
||
},
|
||
{ done: 0, total: 0 }
|
||
);
|
||
const hwRate = Math.round((hw.done / hw.total) * 100);
|
||
document.getElementById("rep-stats").innerHTML =
|
||
`<div><dt>재학생</dt><dd>${active.length}명</dd><dd class="stat-sub">4과목 합산 · 휴학 2 제외</dd></div>` +
|
||
`<div><dt>평균 출석률</dt><dd>${avgAtt}%</dd><dd class="stat-sub">재학생 기준 실계산</dd></div>` +
|
||
`<div><dt>과제 제출률</dt><dd>${hwRate}%</dd><dd class="stat-sub">${hw.done}/${hw.total} 제출</dd></div>` +
|
||
`<div><dt>채점 완료 과목</dt><dd>1/4</dd><dd class="stat-sub">학부모 발송 3/20</dd></div>`;
|
||
}
|
||
|
||
// ── 수강생 상세 모달: 학적 카드 ──
|
||
const stuDialog = document.getElementById("stu-dialog");
|
||
document.addEventListener("click", (e) => {
|
||
const btn = e.target.closest("[data-stu]");
|
||
if (!btn || btn.classList.contains("att-btn")) return;
|
||
const i = +btn.dataset.stu;
|
||
const s = STUDENTS[i];
|
||
document.getElementById("stu-seal").textContent = s.name[0];
|
||
document.getElementById("stu-mark").textContent = s.name[0];
|
||
document.getElementById("stu-dlg-h").innerHTML = `<span class="hl">${s.name}</span>`;
|
||
document.getElementById("stu-meta").textContent = `${s.sub} · ${s.status} · 개강 2026-03-02`;
|
||
document.getElementById("stu-guardian").textContent = `${s.guard} · ${s.phone}`;
|
||
document.getElementById("stu-att").textContent = s.att + "%";
|
||
document.getElementById("stu-hw").textContent = s.hw;
|
||
document.getElementById("stu-memo-n").textContent = s.memo.length + "건";
|
||
|
||
// 2주 출석 스트립 — 출석부 화면과 같은 데이터에서 뽑는다
|
||
const strip = document.getElementById("stu-att-strip");
|
||
if (s.status === "휴학") {
|
||
strip.replaceChildren();
|
||
const na = document.createElement("p");
|
||
na.className = "att-strip-na muted";
|
||
na.textContent = "휴학 중입니다. 복학하면 기록이 쌓입니다.";
|
||
strip.append(na);
|
||
} else {
|
||
const cells = [];
|
||
WEEKS.forEach((w, wi) => {
|
||
w.days.forEach((d, di) => {
|
||
const v = ATT_ALL[wi][i][di];
|
||
cells.push(
|
||
`<span class="att-cell2"><span class="att-mark v-${v === 2 ? "att" : v === 1 ? "late" : "abs"}">${ATT_MARK[v]}</span><span class="att-date mono">${d.slice(5).replace("-", "/")}</span></span>`
|
||
);
|
||
});
|
||
});
|
||
strip.innerHTML = cells.join("");
|
||
}
|
||
|
||
const noteLi = (m) => {
|
||
const li = document.createElement("li");
|
||
const dm = m.match(/^(\d+\/\d+)/);
|
||
const date = document.createElement("span");
|
||
date.className = "note-date mono";
|
||
date.textContent = dm ? dm[1] : "—";
|
||
const text = document.createElement("span");
|
||
text.className = "note-text";
|
||
text.textContent = m;
|
||
li.append(date, text);
|
||
return li;
|
||
};
|
||
const list = document.getElementById("stu-note-list");
|
||
list.replaceChildren(
|
||
...(s.memo.length ? s.memo.map(noteLi) : ["아직 기록이 없습니다."].map((t) => {
|
||
const li = document.createElement("li");
|
||
li.textContent = t;
|
||
return li;
|
||
}))
|
||
);
|
||
const form = document.getElementById("note-form");
|
||
const input = document.getElementById("note-input");
|
||
form.onsubmit = (ev) => {
|
||
ev.preventDefault();
|
||
if (!input.value.trim()) return;
|
||
s.memo.push(input.value.trim());
|
||
input.value = "";
|
||
const li = noteLi(s.memo[s.memo.length - 1]);
|
||
if (list.firstElementChild?.textContent === "아직 기록이 없습니다.") list.replaceChildren();
|
||
list.append(li);
|
||
document.getElementById("stu-memo-n").textContent = s.memo.length + "건";
|
||
renderCounsel(); // 상담 기록 화면도 함께 갱신
|
||
input.focus();
|
||
};
|
||
stuDialog.showModal();
|
||
});
|
||
|
||
// ── 채점 모달: 점수를 고치면 평균이 따라온다 ──
|
||
const asDialog = document.getElementById("as-dialog");
|
||
const scoreInputs = [...document.querySelectorAll(".grade-table .score")];
|
||
const updateAvg = () => {
|
||
const vals = scoreInputs.map((inp) => +inp.value || 0);
|
||
const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
|
||
document.getElementById("as-avg").textContent = avg.toFixed(1);
|
||
};
|
||
scoreInputs.forEach((inp) => inp.addEventListener("input", updateAvg));
|
||
const ASSIGN = { a1: "미분 응용 세트 3 · 수학 심화 B", a2: "자기소개 논술 초고 · 국어 논술 A", a3: "변수와 조건 워크시트 · 코딩 입문", a4: "측정과 그래프 리포트 · 과학 실험" };
|
||
let currentAssign = null;
|
||
document.addEventListener("click", (e) => {
|
||
const btn = e.target.closest("[data-assign]");
|
||
if (!btn) return;
|
||
currentAssign = btn.dataset.assign;
|
||
document.getElementById("as-dlg-meta").textContent = ASSIGN[currentAssign] || "";
|
||
document.getElementById("as-saved").hidden = true;
|
||
asDialog.showModal();
|
||
});
|
||
document.getElementById("as-save").addEventListener("click", () => {
|
||
document.getElementById("as-saved").hidden = false;
|
||
const row = document.querySelector(`#as-table .row-open[data-assign="${currentAssign}"]`);
|
||
const cell = row?.closest("tr").querySelector(".state");
|
||
if (cell) { cell.textContent = "채점 완료"; cell.classList.remove("state-wait"); }
|
||
});
|
||
|
||
// ── 모달 공용: 닫기 · 배경 탭 ──
|
||
document.querySelectorAll(".modal").forEach((d) => {
|
||
d.addEventListener("click", (e) => { if (e.target === d) d.close(); });
|
||
d.querySelector("[data-close]")?.addEventListener("click", () => d.close());
|
||
});
|
||
|
||
// ── 알림 센터 ──
|
||
const bell = document.getElementById("bell");
|
||
const bellMenu = document.getElementById("bell-menu");
|
||
const bellDot = document.getElementById("bell-dot");
|
||
const syncBell = () => {
|
||
const n = bellMenu.querySelectorAll(".ntc.is-unread").length;
|
||
bellDot.hidden = n === 0;
|
||
bell.setAttribute("aria-label", n > 0 ? "읽지 않은 알림 " + n + "건" : "알림");
|
||
};
|
||
bell.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
const open = bellMenu.classList.toggle("is-open");
|
||
bell.classList.toggle("is-open", open);
|
||
bell.setAttribute("aria-expanded", open);
|
||
});
|
||
document.addEventListener("click", (e) => {
|
||
if (!bellMenu.contains(e.target) && !bell.contains(e.target)) {
|
||
bellMenu.classList.remove("is-open");
|
||
bell.classList.remove("is-open");
|
||
bell.setAttribute("aria-expanded", "false");
|
||
}
|
||
});
|
||
document.getElementById("bell-all").addEventListener("click", () => {
|
||
bellMenu.querySelectorAll(".ntc.is-unread").forEach((n) => n.classList.remove("is-unread"));
|
||
syncBell();
|
||
});
|
||
syncBell();
|
||
|
||
// ── 공지: 분류 필터 ──
|
||
document.getElementById("nt-chips").addEventListener("click", (e) => {
|
||
const chip = e.target.closest("[data-nt]");
|
||
if (!chip) return;
|
||
document.querySelectorAll("#nt-chips .chip").forEach((c) => {
|
||
c.classList.toggle("is-on", c === chip);
|
||
c.setAttribute("aria-pressed", String(c === chip));
|
||
});
|
||
const cat = chip.dataset.nt;
|
||
document.querySelectorAll(".notice-row").forEach((row) => {
|
||
const li = row.closest("li");
|
||
const isPinned = row.querySelector(".pin") !== null;
|
||
row.hidden = cat !== "전체" && !isPinned && row.querySelector(".tag").textContent.trim() !== cat;
|
||
if (li) li.hidden = row.hidden;
|
||
});
|
||
});
|
||
|
||
// ── 공지: 열기 + 읽음 ──
|
||
document.querySelectorAll(".notice-row").forEach((row) => {
|
||
row.addEventListener("click", () => {
|
||
row.classList.toggle("is-open");
|
||
row.classList.remove("is-unread");
|
||
row.setAttribute("aria-expanded", row.classList.contains("is-open"));
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|