Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ca40745
feat: 수리점 페이지 라우트 추가
jwj0620gcu Jan 30, 2026
db746fb
feat:헤더, 지도 섹션, 수리점 컴포넌트 구현
jwj0620gcu Jan 30, 2026
fe80206
feat: 서치바 크기 변주 가능하게 수정
jwj0620gcu Jan 30, 2026
0b3eaa8
feat:카카오 맵 렌더링
jwj0620gcu Jan 30, 2026
cac5f32
feat: 카카오맵과 서치바 지도 연결, 수리점 검색, 마커 렌더링
jwj0620gcu Jan 30, 2026
980b444
feat: 수리점 확장 검색 가능, 페이지네이션 가능
jwj0620gcu Jan 30, 2026
8e6e3a3
feat:마커 인포윈도우(전화/상세/길찾기)
jwj0620gcu Jan 30, 2026
28f9ead
마커 클릭시 지도 중앙 이동 적용
jwj0620gcu Jan 30, 2026
195d13d
카드 연락, 길 찾기 버튼 연결, ui 결과 분기
jwj0620gcu Jan 30, 2026
8b85044
feat:문구,마커 ui,윈도우 ui 변경
jwj0620gcu Jan 30, 2026
58da8f6
feat:기본 마커에서 민트색으로 변경
jwj0620gcu Jan 30, 2026
fd6c274
refactor:로직 모델로 분리 및 공용 타입 분리
jwj0620gcu Jan 30, 2026
0715c5e
feat:반응형 적용
jwj0620gcu Jan 30, 2026
e540c0f
fix:린트 오류 해결 및 prettier 포맷 적용
jwj0620gcu Jan 30, 2026
7e8efe2
Merge branch 'main' into 73-feat/수리점-페이지-제작
KyeongJooni Jan 31, 2026
65ae7a2
Merge branch 'main' into 73-feat/수리점-페이지-제작
KyeongJooni Jan 31, 2026
9f20394
refactor:헤더, 미니맵, 카드 모두 같은 폭 맞추기
jwj0620gcu Jan 31, 2026
38e367f
refactor: URL 상수 처리
jwj0620gcu Jan 31, 2026
f3fa682
refactor:clickable 옵션 추가
jwj0620gcu Jan 31, 2026
951effc
refactor:검색 중 에러 처리 추가
jwj0620gcu Jan 31, 2026
eca23b4
중복 파트 getKakaoMaps() 핼퍼로 접그ㅡㄴ
jwj0620gcu Jan 31, 2026
f072a72
refactor:새 variant 추가하기(버튼)
jwj0620gcu Jan 31, 2026
21bb1a0
style:prettier format 적용
jwj0620gcu Jan 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions public/repair-marker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion src/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ export const links = () => [
{ rel: 'manifest', href: '/site.webmanifest' },
];

const kakaoMapKey = import.meta.env.VITE_KAKAO_JS_KEY;
const kakaoMapSrc = kakaoMapKey
? `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${kakaoMapKey}&libraries=services&autoload=false`
: null;
Comment on lines +15 to +18

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 SDK URL 직접 하드 코딩 되어있는데 상수로 분리하거나 env에 넣어서 관리하면 더 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수 처리했습니다!


export default function Root() {
return (
<html lang="ko">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="apple-mobile-web-app-title" content="LOOPiT" />
<meta name="apple-mobile-web-app-title" content="LOOPIT" />
<Meta />
<Links />
{kakaoMapSrc && <script src={kakaoMapSrc} defer />}
</head>
<body className="w-full overflow-x-hidden">
<AppProviders>
Expand Down
1 change: 1 addition & 0 deletions src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default [
index('routes/(main)/_index.tsx'),
route('sell', 'routes/(main)/sell.tsx'),
route('sell/confirm', 'routes/(main)/sell.confirm.tsx'),
route('repair', 'routes/(main)/repair.tsx'),
route('mypage', 'routes/(main)/mypage.tsx'),
route('mypage/settings', 'routes/(main)/mypage.settings.tsx'),
route('mypage/profile', 'routes/(main)/mypage.profile.tsx'),
Expand Down
3 changes: 3 additions & 0 deletions src/app/routes/(main)/repair.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { RepairPage } from '@pages/repair';

export default RepairPage;
1 change: 1 addition & 0 deletions src/pages/repair/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as RepairPage } from './ui/RepairPage';
83 changes: 83 additions & 0 deletions src/pages/repair/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export type RepairShop = {
id: string;
name: string;
address: string;
lat: number;
lng: number;
phone?: string;
distance?: number;
placeUrl?: string;
};

export type KakaoAddressResult = {
x: string;
y: string;
};

export type KakaoPlace = {
id: string;
place_name: string;
road_address_name: string;
address_name: string;
y: string;
x: string;
phone: string;
distance?: string;
place_url?: string;
};

export type KakaoPagination = {
hasNextPage: boolean;
nextPage: () => void;
};

export type KakaoMapInstance = {
setCenter: (latLng: unknown) => void;
setBounds: (bounds: unknown) => void;
panTo: (latLng: unknown) => void;
};

export type KakaoMarker = {
setMap: (map: KakaoMapInstance | null) => void;
};

export type KakaoOverlay = {
setMap: (map: KakaoMapInstance | null) => void;
};

export type KakaoLatLngBounds = {
extend: (latLng: unknown) => void;
};

export type KakaoMapsServices = {
Geocoder: new () => {
addressSearch: (query: string, cb: (result: unknown[], status: string) => void) => void;
};
Places: new () => {
keywordSearch: (
keyword: string,
cb: (data: unknown[], status: string, pagination: KakaoPagination) => void,
options: { location: unknown; radius: number }
) => void;
};
Status: { OK: string };
};

export type KakaoMaps = {
LatLng: new (lat: number, lng: number) => unknown;
Map: new (container: HTMLElement, options: { center: unknown; level: number }) => KakaoMapInstance;
CustomOverlay: new (options: {
yAnchor: number;
zIndex: number;
content?: string;
position?: unknown;
}) => KakaoOverlay;
Comment on lines +69 to +75

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clickable 타입 정의해야 할 것 같습니다 TypeScript 에러 발생할 수 도 있을 것 같습니다...!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정의해서 추가했습니다!

Marker: new (options: { map: KakaoMapInstance; position: unknown; image?: unknown }) => KakaoMarker;
MarkerImage: new (src: string, size: unknown, options: { offset: unknown }) => unknown;
Size: new (width: number, height: number) => unknown;
Point: new (x: number, y: number) => unknown;
LatLngBounds: new () => KakaoLatLngBounds;
event: { addListener: (target: unknown, event: string, handler: () => void) => void };
load?: (callback: () => void) => void;
services?: KakaoMapsServices;
};
147 changes: 147 additions & 0 deletions src/pages/repair/model/useRepairMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useEffect, useRef, useState } from 'react';
import type { KakaoMapInstance, KakaoMaps, KakaoMarker, KakaoOverlay, RepairShop } from './types';

const buildOverlayContent = (shop: RepairShop) => {
const phoneLink = shop.phone ? `<a href="tel:${shop.phone}">전화</a>` : '';
const routeLink = `https://map.kakao.com/link/to/${encodeURIComponent(shop.name)},${shop.lat},${shop.lng}`;
const detailLink = shop.placeUrl ? `<a href="${shop.placeUrl}" target="_blank" rel="noreferrer">상세</a>` : '';
const actions = [phoneLink, detailLink, `<a href="${routeLink}" target="_blank" rel="noreferrer">길찾기</a>`]
.filter(Boolean)
.join(' · ');

return `
<div style="position:relative; transform:translateY(-8px);">
<div style="padding:var(--padding-m) var(--spacing-s); font-size:12px; line-height:1.4; width:260px; background:var(--color-green-100); border-radius:var(--radius-m); border:2px solid var(--color-green-700); box-shadow:0 8px 20px rgba(17,203,176,0.18);">
<div style="font-weight:700; margin-bottom:var(--spacing-xxxxs); word-break:break-word; color:var(--color-green-900);">${shop.name}</div>
<div style="color:var(--color-green-800); margin-bottom:6px; word-break:break-word;">${shop.address}</div>
<div style="color:var(--color-green-900); font-weight:600; word-break:keep-all;">${actions}</div>
</div>
<div style="position:absolute; left:50%; bottom:-8px; transform:translateX(-50%); width:0; height:0; border-left:8px solid transparent; border-right:8px solid transparent; border-top:8px solid var(--color-green-100);"></div>
<div style="position:absolute; left:50%; bottom:-10px; transform:translateX(-50%); width:0; height:0; border-left:9px solid transparent; border-right:9px solid transparent; border-top:9px solid var(--color-green-700);"></div>
</div>
`;
};

export const useRepairMap = () => {
const mapRef = useRef<HTMLDivElement | null>(null);
const mapInstanceRef = useRef<KakaoMapInstance | null>(null);
const markersRef = useRef<KakaoMarker[]>([]);
const overlayRef = useRef<KakaoOverlay | null>(null);
const [isMapReady, setIsMapReady] = useState(false);

useEffect(() => {
const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동일한 선언이 중복되고 있는 것 같습니다 상수로 추출하거나 헬퍼 함수로 분리하면 좋을 것 같아요! 😄

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getKakaoMaps() 헬퍼 함수로 분리했습니다!


if (!mapRef.current) {
return;
}

const initMap = () => {
if (!kakao?.maps || !mapRef.current) {
return;
}

const center = new kakao.maps.LatLng(37.5665, 126.978);
const options = { center, level: 4 };

const map = new kakao.maps.Map(mapRef.current, options);
mapInstanceRef.current = map;
overlayRef.current = new kakao.maps.CustomOverlay({ yAnchor: 1, zIndex: 10, clickable: true });
kakao.maps.event.addListener(map, 'click', () => {
overlayRef.current?.setMap(null);
});
setIsMapReady(true);
};

if (kakao?.maps?.load) {
kakao.maps.load(initMap);
return;
}

const timer = window.setInterval(() => {
if (kakao?.maps?.load) {
window.clearInterval(timer);
kakao.maps.load(initMap);
}
}, 50);

return () => window.clearInterval(timer);
}, []);

const clearMarkers = () => {
markersRef.current.forEach((marker) => marker.setMap(null));
markersRef.current = [];
};

const setCenter = (lat: number, lng: number) => {
const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao;
const map = mapInstanceRef.current;
if (!kakao?.maps || !map) {
return;
}
map.setCenter(new kakao.maps.LatLng(lat, lng));
};

const openOverlayForShop = (shop: RepairShop) => {
const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao;
const map = mapInstanceRef.current;
if (!kakao?.maps || !map) {
return;
}

const position = new kakao.maps.LatLng(shop.lat, shop.lng);
map.panTo(position);
overlayRef.current?.setMap(null);

const overlay = new kakao.maps.CustomOverlay({
content: buildOverlayContent(shop),
position,
yAnchor: 1,
zIndex: 10,
clickable: true,
});

overlay.setMap(map);
overlayRef.current = overlay;
};

const setMarkers = (shops: RepairShop[]) => {
const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao;
const map = mapInstanceRef.current;
if (!kakao?.maps || !map) {
return;
}

clearMarkers();
if (shops.length === 0) {
return;
}

const markerImage = new kakao.maps.MarkerImage('/repair-marker.svg', new kakao.maps.Size(36, 48), {
offset: new kakao.maps.Point(18, 48),
});

const bounds = new kakao.maps.LatLngBounds();
shops.forEach((shop) => {
const position = new kakao.maps.LatLng(shop.lat, shop.lng);
const marker = new kakao.maps.Marker({ map, position, image: markerImage });
kakao.maps.event.addListener(marker, 'click', () => {
openOverlayForShop(shop);
});

markersRef.current.push(marker);
bounds.extend(position);
});

map.setBounds(bounds);
};

return {
mapRef,
isMapReady,
setCenter,
setMarkers,
clearMarkers,
openOverlayForShop,
};
};
Loading
Loading