별 개수: 50
속도: 2

JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

nextjs/웹사이트 만들기

산정보 보기 웹사이트 만들기(2) - 지역 이동 링크를 지도 svg에 연결하기

lamarcK 2025. 4. 14. 10:22

1. 2차 개선 : 지도 클릭으로 이동하도록 개선

구현 아이디어. 각 구역의 링크부분을 각각 지도에 배치하는 방식으로 구현하여 시각적으로 어느 위치에 있는 산인지 파악이 쉽도록 수정

 

지도 데이터는 위키 백과의 svg를 사용했습니다.

 

파일:Map of South Korea-blank.svg - 위키백과, 우리 모두의 백과사전

원본 파일 (SVG 파일, 실제 크기 800 × 1,200 픽셀, 파일 크기: 320 KB) 설명Map of South Korea-blank.svg English: Blank map of territory claimed by South Korea 한국어: 대한민국(남한)의 백지도 날짜 2009년 11월 14일 출처

ko.m.wikipedia.org

 

 

많은 프로그램에서 지역을 클릭하면 해당 지역에 해당하는 정보만 보여주도록 하는데 제대로된 svg만 있으면 구현은 어렵지않다.

★만약에 path가 전체 경로로 그려져 있거나 id가 없으면 직접 구현해야합니다.


2. 1. 개발자 도구에서 svg파일의 path 획득하기

가장 먼저 해야하는 것은 svg 파일의 path와 id를 확인하는 것이다. 해당 경로를 그린 그림이 어떤 id를 가지고 있는지 알아야 링크를 그곳에 연결할 수 있다.

3. 2. path의 id에 데이터 연결하기(매핑)

매핑(Mapping)은 서로 다른 두 데이터를 연결/대응시키는 것을 의미한다. 이부분은 직접 path 경로를 찾고 id를 찾아서 연결시켜야한다.

결국 일단 id를 가져와야 한다는 건데 id를 일일히 찾기는 좀 그러니까 개발자도구의 console 부분에 모든 id를 가져오도록 했다.

만약에 별다른 규칙성이 없는 id라면 어차피 연결하기 위해서 하나하나 찾는게 나을 수도 있다.

typescript
클릭하여 코드 펼치기
// path와 polyline 모두 가져오기
const elements = {};
document.querySelectorAll('path, polyline').forEach(el => {
    if(el.id) elements[el.id] = `#${el.id}`;
});
console.log(elements);

// 또는 더 보기 좋게 형식화하여 출력
const elementString = Array.from(document.querySelectorAll('path, polyline'))
  .filter(el => el.id)
  .map(el => `  '${el.id}': '#${el.id}'`)
  .join(',\n');
console.log('const REGION_PATHS = {\n' + elementString + '\n};');
클릭하여 코드 복사

3.1. 1. 수작업으로 svg의 id와 sql의 province 매핑해주기

수작업으로 위에서 가져온 데이터를 각각 매핑하는 작업을 거쳐줘야한다. 이 부분은 기준이 되는 부분이라서 배열 순회가 불가능하다. 기본적으로 sql에서 가져온 province와 1대1 매핑 시켜줘야한다.

MapComponent.tsx
typescript
클릭하여 코드 펼치기
'use client';

import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { MapComponentProps } from '@/types';

// 지역명과 SVG path id를 매핑하는 객체
const REGION_PATHS: { [key: string]: string } = {
  '경기도': 'gyeonggi',
  '경상남도': 'gyeongnam',
  '충청북도': 'chungbuk',
  '강원도': 'gangwon',
  '충청남도': 'chungnam',
  '경상북도': 'gyeongbuk',
  '전라남도': 'jeonnam',
  '울산광역시': 'ulsan',
  '인천광역시': 'incheon',
  '광주광역시': 'gwangju',
  '대전광역시': 'daejeon',
  '전라북도': 'jeonbuk',
  '부산광역시': 'busan',
  '서울시': 'seoul',
  '제주도': 'jeju',
  '대구광역시': 'daegu'
};
클릭하여 코드 복사

3.2. 2. useEffect를 사용해서 각각 path 부분에 클릭 이벤트 리스너 삽입

fetch를 통해 svg 파일을 불러와주고 쿼리셀렉터로 path를 선택하고 forEach문을 통해 path를 순회하며 이벤트 리스너를 추가해준다. 클릭 됐을 경우 해당 페이지의 상세 페이지로 이동한다.

 

MapComponent.tsx
typescript
클릭하여 코드 펼치기
export default function MapComponent({ className }: MapComponentProps) {
  // SVG를 렌더링할 컨테이너 요소에 대한 ref
  const containerRef = useRef<HTMLDivElement>(null);
  const router = useRouter();

  useEffect(() => {
    // 클릭 이벤트 핸들러를 저장할 Map 객체 생성
    const clickHandlers = new Map();

    // SVG 파일 불러오기
    fetch('/Map_of_South_Korea-blank.svg')
      .then((res) => res.text())
      .then((svgText) => {
        if (containerRef.current) {
          // SVG를 컨테이너에 삽입
          containerRef.current.innerHTML = svgText;
          // SVG 내의 모든 path 요소 선택
          const paths = containerRef.current.querySelectorAll('path');
          
          // 각 path 요소에 클릭 이벤트 리스너 추가
          paths.forEach((path) => {
            const handleClick = () => {
              const clickedId = path.id;
              // 클릭된 path의 id와 매칭되는 지역명 찾기
              const matchedProvince = Object.entries(REGION_PATHS).find(
                ([_, regionId]) => regionId === clickedId
              );
              if (matchedProvince) {
                const [provinceName] = matchedProvince;
                // 해당 지역의 상세 페이지로 이동
                router.push(`/provinces/${provinceName}`);
              }
            };
            path.addEventListener('click', handleClick);
            // 나중에 cleanup을 위해 핸들러 저장
            clickHandlers.set(path, handleClick);
          });
        }
      });

    // 컴포넌트 언마운트 시 이벤트 리스너 제거
    return () => {
      if (containerRef.current) {
        const paths = containerRef.current.querySelectorAll('path');
        paths.forEach((path) => {
          const handler = clickHandlers.get(path);
          if (handler) {
            path.removeEventListener('click', handler);
          }
        });
      }
    };
  }, [router]);

  // SVG를 렌더링할 컨테이너 반환
  return (
    <div
      ref={containerRef}
      className={className}
    />
  );
}
클릭하여 코드 복사

4. 3. 홈페이지에서 프롭스로 호출

app/page.tsx
typescript
클릭하여 코드 펼치기
import { getStates } from '@/lib/db'
import Link from 'next/link'
import MapComponent from '@/components/MapComponent'

export default async function Home() {
  const states = await getStates()

  return (
    <div className="space-y-6">
      <MapComponent 
        states={states} 
        className="w-full h-auto" 
      />      
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      </div>
    </div>
  )
}
클릭하여 코드 복사

지도를 연결했으니 기존의 목록 링크는 삭제해도 되고 오른쪽으로 보내도 괜찮다.

프롭스 연결을 통해서 states란 정보를 보내는데 getStates() 함수를 통해 가져온 값이다.

5. 4. DB 가져오기 재설정

db.ts
typescript
클릭하여 코드 펼치기
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import { Mountain, Province } from '@/types';

let db: any = null;

async function openDb() {
  if (!db) {
    db = await open({
      filename: './mountain01.db',
      driver: sqlite3.Database
    });
  }
  return db;
}

export async function getStates(): Promise<Province[]> {
  const db = await openDb();
  try {
    return await db.all('SELECT DISTINCT PROVINCE FROM T WHERE PROVINCE IS NOT NULL');
  } catch (error) {
    console.error('Error fetching provinces:', error);
    return [];
  }
}

export async function getMountainsByState(province: string): Promise<Mountain[]> {
  const db = await openDb();
  try {
    return await db.all(
      'SELECT FMMNT_INFO_ID as ID, MNTN_NM as NAME, MNTN_INFO_POFLC as ADDRESS, PROVINCE FROM T WHERE PROVINCE IS NOT NULL AND PROVINCE = ?', 
      [province]
    );
  } catch (error) {
    console.error('Error fetching T:', error);
    return [];
  }
}

export async function getMountainById(id: string): Promise<Mountain | null> {
  const db = await openDb();
  try {
    return await db.get(
      `SELECT 
      FMMNT_INFO_ID as ID,
      MNTN_NM as NAME,
      MNTN_INFO_POFLC as ADDRESS,
      MNTN_HGHT as HEIGHT,
      PROVINCE,
      DTL_INFO_CONT as DESCRIPTION,
      MNTN_INFO_IMAGE_URL as IMAGE
    FROM T 
    WHERE FMMNT_INFO_ID = ?`, 
      [id]
    );
  } catch (error) {
    console.error('Error fetching T:', error);
    return null;
  }
}
클릭하여 코드 복사
typescript
클릭하여 코드 펼치기
//맨 처음 도(행정구역)을 가져와서 지도에 할당하기 위한 DB 호출
export async function getStates(): Promise<Province[]> {
//지도로 이동한 곳에서 산의 목록과 대략적인 정보를 보여주기 위한 DB 호출
export async function getMountainsByState(province: string): Promise<Mountain[]> {
//마지막으로 클릭한 산의 상세 정보를 보여주기 위한 DB 호출
export async function getMountainById(id: string): Promise<Mountain | null> {
클릭하여 코드 복사

 


 

이동