🚨
상황보고
00:00:00
📱 홈화면에 앱으로 설치하기브라우저 없이 바로 실행됩니다
🚨서산소방서 출동보고
🚨 출동 기본정보 INCIDENT INFO
📋 접수번호
📍 출동 장소 필수
🔥 발화 대상물
📈 화재 규모
👤 인명 피해
📤 발송자
📝 상황 내용 추가 SITUATION REPORT
📋 보고 순서
📤 자동 생성 보고문
정보를 입력하면 자동 생성됩니다.
고정지휘팀 · 화재조사
✎ 편집을 눌러 번호를 등록하세요.
📞 출동대 연락처
📄
+ 버튼으로 출동대를 추가하세요.
0
스캔
1
기본
2
피해
3
동원
4
조치
5
완성
📷
AI 자동 분석
P119 화면 · 건축물대장 사진을 올리면
보고서 내용을 자동으로 입력해 드립니다
📷
사진을 탭하거나 드래그
JPG · PNG · 여러 장 동시 가능
AI가 분석하고 있습니다...
AI 분석 완료!
또는
STEP 1유형 및 기본정보
AI 자동입력 완료 — 확인 후 필요시 수정하세요.
🚨서산소방서(고정)
',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='?
'+escH(d.original)+''+escH(d.fixed)+'
';errorsEl.appendChild(row);});if(errorsWrap)errorsWrap.style.display='block';} else{errorsEl.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='
'+escH(c.name.charAt(0))+'
'+escH(c.name)+'
'+escH(c.tel)+'
'+(c.memo?'
'+escH(c.memo)+'
':'')+'
📞
';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)+'
'); rows.push('
일시'+escH(dtStr)+'
'); rows.push('
장소'+escH(addr)+'
'); rows.push('
출동'+escH(ppl)+' / '+escH(veh)+'
'); rows.push('
인명피해사망 '+escH(dead)+'명 / 부상 '+escH(inj)+'명
'); if(rpt.actionRows.length){rows.push('
조치사항 ('+rpt.actionRows.length+'건)
');rpt.actionRows.slice(0,3).forEach(function(r){rows.push('
'+(r.time||'--:--')+' '+escH(r.text)+'
');});if(rpt.actionRows.length>3)rows.push('
... 외 '+(rpt.actionRows.length-3)+'건
');} 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 '
'+ico.split(' ')[0]+''+ico.split(' ').slice(1).join(' ')+''+escH(val)+'
';}).join(''):'
이미지에서 읽을 수 있는 정보가 적습니다.
'; 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+='

○ '+(r.time?'('+escForHTML(r.time)+') ':'')+escForHTML(r.text||'')+'

'; }); }else if(rpt.report){ var lines=rpt.report.split('\n'),inAction=false; lines.forEach(function(line){ var l=line.trim(); if(l.indexOf('4. 조치사항')>-1||l.indexOf('4.조치사항')>-1)inAction=true; if(inAction&&(l.startsWith('\u25cb')||l.startsWith('\u2022')||/^\(.+\)/.test(l))){actionItems+='

'+escForHTML(l)+'

';} }); if(!actionItems)actionItems='

○ (내용 없음)

'; }else{actionItems='

○ (내용 없음)

';} var mapImg=rpt.mapPhotoURL?'위치도' :'
미첨부
'; var sceneImg=rpt.scenePhotoURL?'현장사진' :'
미첨부
'; var firstArrival=''; if(d.firstUnit||d.firstDist||d.firstTime){ firstArrival='

※ 선착대: '+escForHTML(d.firstUnit||'?') +(d.firstDist?' '+escForHTML(d.firstDist):'') +(d.firstTime?' (~'+escForHTML(d.firstTime)+')':'') +(d.hqDist||d.hqTime?'  /  본서(서산소방서): '+(d.hqDist?escForHTML(d.hqDist)+' ':'')+(d.hqTime?'(~'+escForHTML(d.hqTime)+')':''):'') +'

'; } var callBadge=d.callCount?'신고건수: '+escForHTML(d.callCount)+'건':''; var LOGO=''; return '' +'' +''+escForHTML((dongName?dongName+' ':'')+escForHTML(d.type||rpt.type||'화재')+' 발생보고')+'' +'
' +'
' +'' +'
' +'
'+(dongName?''+escForHTML(dongName)+' ':'')+''+escForHTML(d.type||rpt.type||'화재')+' 발생보고
' +'
상황관리: '+escForHTML(d.manager||'?')+'   당직근무자: '+escForHTML(d.staff||'?')+'
' +'
' +'
'+escForHTML(shortDate)+'
서산소방서
(상황실)
' +'
' +'
1. 사고개요
' +'

'+callBadge+'○ 일 시: '+escForHTML(dtStr)+'(접수) ~

' +firstArrival +'

○ 대 상: '+escForHTML(d.location||'(장소 미입력)')+'

' +'

○ 경 위: '+escForHTML(d.situation||'(경위 미입력)')+'

' +'
' +'
2. 피해현황
' +(function(){var dead2=parseInt(d.dead||0),inj2=parseInt(d.injured||0);var cas=d.casualty||'';var noHuman=(dead2===0&&inj2===0&&(!cas||cas==='없음'||cas==='해당없음'||cas==='0'));var noProp=(!d.propSummary||d.propSummary==='조사 중'||d.propSummary==='0'||d.propSummary==='없음');return '

○ 인명피해: '+(noHuman?'해당사항 없음':escForHTML(cas)+(dead2||inj2?' (사망 '+dead2+'명, 부상 '+inj2+'명)':''))+'

'+'

○ 재산피해: '+(noProp?'해당사항 없음':escForHTML(d.propSummary))+'

';}()) +''+dmgRows+'
구 분구 조크 기용 도비 고
' +'
' +(function(){var md=d.mobilDetail;if(!md)return '
3. 동원상황

○ 인 원: '+escForHTML(d.people||'?')+'

○ 장 비: '+escForHTML(d.vehicles||'?')+'

';var pd=md.pplDetail,vd=md.vehDetail;var bs='font-size:9.5pt;margin:3px 0 3px 36px;color:#333;line-height:1.8;font-weight:normal;';var vT=vd.fireTot+vd.relTot;var pplMap=[['소방',pd.fire],['경찰',pd.police],['군',pd.army],['관공서',pd.gov],['의료',pd.med],['산림',pd.forest],['기타',pd.etc]];var pplStr=pplMap.filter(function(x){return x[1]>0;}).map(function(x){return x[0]+' '+x[1]+'명';}).join(', ')||'없음';var fireVehMap=[['지휘',vd.cmd],['펌프',vd.pump],['탱크',vd.tank],['구조',vd.rescue],['구급',vd.ems],['화학',vd.chem],['고가/굴절',vd.ladder],['배연',vd.smoke],['조명',vd.light],['특수',vd.special],['기타',vd.etc]];var fireVehStr=fireVehMap.filter(function(x){return x[1]>0;}).map(function(x){return x[0]+' '+x[1]+'대';}).join(', ')||'없음';var relVehMap=[['경찰',vd.rpolice],['군인',vd.rarmy],['관공서',vd.rgov],['의료',vd.rmed],['기타',vd.retc]];var relVehStr=relVehMap.filter(function(x){return x[1]>0;}).map(function(x){return x[0]+' '+x[1]+'대';}).join(', ')||'없음';return '
3. 동원상황
'+'

○ 인 원: 총 '+pd.total+'명

'+'

( '+pplStr+' )

'+'

○ 장 비: 총 '+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
📍현재 출동장소
출동 탭에서 장소를 입력하세요
장소 입력 후 아래 지도앱으로 바로 열 수 있습니다
🔍 주소 직접 검색
지도 앱 바로열기
N
네이버지도
K
카카오맵
G
구글지도
관리자 설정 ADMIN SETTINGS
🔒
관리자 전용
비밀번호를 입력해야 API 키를 관리할 수 있습니다
API 등록 현황
Gemini API
미등록
Kakao API
미등록
📄 앱 정보
개발서산소방서
배포fire-report-5m3.pages.dev