<!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>
댓글 없음:
댓글 쓰기