Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0398b97
docs: CLAUDE.md 네이버 지도 환경변수 및 외부 서비스 추가
suhyun113 Apr 12, 2026
8dc6e12
refactor: ClubMapPage BottomCard를 MapClubInfoCard 공통 컴포넌트로 분리
suhyun113 Apr 12, 2026
7c299c3
feat: InteractiveMapView 공통 컴포넌트 추가 및 반응형 ClubMapModal 생성
suhyun113 Apr 12, 2026
5114f16
refactor: ClubMapPage 제거 및 반응형 모달로 통합
suhyun113 Apr 12, 2026
260d06f
style: 지도 UI 개선 (커서, 텍스트 선택 방지, 반응형 카드 스타일)
suhyun113 Apr 12, 2026
f40969d
feat: 데스크탑 지도 모달 줌 인/아웃 버튼 추가
suhyun113 Apr 12, 2026
1efcdee
refactor: 지도 관련 파일 구조 정리 (컴포넌트/훅/유틸 분리)
suhyun113 Apr 12, 2026
3ddda29
style: 줌 버튼 및 아이콘 두께 조정
suhyun113 Apr 13, 2026
615a26e
feat: 지도 정보 카드 클릭 시 초기 위치로 지도 복귀
suhyun113 Apr 13, 2026
240e29c
style: 지도 마커 반응형 크기 및 테마 색상 적용
suhyun113 Apr 13, 2026
2c02a5d
style: 지도 정보 카드 mini_mobile 반응형 너비 추가
suhyun113 Apr 13, 2026
fce9028
style: prettier 포맷 적용
suhyun113 Apr 14, 2026
56171be
fix: 모바일 모달 높이를 100dvh로 수정하여 주소바 영역 대응
suhyun113 Apr 14, 2026
fab2605
refactor: 지도 인스턴스 any 타입을 NaverMapInstance 인터페이스로 대체
suhyun113 Apr 14, 2026
1533a8a
refactor: NaverMap props에 Pick 제네릭 적용 (ClubProfileCard 패턴 일관성)
suhyun113 Apr 14, 2026
f1f4480
refactor: InteractiveMapView를 useNaverMap 훅으로 통합하여 중복 제거
suhyun113 Apr 14, 2026
228221b
refactor: 모바일에서 불필요한 줌 컨트롤 렌더 제거 및 InteractiveMapView ref 폴백 정리
suhyun113 Apr 14, 2026
643574f
fix: 지도 정보 카드 mobile 반응형 너비 범위 수정
suhyun113 Apr 14, 2026
fdfe0f3
refactor: MapModal 컨트롤 z-index를 로컬 상수로 분리
suhyun113 Apr 25, 2026
6ab0c7f
fix: loadNaverMapScript Race Condition 보강
suhyun113 Apr 25, 2026
4062689
Merge branch 'develop-fe' into feature/#1409-desktop-detail-map-modal…
suhyun113 Apr 25, 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
4 changes: 3 additions & 1 deletion frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ npm run generate:sitemap # sitemap.xml 생성
- **Sentry**: 에러 모니터링 및 성능 추적
- **Channel.io**: 고객 지원 채팅
- **Kakao SDK**: 카카오 공유 기능
- **Naver Map**: 동아리방 위치 지도 (네이버 클라우드 플랫폼)

모든 SDK는 `src/utils/initSDK.ts`에서 초기화되며, 각각 환경 변수 필요.
Comment on lines +59 to 61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Naver Map 초기화 위치 설명이 실제 구현과 다릅니다.

Line 61의 “모든 SDK는 src/utils/initSDK.ts에서 초기화”는 현재 지도 구현과 불일치합니다. Naver Map은 src/utils/loadNaverMapScript.ts에서 동적 로드되므로 문구를 분리해 명시해 주세요.

✍️ 문서 수정 예시
-모든 SDK는 `src/utils/initSDK.ts`에서 초기화되며, 각각 환경 변수 필요.
+Mixpanel/Sentry/Channel.io/Kakao SDK는 `src/utils/initSDK.ts`에서 초기화되며, 각각 환경 변수 필요.
+Naver Map 스크립트는 `src/utils/loadNaverMapScript.ts`에서 동적으로 로드됨.

Also applies to: 74-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/CLAUDE.md` around lines 59 - 61, Update the CLAUDE.md wording to
accurately reflect that most SDKs are initialized in src/utils/initSDK.ts but
Naver Map is dynamically loaded in src/utils/loadNaverMapScript.ts; change the
sentence that currently states “모든 SDK는 src/utils/initSDK.ts에서 초기화” to separate
Naver Map (mentioning src/utils/loadNaverMapScript.ts) and adjust the similar
occurrence around line 74 to match this distinction so readers know Naver Map
uses dynamic script loading.


Expand All @@ -70,6 +71,7 @@ npm run generate:sitemap # sitemap.xml 생성
- `VITE_ENABLE_SENTRY_IN_DEV` - 개발 환경에서 Sentry 활성화 여부 (true/false)
- `VITE_CHANNEL_PLUGIN_KEY` - Channel.io 플러그인 키
- `VITE_KAKAO_JAVASCRIPT_KEY` - Kakao JavaScript 키
- `VITE_NAVER_MAP_CLIENT_ID` - 네이버 지도 API 클라이언트 ID

### 프로젝트 구조

Expand Down Expand Up @@ -205,7 +207,7 @@ const { variant } = useExperiment(mainBannerExperiment);

`.claude/agents/` 디렉토리에 전담 agent 정의:

- `api-hooks-agent.md` - React Query 훅 생성 및 관리 전담
- `API훅부서.md` - React Query 훅 생성 및 관리 전담

Agent 사용 시 해당 문서를 참조하여 일관된 패턴 유지.

Expand Down
33 changes: 0 additions & 33 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ScrollToTop } from '@/hooks/Scroll/ScrollToTop';
import LoginTab from '@/pages/AdminPage/auth/LoginTab/LoginTab';
import PrivateRoute from '@/pages/AdminPage/auth/PrivateRoute/PrivateRoute';
import ClubDetailPage from '@/pages/ClubDetailPage/ClubDetailPage';
import ClubMapPage from '@/pages/ClubMapPage/ClubMapPage';
import MainPage from '@/pages/MainPage/MainPage';
import GlobalStyles from '@/styles/Global.styles';
import { theme } from '@/styles/theme';
Expand Down Expand Up @@ -78,14 +77,6 @@ const App = () => {
</ContentErrorBoundary>
}
/>
<Route
path='/clubDetail/:clubId/map'
element={
<ContentErrorBoundary>
<ClubMapPage />
</ContentErrorBoundary>
}
/>
{/*한국어핸들 */}
<Route
path='/clubDetail/@:clubName'
Expand All @@ -95,14 +86,6 @@ const App = () => {
</ContentErrorBoundary>
}
/>
<Route
path='/clubDetail/@:clubName/map'
element={
<ContentErrorBoundary>
<ClubMapPage />
</ContentErrorBoundary>
}
/>
{/*새로 빌드해서 배포할 앱 주소 url*/}
<Route
path='/webview/club/:clubId'
Expand All @@ -112,14 +95,6 @@ const App = () => {
</ContentErrorBoundary>
}
/>
<Route
path='/webview/club/:clubId/map'
element={
<ContentErrorBoundary>
<ClubMapPage />
</ContentErrorBoundary>
}
/>
<Route
path='/webview/club/@:clubName'
element={
Expand All @@ -128,14 +103,6 @@ const App = () => {
</ContentErrorBoundary>
}
/>
<Route
path='/webview/club/@:clubName/map'
element={
<ContentErrorBoundary>
<ClubMapPage />
</ContentErrorBoundary>
}
/>
<Route
path='/introduce'
element={
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/assets/images/icons/close_button_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styled from 'styled-components';
import { media } from '@/styles/mediaQuery';

export const Container = styled.div`
position: relative;
width: 100%;
height: 100%;
`;

export const MapArea = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
`;

export const InfoCardWrapper = styled.div`
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
z-index: 10;

${media.tablet} {
bottom: 30px;
}
`;
134 changes: 134 additions & 0 deletions frontend/src/components/map/InteractiveMapView/InteractiveMapView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { RefObject, useCallback, useEffect, useRef } from 'react';
import markerIcon from '@/assets/images/icons/marker.svg';
import MapClubInfoCard from '@/components/map/MapClubInfoCard/MapClubInfoCard';
import { ClubLocation } from '@/constants/clubLocation';
import { colors } from '@/styles/theme/colors';
import { loadNaverMapScript } from '@/utils/loadNaverMapScript';
import * as Styled from './InteractiveMapView.styles';

interface InteractiveMapViewProps {
location: ClubLocation;
clubName: string;
clubLogo?: string;
active: boolean;
markerSize?: number;
bubbleFontSize?: number;
bubbleFontWeight?: number;
mapInstanceRef?: RefObject<any>;
}

const InteractiveMapView = ({
location,
clubName,
clubLogo,
active,
markerSize = 40,
bubbleFontSize = 13,
bubbleFontWeight = 700,
mapInstanceRef: externalMapRef,
}: InteractiveMapViewProps) => {
const mapRef = useRef<HTMLDivElement | null>(null);
const internalMapRef = useRef<any>(null);
const mapInstanceRef = externalMapRef || internalMapRef;

useEffect(() => {
if (!active) return;

const timer = setTimeout(() => {
loadNaverMapScript().then(() => {
if (!mapRef.current || !window.naver) return;

const { naver } = window;
const position = new naver.maps.LatLng(location.lat, location.lng);

mapInstanceRef.current = new naver.maps.Map(mapRef.current, {
center: position,
zoom: 17,
logoControl: false,
mapDataControl: false,
scaleControl: false,
});

const markerContent = `
<div style="position: relative; display: inline-block;">
<div style="
position: absolute;
bottom: calc(${markerSize}px + 5px);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
">
<div style="
background: #fff;
border-radius: 50px;
padding: 10px 16px;
font-size: ${bubbleFontSize}px;
font-weight: ${bubbleFontWeight};
color: ${colors.gray[900]};
white-space: nowrap;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
">동아리방</div>
<div style="
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-top: 10px solid #fff;
margin-top: -2px;
"></div>
</div>
<img src="${markerIcon}" style="width: ${markerSize}px; height: ${markerSize}px; display: block;" />
</div>
`;

new naver.maps.Marker({
position,
map: mapInstanceRef.current,
icon: {
content: markerContent,
anchor: new naver.maps.Point(markerSize / 2, markerSize),
},
});
});
}, 100);

return () => {
clearTimeout(timer);
mapInstanceRef.current?.destroy();
mapInstanceRef.current = null;
};
}, [
active,
location.lat,
location.lng,
markerSize,
bubbleFontSize,
bubbleFontWeight,
]);

const handleRecenter = useCallback(() => {
const map = mapInstanceRef.current;
Comment on lines +41 to +42
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.

Recenter 중심으로 돌아간다는 의미인가요??

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.

네, 사용자가 지도를 드래그하여 이동한 뒤 하단 MapClubInfoCard를 클릭하면 원래 동아리 위치 기준으로 지도 중심을 되돌리는 기능입니다!

if (map && window.naver) {
map.setCenter(new window.naver.maps.LatLng(location.lat, location.lng));
}
}, [mapInstanceRef, location.lat, location.lng]);

return (
<Styled.Container>
<Styled.MapArea ref={mapRef} />
<Styled.InfoCardWrapper onClick={handleRecenter}>
<MapClubInfoCard
logo={clubLogo}
name={clubName}
building={location.building}
detailLocation={location.detailLocation}
/>
</Styled.InfoCardWrapper>
</Styled.Container>
);
};

export default InteractiveMapView;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from 'styled-components';
import { media } from '@/styles/mediaQuery';
import { colors } from '@/styles/theme/colors';
import { typography } from '@/styles/theme/typography';

Expand All @@ -7,49 +8,25 @@ const setTypography = (typo: { size: string; weight: number }) => `
font-weight: ${typo.weight};
`;

export const Container = styled.div`
position: relative;
width: 100%;
height: 100dvh;
overflow: hidden;
`;

export const MapWrapper = styled.div`
width: 100%;
height: 100%;
`;

export const BackButton = styled.button`
position: fixed;
top: calc(12px + var(--rn-safe-top, 0px));
left: 16px;
z-index: 10;
width: 36px;
height: 36px;
padding: 0;
border: none;
background-color: ${colors.base.white};
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
`;

export const BottomCard = styled.div`
position: fixed;
bottom: calc(55px + var(--rn-safe-bottom, 0px));
left: 20px;
right: 20px;
z-index: 10;
export const Card = styled.div`
width: 357px;
background-color: ${colors.base.white};
border-radius: 16px;
padding: 24px 16px;
padding: 24px 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 12px;
box-sizing: border-box;

${media.tablet} {
width: 335px;
padding: 24px 16px;
}

${media.mini_mobile} {
width: calc(100vw - 40px);
}
`;

export const ClubLogo = styled.img`
Expand All @@ -70,11 +47,17 @@ export const ClubInfo = styled.div`
`;

export const ClubName = styled.span`
${setTypography(typography.title.title5)};
${setTypography(typography.title.title2)};
color: ${colors.base.black};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
user-select: none;

${media.tablet} {
${setTypography(typography.title.title5)};
}
`;

export const LocationRow = styled.div`
Expand All @@ -90,19 +73,15 @@ export const LocationRow = styled.div`
`;

export const LocationText = styled.span`
${setTypography(typography.paragraph.p6)};
${setTypography(typography.paragraph.p3)};
color: ${colors.gray[600]};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
cursor: default;
user-select: none;

export const StatusMessage = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
${setTypography(typography.paragraph.p4)};
color: ${colors.gray[700]};
text-align: center;
${media.tablet} {
${setTypography(typography.paragraph.p6)};
}
`;
Loading
Loading