<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<title>🚓 특정 poi 기반 출동/순찰 시스템</title>
<!-- 🔽 자동 새로고침: 30초마다 새로고침 -->
<meta http-equiv="refresh" content="30">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f9f9f9; }
h3 { text-align: center; background: #007bff; color: white; margin: 0; padding: 10px; }
#panel { padding: 10px; text-align: center; max-width: 400px; margin: 10px auto; }
#panel input, #panel button {
width: 90%; max-width: 350px; padding: 10px; margin: 5px auto;
font-size: 16px; display: block; border: 1px solid #ccc; border-radius: 5px;
}
#map { height: 70vh; width: 100%; }
#clickedCoords {
width: 90%; max-width: 350px; padding: 8px; margin: 5px auto;
border: 1px solid #007bff; background: #eef6ff; font-size: 14px;
border-radius: 5px; text-align: center;
}
#copyBtn {
width: 90%; max-width: 350px; background: #ff9800; color: white;
border: none; padding: 8px; border-radius: 5px; cursor: pointer;
}
#completeBtn {
background: #28a745; color: white; border: none;
padding: 10px; border-radius: 5px; font-size: 16px;
cursor: pointer; width: 90%; max-width: 350px; margin: 10px auto; display: block;
}
#navigateBtn {
background:#007bff; color:white; border:none;
padding:10px; border-radius:5px; font-size:16px;
cursor:pointer; width:90%; max-width:350px; margin:10px auto; display:block;
}
#upcomingPanel {
position: fixed; top: 10px; right: 10px; width: 250px; max-height: 60vh;
overflow-y: auto; background: rgba(255,255,255,0.95);
border: 1px solid #007bff; border-radius: 5px; padding: 10px;
font-size: 14px; z-index: 9999; box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease; cursor: pointer;
}
#upcomingPanel h4 { margin: 0 0 5px; font-size: 16px; color: #007bff; }
#upcomingList { list-style: none; padding: 0; margin: 0; }
#upcomingList li { padding: 5px 8px; border-bottom: 1px solid #ddd; cursor: pointer; border-radius: 3px; user-select: none; }
#upcomingList li:hover { background: #eef6ff; }
#upcomingPanel.hidden { width: 50px; height: 40px; padding: 5px; overflow: hidden; cursor: pointer; }
#upcomingPanel.hidden h4, #upcomingPanel.hidden ul { display: none; }
#upcomingPanel.hidden::after { content: "⏰"; position: absolute; top: 10px; left: 15px; font-size: 20px; color: #007bff; }
</style>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyA5qR9rGB6Qv43IYSMoHKhArm5gJdapXGk&libraries=places"></script>
</head>
<body>
<h3>🚓 함께 만드는 순찰/출동 poi(신정2)</h3>
<div id="panel">
<input type="text" id="keyword" placeholder="키워드를 입력하세요 (예: L0, L003, 범자, 범 피, 전화번호 일부)" />
<button onclick="searchAndShow()">검색</button>
<div id="clickedCoords">📍 지도에서 위치를 클릭하면 좌표가 여기에 표시됩니다.</div>
<button id="copyBtn" onclick="copyCoords()">좌표 복사</button>
<button id="completeBtn" onclick="completePatrol()">✅ 순찰 완료</button>
<button id="navigateBtn" onclick="navigateToPatrol()">📍 Google Maps 길찾기</button>
</div>
<div id="upcomingPanel">
<h4>⏰ 순찰 후 순찰완료 요망</h4>
<ul id="upcomingList"></ul>
</div>
<div id="map"></div>
<script>
/* ------------------------ ✅ 데이터 정의 ------------------------ */
const lampData = {
"L001": { 분류:"가로등", 고유이름:"L001", 전화번호:"02-2345-4444", 주소:"서울 양천구 합정동", 특이사항:"중요", 좌표:"37.385719,126.658645" },
"L002": { 분류:"가로등", 고유이름:"L002", 전화번호:"02-2345-7744", 주소:"서울 양천구 합정동", 특이사항:"점검 필요", 좌표:"37.386500,126.660000" },
"L003": { 분류:"가로등", 고유이름:"L003", 전화번호:"02-2345-4488", 주소:"", 특이사항:"중요", 좌표:"37.387000,126.661000" },
"CV1(42호)": { 분류:"안전조치", 고유이름:"박00", 전화번호:"", 주소:"오목로37길 11(그린빌라)", 특이사항:"25.5.16~25.8.26. 22:00~23:00, 42호)", 좌표:"37.526140,126.856032" },
"CV3(43호)": { 분류:"안전조치", 고유이름:"이00", 전화번호:"", 주소:"오목로34길7", 특이사항:"7.8~9.7(매일 22~23시)", 좌표:"37.524296,126.855753" },
"CV4(41호)": { 분류:"안전조치", 고유이름:"박00", 전화번호:"", 주소:"신목로5 목동삼성아파트 105동", 특이사항:"5.22~8.21(매일 13~14, 22~23)", 좌표:"37.517046,126.876516" },
"CV5(42호)": { 분류:"안전조치", 고유이름:"허00", 전화번호:"", 주소:"목동로23길35", 특이사항:"4.15~8.14(매일 12~13시)", 좌표:"37.527701,126.859364" },
"CV6(43호)": { 분류:"안전조치", 고유이름:"서00", 전화번호:"", 주소:"은행정로42 신서고등학교", 특이사항:"6.8~9.7(월~금 8~9, 22~23시)", 좌표:"37.523921,126.860085" },
"CV7(43호)": { 분류:"안전조치", 고유이름:"오00", 전화번호:"", 주소:"목동로11길 33", 특이사항:"5.23~8.22(매일 23~24시)", 좌표:"37.522949,126.861280" },
"CV8(42호)": { 분류:"안전조치", 고유이름:"강00", 전화번호:"", 주소:"중앙로52길 50", 특이사항:"7.15~8.14(매일 9~10, 21~22)", 좌표:"37.525925,126.855922" },
"CV10(42호)": { 분류:"안전조치", 고유이름:"김00", 전화번호:"", 주소:"목동로 193", 특이사항:"7.19~8.17(11~12, 21~22))", 좌표:"37.525567,126.858867" },
"CV11(41호)": { 분류:"안전조치", 고유이름:"조00", 전화번호:"", 주소:"신목로5길 15", 특이사항:"7.19~8.18(13~14시)", 좌표:"37.517567,126.872267" },
"CV11-1(41호)": { 분류:"안전조치", 고유이름:"조00", 전화번호:"", 주소:"쌍용아파트 102동", 특이사항:"7.19~8.18(13~14시)", 좌표:"37.518182,126.875558" },
"CV12(41호)": { 분류:"안전조치", 고유이름:"채00", 전화번호:"", 주소:"오목로354 101동", 특이사항:"7.18~8.17(15~16, 22~23)", 좌표:"37.523919,126.876851" },
"CV13(41호)": { 분류:"안전조치", 고유이름:"김000", 전화번호:"", 주소:"오목로300 204동", 특이사항:"7.31~8.30(05~06, 19~20)", 좌표:"37.524183,126.870829" },
"PP1": { 분류:"공중전화", 고유이름:"PP1", 전화번호:"02-2696-2896", 주소:"목동로 189", 특이사항:"제일은행 앞", 좌표:"37.525392,126.864424" },
"PP2": { 분류:"공중전화", 고유이름:"PP2", 전화번호:"02-2065-9837", 주소:"신정중앙로 81", 특이사항:"현대공업사, 점심밥상 앞", 좌표:"37.526729,126.860807" },
"PP3": { 분류:"공중전화", 고유이름:"PP3", 전화번호:"02-2696-7731", 주소:"중앙로 326", 특이사항:"재정마트 앞 노상", 좌표:"37.52496,126.850586" },
"PP4": { 분류:"공중전화", 고유이름:"PP4", 전화번호:"02-2694-9260", 주소:"신월로 337", 특이사항:"우리은행 신정동 금융센터 앞 노상", 좌표:"37.521859,126.858348" },
"PP5": { 분류:"공중전화", 고유이름:"PP5", 전화번호:"02-2654-0174", 주소:"신목로 85", 특이사항:"오목교7번출구 240미터(골드치과 앞)", 좌표:"37.522277,126.874334" },
"PP6": { 분류:"공중전화", 고유이름:"PP6", 전화번호:"02-2061-4217", 주소:"목동동로 152", 특이사항:"신정2치안센터 앞", 좌표:"37.519302,126.870335" },
"PP7": { 분류:"공중전화", 고유이름:"PP7", 전화번호:"02-2654-0136/02-2644-8193", 주소:"신정동 129-85", 특이사항:"청구아파트 101동 앞", 좌표:"37.51979,126.875529" },
"PP8": { 분류:"공중전화", 고유이름:"PP8", 전화번호:"02-2654-0262", 주소:"목동동로 12길 60", 특이사항:"현대아파트 입구", 좌표:"37.522168,126.877685" },
"PP9": { 분류:"공중전화", 고유이름:"PP9", 전화번호:"02-2061-6579", 주소:"신목로 5", 특이사항:"신정삼성제2관리사무소 앞", 좌표:"37.517164,126.876795" },
"PP10": { 분류:"공중전화", 고유이름:"PP10", 전화번호:"02-2654-4323/02-2654-0138", 주소:"신목로 10", 특이사항:"목동현대아파트 상가 앞", 좌표:"37.517926,126.876297" },
"PP11": { 분류:"공중전화", 고유이름:"PP11", 전화번호:"02-2652-9138", 주소:"오목로 245", 특이사항:"목동역 지하 1층(화장실 앞)", 좌표:"37.526109,126.8643" },
"PP12": { 분류:"공중전화", 고유이름:"PP12", 전화번호:"02-2061-0296", 주소:"목동로 225", 특이사항:"홍익병원 본관 2층", 좌표:"37.528466,126.863688" },
};
const patrolSchedule = [
{ id:"CV1(42호)", startDate:"2025-05-16", endDate:"2025-08-26", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"22:00", end:"23:00" }] },
{ id:"CV3(43호)", startDate:"2025-07-08", endDate:"2025-09-07", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"22:00", end:"23:00" }] },
{ id:"CV4(41호)", startDate:"2025-05-22", endDate:"2025-08-21", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"13:00", end:"14:00" },{ start:"22:00", end:"23:00" }] },
{ id:"CV5(42호)", startDate:"2025-04-15", endDate:"2025-08-14", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"12:00", end:"13:00" }] },
{ id:"CV6(43호)", startDate:"2025-06-08", endDate:"2025-09-07", daysOfWeek:[1,2,3,4,5], timeRanges:[{ start:"08:00", end:"09:00" },{ start:"22:00", end:"23:00" }] },
{ id:"CV7(43호)", startDate:"2025-05-23", endDate:"2025-08-22", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"23:00", end:"00:00" }] },
{ id:"CV8(42호)", startDate:"2025-07-15", endDate:"2025-08-14", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"09:00", end:"10:00" },{ start:"21:00", end:"22:00" }] },
{ id:"CV10(42호)", startDate:"2025-07-19", endDate:"2025-08-17", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"11:00", end:"12:00" },{ start:"21:00", end:"22:00" }] },
{ id:"CV11(41호)", startDate:"2025-07-19", endDate:"2025-08-18", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"13:00", end:"14:00" }] },
{ id:"CV11-1(41호)", startDate:"2025-07-19", endDate:"2025-08-18", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"17:00", end:"18:00" },{ start:"21:00", end:"22:00" }] },
{ id:"CV12(41호)", startDate:"2025-07-18", endDate:"2025-08-17", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"15:00", end:"16:00" },{ start:"22:00", end:"23:00" }] },
{ id:"CV13(41호)", startDate:"2025-07-31", endDate:"2025-08-30", daysOfWeek:[0,1,2,3,4,5,6], timeRanges:[{ start:"05:00", end:"06:00" },{ start:"19:00", end:"20:00" }] },
];
/* ✅ 날짜/시간 유틸 함수 */
function isWithinDateRange(dateStr,startDateStr,endDateStr){ const d=new Date(dateStr); return d>=new Date(startDateStr)&&d<=new Date(endDateStr); }
function isDayAllowed(date,days){return days.includes(date.getDay());}
function parseCoords(s){const[a,b]=s.split(",").map(Number);return{lat:a,lng:b};}
function pad(n){ return n.toString().padStart(2,"0"); }
function normalizeText(str){ return (str||"").toLowerCase().replace(/[\s\-\_\.,]/g,""); }
function isSequenceMatch(text, keyword){ let idx=0; for(const ch of keyword){ idx=text.indexOf(ch, idx); if(idx === -1) return false; idx++; } return true; }
/* ✅ 로컬스토리지 관리 */
function getTodayKey(){ return "patrol_" + new Date().toISOString().slice(0,10); }
function loadCompletedPatrols(){ const data=localStorage.getItem(getTodayKey()); return data?new Set(JSON.parse(data)):new Set(); }
function saveCompletedPatrols(){ localStorage.setItem(getTodayKey(), JSON.stringify([...completedPatrols])); }
let completedPatrols = loadCompletedPatrols();
/* ✅ 상태 변수 */
let activePatrol = null;
let lastClickedCoord = "";
let map, markers=[], infoWindow;
/* ✅ 순찰 리스트 표시 */
function showUpcomingPatrols(){
const now=new Date(), nowM=now.getHours()*60+now.getMinutes();
const list=document.getElementById("upcomingList");
list.innerHTML="";
const upcoming=[];
patrolSchedule.forEach(task=>{
if(!isWithinDateRange(now.toISOString().slice(0,10),task.startDate,task.endDate)) return;
if(!isDayAllowed(now,task.daysOfWeek)) return;
task.timeRanges.forEach(r=>{
const [sH,sM]=r.start.split(":").map(Number);
const [eH,eM]=r.end.split(":").map(Number);
const startM=sH*60+sM-30;
const endM=eH*60+eM+30;
const isOngoing=nowM>=sH*60+sM && nowM<=eH*60+eM;
const isInWindow=nowM>=startM && nowM<=endM;
const patrolKey=`${task.id}|${r.start}~${r.end}`;
if(isInWindow && !completedPatrols.has(patrolKey)){
upcoming.push({task,hour:sH,minute:sM,endHour:eH,endMinute:eM,key:patrolKey,isInTime:isOngoing});
}
});
});
if(upcoming.length===0){ list.innerHTML="<li>✅ 예정 없음</li>"; return; }
upcoming.sort((a,b)=>(a.hour*60+a.minute)-(b.hour*60+b.minute));
upcoming.forEach(item=>{
const li=document.createElement("li");
li.style.color=item.isInTime?"blue":"black";
li.textContent=`${item.task.id} - ${pad(item.hour)}:${pad(item.minute)}~${pad(item.endHour)}:${pad(item.endMinute)}`;
li.onclick=()=>{
activePatrol={task:item.task,hour:item.hour,minute:item.minute,key:item.key};
const id = item.task.id;
if(!lampData[id]){ alert("해당 ID의 좌표 정보가 없습니다."); return; }
const pos=parseCoords(lampData[id].좌표);
window.open(`https://www.google.com/maps/dir/?api=1&destination=${pos.lat},${pos.lng}`,"_blank");
};
list.appendChild(li);
});
}
/* ------------------------ ✅ 지도 기능 ------------------------ */
function initMap(){
map=new google.maps.Map(document.getElementById("map"),{center:{lat:37.3857,lng:126.6586},zoom:15});
infoWindow=new google.maps.InfoWindow();
map.addListener("click",e=>{
lastClickedCoord=`${e.latLng.lat().toFixed(6)},${e.latLng.lng().toFixed(6)}`;
document.getElementById("clickedCoords").innerText=`📍 클릭 좌표: ${lastClickedCoord}`;
});
}
function copyCoords(){ if(!lastClickedCoord){ alert("⚠️ 먼저 지도를 클릭하세요."); return; } navigator.clipboard.writeText(lastClickedCoord).then(()=>alert("✅ 복사됨: "+lastClickedCoord)); }
function clearMarkers(){ markers.forEach(m=>m.setMap(null)); markers=[]; }
function addMarker(id,pos,color){
const m=new google.maps.Marker({position:pos,map,label:id,icon:{url:`http://maps.google.com/mapfiles/ms/icons/${color}-dot.png`}});
m.addListener("click", () => {
const data = lampData[id];
infoWindow.setContent(`
<div style="min-width:150px;">
<strong>${data.고유이름}</strong><br/>
<b>분류:</b> ${data.분류}<br/>
<b>전화번호:</b> ${data.전화번호}<br/>
<b>주소:</b> ${data.주소}<br/>
<b>특이사항:</b> ${data.특이사항}
</div>
` );
infoWindow.open(map, m);
});
m.addListener("dblclick",()=>{const c=lampData[id].좌표.split(",");window.open(`https://www.google.com/maps/dir/?api=1&destination=${c[0]},${c[1]}`,"_blank");});
markers.push(m);
}
/* ✅ 검색 */
function searchAndShow(){
const kw=document.getElementById("keyword").value.trim();
if(!kw){ alert("⚠️ 키워드 입력하세요."); return; }
clearMarkers();
const kwNoSpace=kw.replace(/\s+/g,"").toLowerCase();
if(kwNoSpace.length<2){ alert("⚠️ 최소 2글자 이상 입력"); return; }
const res=Object.keys(lampData).filter(id=>{
const d=lampData[id];
const name=normalizeText(d.고유이름);
if(name===kwNoSpace) return true;
if(/^l\d+$/i.test(d.고유이름)&&d.고유이름.toLowerCase().endsWith(kwNoSpace)) return true;
if(isSequenceMatch(name,kwNoSpace)) return true;
if(normalizeText(d.전화번호).includes(kwNoSpace)) return true;
if([d.주소,d.분류,d.특이사항].some(f=>isSequenceMatch(normalizeText(f),kwNoSpace))) return true;
return false;
}).map(id=>({id,pos:parseCoords(lampData[id].좌표)}));
if(res.length===0){ alert("❌ 검색 결과 없음"); return; }
const bounds=new google.maps.LatLngBounds();
res.forEach(r=>{addMarker(r.id,r.pos,"blue");bounds.extend(r.pos);});
map.fitBounds(bounds);
}
/* ✅ 길찾기 버튼 */
function navigateToPatrol(){
if(!lastClickedCoord){ alert("⚠️ 먼저 지도를 클릭하여 목적지를 선택하세요."); return; }
const [lat,lng]=lastClickedCoord.split(",");
window.open(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`,"_blank");
}
/* ✅ 📌 순찰 완료 → 로그 파일에 append */
function completePatrol(){
if(!activePatrol){ alert("🚫 진행 중인 순찰이 없습니다."); return; }
completedPatrols.add(activePatrol.key);
saveCompletedPatrols();
const patrolId = activePatrol.task.id; // (예: "CV11(41호)")
const data = lampData[patrolId];
const patrolTime = new Date().toLocaleString("ko-KR");
const logLine = `아이디: ${patrolId}, 순찰시간: ${patrolTime}, 고유이름: ${data?.고유이름 || "이름 정보 없음"}, 주소: ${data?.주소 || "주소 정보 없음"}\n`;
// ✅ 기존 로그가 있으면 이어붙이기
const existingLog = localStorage.getItem("patrol_text_log") || "";
const newLog = existingLog + logLine;
localStorage.setItem("patrol_text_log", newLog);
// ✅ 하나의 파일로 다운로드
const blob = new Blob([newLog], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "patrol_log.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert(`✅ ${activePatrol.task.id} (${activePatrol.key.split("|")[1]}) 순찰 완료!`);
activePatrol=null;
showUpcomingPatrols();
}
/* ✅ 초기화 */
document.addEventListener("DOMContentLoaded",()=>{
document.getElementById("upcomingPanel").addEventListener("click",e=>{
if(e.target.tagName.toLowerCase()!=="li") e.currentTarget.classList.toggle("hidden");
});
});
window.onload=()=>{initMap();showUpcomingPatrols();setInterval(showUpcomingPatrols,60000);};
</script>
</body>
</html>
댓글 없음:
댓글 쓰기