맛집 탐색 프로그램 🍽️

heeezni·2025년 6월 5일
post-thumbnail

SPA

  • SPA : Single Page Application
    한 개의 페이지로 이루어진 애플리케이션

  • 페이지 전체를 새로고침🔄 하지 않고,
    동적으로 필요한 데이터나 화면 일부만 바꿔서 보여주는 웹앱 구조

  • SPA는 HTML 파일이 1개
    → 자바스크립트가 모든 뷰를 전환함

  • MPA는 HTML 파일이 여러 개
    <a href>나 window.open()으로 페이지 이동함
구분.js 기반 SPA 구조.html 여러 개로 만든 구조
✅ 진짜 SPA✔️ (Yes)❌ (아님, MPA에 가까움)
페이지 수1개 (예: index.html)여러 개 (main.html, list.html, regist.html 등)
전환 방식JS로 DOM만 교체 (ex. showView())링크 클릭 시 페이지 전체 이동
새로고침없음 (AJAX, 이벤트로만 전환)있음 (다른 HTML로 이동하면 리로드)
사용자 경험앱처럼 매끄러움로딩 지연 있음
속도빠름 (변경된 부분만 렌더링)느림 (전체 페이지 다시 요청)
유지비용높지만 관리 잘하면 효율적초기엔 쉬움, 많아지면 비효율적

사용한 API

Free Google API Key

구글API 사용하기

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY&callback=myMap"></script>

API Key 삭제 습관 들이기

  • 많은 API Key는 네이버, 카카오, 구글 등 외부 서비스와 연결돼 있고, 누군가 내 키를 복사해서 무단으로 API 호출을 하면 요금 청구될 수도 있음
  • 악의적인 사용자가 내 키를 이용해 서버를 공격하거나, 내 한도(limit)를 다 써버릴 수도 있음

1️⃣ Basic - Set The Map Properties

위도경도

var mapProp = {
    center:new google.maps.LatLng(51.508742,-0.120850),
    zoom:5,
};
var map = new

2️⃣ Overlays - Add a marker + Animate the Marker + Icon Instead of Marker

var marker = new google.maps.Marker({
  position:myCenter, //경도 위도 지정
  animation:google.maps.Animation.BOUNCE, //마커 효과
  icon:'pinkball.png'
});
marker.setMap(map);

3️⃣ Events - Click The Marker to Zoom

// Zoom to 9 when clicking on marker
google.maps.event.addListener(marker,'click',function() {
  map.setZoom(9);
  map.setCenter(marker.getPosition());
});

4️⃣ Overlays - InfoWindow

var infowindow = new google.maps.InfoWindow({
  content:"Hello World!"
});

infowindow.open(map,marker);

1. Google Maps API 로드

body태그 닫는 곳 바로 위에

<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCutucJ6ILzhy-Lz-nSsePe30kyW_1gOvQ
&callback=initMap"></script>
</body>

2. JavaScript 로직

index.html

var storeList=[]; // 등록한 가게들을 저장할 배열
let map;
let profileData; // <img src>에 넣을 base64 문자열 데이터

구글맵 로드 완료 시 호출될 콜백 함수

  • Google Maps API에서 자동 호출하는 콜백 함수로, 지도를 초기화함
  • 중심 좌표와 확대 레벨 설정 후, #content 영역에 맵을 생성함
function initMap(){
  	// 1️⃣ 맵 기본 속성
    let mapProp = {
        center: new google.maps.LatLng(37.50973, 127.0555), //지도의 중심좌표 (위도,경도)
        zoom: 16, //확대 레벨
    };
    map = new google.maps.Map(document.getElementById("content"), mapProp);
}

가게 등록 함수 regist()

  • 입력된 상호명, 연락처, 위치정보(위도,경도), 대표 이미지, 아이콘 정보를 객체에 담아 storeList에 저장
  • 입력된 위치로 지도에 마커 생성 후, 마커 클릭 시 InfoWindow 띄우고 확대/중심 이동 처리
function regist(){
    let store = {
        store_name: document.getElementById("store_name").value,
        tel: document.getElementById("tel").value,
        pos: document.getElementById("pos").value,
        profileImg: profileData,
        iconImg: document.getElementById("icon").value
    };
    storeList.push(store);

    // 위도, 경도를 파싱하여 마커 생성
    let latiLogi = store.pos.split(",");
    let pos = new google.maps.LatLng(parseFloat(latiLogi[0]), parseFloat(latiLogi[1]));
	// 2️⃣ 마커 추가 + 애니메이션 효과
    let marker = new google.maps.Marker({
        position: pos,
        animation: google.maps.Animation.BOUNCE,
        icon: {
            url: document.getElementById("icon").value,
            scaledSize: new google.maps.Size(50, 50)
        }
    });

    marker.setMap(map);

    // 마커 클릭하면 줌 땡겨짐 3️⃣~ 
    google.maps.event.addListener(marker, 'click', function() {
      	// 4️⃣ 마커 클릭 시 정보창 표시
        let infowindow = new google.maps.InfoWindow({
            content: "여기 존맛!"
        });

        infowindow.open(map, marker);
      	// ~3️⃣
        map.setZoom(18);
        map.setCenter(marker.getPosition());
    });
}

외부에서 받은 전체 가게 객체를 지도에 등록 registAll(obj)

  • 외부에서 받은 storeList를 반복하며 마커 생성
  • 각 마커에 store 정보 기반 InfoWindow와 애니메이션 적용
  • regist()와 유사한 로직을 반복문 안에 직접 구현 (일괄등록이기 때문)

registAll(obj) 호출 시,
obj는 다른 창 (regist.html)에서 이러한 형태

opener.registAll({ storeList: opener.storeList });
function registAll(obj){
    // 지도에 마커 출력
    for(let i = 0; i < obj.storeList.length; i++){
        let store = obj.storeList[i];
      	// 위도, 경도를 파싱하여 마커 생성
        let latiLogi = store.pos.split(",");
        let pos = new google.maps.LatLng(parseFloat(latiLogi[0]), parseFloat(latiLogi[1]));
		// 2️⃣ 마커 추가 + 애니메이션 효과
        let marker = new google.maps.Marker({
            position: pos,
            animation: google.maps.Animation.BOUNCE,
            icon: {
                url: store.iconImg,
                scaledSize: new google.maps.Size(50, 50)
            }
        });

        marker.setMap(map);
		// 마커 클릭하면 줌 땡겨짐 3️⃣~ 
        google.maps.event.addListener(marker, 'click', function() {
          	// 4️⃣ 마커 클릭 시 정보창 표시
            let infowindow = new google.maps.InfoWindow({
                content: store.store_name
            });

            infowindow.open(map, marker);
          	// ~3️⃣
            map.setZoom(18);
            map.setCenter(marker.getPosition());
        });
    }
}

✅ 리팩터링 中 공통 함수로 분리하기

regist()registAll() 모두 마커를 생성해서 지도에 표시하는 공통 로직이 있으니,
그 부분을 공통 함수로 분리할 수 있음 ➡ 리팩터링

목록 보기: 새 창 열기 getList()

  • 새 창(list.html)에 지금까지 등록된 맛집 목록을 출력
function getList(){
    window.open("list.html", "_blank", "width=550px,height=500px");
}

대표 이미지 미리보기 previewImg(e)

  • 사용자가 선택한 파일(이미지)을 FileReader로 읽고,
    base64 dataURL 형태로 변환하여 미리보기 영역에 표시
  • 읽은 data는 profileData에 저장하여 등록 시 사용
function previewImg(e){
    // FileReader를 통해 이미지 파일을 문자열 형태(dataURL)로 읽음
    let reader = new FileReader();
    reader.onload = function(data){
        document.getElementById("preview").src = data.target.result;
        profileData = data.target.result;
    }
    reader.readAsDataURL(e.target.files[0]);
  	/* 이벤트 객체(e) 디버깅해보면
  target의 유사배열 files[0]에 이미지 파일 들어있었음 */
}

일괄 등록용 새 창 열기 batchRegist()

  • 새 창(regist.html)을 띄워 다수의 맛집을 일괄 등록할 수 있도록 함
function batchRegist(){
    let url = "regist.html";
    let name = "pop";
    let options = "width=500px, height=500px";
    window.open(url, name, options);
}

페이지 로드 시 이벤트 바인딩

addEventListener("load", function(){
    // 이미지 파일 선택 시 미리보기 실행
    document.getElementById("profile").addEventListener("change", function(e){
        previewImg(e);
    });
  
    // 등록 버튼
    document.querySelector("#aside_regist :nth-child(7)").addEventListener("click", function(){
        regist();
    });
  
    // 일괄 등록 버튼
    document.querySelector("#aside_regist :nth-child(8)").addEventListener("click", function(){
        batchRegist();
    });
  
    // 목록 보기 버튼
    document.querySelector("#aside_regist :nth-child(9)").addEventListener("click", function(){
        getList();
    });
});

list.js

let obj; // JSON으로 파싱된 데이터 객체를 담을 변수
// 파일 선택 시 실행되는 함수
function loadData(e){
    console.log(e); // e.target.files[0]에 선택한 파일이 들어 있음
    let file = e.target.files[0]; // 사용자가 선택한 파일

    // FileReader를 이용해 텍스트 파일 읽기
    let reader = new FileReader();
    reader.onload = function(data){
        // 읽어들인 텍스트는 JSON 문자열이므로, 객체로 변환해야 사용 가능
        obj = JSON.parse(data.target.result);
        console.log("파싱결과는 ", obj);

        printTable(obj); // 테이블로 화면에 출력
    };

    reader.readAsText(file); // 텍스트로 읽기 시작 (비동기)
}
// 전달받은 객체를 테이블 형태로 출력
function printTable(obj){
    let tag = "<table width='100%' border='1'>";
    tag += "<tr>";
    tag += "<td>No</td>";
    tag += "<td>상호명</td>";
    tag += "<td>연락처</td>";
    tag += "<td>위도경도</td>";
    tag += "<td>대표사진</td>";
    tag += "</tr>";

    // obj.storeList 배열의 각 요소(가게)를 출력
    let n = obj.storeList.length; // 출력용 번호
    for(let i = 0; i < obj.storeList.length; i++){
        let store = obj.storeList[i]; // i번째 가게

        tag += "<tr>";
        tag += "<td>" + n-- + "</td>";
        tag += "<td>" + store.store_name + "</td>";
        tag += "<td>" + store.tel + "</td>";
        tag += "<td>" + store.pos + "</td>";
        tag += "<td>대표사진</td>"; // (추후 이미지 처리 가능)
        tag += "</tr>";
    }

    tag += "</table>";
    document.getElementById("content").innerHTML = tag;
}
// 부모 창(opener)의 registAll() 함수에 현재 객체(obj)를 넘겨 마커 등록
function showIcons(){
    window.opener.registAll(obj); // 부모 창의 마커 등록 함수 호출
    window.close(); // 현재 창 닫기
}
// 이벤트 바인딩
addEventListener("load", function(){
    // 파일 선택 시 loadData 실행
    document.querySelector("#header input[type='file']").addEventListener("change", function(e){
        loadData(e);
    });

    // 지도에 적용하기 버튼 클릭 시 showIcons 실행
    document.querySelector("#footer button").addEventListener("click", function(){
        showIcons();
    });
});

3. 주의사항

• YOUR_API_KEY는 반드시 Google Cloud에서 발급받은 실제 키로 교체하기
• API 키는 깃허브에 절대 올리지 말고 .env 파일이나 비공개 저장소에서 관리하기
• 페이지는 하나지만 JavaScript를 통해 상태와 화면을 계속 갱신하는 게 핵심

profile
아이들의 가능성을 믿었던 마음 그대로, 이제는 나의 가능성을 믿고 나아가는 중입니다.🌱

0개의 댓글