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라면 어차피 연결하기 위해서 하나하나 찾는게 나을 수도 있다.
// 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 매핑 시켜줘야한다.
'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를 순회하며 이벤트 리스너를 추가해준다. 클릭 됐을 경우 해당 페이지의 상세 페이지로 이동한다.
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. 홈페이지에서 프롭스로 호출
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 가져오기 재설정
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;
}
}
클릭하여 코드 복사
//맨 처음 도(행정구역)을 가져와서 지도에 할당하기 위한 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> {
클릭하여 코드 복사
'nextjs > 웹사이트 만들기' 카테고리의 다른 글
별의 반짝임 효과 구현하기 (0) | 2025.04.15 |
---|---|
산정보 보기 웹사이트 만들기(5) - path의 transform 효과로 박스가 제대로 나타나지 않는 문제 해결 (1) | 2025.04.15 |
산정보 보기 웹사이트 만들기(4) - 지도 꾸미기 (0) | 2025.04.14 |
산정보 보기 웹사이트 만들기(3) - 각 산의 상세정보 페이지 만들기 (1) | 2025.04.14 |
산정보 보기 웹사이트 만들기(1) - sql을 사용해서 데이터 가져오기 (0) | 2025.04.14 |