1/* TF ONE bid multi-notice selector P0 · 2026-09-03 2 * - detects ambiguous bid-list AI reviews 3 * - lets staff specify row / notice title / notice no without re-uploading the list 4 * - re-analyzes the same resource against exactly one target notice 5 * - preserves the normal review/registration form and applies the targeted analysis job 6 */ 7(()=>{ 8'use strict'; 9const KEY='__TFONE_BID_MULTI_NOTICE_SELECTOR_P0__'; 10if(globalThis[KEY])return; 11const state={active:false,lastAnalyze:null,targeted:null,observer:null,enhanced:0,lastError:''}; 12const nativeFetch=globalThis.fetch.bind(globalThis); 13const $=(s,r=document)=>r.querySelector(s), $$=(s,r=document)=>Array.from(r.querySelectorAll(s)); 14function parseBody(init){try{return typeof init?.body==='string'?JSON.parse(init.body):null}catch{return null}} 15function ambiguityText(r){return [r?.summary,...(r?.risks||[]),...(r?.next_actions||[])].filter(Boolean).join(' ')} 16function ambiguous(r){const t=ambiguityText(r);return /ë¨ì¼\s*(?:ì ì°°)?ê³µê³ .*(?:ì§ì |í¹ì )|íë²í¸|index|ìë¡\s*ë¤ë¥¸\s*ì ì°°ê³µê³ |ë¤ì\s*(?:ì\s*)?ì ì°°ê³µê³ |ì¬ë¬\s*(?:ê°ì\s*)?ì ì°°ê³µê³ |ê³µê³ .*목ë¡/i.test(t)} 17async function authToken(){ 18 try{for(let i=0;i<localStorage.length;i++){const k=localStorage.key(i)||'';if(!/^sb-.*-auth-token$/.test(k))continue;const v=JSON.parse(localStorage.getItem(k)||'{}');const t=v?.access_token||v?.currentSession?.access_token||v?.session?.access_token;if(t)return t}}catch{} 19 try{const m=await import('https://esm.sh/@supabase/supabase-js@2');const createClient=m.createClient||m.default?.createClient;if(typeof createClient==='function'){const c=createClient('https://tfswpzkwdmghybmxstpj.supabase.co','sb_publishable_IdMAgUkKrRkqG0RM6HGhxQ_LhAXjbdN',{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:false}});let {data}=await c.auth.getSession();let s=data?.session||null;const exp=Number(s?.expires_at||0)*1000;if(s&&exp&&exp-Date.now()<60000){const r=await c.auth.refreshSession();s=r.data?.session||s}if(s?.access_token)return s.access_token}}catch{} 20 return ''; 21} 22function localInput(v){if(!v)return'';const s=String(v);if(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(s))return s.slice(0,16);try{const d=new Date(v);if(!Number.isFinite(d.getTime()))return'';const z=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${z(d.getMonth()+1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}`}catch{return''}} 23function setValue(form,id,v){const el=form.querySelector(id);if(!el)return;if(el.tagName==='SELECT'){const x=String(v??'');if([...el.options].some(o=>o.value===x||o.text===x))el.value=x;return}el.value=v??''} 24function populate(form,out){const r=out?.result||{};setValue(form,'#aiBidOrg',r.organization_name);setValue(form,'#aiBidTitle',r.title);setValue(form,'#aiBidNotice',r.notice_no);setValue(form,'#aiBidBudget',r.budget);setValue(form,'#aiBidDeadline',localInput(r.deadline));setValue(form,'#aiBidPt',localInput(r.presentation_at));setValue(form,'#aiBidPlace',r.presentation_place);setValue(form,'#aiBidGrade',r.fit_grade||'íì¸íì');setValue(form,'#aiBidContract',r.contract_period);setValue(form,'#aiBidQual',r.qualification);setValue(form,'#aiBidDocs',(r.required_documents||[]).join('\n'));setValue(form,'#aiBidRisks',(r.risks||[]).join('\n'));setValue(form,'#aiBidNext',(r.next_actions||[]).join('\n')); 25 const card=form.closest('.modal-card');const head=card?.querySelector('.ai-review-head');if(head){const b=head.querySelector('b');if(b)b.textContent=r.summary||'ì í ê³µê³ ë¶ì ìë£';const small=head.querySelector('small');if(small)small.textContent=`AI ì 뢰ë ${Math.round(Number(r.confidence||0)*100)}%`;const pill=head.querySelector('.pill');if(pill){pill.textContent=r.fit_grade||'íì¸íì';pill.classList.toggle('important',r.fit_grade==='A')}} 26 form.dataset.tfoneTargetAnalysisId=out.analysis_id||'';state.targeted={analysisId:out.analysis_id,result:r,resourceId:state.lastAnalyze?.resourceId||null,targetHint:out.target_hint||''}; 27} 28function style(){if($('#tfone-bid-target-style'))return;const s=document.createElement('style');s.id='tfone-bid-target-style';s.textContent=` 29 .tfone-bid-target{grid-column:1/-1;border:1px solid #e5c77d;border-left:5px solid #d59a22;background:#fffaf0;border-radius:13px;padding:12px 13px;margin:0 0 12px;color:#3b4650}.tfone-bid-target b{display:block;color:#173f5a;font-size:13px;margin-bottom:5px}.tfone-bid-target p{font-size:10px;line-height:1.55;color:#687b89;margin:4px 0 9px}.tfone-bid-target-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.tfone-bid-target input{min-width:0;width:100%;border:1px solid #cbd8e2;border-radius:10px;padding:10px 11px;font-size:12px;background:#fff}.tfone-bid-target button{border:0;border-radius:10px;background:#0b2d4f;color:#fff;font-weight:900;padding:10px 13px;white-space:nowrap}.tfone-bid-target button:disabled{opacity:.55}.tfone-bid-target .target-state{display:block;font-size:10px;font-weight:800;margin-top:7px;color:#8b5b21}.tfone-bid-target.done{border-color:#93cdb0;background:#f2fbf6}.tfone-bid-target.done .target-state{color:#18724c}.tfone-bid-target-help{grid-column:1/-1;font-size:9px;color:#728492;margin-top:6px} 30 @media(max-width:640px){.tfone-bid-target-row{grid-template-columns:1fr}.tfone-bid-target button{width:100%}} 31 `;document.head.appendChild(s)} 32async function targetedAnalyze(form,hint,btn,status){ 33 const resourceId=state.lastAnalyze?.resourceId;if(!resourceId)throw new Error('ì본 ìë£ ìë³ì를 ì°¾ì§ ëª»íìµëë¤. ìë£í¨ìì AI ë¶ìì ë¤ì ëë¬ ì£¼ì¸ì.'); 34 btn.disabled=true;status.textContent='ì í ê³µê³ ë§ ë¤ì ë¶ì ì¤...'; 35 try{ 36 const token=await authToken();if(!token)throw new Error('ë¡ê·¸ì¸ ì¸ì ì íì¸íì§ ëª»íìµëë¤. ë¤ì ë¡ê·¸ì¸ í ìëí´ ì£¼ì¸ì.'); 37 const res=await nativeFetch('/api/bid-target-analyze',{method:'POST',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({resource_id:resourceId,target_hint:hint})}); 38 const text=await res.text();let out={};try{out=text?JSON.parse(text):{}}catch{} 39 if(!res.ok)throw new Error(out.error||`ì í ê³µê³ ë¶ì ì¤ë¥ ${res.status}`); 40 populate(form,out);const panel=form.querySelector('.tfone-bid-target');if(panel)panel.classList.add('done');status.textContent='ì í ê³µê³ ë¶ì ìë£ Â· ìë ë´ì© íì¸ í ë±ë¡'; 41 const input=form.querySelector('.tfone-bid-target-input');if(input)input.disabled=true;btn.textContent='ë¶ì ìë£'; 42 }catch(e){state.lastError=String(e?.message||e);status.textContent=`ë¶ì ì¤í¨ · ${state.lastError}`;btn.disabled=false;throw e} 43} 44function enhance(form){if(!form||form.dataset.tfoneBidTargetEnhanced)return;const r=state.lastAnalyze?.out?.result||{};
44if(!ambiguous(r))return;style();form.dataset.tfoneBidTargetEnhanced='1';state.enhanced++; 45 const first=form.querySelector('.form-grid');if(!first)return;const panel=document.createElement('div');panel.className='tfone-bid-target';panel.innerHTML=`<b>ëª©ë¡ íì¼ Â· ë¶ìí ê³µê³ 1ê±´ì ì§ì í´ ì£¼ì¸ì</b><p>íì¼ì ë¤ì ì¬ë¦´ íì ììµëë¤. <strong>íë²í¸, ê³µê³ ëª , ê³µê³ ë²í¸ ì¤ íë</strong>를 ì ë ¥íë©´ ê°ì ì본ìì ê·¸ ê³µê³ ë§ ë¤ì ì½ìµëë¤.</p><div class="tfone-bid-target-row"><input class="tfone-bid-target-input" placeholder="ì: 5í / ââë³µì§ê´ ììì¬ ë©í / R26BK..."><button type="button" class="tfone-bid-target-run">ì í ê³µê³ ë¶ì</button></div><span class="target-state">ìì§ ëì ê³µê³ ë¥¼ ì§ì íì§ ìììµëë¤.</span><div class="tfone-bid-target-help">ê³µê³ ë³ ì¸ë¶ ì문·ì ììì²ìê° ë³ë íì¼ì´ë©´ ì´ ëª©ë¡ì 먼ì í¹ì í ë¤, ìë£í¨ìì ê·¸ ì문ì âì ì°°ê³µê³ âë¡ ì¶ê° ë±ë¡íë©´ ë©ëë¤.</div>`;first.prepend(panel); 46 const input=panel.querySelector('.tfone-bid-target-input'),btn=panel.querySelector('.tfone-bid-target-run'),status=panel.querySelector('.target-state');btn.onclick=async()=>{const hint=input.value.trim();if(!hint){status.textContent='íë²í¸Â·ê³µê³ ëª Â·ê³µê³ ë²í¸ ì¤ íë를 ì ë ¥í´ ì£¼ì¸ì.';input.focus();return}try{await targetedAnalyze(form,hint,btn,status)}catch{}}; 47} 48function scan(){const form=$('#aiBidReview');if(form)enhance(form)} 49function beforeFetch(input,init){ 50 const url=typeof input==='string'?input:(input?.url||''); 51 if(/\/api\/ai-analysis-apply(?:\?|$)/.test(url)){ 52 const body=parseBody(init),form=$('#aiBidReview');const targetId=form?.dataset?.tfoneTargetAnalysisId; 53 if(body&&targetId&&state.targeted?.result){body.analysis_id=targetId;body.result={...state.targeted.result,...body.result,summary:state.targeted.result.summary,confidence:state.targeted.result.confidence};init={...(init||{}),body:JSON.stringify(body)}} 54 } 55 return {input,init}; 56} 57async function afterFetch(input,init,response){ 58 const url=typeof input==='string'?input:(input?.url||''); 59 if(/\/api\/ai-resource-analyze(?:\?|$)/.test(url)){ 60 const req=parseBody(init);if(req?.analysis_type==='bid')try{const out=await response.clone().json();state.lastAnalyze={resourceId:req.resource_id,out,at:Date.now()};setTimeout(scan,0);setTimeout(scan,120)}catch{} 61 } 62 return response; 63} 64function start(){if(state.active)return;state.active=true;style();scan();for(const ms of [80,180,360,700])setTimeout(scan,ms);document.addEventListener('tfone:route',scan);addEventListener('pageshow',scan,{passive:true})} 65function stop(){document.removeEventListener('tfone:route',scan);removeEventListener('pageshow',scan);state.observer=null;state.active=false} 66globalThis[KEY]={start,stop,scan,beforeFetch,afterFetch,state:()=>({...state,observer:!!state.observer})}; 67if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start(); 68})();
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.