',zIndex:1});
rptKakaoIW.open(rptKakaoMap,rptKakaoMarker);
// rptMapSection removed
},300);
}
/* 실시간 미리보기 */
var rptPreviewTimer=null;
function rptUpdatePreview(){
clearTimeout(rptPreviewTimer);
rptPreviewTimer=setTimeout(function(){
var pw=document.getElementById('rptPreviewWrap');
if(!pw||pw.style.display==='none')return;
rptRefreshPreviewFrame();
},600);
}
function rptRefreshPreviewFrame(){
// 현재 입력값으로 data 구성
var dtEl=document.getElementById('rptDt');
var dtObj2=dtEl&&dtEl.value?new Date(dtEl.value):new Date();
var yy2=String(dtObj2.getFullYear()).slice(2),mo2=dtObj2.getMonth()+1,dd3=dtObj2.getDate();
var dayN=['일','월','화','수','목','금','토'];
var dtFormatted2=dtObj2.getFullYear()+'. '+mo2+'. '+dd3+'.('+dayN[dtObj2.getDay()]+') '
+String(dtObj2.getHours()).padStart(2,'0')+':'+String(dtObj2.getMinutes()).padStart(2,'0');
rpt.data={
type:rpt.type||'화재',station:'서산소방서',
datetime:dtFormatted2,
location:(document.getElementById('rptAddr')||{}).value||'',
callCount:(document.getElementById('rptCallCount')||{}).value||'',
manager:(document.getElementById('rptManager')||{}).value||'',
staff:(document.getElementById('rptStaff')||{}).value||'',
people:(function(){var m=rptGetMobilData();return m.ppl;}()),
vehicles:(function(){var m=rptGetMobilData();return m.veh;}()),
mobilDetail:(function(){return rptGetMobilData();}()),
dead:(document.getElementById('rptDead')||{}).value||'0',
injured:(document.getElementById('rptInj')||{}).value||'0',
casualty:(document.getElementById('rptCasualty')||{}).value||'없음',
propSummary:(document.getElementById('rptPropSummary')||{}).value||'',
situation:(document.getElementById('rptSit')||{}).value||'',
};
var html2=rptBuildHTML();
var pw=document.getElementById('rptPreviewWrap');
if(!pw)return;
var iframes=pw.querySelectorAll('iframe');
iframes.forEach(function(f){f.remove();});
var iframe=document.createElement('iframe');
iframe.style.cssText='width:100%;height:640px;border:none;display:block;';
iframe.setAttribute('sandbox','allow-same-origin');
document.getElementById('rptPreviewContent').appendChild(iframe);
var doc=iframe.contentDocument||iframe.contentWindow.document;
doc.open();doc.write(html2);doc.close();
setTimeout(function(){try{var h=doc.body.scrollHeight;if(h>200)iframe.style.height=h+'px';}catch(e){}},400);
}
/* 창 크기 변경 시 미리보기 scale 재계산 */
var rptResizeTimer=null;
window.addEventListener('resize',function(){
clearTimeout(rptResizeTimer);
rptResizeTimer=setTimeout(function(){
var frame=document.getElementById('rptLiveFrame');
if(!frame||!frame.querySelector('iframe'))return;
rptLiveUpdate();
},300);
});
function gn(id){var e=document.getElementById(id);return e?parseInt(e.value)||0:0;}
function sv(id,v){var e=document.getElementById(id);if(e)e.textContent=v;}
function rptCalcPpl(){var t=gn('ppl_fire')+gn('ppl_police')+gn('ppl_army')+gn('ppl_gov')+gn('ppl_med')+gn('ppl_forest')+gn('ppl_etc');sv('ppl_total',t);sv('ppl_grand_total',t);return t;}
function rptCalcVeh(){var ft=gn('veh_cmd')+gn('veh_pump')+gn('veh_tank')+gn('veh_rescue')+gn('veh_ems')+gn('veh_chem')+gn('veh_ladder')+gn('veh_smoke')+gn('veh_light')+gn('veh_special')+gn('veh_etc');var rt=gn('rel_police')+gn('rel_army')+gn('rel_gov')+gn('rel_med')+gn('rel_etc');sv('veh_fire_total',ft);sv('veh_rel_total',rt);sv('veh_grand_total',ft+rt);return{fireTot:ft,relTot:rt};}
function rptSetMobilField(id,val){var e=document.getElementById(id);if(e)e.value=parseInt(val)||0;}
function rptGetMobilData(){rptCalcPpl();var vr=rptCalcVeh();var pd={fire:gn('ppl_fire'),police:gn('ppl_police'),army:gn('ppl_army'),gov:gn('ppl_gov'),med:gn('ppl_med'),forest:gn('ppl_forest'),etc:gn('ppl_etc'),total:0};pd.total=pd.fire+pd.police+pd.army+pd.gov+pd.med+pd.forest+pd.etc;var vd={cmd:gn('veh_cmd'),pump:gn('veh_pump'),tank:gn('veh_tank'),rescue:gn('veh_rescue'),ems:gn('veh_ems'),chem:gn('veh_chem'),ladder:gn('veh_ladder'),smoke:gn('veh_smoke'),light:gn('veh_light'),special:gn('veh_special'),etc:gn('veh_etc'),fireTot:vr.fireTot,rpolice:gn('rel_police'),rarmy:gn('rel_army'),rgov:gn('rel_gov'),rmed:gn('rel_med'),retc:gn('rel_etc'),relTot:vr.relTot};return{ppl:pd.total+'명',veh:(vd.fireTot+vd.relTot)+'대',pplDetail:pd,vehDetail:vd};}
function init(){
renderOutput();startClock();checkShareSupport();initPWA();
renderFixedContacts();renderContactList('dispatch');renderContactList('agency');
}
function initPWA(){
window.addEventListener('beforeinstallprompt',function(e){e.preventDefault();deferredInstallPrompt=e;if(localStorage.getItem('pwa_dismissed')!=='1')document.getElementById('installBanner').classList.add('show');});
window.addEventListener('appinstalled',function(){document.getElementById('installBanner').classList.remove('show');deferredInstallPrompt=null;showToast('✓ 홈화면에 설치됐습니다!');});
}
function doInstall(){if(!deferredInstallPrompt)return;deferredInstallPrompt.prompt();deferredInstallPrompt.userChoice.then(function(r){if(r.outcome==='accepted')document.getElementById('installBanner').classList.remove('show');deferredInstallPrompt=null;});}
function dismissInstall(){document.getElementById('installBanner').classList.remove('show');localStorage.setItem('pwa_dismissed','1');}
function startClock(){function tick(){var n=new Date();document.getElementById('clock').textContent=pad(n.getHours())+':'+pad(n.getMinutes())+':'+pad(n.getSeconds());}tick();setInterval(tick,1000);}
function pad(n){return String(n).padStart(2,'0');}
function escH(t){return String(t).replace(/&/g,'&').replace(//g,'>');}
function gv(id){var e=document.getElementById(id);return e?e.value.trim():'';}
function switchTab(name,btn){
document.querySelectorAll('.screen').forEach(function(s){s.classList.remove('on');});
document.querySelectorAll('.nav-btn').forEach(function(b){b.classList.remove('on');});
document.getElementById('tab-'+name).classList.add('on');
if(btn)btn.classList.add('on');
if(name==='map')updateMapLinks();
if(name==='settings')updateKeyStatus();
if(name!=='settings'){var lk=document.getElementById('settingsLock');var sc=document.getElementById('settingsContent');if(lk)lk.style.display='block';if(sc)sc.style.display='none';}
if(name==='report'){updateMapLinks();rptAutoFill();setTimeout(rptLiveUpdate,600);}
window.scrollTo({top:0,behavior:'smooth'});
}
function openMoreSub(sub){
document.getElementById('moreHome').style.display='none';
document.querySelectorAll('.sub-screen').forEach(function(s){s.classList.remove('on');});
document.getElementById('moreSub-'+sub).classList.add('on');
if(sub==='reportGen'){rptAutoFill();setTimeout(rptLiveUpdate,600);}
window.scrollTo({top:0,behavior:'smooth'});
}
function closeMoreSub(){
document.getElementById('moreHome').style.display='block';
document.querySelectorAll('.sub-screen').forEach(function(s){s.classList.remove('on');});
window.scrollTo({top:0,behavior:'smooth'});
}
function switchHistSeg(seg,btn){
document.querySelectorAll('#tab-history .seg-btn').forEach(function(b){b.classList.remove('on');});
btn.classList.add('on');
document.getElementById('histSeg-hist').style.display=seg==='hist'?'block':'none';
document.getElementById('histSeg-stat').style.display=seg==='stat'?'block':'none';
if(seg==='stat')renderStats();
}
function switchContactSeg(seg,btn){
document.querySelectorAll('#tab-contacts .seg-btn').forEach(function(b){b.classList.remove('on');});
btn.classList.add('on');
document.getElementById('contactSeg-dispatch').style.display=seg==='dispatch'?'block':'none';
document.getElementById('contactSeg-agency').style.display=seg==='agency'?'block':'none';
}
function addReport(){
var c=document.getElementById('reportContent').value.trim();
if(!c){showToast('내용을 입력해 주세요.');return;}
reportCount++;
var n=new Date();
reports.push({num:reportCount,time:pad(n.getHours())+':'+pad(n.getMinutes()),content:c});
document.getElementById('reportContent').value='';
renderReportList();renderOutput();showToast('제'+reportCount+'보 추가됐습니다.');
}
function renderReportList(){
var wrap=document.getElementById('reportList');
var card=document.getElementById('reportListCard');
wrap.innerHTML='';
if(!reports.length){card.style.display='none';return;}
card.style.display='block';
reports.forEach(function(r,i){
var d=document.createElement('div');d.className='report-item';
d.innerHTML='제'+r.num+'보'+r.time+''+escH(r.content)+'×';
wrap.appendChild(d);
});
}
function delReport(i){reports.splice(i,1);reports.forEach(function(r,j){r.num=j+1;});reportCount=reports.length;renderReportList();renderOutput();}
function extractDong(addr){if(!addr)return '';var m=addr.match(/(\S+(?:읍|면|동|가|리))/g);return m?m[m.length-1]:'';}
function buildRow2(loc,extras){var parts=[];if(loc)parts.push(loc);reports.forEach(function(r){parts.push(r.content);});extras.forEach(function(v){if(v)parts.push(v);});return parts.join(' / ');}
function setSeq(seq,btn){curSeq=seq;document.querySelectorAll('.seq-btn').forEach(function(b){b.classList.remove('on');});btn.classList.add('on');renderOutput();}
function renderOutput(){
var loc=gv('location'),sig=gv('sigName'),dong=extractDong(loc),lines=[];
var obj=gv('fireObject'),scl=gv('fireScale'),cas=gv('fireCasualty');
var t=dong&&obj?dong+' '+obj+'화재':dong?dong+' 화재':obj?obj+' 화재':'화재';
lines.push('['+t+']('+curSeq+')');
var parts=[];
if(loc)parts.push(loc);
reports.forEach(function(r){parts.push(r.content);});
if(scl)parts.push(scl);
if(cas)parts.push(cas);
if(parts.length)lines.push(parts.join(' / '));
if(sig)lines.push('<'+sig+'>');
document.getElementById('outputBox').textContent=lines.join('\n').trimEnd();
var sp=document.getElementById('spellSection');
if(sp&&sp.style.display!=='none'){var si=document.getElementById('spellInput');if(si)si.value=lines.join('\n').trimEnd();var rw=document.getElementById('spellResultWrap');if(rw)rw.style.display='none';}
}
function checkShareSupport(){if(!navigator.share){var b=document.getElementById('sendBtn');if(b)b.style.display='none';var w=document.getElementById('noShareWrap');if(w)w.style.display='block';}}
function sendReport(){var text=document.getElementById('outputBox').textContent;var btn=document.getElementById('sendBtn');if(!navigator.share){copyOnly(text,btn,'📤 전파하기');return;}navigator.share({title:'',text:text}).then(function(){btn.textContent='? 전파 완료!';btn.classList.add('done');setTimeout(function(){btn.innerHTML='📤 전파하기';btn.classList.remove('done');},2500);}).catch(function(err){if(err.name!=='AbortError')showToast('공유에 실패했습니다.');});}
function copyOnly(text,btn,resetLabel){function ok(){var orig=btn.textContent;btn.textContent='? 복사됐습니다!';btn.classList.add('done');setTimeout(function(){btn.innerHTML=resetLabel||orig;btn.classList.remove('done');},2500);}if(navigator.clipboard){navigator.clipboard.writeText(text).then(ok);}else{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok();}}
function copyOnlyText(){var text=document.getElementById('outputBox').textContent;var btn=document.getElementById('copyOnlyBtn');copyOnly(text,btn,'📋 텍스트만 복사');}
function toggleSpell(){var sec=document.getElementById('spellSection');var btn=document.getElementById('spellToggleBtn');if(sec.style.display==='none'){sec.style.display='block';btn.textContent='✓ 맞춤법 검사 닫기';loadReportToSpell();}else{sec.style.display='none';btn.textContent='✓ 전파 전 맞춤법 검사';document.getElementById('spellResultWrap').style.display='none';}}
function loadReportToSpell(){var text=document.getElementById('outputBox').textContent.trim();var el=document.getElementById('spellInput');if(!el)return;if(!text||text.indexOf('자동 생성')>-1){showToast('보고문을 먼저 작성해 주세요.');return;}el.value=text;}
function getCorrectedText(){var el=document.getElementById('spellCorrected');return el?el.textContent.trim():'';}
function sendCorrectedReport(){var text=getCorrectedText();if(!text){showToast('교정 텍스트가 없습니다.');return;}if(!navigator.share){if(navigator.clipboard)navigator.clipboard.writeText(text).then(function(){showToast('? 교정본 복사됨');});return;}navigator.share({title:'',text:text}).then(function(){showToast('? 교정본 전파 완료!');}).catch(function(err){if(err.name!=='AbortError')showToast('공유 실패');});}
function copyCorrectedOnly(){var text=getCorrectedText();if(!text){showToast('교정 텍스트가 없습니다.');return;}if(navigator.clipboard)navigator.clipboard.writeText(text).then(function(){showToast('? 교정본 복사됨');});else{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);showToast('? 교정본 복사됨');}}
async function runSpell(){
var input=(document.getElementById('spellInput').value||'').trim();
if(!input){showToast('검사할 텍스트를 입력해 주세요.');return;}
var btn=document.getElementById('spellBtn'),status=document.getElementById('spellStatus'),resultWrap=document.getElementById('spellResultWrap');
btn.disabled=true;btn.textContent='? 검사 중...';
if(status)status.style.display='block';if(resultWrap)resultWrap.style.display='none';
var prompt='당신은 대한민국 소방 문서 전문 교정관입니다.\n아래 [원문]의 맞춤법과 띄어쓰기만 교정하여 교정된 텍스트만 출력하세요.\n규칙:\n- 소방·행정 전문용어는 절대 수정하지 마세요.\n- 원문의 줄바꿈 구조를 그대로 유지하세요.\n- 교정된 텍스트만 출력하세요. 설명 일체 금지.\n\n[원문]\n'+input;
try{
var resp=await fetch(GEMINI_URL+GEMINI_API_KEY,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({contents:[{parts:[{text:prompt}]}],generationConfig:{temperature:0.1,maxOutputTokens:1500}})});
if(!resp.ok){var ed=await resp.json();throw new Error((ed.error&&ed.error.message)||resp.status);}
var data=await resp.json();
var corrected=((((data.candidates||[])[0]||{}).content||{}).parts||[{}])[0].text||'';
if(!corrected)throw new Error('응답이 비어있습니다.');
corrected=corrected.trim();
var origLines=input.split('\n'),corrLines=corrected.split('\n'),diffs=[];
for(var i=0;i0;
var correctedEl=document.getElementById('spellCorrected');if(correctedEl)correctedEl.textContent=corrected;
var errorsWrap=document.getElementById('spellErrorsWrap'),errorsEl=document.getElementById('spellErrors');errorsEl.innerHTML='';
if(hasError){diffs.forEach(function(d){var row=document.createElement('div');row.style.cssText='padding:6px 0;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:flex-start;';row.innerHTML='?
';if(errorsWrap)errorsWrap.style.display='block';}
if(resultWrap)resultWrap.style.display='block';
showToast(hasError?'맞춤법 오류가 발견됐습니다.':'? 맞춤법 이상 없습니다!');
}catch(e){var msg=e.message||'';if(msg.indexOf('API_KEY')!==-1||msg.indexOf('401')!==-1)showToast('API 키 오류');else if(msg.indexOf('429')!==-1)showToast('사용량 한도 초과');else showToast('맞춤법 검사 실패.');}
finally{btn.disabled=false;btn.textContent='✓ 검사 시작';if(status)status.style.display='none';}
}
function clearAll(){
var has=reports.length||gv('location');
if(has&&!confirm('현재 작성 내용을 초기화할까요?'))return;
reports=[];reportCount=0;
ALL_FIELD_IDS.forEach(function(id){var e=document.getElementById(id);if(e)e.value='';});
var rc=document.getElementById('reportContent');if(rc)rc.value='';
curSeq='1보';
document.querySelectorAll('.seq-btn').forEach(function(b){b.classList.remove('on');});
var fsb=document.querySelector('.seq-btn');if(fsb)fsb.classList.add('on');
renderReportList();renderOutput();showToast('초기화됐습니다.');
}
function setCat(cat,btn){curCat=cat;document.querySelectorAll('.tpl-cat').forEach(function(t){t.classList.remove('on');});btn.classList.add('on');renderTplList();document.getElementById('tplEditCard').style.display='none';}
function renderTplList(){
var list=document.getElementById('tplList');list.innerHTML='';
var items=TEMPLATES[curCat]||[];
if(!items.length){list.innerHTML='
템플릿이 없습니다.
';return;}
items.forEach(function(tpl){var d=document.createElement('div');d.className='tpl-item';d.innerHTML=''+tpl.title+''+tpl.bTxt+'';d.onclick=function(){openTplEdit(tpl);};list.appendChild(d);});
}
function openTplEdit(tpl){curTplOrig=tpl.text;document.getElementById('tplEditTitle').textContent=tpl.title;document.getElementById('tplEditArea').value=tpl.text;var card=document.getElementById('tplEditCard');card.style.display='block';card.scrollIntoView({behavior:'smooth',block:'nearest'});}
function resetTpl(){document.getElementById('tplEditArea').value=curTplOrig;showToast('원래 내용으로 되돌렸습니다.');}
function sendTpl(){var text=document.getElementById('tplEditArea').value.trim();if(!text){showToast('내용을 입력해 주세요.');return;}if(!navigator.share){copyTplOnly();return;}navigator.share({title:'',text:text}).then(function(){showToast('전파 완료!');}).catch(function(err){if(err.name!=='AbortError')showToast('공유 실패');});}
function copyTplOnly(){var text=document.getElementById('tplEditArea').value.trim();if(!text){showToast('내용을 입력해 주세요.');return;}if(navigator.clipboard){navigator.clipboard.writeText(text).then(function(){showToast('복사됐습니다.');});}else{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);showToast('복사됐습니다.');}}
function addDispLog(overrideText){
dispLogSeq++;
var n=new Date(),ts=pad(n.getHours())+':'+pad(n.getMinutes()),date=n.getFullYear()+'-'+pad(n.getMonth()+1)+'-'+pad(n.getDate());
var fullText=overrideText||document.getElementById('outputBox').textContent.trim();
dispLog.unshift({seq:dispLogSeq,date:date,time:ts,outType:curType,accNo:gv('accNo'),place:gv('location'),fullText:fullText,open:false});
addStatEntry(curType,ts);
if(curSeq!=='최종'){var seqMap={'출동':'2보','2보':'3보','3보':'4보','4보':'최종'};var next=seqMap[curSeq]||'최종';curSeq=next;document.querySelectorAll('.seq-btn').forEach(function(b){b.classList.remove('on');});document.querySelectorAll('.seq-btn').forEach(function(b){if(b.textContent.trim()===next)b.classList.add('on');});renderOutput();}
}
function renderHistory(){
var fire=0,rescue=0,ems=0;
dispLog.forEach(function(h){if(h.outType==='화재')fire++;else if(h.outType==='구조')rescue++;else ems++;});
var se=function(id,v){var e=document.getElementById(id);if(e)e.textContent=v;};
se('histTotal',dispLog.length);se('histFire',fire);se('histRescue',rescue);se('histEms',ems);
var wrap=document.getElementById('historyListWrap'),empty=document.getElementById('historyEmpty');
if(!wrap)return;
wrap.querySelectorAll('.hist-card,.hist-date-hdr').forEach(function(el){el.remove();});
if(!dispLog.length){if(empty)empty.style.display='block';return;}
if(empty)empty.style.display='none';
var dateGroups={};
dispLog.forEach(function(h){if(!dateGroups[h.date])dateGroups[h.date]=[];dateGroups[h.date].push(h);});
Object.keys(dateGroups).sort().reverse().forEach(function(date){
var dh=document.createElement('div');dh.className='hist-date-hdr';
dh.style.cssText='font-size:11px;font-weight:800;color:var(--ink4);padding:10px 4px 6px;letter-spacing:0.5px;border-bottom:1px solid var(--border);margin-bottom:6px;';
var parts=date.split('-');dh.textContent=parts[0]+'년 '+parseInt(parts[1])+'월 '+parseInt(parts[2])+'일';
wrap.appendChild(dh);
dateGroups[date].forEach(function(h){
var typeColor=h.outType==='화재'?'var(--orange)':h.outType==='구조'?'var(--blue)':'var(--red)';
var card=document.createElement('div');card.className='hist-card';
var head=document.createElement('div');head.className='hist-head';
head.innerHTML=''+escH(h.outType)+''+h.time+''+(h.place?escH(h.place):'장소 미입력')+''+(h.accNo?''+escH(h.accNo)+'':'')+'▼';
var detail=document.createElement('div');detail.className='hist-detail';
detail.innerHTML='
📤 전파한 보고문
'+escH(h.fullText||'(내용 없음)')+'
';
head.onclick=function(){var isOpen=detail.classList.contains('open');detail.classList.toggle('open');var arr=document.getElementById('hist-arrow-'+h.seq);if(arr)arr.textContent=isOpen?'▼':'▲';};
card.appendChild(head);card.appendChild(detail);wrap.appendChild(card);
});
});
}
function reuseHistory(seq){var h=dispLog.find(function(x){return x.seq===seq;});if(!h)return;var le=document.getElementById('location');if(le&&h.place)le.value=h.place;var ae=document.getElementById('accNo');if(ae&&h.accNo)ae.value=h.accNo;renderOutput();showToast('장소·접수번호를 불러왔습니다.');switchTab('dispatch',document.getElementById('nav-dispatch'));}
function copyHistText(seq){var h=dispLog.find(function(x){return x.seq===seq;});if(!h||!h.fullText)return;if(navigator.clipboard){navigator.clipboard.writeText(h.fullText).then(function(){showToast('복사됐습니다.');});}else{var ta=document.createElement('textarea');ta.value=h.fullText;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);showToast('복사됐습니다.');}}
function clearHistory(){if(!dispLog.length){showToast('삭제할 이력이 없습니다.');return;}if(!confirm('보고이력을 모두 삭제할까요?'))return;dispLog=[];dispLogSeq=0;renderHistory();showToast('삭제됐습니다.');}
var FIXED_KEY='sobang_fixed_contacts_v1';
function loadFixedContacts(){try{return JSON.parse(localStorage.getItem(FIXED_KEY))||{n1:'지휘팀',t1:'',m1:'',n2:'화재조사',t2:'',m2:''};} catch(e){return{n1:'지휘팀',t1:'',m1:'',n2:'화재조사',t2:'',m2:''};}}
function renderFixedContacts(){
var d=loadFixedContacts();
var r1=document.getElementById('fixed1Row'),r2=document.getElementById('fixed2Row'),emp=document.getElementById('fixedEmpty');
if(d.t1){document.getElementById('fixed1Name').textContent=d.n1||'지휘팀';document.getElementById('fixed1Tel').textContent=d.t1;document.getElementById('fixed1Memo').textContent=d.m1||'';document.getElementById('fixed1Call').href='tel:'+d.t1;r1.style.display='flex';}else{r1.style.display='none';}
if(d.t2){document.getElementById('fixed2Name').textContent=d.n2||'화재조사';document.getElementById('fixed2Tel').textContent=d.t2;document.getElementById('fixed2Memo').textContent=d.m2||'';document.getElementById('fixed2Call').href='tel:'+d.t2;r2.style.display='flex';}else{r2.style.display='none';}
if(emp)emp.style.display=(!d.t1&&!d.t2)?'block':'none';
}
function toggleFixedEdit(){
var editEl=document.getElementById('fixedContactEdit'),viewEl=document.getElementById('fixedContactView'),btn=document.getElementById('fixedEditBtn');
var isEdit=editEl.style.display!=='none';
if(isEdit){editEl.style.display='none';viewEl.style.display='block';btn.textContent='✏ 편집';}
else{var d=loadFixedContacts();document.getElementById('fixed1NameInput').value=d.n1||'';document.getElementById('fixed1TelInput').value=d.t1||'';document.getElementById('fixed1MemoInput').value=d.m1||'';document.getElementById('fixed2NameInput').value=d.n2||'';document.getElementById('fixed2TelInput').value=d.t2||'';document.getElementById('fixed2MemoInput').value=d.m2||'';editEl.style.display='block';viewEl.style.display='none';btn.textContent='? 닫기';}
}
function saveFixedContacts(){
var data={n1:document.getElementById('fixed1NameInput').value.trim()||'지휘팀',t1:document.getElementById('fixed1TelInput').value.trim(),m1:document.getElementById('fixed1MemoInput').value.trim(),n2:document.getElementById('fixed2NameInput').value.trim()||'화재조사',t2:document.getElementById('fixed2TelInput').value.trim(),m2:document.getElementById('fixed2MemoInput').value.trim()};
try{localStorage.setItem(FIXED_KEY,JSON.stringify(data));}catch(e){}
renderFixedContacts();toggleFixedEdit();showToast('고정 연락처가 저장됐습니다.');
}
var CONTACT_KEYS={dispatch:'sobang_dispatch_v1',agency:'sobang_agency_v1'};
function loadContacts(type){try{return JSON.parse(localStorage.getItem(CONTACT_KEYS[type])||'[]');}catch(e){return[];}}
function saveContacts(type,arr){try{localStorage.setItem(CONTACT_KEYS[type],JSON.stringify(arr));}catch(e){}}
function addContact(type){var card=document.getElementById(type+'AddCard');if(card){card.style.display='block';card.scrollIntoView({behavior:'smooth',block:'nearest'});}var ne=document.getElementById(type+'Name');if(ne)ne.focus();}
function cancelContact(type){var card=document.getElementById(type+'AddCard');if(card)card.style.display='none';['Name','Tel','Memo'].forEach(function(f){var e=document.getElementById(type+f);if(e)e.value='';});}
function saveContact(type){
var name=((document.getElementById(type+'Name')||{}).value||'').trim(),tel=((document.getElementById(type+'Tel')||{}).value||'').trim(),memo=((document.getElementById(type+'Memo')||{}).value||'').trim();
if(!name){showToast('이름을 입력해 주세요.');return;}if(!tel){showToast('전화번호를 입력해 주세요.');return;}
var arr=loadContacts(type);arr.push({id:Date.now(),name:name,tel:tel,memo:memo});
saveContacts(type,arr);cancelContact(type);renderContactList(type);showToast('연락처가 저장됐습니다.');
}
function deleteContact(type,id){if(!confirm('삭제할까요?'))return;var arr=loadContacts(type).filter(function(c){return c.id!==id;});saveContacts(type,arr);renderContactList(type);}
function renderContactList(type){
var arr=loadContacts(type),list=document.getElementById(type+'List'),empty=document.getElementById(type+'Empty');
if(!list)return;list.innerHTML='';
if(!arr.length){if(empty)empty.style.display='block';return;}
if(empty)empty.style.display='none';
arr.forEach(function(c){var d=document.createElement('div');d.className='contact-item';d.innerHTML='
';list.appendChild(d);});
}
var STATS_KEY='sobang_stats_v1';
function getTodayKey(){var n=new Date();return n.getFullYear()+'-'+(n.getMonth()+1)+'-'+n.getDate();}
function loadStats(){try{var r=localStorage.getItem(STATS_KEY);return r?JSON.parse(r):null;}catch(e){return null;}}
function addStatEntry(dispType,ts){var today=getTodayKey(),data=loadStats();if(!data||data.date!==today)data={date:today,total:0,fire:0,rescue:0,ems:0,timeline:[]};data.total++;if(dispType==='화재')data.fire++;else if(dispType==='구조')data.rescue++;else data.ems++;data.timeline.push({type:dispType,time:ts});try{localStorage.setItem(STATS_KEY,JSON.stringify(data));}catch(e){}}
function clearStats(){if(!confirm('통계를 초기화할까요?'))return;try{localStorage.removeItem(STATS_KEY);}catch(e){}renderStats();showToast('초기화됐습니다.');}
function renderStats(){
var data=loadStats(),today=getTodayKey();
var dateEl=document.getElementById('statsDate');if(dateEl){var n=new Date();dateEl.textContent=n.getFullYear()+'년 '+(n.getMonth()+1)+'월 '+n.getDate()+'일';}
var fire=0,rescue=0,ems=0,total=0;if(data&&data.date===today){fire=data.fire||0;rescue=data.rescue||0;ems=data.ems||0;total=data.total||0;}
var se=function(id,v){var e=document.getElementById(id);if(e)e.textContent=v;};
se('statTotal',total);se('statFire',fire);se('statRescue',rescue);se('statEms',ems);
var tlEl=document.getElementById('statsTimeline');if(!tlEl)return;tlEl.innerHTML='';
if(!data||data.date!==today||!data.timeline||!data.timeline.length){tlEl.innerHTML='
전파 이력이 없습니다.
';return;}
var slots={};data.timeline.forEach(function(t){var hour=parseInt(t.time.split(':')[0],10);var slot=Math.floor(hour/2)*2;var key=pad(slot)+'~'+pad(slot+2)+'시';slots[key]=(slots[key]||0)+1;});
var maxVal=Math.max.apply(null,Object.values(slots));
Object.keys(slots).sort().forEach(function(key){var cnt=slots[key],pct=maxVal>0?Math.round(cnt/maxVal*100):0;var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border);';row.innerHTML=''+key+'
'+cnt+'건';tlEl.appendChild(row);});
}
function searchMapAddr(){
var q=(document.getElementById('mapSearchInput').value||'').trim();
if(!q)return;
window.open('https://map.naver.com/v5/search/'+encodeURIComponent(q),'_blank');
}
function updateMapLinks(){
var loc=gv('location');
var lt=document.getElementById('mapLocText');
var lh=document.getElementById('mapLocHint');
if(lt){
if(loc){lt.textContent=loc;lt.style.color='var(--ink)';if(lh)lh.textContent='아래 지도앱을 탭하면 바로 열립니다';}
else{lt.textContent='출동 탭에서 장소를 입력하세요';lt.style.color='var(--ink4)';}
}
var boxEl=document.getElementById('locStatusBox'),mainEl=document.getElementById('locStatusMain');
if(!loc){if(boxEl)boxEl.className='loc-status-box no-loc';if(mainEl)mainEl.textContent='출동장소 미입력';['mapNaver','mapKakao','mapGoogle'].forEach(function(id){var el=document.getElementById(id);if(el)el.classList.remove('active-loc');});['naverLocLabel','kakaoLocLabel','googleLocLabel'].forEach(function(id){var el=document.getElementById(id);if(el)el.style.display='none';});return;}
if(boxEl)boxEl.className='loc-status-box has-loc';if(mainEl)mainEl.textContent='🗺 '+loc;
var enc=encodeURIComponent(loc),s=loc.length>20?loc.substring(0,20)+'…':loc;
var mapLinks={mapNaver:'https://map.naver.com/v5/search/'+enc,mapKakao:'https://map.kakao.com/link/search/'+enc,mapGoogle:'https://maps.google.com/maps?q='+enc};
Object.keys(mapLinks).forEach(function(id){var el=document.getElementById(id);if(el){el.href=mapLinks[id];el.classList.add('active-loc');}});
['naverLocLabel','kakaoLocLabel','googleLocLabel'].forEach(function(id){var el=document.getElementById(id);if(el){el.textContent='🗺 '+s+' 으로 열기';el.style.display='block';}});
}
/* 동향보고서 */
function rptAutoFill(){
var loc=gv('location');var addrEl=document.getElementById('rptAddr');if(addrEl&&!addrEl.value&&loc)addrEl.value=loc;
var dtEl=document.getElementById('rptDt');if(dtEl&&!dtEl.value){var now=new Date();dtEl.value=new Date(now-now.getTimezoneOffset()*60000).toISOString().slice(0,16);}
}
function rptSetSteps(n){for(var i=0;i<=5;i++){var el=document.getElementById('rptS'+i);if(!el)continue;el.className='rpt-step';if(i유형'+escH(type)+'
');}
el.innerHTML=rows.join('');
}
function rptSelType(btn,type){document.querySelectorAll('.rpt-type-btn').forEach(function(b){b.classList.remove('sel');});btn.classList.add('sel');rpt.type=type;showToast('? '+type+' 선택됨');rptLiveUpdate();}
function rptSkipScan(){rptGoTo(1);}
function rptHandleScanUpload(input){
var files=Array.from(input.files);if(!files.length)return;
rpt.scanImages=[];
var thumbRow=document.getElementById('rptScan0ThumbRow');thumbRow.innerHTML='';
var loaded=0;
files.forEach(function(file){
var reader=new FileReader();
reader.onload=function(e){var b64=e.target.result.split(',')[1];rpt.scanImages.push({b64:b64,type:file.type,url:e.target.result});var img=document.createElement('img');img.src=e.target.result;img.style.cssText='width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid var(--navy);';thumbRow.appendChild(img);loaded++;if(loaded===files.length)document.getElementById('rptScan0Previews').style.display='block';};
reader.readAsDataURL(file);
});
}
async function rptRunScanAI(){
if(!rpt.scanImages||!rpt.scanImages.length){showToast('사진을 먼저 업로드해주세요.');return;}
var btn=document.getElementById('rptScan0Btn'),statusDiv=document.getElementById('rptScan0Status'),msgEl=document.getElementById('rptScan0Msg'),bar=document.getElementById('rptScan0Bar'),doneDiv=document.getElementById('rptScan0Done'),summaryEl=document.getElementById('rptScan0Summary'),errBox=document.getElementById('rptScan0ErrBox');
btn.disabled=true;btn.innerHTML='
분석 중...';
statusDiv.style.display='block';doneDiv.style.display='none';if(errBox)errBox.style.display='none';
var pct=0;
var _msgs=['AI가 이미지를 분석하고 있습니다...','P119 정보 추출 중...','건축물 정보 확인 중...','데이터 정리 중... 잠시만 기다려주세요.'];
var _mi=0;
var pint=setInterval(function(){pct=Math.min(pct+2,88);bar.style.width=pct+'%';if(msgEl&&pct>0&&pct%22===0){_mi=(_mi+1)%_msgs.length;msgEl.textContent=_msgs[_mi];}},150);
var prompt=[
'당신은 대한민국 소방청 문서 분석 전문 AI입니다.',
'첨부된 이미지들을 모두 정밀 분석하세요. P119현장지원시스템 화면과 건축물대장이 포함될 수 있습니다.',
'모든 이미지를 종합하여 하나의 JSON으로만 출력하세요.',
'',
'【절대 규칙】',
'- JSON 객체 하나만 출력. 마크다운 코드블록, 설명 텍스트 절대 없이.',
'- 없는 항목은 null. 숫자 필드는 반드시 정수(0 포함).',
'',
'【P119현장지원시스템 화면에서 추출】',
'■ 사건개요: 재난명→incidentType(화재/구조/구급/기타), 일시→datetime(YYYY-MM-DDTHH:MM), 장소→location, 재난명+내용→situation, 신고건수→callCount',
'■ 피해상황: 사망→dead, 부상→injured, 실종→missing(정수), 인명요약→casualty, 피해→propSummary',
'■ 자원동원: 소방→ppl_fire, 경찰→ppl_police, 군→ppl_army, 관공서→ppl_gov, 의료→ppl_med, 산림→ppl_forest, 기타→ppl_etc',
'■ 소방장비: 지휘→veh_cmd, 펌프→veh_pump, 탱크→veh_tank, 구조→veh_rescue, 구급→veh_ems, 화학→veh_chem, 고가굴절→veh_ladder, 배연→veh_smoke, 조명→veh_light, 특수→veh_special, 기타→veh_etc',
'■ 유관기관: 경찰→rel_police, 군인→rel_army, 관공서→rel_gov, 의료→rel_med, 기타→rel_etc',
'■ 주요조치사항→actions배열: 시간(2026-04-24 12:23:55→HH:MM), 내용(대괄호제거)',
'',
'【건축물대장에서 추출】',
'■ floorData배열: floor/struct/size(예:460㎡)/use/note, 이하여백 제외',
'■ buildingTotalArea(연면적숫자), buildingArea(건축면적), buildingUse(용도), buildingFloors(층수), buildingHeight(높이)',
'',
'【JSON 스키마】',
'{"incidentType":"화재","datetime":"YYYY-MM-DDTHH:MM","location":"주소","callCount":null,"manager":null,"staff":null,"situation":"경위","dead":0,"injured":0,"missing":0,"casualty":"해당없음","propSummary":"피해없음","ppl_fire":0,"ppl_police":0,"ppl_army":0,"ppl_gov":0,"ppl_med":0,"ppl_forest":0,"ppl_etc":0,"veh_cmd":0,"veh_pump":0,"veh_tank":0,"veh_rescue":0,"veh_ems":0,"veh_chem":0,"veh_ladder":0,"veh_smoke":0,"veh_light":0,"veh_special":0,"veh_etc":0,"rel_police":0,"rel_army":0,"rel_gov":0,"rel_med":0,"rel_etc":0,"actions":[{"time":"HH:MM","text":"내용"}],"floorData":[{"floor":"1층","struct":"철근콘크리트구조","size":"460㎡","use":"공공업무시설","note":""}],"buildingTotalArea":null,"buildingArea":null,"buildingUse":null,"buildingFloors":null,"buildingHeight":null}'
].join('\n');
try{
var parts=rpt.scanImages.slice(0,4).map(function(img){return {inlineData:{mimeType:img.type||'image/jpeg',data:img.b64}};});
parts.push({text:prompt});
var resp=await fetch(GEMINI_URL+GEMINI_API_KEY,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({contents:[{parts:parts}],generationConfig:{temperature:0.05,maxOutputTokens:8192,responseMimeType:'application/json'}})});
if(!resp.ok){var errD=await resp.json();throw new Error('API 오류: '+((errD.error&&errD.error.message)||resp.status));}
var data=await resp.json();
var candidate=(data.candidates||[])[0]||{},finishReason=candidate.finishReason||'',raw=((candidate.content||{}).parts||[{}])[0].text||'';
if(finishReason&&finishReason!=='STOP'&&finishReason!=='stop'){var openBraces=0,openBrackets=0;for(var ci=0;ci0){suffix+=']';openBrackets--;}while(openBraces>0){suffix+='}';openBraces--;}if(suffix)raw=raw+suffix;}
var parsed=null,cleanSteps=[raw.trim(),raw.replace(/```json\s*/gi,'').replace(/```\s*/g,'').trim(),raw.replace(/^[^{]*/,'').replace(/[^}]*$/,'').trim()];
for(var si=0;si=16){dtEl.value=dtVal;summary.push('일시: '+parsed.datetime);}}
if(parsed.location){document.getElementById('rptAddr').value=parsed.location;summary.push('장소: '+parsed.location);rptOnAddrInput();}
if(parsed.callCount!=null){document.getElementById('rptCallCount').value=parsed.callCount;summary.push('신고: '+parsed.callCount+'건');}
if(parsed.manager){document.getElementById('rptManager').value=parsed.manager;summary.push('관리: '+parsed.manager);}
if(parsed.staff){document.getElementById('rptStaff').value=parsed.staff;summary.push('담당: '+parsed.staff);}
if(parsed.situation){document.getElementById('rptSit').value=parsed.situation;summary.push('경위: '+parsed.situation.substring(0,30)+(parsed.situation.length>30?'…':''));}
if(parsed.dead!=null)document.getElementById('rptDead').value=parseInt(parsed.dead)||0;
if(parsed.injured!=null)document.getElementById('rptInj').value=parseInt(parsed.injured)||0;
if(parsed.casualty){document.getElementById('rptCasualty').value=parsed.casualty;summary.push('인명: '+parsed.casualty);}
if(parsed.propSummary){document.getElementById('rptPropSummary').value=parsed.propSummary;summary.push('재산: '+String(parsed.propSummary).substring(0,20));}
// 동원인원 - P119 기관별 직접 매핑
var _pk=['ppl_fire','ppl_police','ppl_army','ppl_gov','ppl_med','ppl_forest','ppl_etc'];
var _vk=['veh_cmd','veh_pump','veh_tank','veh_rescue','veh_ems','veh_chem','veh_ladder','veh_smoke','veh_light','veh_special','veh_etc','rel_police','rel_army','rel_gov','rel_med','rel_etc'];
var _pt=0,_vt=0;
_pk.forEach(function(k){var v=parseInt(parsed[k]);if(!isNaN(v)){rptSetMobilField(k,v);_pt+=v;}});
_vk.forEach(function(k){var v=parseInt(parsed[k]);if(!isNaN(v)){rptSetMobilField(k,v);_vt+=v;}});
rptCalcPpl();rptCalcVeh();
if(_pt>0)summary.push('\u{1F46E} 인원 총 '+_pt+'명 자동입력');
if(_vt>0)summary.push('\u{1F692} 장비 총 '+_vt+'대 자동입력');
// personnel/equipment 텍스트 fallback
if(_pt===0&&parsed.personnel){var pM=String(parsed.personnel).match(/(\d+)/);if(pM){rptSetMobilField('ppl_fire',pM[1]);rptCalcPpl();summary.push('\u{1F46E} 인원: '+parsed.personnel);}}
if(_vt===0&&parsed.equipment){var eq=String(parsed.equipment);[{k:'veh_pump',r:/펌프\s*(\d+)/},{k:'veh_tank',r:/탱크\s*(\d+)/},{k:'veh_rescue',r:/구조\s*(\d+)/},{k:'veh_ems',r:/구급\s*(\d+)/},{k:'veh_chem',r:/화학\s*(\d+)/},{k:'veh_ladder',r:/(?:고가|굴절)\s*(\d+)/},{k:'veh_cmd',r:/지휘\s*(\d+)/},{k:'veh_etc',r:/기타\s*(\d+)/}].forEach(function(m){var r=eq.match(m.r);if(r)rptSetMobilField(m.k,r[1]);});rptCalcVeh();summary.push('\u{1F692} 장비: '+eq);}
if(parsed.actions&&Array.isArray(parsed.actions)&&parsed.actions.length){
rpt.actionRows=[];
parsed.actions.forEach(function(a){
if(!a||!a.text)return;
var t=String(a.time||'');var tm=t.match(/(\d{1,2}):(\d{2})/);
var ct=tm?(String(tm[1]).padStart(2,'0')+':'+tm[2]):'';
var cx=String(a.text||'').replace(/^\[.*?\]\s*/,'').trim();
if(cx)rptAddActionRow(ct,cx);
});
if(rpt.actionRows.length)summary.push('조치: '+rpt.actionRows.length+'건 추출');
}
if(parsed.floorData&&Array.isArray(parsed.floorData)&&parsed.floorData.length){
rpt.dmgRows=[];
parsed.floorData.forEach(function(f){
if(!f||String(f.floor||'').indexOf('여백')>=0)return;
var sz=String(f.size||'');
if(sz&&sz.indexOf('\u33a1')<0&&!isNaN(parseFloat(sz)))sz=sz+'\u33a1';
rptAddDmgRow(f.floor||'',f.struct||'',sz,f.use||'',f.note||'');
});
if(rpt.dmgRows.length)summary.push('층별: '+rpt.dmgRows.length+'개 추출');
}
if(parsed.buildingUse)summary.push('건물: '+parsed.buildingUse+(parsed.buildingFloors?' '+parsed.buildingFloors+'층':''));
if(parsed.floorData&&Array.isArray(parsed.floorData)&&parsed.floorData.length){rpt.dmgRows=[];parsed.floorData.forEach(function(f){if(f)rptAddDmgRow(f.floor,f.struct,f.size,f.use,f.note);});if(rpt.dmgRows.length)summary.push('층별: '+rpt.dmgRows.length+'개');}
['rptP1AiBanner','rptP2AiBanner','rptP3AiBanner','rptP4AiBanner'].forEach(function(id){var el=document.getElementById(id);if(el)el.classList.add('show');});
summaryEl.innerHTML=summary.length?summary.map(function(s){var parts2=s.split(': ');var ico=parts2[0]||'';var val=parts2.slice(1).join(': ').trim();return '
';
statusDiv.style.display='none';doneDiv.style.display='block';btn.disabled=false;btn.innerHTML='⚡ 다시 분석';
setTimeout(function(){doneDiv.scrollIntoView({behavior:'smooth',block:'nearest'});},100);
}else{throw new Error('AI 응답 파싱 실패');}
}catch(e){clearInterval(pint);statusDiv.style.display='none';bar.style.width='0%';btn.disabled=false;btn.innerHTML='⚡ 다시 분석';doneDiv.style.display='none';if(errBox){errBox.style.display='flex';var errSpan=errBox.querySelector('span');if(errSpan)errSpan.textContent=e.message&&e.message.length>80?e.message.substring(0,80)+'…':e.message||'알 수 없는 오류';}showToast('? '+(e.message||'분석 실패').substring(0,40));console.error('[rptRunScanAI]',e);}
}
/* ★ 공식 보고서 생성 */
async function rptRunGenerate(){
rptGoTo(5);
var badge=document.getElementById('rptTypeBadge');if(badge)badge.textContent=rpt.type;
var dt=document.getElementById('rptDt').value;
var dtObj=dt?new Date(dt):new Date();
var yy=String(dtObj.getFullYear()).slice(2);
var mo=dtObj.getMonth()+1;
var dd2=dtObj.getDate();
var hh=pad(dtObj.getHours());
var mm2=pad(dtObj.getMinutes());
var dayNames=['일','월','화','수','목','금','토'];
var dayStr=dayNames[dtObj.getDay()];
var dtFormatted=dtObj.getFullYear()+'. '+mo+'. '+dd2+'.('+dayStr+') '+hh+':'+mm2;
var addr=document.getElementById('rptAddr').value.trim()||'(장소 미입력)';
var callCount=(document.getElementById('rptCallCount')||{}).value||'';
var manager=(document.getElementById('rptManager')||{}).value||'';
var staff=(document.getElementById('rptStaff')||{}).value||'';
var _md=rptGetMobilData();
var ppl=_md.ppl||'?';
var veh=_md.veh||'?';
var dead=document.getElementById('rptDead').value||'0';
var inj=document.getElementById('rptInj').value||'0';
var casualty=(document.getElementById('rptCasualty')||{}).value||'없음';
var propSummary=(document.getElementById('rptPropSummary')||{}).value||'';
var sit=document.getElementById('rptSit').value.trim()||'(경위 미입력)';
var dmgLines=rpt.dmgRows.map(function(r){return r.floor+' / '+r.struct+' / '+r.size+' / '+r.use+(r.note?' ('+r.note+')':'');}).join('\n');
var actionLines=rpt.actionRows.map(function(r){return r.time?'('+r.time+') '+r.text:r.text;}).join('\n');
rpt.data={
type:rpt.type,station:'서산소방서',
datetime:dtFormatted,
location:addr,callCount:callCount,
manager:manager,staff:staff,
people:ppl,vehicles:veh,mobilDetail:_md,
dead:dead,injured:inj,
casualty:casualty,propSummary:propSummary,
situation:sit,
};
var pb=document.getElementById('rptProgBar');var pct=0;
var pint=setInterval(function(){pct=Math.min(pct+3,88);pb.style.width=pct+'%';},200);
/* ── 강화된 프롬프트: 공식문서 형식 ── */
var dongName=extractDongForRpt(addr);
var prompt=[
'당신은 대한민국 소방청 문서 작성 AI입니다.',
'아래 사고 정보를 바탕으로 서산소방서 공식 발생보고서를 작성해주세요.',
'',
'【필수 형식】',
'- 각 섹션은 "1. 사고개요", "2. 피해현황", "3. 동원상황", "4. 조치사항" 제목으로 시작',
'- 각 항목은 " ○ 항목명: 내용" 형식',
'- 조치사항은 "(시간) 내용" 형식으로 시간순 나열',
'- 공문서 문체 사용 (경어체 금지, 간결·명확하게)',
'- 각 섹션 사이 빈 줄 하나',
'',
'【사고정보】',
'- 유형: '+rpt.type,
'- 소방서: 서산소방서',
'- 일시: '+dtFormatted+(callCount?' / 신고건수: '+callCount+'건':''),
'- 장소: '+addr+(dongName?' ('+dongName+' 일대)':''),
'- 상황관리: '+(manager||'?'),
'- 담당근무자: '+(staff||'?'),
'- 출동인원: '+ppl+' / 출동장비: '+veh,
'- 인명피해: '+casualty+' (사망 '+dead+'명, 부상 '+inj+'명)',
(propSummary?'- 재산피해: '+propSummary:''),
'- 사고경위: '+sit,
(dmgLines?'- 층별현황:\n'+dmgLines:''),
(actionLines?'- 조치사항 타임라인:\n'+actionLines:''),
(rpt.distNote?'- 선착대 거리: '+rpt.distNote:''),
'',
'위 정보로 공식 발생보고서 본문만 출력. 제목 없이 1. 사고개요부터 시작.'
].filter(Boolean).join('\n');
var genStatusEl=document.getElementById('rptGenStatus');
if(genStatusEl)genStatusEl.textContent='공식 양식으로 작성 중...';
try{
var resp=await fetch(GEMINI_URL+GEMINI_API_KEY,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({contents:[{parts:[{text:prompt}]}],generationConfig:{temperature:0.15,maxOutputTokens:2000}})});
if(!resp.ok){var errD2=await resp.json();throw new Error((errD2.error&&errD2.error.message)||resp.status);}
var data2=await resp.json();
rpt.report=((((data2.candidates||[])[0]||{}).content||{}).parts||[{}])[0].text||'생성 실패';
rpt.data.report=rpt.report;
clearInterval(pint);pb.style.width='100%';
document.getElementById('rptDots').style.display='none';
if(genStatusEl)genStatusEl.textContent='? 작성 완료!';
var oc=document.getElementById('rptOutputCard');
if(oc){
oc.style.display='block';
var meta=document.getElementById('rptFinalMeta');
if(meta)meta.textContent='서산소방서 · '+dtFormatted;
var title=document.getElementById('rptFinalTitle');
if(title)title.textContent=dongName?dongName+' '+rpt.type+' 발생보고서':'서산소방서 '+rpt.type+' 발생보고서';
var badge2=document.getElementById('rptTypeBadge2');if(badge2)badge2.textContent=rpt.type;
// 우측 미리보기 패널에도 최종 결과 표시
rptLiveUpdate();
}
}catch(e){
clearInterval(pint);
document.getElementById('rptDots').style.display='none';
if(genStatusEl)genStatusEl.textContent='오류 발생';
showToast('보고서 생성 실패: '+e.message);
console.error('[rptRunGenerate]',e);
}
}
/* ★ 공식 양식 HTML 빌더 */
function escForHTML(s){return String(s||'').replace(/&/g,'&').replace(//g,'>');}
function extractDongForRpt(addr){
if(!addr)return '';
var m=addr.match(/(\S+(?:읍|면|동|가|리))/g);
return m?m[m.length-1]:'';
}
function rptBuildHTML(){
var d=rpt.data;
var now=new Date();
var dtStr=d.datetime||now.toLocaleString('ko-KR');
var dtObj=now;
if(d.datetime){var raw2=d.datetime.replace(/\.\s*/g,'-').replace(/\(.\)/,'').replace(/\s+/g,' ').trim();var parsed=new Date(raw2);if(!isNaN(parsed.getTime()))dtObj=parsed;}
var yy=String(dtObj.getFullYear()).slice(2);
var mo=dtObj.getMonth()+1;
var dd2=dtObj.getDate();
var dayNames=['일','월','화','수','목','금','토'];
var dayStr=dayNames[dtObj.getDay()];
var shortDate="'"+yy+'.'+mo+'.'+dd2+'('+dayStr+')';
var dongName=extractDongForRpt(d.location||'');
var dmgRows='';
if(rpt.dmgRows&&rpt.dmgRows.length){
rpt.dmgRows.forEach(function(r){
dmgRows+='
'+escForHTML(r.floor||'')+'
'+escForHTML(r.struct||'')+'
'+escForHTML(r.size||'')+'
'+escForHTML(r.use||'')+'
'+escForHTML(r.note||'')+'
';
});
}else{dmgRows='
해당 없음
';}
var actionItems='';
if(rpt.actionRows&&rpt.actionRows.length){
rpt.actionRows.forEach(function(r){
actionItems+='
○ 장 비: 총 '+vT+'대 ( 소방 '+vd.fireTot+'대 / 유관기관 '+vd.relTot+'대 )
'+(vd.fireTot>0?'
- 소방: '+fireVehStr+'
':'')+(vd.relTot>0?'
- 유관기관: '+relVehStr+'
':'')+'
';}())
+'
4. 조치사항
'+actionItems+'
'
+'
5. 위치도 및 현장사진
'
+'
'
+'
위치도
'+mapImg+'
'
+'
현장사진
'+sceneImg+'
'
+'
'
+'
'
+'
';
}
/* ── 미리보기: 우측 패널 스크롤로 대체 ── */
function rptShowPreview(){
// 우측 패널이 있으면 스크롤, 없으면 rptLiveUpdate
var previewEl=document.getElementById('rptSplitPreview');
if(previewEl){
previewEl.scrollIntoView({behavior:'smooth',block:'start'});
rptLiveUpdate();
showToast('우측 미리보기 패널을 확인하세요');
}
}
function rptDownload(fmt){
if(fmt==='pdf'){
var win=window.open('','_blank');
if(win){
win.document.write(rptBuildHTML());
win.document.close();
setTimeout(function(){win.print();},800);
rptShowMsg('Ctrl+P → "PDF로 저장" 선택하세요',true);
}else{
rptShowMsg('팝업 차단을 해제해주세요.',false);
}
}
}
/* ── JPG 변환 공통 함수 ── */
function rptBuildJpgCanvas(callback){
// html2canvas 로 보고서 HTML → canvas → JPG blob
var reportHtml = rptBuildHTML();
// 숨김 iframe에 보고서 렌더 후 캡처
var iframe = document.createElement('iframe');
iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;height:1px;border:none;';
document.body.appendChild(iframe);
var doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open(); doc.write(reportHtml); doc.close();
setTimeout(function(){
iframe.style.height = doc.body.scrollHeight + 'px';
// html2canvas CDN 동적 로드
if(typeof html2canvas === 'undefined'){
var s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
s.onload = function(){
html2canvas(doc.body, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
width: 794,
height: doc.body.scrollHeight
}).then(function(canvas){
document.body.removeChild(iframe);
canvas.toBlob(function(blob){ callback(blob, canvas); }, 'image/jpeg', 0.92);
}).catch(function(e){
document.body.removeChild(iframe);
rptShowMsg('이미지 변환 실패: ' + e.message, false);
});
};
document.head.appendChild(s);
} else {
html2canvas(doc.body, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
width: 794,
height: doc.body.scrollHeight
}).then(function(canvas){
document.body.removeChild(iframe);
canvas.toBlob(function(blob){ callback(blob, canvas); }, 'image/jpeg', 0.92);
}).catch(function(e){
document.body.removeChild(iframe);
rptShowMsg('이미지 변환 실패: ' + e.message, false);
});
}
}, 800);
}
/* ── JPG 저장 ── */
function rptSaveJpg(){
if(!rpt.report && !rpt.data.location){showToast('먼저 보고서를 생성해 주세요.');return;}
rptShowMsg('이미지 변환 중...', true);
rptBuildJpgCanvas(function(blob){
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
var d = rpt.data;
var now = new Date();
var fname = (d.location ? d.location.replace(/\s/g,'').substring(0,10) : '발생보고서')
+ '_' + now.getFullYear()
+ ('0'+(now.getMonth()+1)).slice(-2)
+ ('0'+now.getDate()).slice(-2)
+ '_' + ('0'+now.getHours()).slice(-2)
+ ('0'+now.getMinutes()).slice(-2)
+ '.jpg';
a.href = url;
a.download = fname;
a.click();
URL.revokeObjectURL(url);
rptShowMsg('? 저장 완료!', true);
});
}
/* ── JPG 전파 (카톡·문자 등) ── */
function rptShareJpg(){
if(!rpt.report && !rpt.data.location){showToast('먼저 보고서를 생성해 주세요.');return;}
rptShowMsg('이미지 변환 중...', true);
rptBuildJpgCanvas(function(blob){
var d = rpt.data;
var now = new Date();
var fname = (d.location ? d.location.replace(/\s/g,'').substring(0,10) : '발생보고서')
+ '_' + now.getFullYear()
+ ('0'+(now.getMonth()+1)).slice(-2)
+ ('0'+now.getDate()).slice(-2)
+ '.jpg';
var file = new File([blob], fname, {type:'image/jpeg'});
if(navigator.canShare && navigator.canShare({files:[file]})){
navigator.share({
files: [file],
title: '발생보고서',
text: (d.location||'') + ' ' + (d.type||'') + ' 발생보고서'
}).then(function(){
rptShowMsg('? 전파 완료!', true);
}).catch(function(e){
if(e.name !== 'AbortError') rptShowMsg('전파 실패: ' + e.message, false);
});
} else {
// 공유 미지원 → 저장으로 대체
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = fname; a.click();
URL.revokeObjectURL(url);
rptShowMsg('? 이 기기는 직접 전파가 지원되지 않아 저장했습니다.', true);
}
});
}
function rptCopyText(){
if(!rpt.report){showToast('생성된 보고서가 없습니다.');return;}
if(navigator.clipboard){navigator.clipboard.writeText(rpt.report).then(function(){rptShowMsg('텍스트가 복사됐습니다!',true);});}
else{var ta=document.createElement('textarea');ta.value=rpt.report;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);rptShowMsg('텍스트가 복사됐습니다!',true);}
}
function rptShowMsg(msg,ok){var el=document.getElementById('rptMsg');if(!el)return;el.textContent=msg;el.className='rpt-msg '+(ok?'ok':'fail');el.style.display='block';setTimeout(function(){el.style.display='none';},4000);}
function rptReset(){
rpt.type='';rpt.report='';rpt.data={};rpt.dmgRows=[];rpt.actionRows=[];rpt.mapPhotoURL=null;rpt.scenePhotoURL=null;rpt.distNote='';rpt.scanImages=[];
document.querySelectorAll('.rpt-type-btn').forEach(function(b){b.classList.remove('sel');});
['rptDt','rptCallCount','rptAddr','rptManager','rptStaff','rptSit','rptCasualty','rptPropSummary','rptFirstUnit','rptFirstDist','rptFirstTime','rptHqDist','rptHqTime'].forEach(function(id){var el=document.getElementById(id);if(el)el.value='';});
['ppl_fire','ppl_police','ppl_army','ppl_gov','ppl_med','ppl_forest','ppl_etc','veh_cmd','veh_pump','veh_tank','veh_rescue','veh_ems','veh_chem','veh_ladder','veh_smoke','veh_light','veh_special','veh_etc','rel_police','rel_army','rel_gov','rel_med','rel_etc'].forEach(function(id){var e=document.getElementById(id);if(e)e.value=0;});
rptCalcPpl();rptCalcVeh();
var dd=document.getElementById('rptDead');if(dd)dd.value='0';var di=document.getElementById('rptInj');if(di)di.value='0';
['rptP1AiBanner','rptP2AiBanner','rptP3AiBanner','rptP4AiBanner'].forEach(function(id){var el=document.getElementById(id);if(el)el.classList.remove('show');});
var mp=document.getElementById('rptMapPreview');if(mp)mp.innerHTML='';var sc=document.getElementById('rptScenePreview');if(sc)sc.innerHTML='';
var db=document.getElementById('rptDmgBody');if(db)db.innerHTML='';var al=document.getElementById('rptActionList');if(al)al.innerHTML='';
var pb=document.getElementById('rptProgBar');if(pb)pb.style.width='0%';
var oc=document.getElementById('rptOutputCard');if(oc)oc.style.display='none';
var ds=document.getElementById('rptDots');if(ds)ds.style.display='inline-flex';
var gs=document.getElementById('rptGenStatus');if(gs)gs.textContent='서산소방서 공식 양식으로 작성 중...';
var sp0=document.getElementById('rptScan0Previews');if(sp0)sp0.style.display='none';
var st0=document.getElementById('rptScan0Status');if(st0)st0.style.display='none';
var sd0=document.getElementById('rptScan0Done');if(sd0)sd0.style.display='none';
var sb0=document.getElementById('rptScan0Btn');if(sb0){sb0.disabled=false;sb0.innerHTML='⚡ AI 자동 분석 시작';}
var si0=document.getElementById('rptScan0Input');if(si0)si0.value='';
var th=document.getElementById('rptScan0ThumbRow');if(th)th.innerHTML='';
var bar0=document.getElementById('rptScan0Bar');if(bar0)bar0.style.width='0%';
var eb=document.getElementById('rptScan0ErrBox');if(eb)eb.style.display='none';
// preview wrap removed - handled by rptLiveFrame
rptGoTo(0);showToast('초기화됐습니다.');
}
/* 피해 테이블 */
function rptAddDmgRow(fl,st,sz,us,nt){var id=Date.now()+Math.random();rpt.dmgRows.push({id:id,floor:fl||'',struct:st||'',size:sz||'',use:us||'',note:nt||''});rptRenderDmgInput();rptLiveUpdate();}
function rptRenderDmgInput(){
var body=document.getElementById('rptDmgBody');body.innerHTML='';
rpt.dmgRows.forEach(function(r){var tr=document.createElement('tr');tr.innerHTML='
';body.appendChild(tr);});
}
function rptDmgUp(id,key,val){var r=rpt.dmgRows.find(function(x){return String(x.id)===String(id);});if(r)r[key]=val;}
function rptDmgDel(id){rpt.dmgRows=rpt.dmgRows.filter(function(x){return String(x.id)!==String(id);});rptRenderDmgInput();rptLiveUpdate();}
/* 조치사항 */
function rptAddActionRow(time,text){var id=Date.now()+Math.random();rpt.actionRows.push({id:id,time:time||'',text:text||''});rptRenderActionInput();rptLiveUpdate();}
function rptRenderActionInput(){
var list=document.getElementById('rptActionList');list.innerHTML='';
rpt.actionRows.forEach(function(r){var div=document.createElement('div');div.className='rpt-action-row';div.innerHTML='';list.appendChild(div);});
}
function rptActionUp(id,key,val){var r=rpt.actionRows.find(function(x){return String(x.id)===String(id);});if(r)r[key]=val;}
function rptActionDel(id){rpt.actionRows=rpt.actionRows.filter(function(x){return String(x.id)!==String(id);});rptRenderActionInput();rptLiveUpdate();}
function rptHandlePhotoDrop(event,type){
var files=event.dataTransfer.files;
if(!files||!files.length)return;
rptHandlePhoto({files:files},type);
}
function rptClearPhoto(type){
if(type==='map'){rpt.mapPhotoURL=null;var el=document.getElementById('rptMapPreview');if(el)el.innerHTML='';}
else{rpt.scenePhotoURL=null;var el=document.getElementById('rptScenePreview');if(el)el.innerHTML='';}
rptLiveUpdate();showToast('사진이 삭제됐습니다.');
}
function rptHandlePhoto(input,type){
var file=input.files[0];if(!file)return;
var reader=new FileReader();
reader.onload=function(e){
var url=e.target.result;
var previewId=type==='map'?'rptMapPreview':'rptScenePreview';
var el=document.getElementById(previewId);
if(el)el.innerHTML='';
if(type==='map')rpt.mapPhotoURL=url;else rpt.scenePhotoURL=url;
};
reader.readAsDataURL(file);
}
function rptOnAddrInput(){
clearTimeout(rpt.distTimer);
var addr=(document.getElementById('rptAddr')||{}).value||'';
// removed
rpt.distTimer=setTimeout(function(){rptGeocode(addr.trim());},700);
rptUpdatePreview();
}
function rptGeocode(addr){
fetch('https://dapi.kakao.com/v2/local/search/address.json?query='+encodeURIComponent(addr),{headers:{'Authorization':'KakaoAK '+KAKAO_REST_KEY}})
.then(function(r){return r.json();}).then(function(data){
var docs=data.documents;
if(!docs||!docs.length){
return fetch('https://dapi.kakao.com/v2/local/search/keyword.json?query='+encodeURIComponent(addr),{headers:{'Authorization':'KakaoAK '+KAKAO_REST_KEY}})
// removed
}
rptShowDist(parseFloat(docs[0].y),parseFloat(docs[0].x));
}).catch(function(){document.getElementById('rptDistRows').innerHTML='
? 조회 실패
';});
}
function rptShowDist(lat,lng){
rpt.incidentLatLng={lat:lat,lng:lng};
var _addr=(document.getElementById('rptAddr')||{}).value||'현장';
rptMoveMap(lat,lng,_addr);
var kl=document.getElementById('rptMapKakaoLink');
if(kl){kl.href='https://map.kakao.com/link/map/'+encodeURIComponent(_addr)+','+lat+','+lng;}
var ms=document.getElementById('rptMapSection');
if(ms)ms.style.display='block';
}
function showToast(msg){var el=document.getElementById('toast');el.textContent=msg;el.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(function(){el.classList.remove('show');},2400);}
// 카카오 SDK 로드 완료 후 init 실행
if(window.kakao && window.kakao.maps && typeof kakao.maps.load==='function'){
kakao.maps.load(function(){
window.kakaoMapReady = true;
init();
});
} else {
init();
}
/* ═══════════════════════════════════════
API 키 설정
═══════════════════════════════════════ */
/* ═══════════════════════════════════════
설정 비밀번호 잠금
═══════════════════════════════════════ */
var SETTINGS_PW_HASH = localStorage.getItem('sobang_pw_hash') || '';
var SETTINGS_DEFAULT_PW = '119119'; // 기본 비밀번호
function hashStr(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
var ch = str.charCodeAt(i);
hash = ((hash << 5) - hash) + ch;
hash = hash & hash;
}
return String(hash);
}
// 비밀번호 초기화 (최초 1회)
(function(){
if(!localStorage.getItem('sobang_pw_hash')){
var h = 0, s = '119119';
for(var i=0;i