2025년 8월 1일 금요일

anyangcheon_poi

<!DOCTYPE html>


<html lang="ko">


<head>


  <meta charset="utf-8" />


  <title>🚓 가로등 위치 기반 현장 출동 시스템 (수동 순찰 버전)</title>


  <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;


    }


    #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>🚓 가로등 위치 기반 출동 시스템 (수동 순찰)</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>


  </div>




  <div id="upcomingPanel">


    <h4>⏰ 순찰 후 순찰완료 요망</h4>


    <ul id="upcomingList"></ul>


  </div>




  <div id="map"></div>




<script>


let map, markers = [], infoWindow;


let lastClickedCoord = "";


let activePatrol = null;


let completedPatrols = new Set();




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": { 분류:"범죄피해자", 고유이름:"CV1", 전화번호:"", 주소:"오목로37길 11(그린빌라 102호)", 특이사항:"25.5.16~25.8.26. 22:00~23:00, 42호)", 좌표:"37.526140,126.856032" },

  "CV2": { 분류:"범죄피해자", 고유이름:"CV2", 전화번호:"", 주소:"오목로24길 19-22 2층", 특이사항:"7.5~8.4(매일 22~23시)", 좌표:"37.522740,126.854003" },

  "CV3": { 분류:"범죄피해자", 고유이름:"CV3", 전화번호:"", 주소:"오목로34길7, 302호", 특이사항:"7.8~9.7(매일 22~23시)", 좌표:"37.524296,126.855753" },

  "CV4": { 분류:"범죄피해자", 고유이름:"CV4", 전화번호:"", 주소:"신목로5 목동삼성아파트 105동 107호", 특이사항:"5.22~8.21(매일 13~14, 22~23)", 좌표:"37.517046,126.876516" },

  "CV5": { 분류:"범죄피해자", 고유이름:"CV5", 전화번호:"", 주소:"목돌로23길35 B동 지층", 특이사항:"4.15~8.14(매일 12~13시)", 좌표:"37.527701,126.859364" },

  "CV6": { 분류:"범죄피해자", 고유이름:"CV6", 전화번호:"", 주소:"은행정로42 신서고등학교", 특이사항:"6.8~9.7(월~금 8~9, 22~23시)", 좌표:"37.523921,126.860085" },

  "CV7": { 분류:"범죄피해자", 고유이름:"CV7", 전화번호:"", 주소:"목동로11길 33 401(키즈뉴빌리지)", 특이사항:"5.23~8.22(매일 23~24시)", 좌표:"37.522949,126.861280" },

  "CV8": { 분류:"범죄피해자", 고유이름:"CV8", 전화번호:"", 주소:"중앙로52길 50 201호", 특이사항:"7.15~8.14(매일 9~10, 21~22)", 좌표:"37.525925,126.855922" },

  "CV9": { 분류:"범죄피해자", 고유이름:"CV9", 전화번호:"", 주소:"목동로19길 31 건향파크빌 402호", 특이사항:"7.8~8.7(순찰미실시)", 좌표:"37.524588,126.861030" },

  "CV10": { 분류:"범죄피해자", 고유이름:"CV10", 전화번호:"", 주소:"목동로 193 5층(아뜰리에 학원)", 특이사항:"7.19~8.17(11~12, 21~22))", 좌표:"37.525567,126.858867" },

  "CV11": { 분류:"범죄피해자", 고유이름:"CV11", 전화번호:"", 주소:"신목로5길 15 1층(화이트스완)", 특이사항:"7.19~8.18(13~14시)", 좌표:"37.517567,126.872267" },

  "CV12": { 분류:"범죄피해자", 고유이름:"CV12", 전화번호:"", 주소:"오목로354 101동 1002호", 특이사항:"7.18~8.17(15~16, 22~23)", 좌표:"37.523919,126.876851" },

  "CV13": { 분류:"범죄피해자", 고유이름:"CV13", 전화번호:"", 주소:"오목로300 204동 701", 특이사항:"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:"CV2(43호)", startDate:"2025-07-05", endDate:"2025-08-04", 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"); }




// 최소 2글자 이상 연속 substring 반환


function getSubstrings(str, minLen = 2) {


  const substrings = [];


  for(let len = minLen; len <= str.length; len++) {


    for(let i = 0; i <= str.length - len; i++) {


      substrings.push(str.substring(i, i + len));


    }


  }


  return substrings;


}




// 텍스트 정규화 (소문자, 띄어쓰기/특수문자 제거)


function normalizeText(str){


  return (str||"").toLowerCase().replace(/[\s\-\_\.,]/g,"");


}




// 문자 순서대로 포함 여부 검사 (띄어쓰기 무시하고 순서만 맞으면 true)


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 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;




      if(isInWindow && !completedPatrols.has(task.id+"|"+sH+"|"+sM)){


        upcoming.push({task,hour:sH,minute:sM,endHour:eH,endMinute:eM,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 };


      const baseId = item.task.id.split("(")[0];


      if (!lampData[baseId]) {


        alert("해당 ID의 좌표 정보가 없습니다.");


        return;


      }


      const pos = parseCoords(lampData[baseId].좌표);


      window.open(`https://www.google.com/maps/dir/?api=1&destination=${pos.lat},${pos.lng}`, "_blank");


    };


    list.appendChild(li);


  });


}




// 순찰 완료 버튼


function completePatrol(){


  if(!activePatrol){


    alert("🚫 진행 중인 순찰이 없습니다.");


    return;


  }


  completedPatrols.add(activePatrol.task.id + "|" + activePatrol.hour + "|" + activePatrol.minute);


  alert(`✅ ${activePatrol.task.id} 순찰 완료!`);


  activePatrol = null;


  showUpcomingPatrols();


}




// 지도 및 클릭 좌표 복사


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];


    const content = `


      <div style="min-width:150px;">


        <strong>${data.고유이름}</strong><br/>


        분류: ${data.분류}<br/>


        전화번호: ${data.전화번호}<br/>


        주소: ${data.주소}<br/>


        특이사항: ${data.특이사항}


      </div>`;


    infoWindow.setContent(content);


    infoWindow.open(map, m);


  });




  m.addListener("dblclick", () => {


    const coords = lampData[id].좌표.split(",");


    const lat = coords[0].trim();


    const lng = coords[1].trim();


    window.open(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`, "_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 data = lampData[id];




    // 고유이름(ID) 필드 검사


    const name = normalizeText(data.고유이름);


    if(name === kwNoSpace) return true; // 정확 일치


    // ID 같은 경우 L001 형태에서 kwNoSpace가 접미사로 맞으면 true (예: "001" 입력 시 L001 매칭)


    if(/^l\d+$/i.test(data.고유이름) && data.고유이름.toLowerCase().endsWith(kwNoSpace)) return true;


    if(isSequenceMatch(name, kwNoSpace)) return true;




    // 전화번호 필드에서만 매칭


    const phone = normalizeText(data.전화번호);


    if(phone.includes(kwNoSpace)) return true;




    // 주소, 분류, 특이사항 각각에서만 매칭 (문자 순서 포함)


    const fields = [normalizeText(data.주소), normalizeText(data.분류), normalizeText(data.특이사항)];


    if(fields.some(f => isSequenceMatch(f, kwNoSpace))) return true;




    // 좌표 필드에서만 매칭 (숫자 쉼표 포함 그대로)


    const coords = normalizeText(data.좌표);


    if(coords.includes(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);


}




// 초기화 및 이벤트 바인딩


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>

댓글 없음:

댓글 쓰기