|
122 | 122 | .stat-sub{margin-top:5px;font-size:11px;color:var(--silver)} |
123 | 123 | .stat-head-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px} |
124 | 124 | .stat-head-row .stat-name{margin-bottom:0} |
125 | | -.stat-cycles{font-size:10px;color:var(--stone);background:var(--light-gray);border-radius:9999px;padding:2px 7px;font-weight:500;white-space:nowrap;flex-shrink:0} |
126 | | -.cycle-row{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:10px} |
127 | | -.cycle-dots{display:flex;gap:4px;align-items:center} |
128 | | -.cycle-dot{width:9px;height:9px;border-radius:9999px;background:var(--light-gray)} |
| 125 | +.stat-cycles{font-size:10px;color:var(--stone);background:rgba(0,0,0,.08);border-radius:9999px;padding:2px 7px;font-weight:500;white-space:nowrap;flex-shrink:0} |
| 126 | +.cycle-row{display:flex;align-items:flex-start;justify-content:space-between;gap:6px;margin-bottom:10px} |
| 127 | +.cycle-dots{display:flex;gap:7px;align-items:flex-start} |
| 128 | +.cycle-dot-wrap{display:flex;flex-direction:column;align-items:center;gap:3px} |
| 129 | +.cycle-dot{width:12px;height:12px;border-radius:9999px;background:rgba(0,0,0,.13);cursor:pointer;transition:background .18s,transform .15s;flex-shrink:0} |
| 130 | +.cycle-dot:hover{transform:scale(1.28)} |
129 | 131 | .cycle-dot.filled{background:var(--black)} |
130 | | -.cycle-badge{font-size:10px;color:var(--stone);white-space:nowrap} |
| 132 | +.cycle-date{font-size:9px;color:var(--stone);line-height:1;min-height:10px;opacity:.75;white-space:nowrap} |
| 133 | +.cycle-badge{font-size:10px;color:var(--stone);white-space:nowrap;padding-top:1px} |
131 | 134 | .cycle-badge.fee-due{color:#c2410c;font-weight:700} |
| 135 | +.cycle-complete-btn{font-family:'Pretendard',sans-serif;font-size:10px;color:var(--white);background:#c2410c;border:none;border-radius:9999px;padding:3px 11px;cursor:pointer;font-weight:600;display:block;margin-bottom:8px;margin-left:auto} |
| 136 | +.cycle-complete-btn:hover{background:#9a3412} |
132 | 137 |
|
133 | 138 | .records-header{margin-bottom:16px} |
134 | 139 | .records-title{font-size:20px;font-weight:700;letter-spacing:-.03em;color:var(--black)} |
|
383 | 388 |
|
384 | 389 | function switchTab(dk,sid){activeSessionTab[dk]=sid;renderDetail()} |
385 | 390 |
|
| 391 | +const CARD_COLORS=['#eff6ff','#f0fdf4','#fdf4ff','#fff7ed','#fdf2f8','#f0fdfa','#fefce8','#fef2f2','#f5f3ff','#ecfeff','#fff1f2','#f0f9ff']; |
| 392 | +function strHash(s){let h=5381;for(let i=0;i<s.length;i++)h=((h<<5)+h)+s.charCodeAt(i);return h>>>0;} |
| 393 | +function loadCycleData(){try{return JSON.parse(localStorage.getItem('beatcheck_cycle')||'{}')}catch(e){return{}}} |
| 394 | +function saveCycleData(d){localStorage.setItem('beatcheck_cycle',JSON.stringify(d))} |
| 395 | +let cycleData=loadCycleData(); |
| 396 | +function toggleCycleSession(sid,idx){ |
| 397 | + if(!cycleData[sid])cycleData[sid]={completedCycles:0,sessions:[null,null,null,null]}; |
| 398 | + const s=cycleData[sid]; |
| 399 | + if(!s.sessions||s.sessions.length!==4)s.sessions=[null,null,null,null]; |
| 400 | + if(s.sessions[idx])s.sessions[idx]=null; |
| 401 | + else{const t=new Date();s.sessions[idx]=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,'0')}-${String(t.getDate()).padStart(2,'0')}`;} |
| 402 | + saveCycleData(cycleData);renderStats(); |
| 403 | +} |
| 404 | +function completeCycle(sid){ |
| 405 | + if(!cycleData[sid])cycleData[sid]={completedCycles:0,sessions:[null,null,null,null]}; |
| 406 | + cycleData[sid].completedCycles=(cycleData[sid].completedCycles||0)+1; |
| 407 | + cycleData[sid].sessions=[null,null,null,null]; |
| 408 | + saveCycleData(cycleData);renderStats(); |
| 409 | +} |
386 | 410 | function renderStats(){ |
387 | 411 | const sec=document.getElementById('statsSection'); |
388 | | - const sm={}; |
389 | | - Object.values(SCHEDULE).forEach(d=>d.sessions.forEach(s=>s.students.forEach(st=>{if(!sm[st.id])sm[st.id]={name:st.name,sessions:0,present:0}}))); |
| 412 | + const sm={};const orderedIds=[]; |
| 413 | + Object.values(SCHEDULE).forEach(d=>d.sessions.forEach(s=>s.students.forEach(st=>{ |
| 414 | + if(!sm[st.id]){sm[st.id]={name:st.name,sessions:0,present:0};orderedIds.push(st.id);} |
| 415 | + }))); |
390 | 416 | const todayD=new Date(today.getFullYear(),today.getMonth(),today.getDate()); |
391 | 417 | Object.entries(attendance).forEach(([dk,records])=>{ |
392 | 418 | const{y,m,d}=parseDK(dk);if(new Date(y,m,d)>todayD)return; |
|
396 | 422 | else if(val===false)sm[sid].sessions++; |
397 | 423 | }); |
398 | 424 | }); |
399 | | - const students=Object.entries(sm).filter(([,v])=>v.sessions>0).sort(([,a],[,b])=>(b.present/Math.max(b.sessions,1))-(a.present/Math.max(a.sessions,1))); |
400 | | - if(!students.length){sec.innerHTML='<div class="records-empty"><div class="empty-icon">📊</div><p style="font-size:14px;color:var(--silver)">아직 출석 기록이 없습니다</p></div>';return} |
401 | | - sec.innerHTML=`<div class="stats-grid">${students.map(([id,data])=>{ |
| 425 | + if(!orderedIds.length){sec.innerHTML='<div class="records-empty"><div class="empty-icon">📊</div><p style="font-size:14px;color:var(--silver)">아직 출석 기록이 없습니다</p></div>';return} |
| 426 | + sec.innerHTML=`<div class="stats-grid">${orderedIds.map(id=>{ |
| 427 | + const data=sm[id]; |
402 | 428 | const pct=data.sessions>0?Math.round(data.present/data.sessions*100):0; |
403 | | - const isFeeDue=data.present>0&&data.present%4===0; |
404 | | - const cyclePos=isFeeDue?4:data.present%4; |
405 | | - const completedCycles=Math.floor(data.present/4); |
406 | | - const dots=Array(4).fill(0).map((_,i)=>`<span class="cycle-dot${i<cyclePos?' filled':''}"></span>`).join(''); |
407 | | - const badgeText=isFeeDue?'레슨비 납부 시점!':`${4-cyclePos}회 후 레슨비`; |
408 | | - const badgeCls=isFeeDue?'cycle-badge fee-due':'cycle-badge'; |
409 | | - return`<div class="stat-card"><div class="stat-head-row"><div class="stat-name">${data.name}</div><span class="stat-cycles">${completedCycles}사이클</span></div><div class="cycle-row"><div class="cycle-dots">${dots}</div><span class="${badgeCls}">${badgeText}</span></div><div class="stat-bar-row"><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:${pct}%"></div></div><span class="stat-pct">${pct}%</span></div><div class="stat-sub">출석 ${data.present}/${data.sessions}회</div></div>`; |
| 429 | + const cd=cycleData[id]||{completedCycles:0,sessions:[null,null,null,null]}; |
| 430 | + const sess=cd.sessions&&cd.sessions.length===4?cd.sessions:[null,null,null,null]; |
| 431 | + const filledCount=sess.filter(x=>x!==null).length; |
| 432 | + const isFeeDue=filledCount===4; |
| 433 | + const bgColor=CARD_COLORS[strHash(id)%CARD_COLORS.length]; |
| 434 | + const dots=sess.map((date,i)=>{ |
| 435 | + const dl=date?`${parseInt(date.split('-')[1])}/${parseInt(date.split('-')[2])}`:'' |
| 436 | + return`<div class="cycle-dot-wrap"><span class="cycle-dot${date?' filled':''}" onclick="toggleCycleSession('${id}',${i})"></span><span class="cycle-date">${dl}</span></div>`; |
| 437 | + }).join(''); |
| 438 | + const rightBadge=isFeeDue?`<span class="cycle-badge fee-due">레슨비!</span>`:`<span class="cycle-badge">${4-filledCount}회 후 레슨비</span>`; |
| 439 | + const completeBtn=isFeeDue?`<button class="cycle-complete-btn" onclick="completeCycle('${id}')">사이클 완료</button>`:''; |
| 440 | + return`<div class="stat-card" style="background:${bgColor}"><div class="stat-head-row"><div class="stat-name">${data.name}</div><span class="stat-cycles">${cd.completedCycles||0}사이클</span></div><div class="cycle-row"><div class="cycle-dots">${dots}</div>${rightBadge}</div>${completeBtn}<div class="stat-bar-row"><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:${pct}%"></div></div><span class="stat-pct">${pct}%</span></div><div class="stat-sub">출석 ${data.present}/${data.sessions}회</div></div>`; |
410 | 441 | }).join('')}</div>`; |
411 | 442 | } |
412 | 443 |
|
|
0 commit comments