1/* TF ONE attendance + leave P0.3 · login-popup handoff · 2026-09-18 */ 2(()=>{ 3'use strict'; 4const KEY='__TFONE_ATTENDANCE_P0__'; 5try{globalThis[KEY]?.stop?.()}catch{} 6const VERSION='p0.3-login-popup-handoff-20260918'; 7const SUPABASE_URL='https://tfswpzkwdmghybmxstpj.supabase.co'; 8const SUPABASE_KEY='sb_publishable_IdMAgUkKrRkqG0RM6HGhxQ_LhAXjbdN'; 9const APPROVAL_ADMIN_SRC='/post162/tfone-approval-admin-p0.js?v=p0.4-substitute-agreement-line-20260911'; 10const state={active:true,ready:false,busy:false,lastError:'',lastAt:0,observer:null,observedMain:null,timer:0,lastAtt:null,lastSchedule:null,leaveLoader:null,fastPaints:0}; 11const $=(s,r=document)=>r.querySelector(s); 12const $$=(s,r=document)=>Array.from(r.querySelectorAll(s)); 13const route=()=>{try{return String(globalThis.__TFONE_CANONICAL_ROUTE__||new URL(location.href).searchParams.get('view')||'today').trim().toLowerCase()}catch{return'today'}}; 14const kstDate=()=>new Intl.DateTimeFormat('en-CA',{timeZone:'Asia/Seoul',year:'numeric',month:'2-digit',day:'2-digit'}).format(new Date()); 15const fmt=v=>v?new Intl.DateTimeFormat('ko-KR',{timeZone:'Asia/Seoul',month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}).format(new Date(v)):'-'; 16let sb=null; 17function clearLegacyLoading(){const el=$('#tfone-startup-loading-p0');if(el)el.remove()} 18async function client(){if(sb)return sb;const m=await import('https://esm.sh/@supabase/supabase-js@2');sb=m.createClient(SUPABASE_URL,SUPABASE_KEY,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true}});return sb} 19async function ctx(){const c=await client();const {data:{session},error:se}=await c.auth.getSession();if(se)throw se;if(!session?.user?.id)return null;const uid=session.user.id;const [{data:att,error:ae},{data:schedule,error:we}]=await Promise.all([c.from('attendance_records').select('*').eq('user_id',uid).eq('work_date',kstDate()).maybeSingle(),c.from('work_schedules').select('*').eq('user_id',uid).eq('work_date',kstDate()).maybeSingle()]);if(ae)throw ae;if(we)throw we;return {c,uid,att,schedule}} 20function toast(msg,bad=false){let el=$('#tfone-attendance-toast');if(!el){el=document.createElement('div');el.id='tfone-attendance-toast';Object.assign(el.style,{position:'fixed',left:'50%',bottom:'86px',transform:'translateX(-50%)',zIndex:'2147483100',padding:'10px 14px',borderRadius:'10px',font:'700 12px system-ui',boxShadow:'0 8px 26px rgba(0,0,0,.18)'});document.body.appendChild(el)}el.textContent=msg;el.style.background=bad?'#8f2f2f':'#123e59';el.style.color='#fff';el.hidden=false;setTimeout(()=>{el.hidden=true},2400)} 21async function rpc(name){if(state.busy)return;state.busy=true;try{const x=await ctx();if(!x)throw new Error('ë¡ê·¸ì¸ì´ íìí©ëë¤.');const {error}=await x.c.rpc(name);if(error)throw error;toast(name==='record_checkout'?'í´ê·¼ ì²´í¬ëììµëë¤.':'ì¶ê·¼ ì²´í¬ëììµëë¤.');state.lastError='';state.lastAt=Date.now();await refresh(true)}catch(e){state.lastError=String(e?.message||e);toast((name==='record_checkout'?'í´ê·¼':'ì¶ê·¼')+' ì²´í¬ ì¤ë¥ · '+state.lastError,true)}finally{state.busy=false}} 22function scheduleText(s){return s?.start_time?`${String(s.start_time).slice(0,5)} ~ ${String(s.end_time||'').slice(0,5)}`:'미ì¤ì '} 23function attendanceSignature(att,schedule){return [att?.status||'none',att?.first_login_at||'',att?.checked_out_at||'',schedule?.start_time||'',schedule?.end_time||''].join('|')} 24function leaveButton(){return '<button type="button" data-tfone-leave-apply="1" class="secondary full" style="margin-top:8px">í´ê° ì ì²</button>'} 25function cardHtml(att,schedule,{fast=false}={}){const checkedIn=!!att?.first_login_at;const status=checkedIn?(att?.status||'ì¶ê·¼'):(fast?'íì¸ ì¤':'미ì¶ê·¼');return `<div id="tfone-attendance-card-p0" class="card" data-tfone-attendance-p0="1" style="margin-bottom:12px;border-top:4px solid #0b2d4f"><div class="head"><b>ì¤ë ê·¼í</b><span class="badge">${status}</span></div><div class="profile-row"><span>근무 ìì </span><b>${scheduleText(schedule)}</b></div><div class="profile-row"><span>ì¶ê·¼</span><b>${fmt(att?.first_login_at)}</b></div><div class="profile-row"><span>í´ê·¼</span><b>${fmt(att?.checked_out_at)}</b></div><div class="meta">ì¶ê·¼Â·í´ê·¼ì ë²í¼ì ë른 ì¤ì ìê°ì¼ë¡ 기ë¡í©ëë¤.</div>${fast&&!state.ready?'<div class="meta" style="margin:7px 0">ìµì ê·¼í를 ë¤ìì ê°±ì ì¤ì ëë¤.</div>':!checkedIn?'<button id="tfoneAttendanceCheckin" class="primary full">ì¶ê·¼ ì²´í¬</button>':!att.checked_out_at?'<button id="tfoneAttendanceCheckout" class="secondary full">í´ê·¼ ì²´í¬</button>':'<span class="pill done">ê·¼í ìë£</span>'}${leaveButton()}</div>`} 26function bind(root=document){$('#tfoneAttendanceCheckin',root)?.addEventListener('click',()=>rpc('record_first_app_open'));$('#tfoneAttendanceCheckout',root)?.addEventListener('click',()=>rpc('record_checkout'));$$('[data-tfone-leave-apply="1"]',root).forEach(b=>{if(b.dataset.tfoneLeaveBound==='1')return;b.dataset.tfoneLeaveBound='1';b.addEventListener('click',openLeaveModal)})} 27function promoteTodayCard(card){const main=$('#main');if(!main||!card||card.dataset.tfoneAttendancePromoted==='1')return;card.dataset.tfoneAttendancePromoted='1';main.insertAdjacentElement('afterbegin',card)} 28function fixToday(){if(route()!=='today')return;$('#tfone-attendance-card-p0')?.remove();const heads=$$('.card .head b,#main .head b');for(const h of heads){if(['ê·¼í','ì¤ë ê·¼í'].includes(String(h.textContent||'').trim()))h.closest('.card')?.remove()}} 29function fixApproval(att,schedule){if(route()!=='approval'){$('#tfone-attendance-card-p0')?.remove();return}const main=$('#main');if(!main)return;$('#tfone-attendance-fast-shell-p0')?.remove();
29const title=$('.menu-title',main);if(title&&/ê²°ì¬|ì°ì°¨/.test(title.textContent||'')&&title.textContent!=='ê·¼í·ì°ì°¨Â·ê²°ì¬')title.textContent='ê·¼í·ì°ì°¨Â·ê²°ì¬';const sig=attendanceSignature(att,schedule),old=$('#tfone-attendance-card-p0');if(old?.dataset.attendanceSignature===sig){bind(old);return}const wrap=document.createElement('div');wrap.innerHTML=cardHtml(att,schedule);const next=wrap.firstElementChild;next.dataset.attendanceSignature=sig;if(old)old.replaceWith(next);else main.insertAdjacentElement('afterbegin',next);bind(next)} 30function paintApprovalFast(force=false){const main=$('#main');if(!main||(!force&&route()!=='approval'))return false;if($('#tfone-attendance-fast-shell-p0',main))return true;const r=route();if(!force&&r!=='approval')return false;const liveTitle=$('.menu-title',main);if(r==='approval'&&liveTitle&&/ê·¼í|ì°ì°¨|ê²°ì¬/.test(liveTitle.textContent||'')){fixApproval(state.lastAtt,state.lastSchedule);return true}const shell=document.createElement('div');shell.id='tfone-attendance-fast-shell-p0';shell.dataset.tfoneFastApproval='1';shell.innerHTML=`<div class="card" style="margin-bottom:12px;border-top:4px solid #0b2d4f"><div class="head"><div><b>ê·¼í·ì°ì°¨Â·ê²°ì¬</b><div class="meta">íë©´ì 먼ì ì´ê³ ìµì ë°ì´í°ë¥¼ ë¤ìì ê°±ì í©ëë¤.</div></div><span class="badge">ì¦ì íì</span></div></div>${cardHtml(state.lastAtt,state.lastSchedule,{fast:true})}`;main.replaceChildren(shell);bind(shell);state.fastPaints++;return true} 31async function ensureApprovalAdmin(){const ready=globalThis.__TFONE_APPROVAL_ADMIN_P0__;if(ready?.openLeaveRequest)return ready;if(state.leaveLoader)return state.leaveLoader;state.leaveLoader=new Promise((resolve,reject)=>{let s=document.querySelector('script[data-tfone-approval-admin-p0="1"]');const done=()=>{const api=globalThis.__TFONE_APPROVAL_ADMIN_P0__;if(api?.openLeaveRequest)resolve(api);else reject(new Error('ë체근무ì í´ê° ì ì² ëª¨ëì ì´ ì ììµëë¤.'))};const fail=()=>reject(new Error('í´ê° ì ì² ëª¨ëì ë¶ë¬ì¤ì§ 못íìµëë¤.'));if(s){s.addEventListener('load',done,{once:true});s.addEventListener('error',fail,{once:true});setTimeout(()=>globalThis.__TFONE_APPROVAL_ADMIN_P0__?.openLeaveRequest&&done(),0);return}s=document.createElement('script');s.src=APPROVAL_ADMIN_SRC;s.defer=true;s.setAttribute('data-tfone-approval-admin-p0','1');s.addEventListener('load',done,{once:true});s.addEventListener('error',fail,{once:true});document.head.appendChild(s)}).finally(()=>{state.leaveLoader=null});return state.leaveLoader} 32async function openLeaveModal(e){e?.preventDefault?.();e?.stopPropagation?.();$('#tfone-leave-modal-p0')?.remove();try{const api=await ensureApprovalAdmin();api.openLeaveRequest()}catch(err){state.lastError=String(err?.message||err);toast('í´ê° ì ì² íë©´ ì¤ë¥ · '+state.lastError,true)}} 33async function refresh(force=false){if(!state.active)return false;const r=route();if(!['today','approval'].includes(r)){$('#tfone-attendance-card-p0')?.remove();clearLegacyLoading();return false}try{const x=await ctx();if(!x)return false;state.lastAtt=x.att||null;state.lastSchedule=x.schedule||null;state.ready=true;fixToday(state.lastAtt,state.lastSchedule);fixApproval(state.lastAtt,state.lastSchedule);state.lastError='';state.lastAt=Date.now();clearLegacyLoading();return true}catch(e){state.lastError=String(e?.message||e);clearLegacyLoading();return false}} 34function scheduleDomFix(){clearTimeout(state.timer);state.timer=setTimeout(()=>{const r=route();if(r==='approval'&&!$('#tfone-attendance-card-p0'))fixApproval(state.lastAtt,state.lastSchedule);else if(r==='today')fixToday(state.lastAtt,state.lastSchedule)},45)} 35function bindObserver(){const main=$('#main');if(!main||main===state.observedMain)return;state.observer?.disconnect();state.observedMain=main;state.observer=new MutationObserver(scheduleDomFix);state.observer.observe(main,{childList:true})} 36function interceptLeave(e){const b=e.target?.closest?.('button,a');if(!b||b.closest?.('.tfaa-modal')||b.closest?.('#tfone-leave-modal-p0'))return;const t=String(b.textContent||'').replace(/\s+/g,' ').trim();if(b.matches?.('[data-tfone-leave-apply="1"]')||/^\+?\s*í´ê°\s*ì ì²$/.test(t)){e.preventDefault();e.stopImmediatePropagation();openLeaveModal(e)}} 37function approvalHint(){clearLegacyLoading();requestAnimationFrame(()=>paintApprovalFast(true));setTimeout(()=>{bindObserver();if(route()==='approval')refresh(true)},0)} 38function onRoute(){clearLegacyLoading();bindObserver();if(route()==='approval'){paintApprovalFast();setTimeout(()=>refresh(true),0)}else setTimeout(()=>refresh(),0)} 39function start(){state.active=true;clearLegacyLoading();$('#tfone-leave-modal-p0')?.remove();document.addEventListener('click',interceptLeave,true);document.addEventListener('click',e=>{const b=e.target?.closest?.('[data-v="approval"],[data-ra-route="approval"],[data-view="approval"]');if(b)approvalHint()},true);document.addEventListener('tfone:route',onRoute);addEventListener('popstate',onRoute,{passive:true});addEventListener('pageshow',onRoute,{passive:true});bindObserver();setTimeout(()=>refresh(true),0);if('requestIdleCallback'in globalThis)requestIdleCallback(()=>ensureApprovalAdmin().catch(()=>{}),{timeout:1800});else setTimeout(()=>ensureApprovalAdmin().catch(()=>{}),700)} 40function stop(){state.active=false;clearTimeout(state.timer);state.observer?.disconnect();state.observer=null;state.observedMain=null;$('#tfone-attendance-card-p0')?.remove();$('#tfone-attendance-fast-shell-p0')?.remove();clearLegacyLoading();$('#tfone-leave-modal-p0')?.remove();document.removeEventListener('click',interceptLeave,true);document.removeEventListener('tfone:route',onRoute);removeEventListener('popstate',onRoute);removeEventListener('pageshow',onRoute)} 41globalThis[KEY]={version:VERSION,start,stop,refresh,checkin:()=>rpc('record_first_app_open'),checkout:()=>rpc('record_checkout'),applyLeave:openLeaveModal,paintApprovalFast,state:()=>({...state,observer:!!state.observer,leaveLoader:!!state.leaveLoader})}; 42if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start(); 43})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.