Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d5051cd
feat: 현재 위치 받는 유틸함수 추가
H0ngJu Jan 24, 2026
8a27e9d
feat: MapView 컴포넌트 추가
H0ngJu Jan 24, 2026
df39ae7
feat: 지도위 이미지 핀 추가
H0ngJu Jan 24, 2026
4e38a98
test: 지도위 이미지 핀 스토리북 추가
H0ngJu Jan 24, 2026
b310a7c
refactor: Circle 버튼에 blur 추가
H0ngJu Jan 26, 2026
fff3e0b
Merge remote-tracking branch 'origin/develop' into feat/#25-map
H0ngJu Jan 26, 2026
7486164
feat: BottomSheet 기본 구조 추가
H0ngJu Jan 26, 2026
39b0b1a
feat: 메인 지도 화면 추가
H0ngJu Jan 26, 2026
331438e
feat: MapView 컴포넌트 추가
H0ngJu Jan 26, 2026
1ac9e5d
chore: mapbox 의존성 추가
H0ngJu Jan 26, 2026
bab7c9e
refactor: theme에 blur 옵션 추가
H0ngJu Jan 26, 2026
ddaa2ef
chore: format:fix 적용
H0ngJu Jan 27, 2026
a9b0bb7
refactor: AlbumContainer for문 -> Map으로 변경
H0ngJu Jan 27, 2026
88fad06
refactor: AlbumContainer type이 small인 경우 border-radius 4px 적용
H0ngJu Jan 27, 2026
953291e
refactor: AlbumContainer type이 small 인 경우 gap 수정
H0ngJu Jan 27, 2026
a19a0a4
feat: Album 타입 추가
H0ngJu Jan 27, 2026
7c73cdd
feat: 바텀시트 small일 때 보여줄 AlbumRow 컴포넌트 추가
H0ngJu Jan 27, 2026
f739fcb
style: format:fix
H0ngJu Jan 27, 2026
bd88d8f
feat: MenuButton의 panel 열림 위치 방향 커스텀 Prop 추가
H0ngJu Jan 27, 2026
ff32029
chore: albumRow 오타 수정
H0ngJu Jan 27, 2026
2c210cb
chore: Map용 임시 MockData 추가
H0ngJu Jan 27, 2026
8bb28b3
refactor: album 컴포넌트 스타일 수정
H0ngJu Jan 27, 2026
c72bca3
refactor: 바텀시트 비즈니스 로직 분리
H0ngJu Jan 27, 2026
8df835e
feat: 바텀시트 컨텐츠 렌더링 분리
H0ngJu Jan 27, 2026
79b1c1e
feat: 바텀시트 도메인, 높이 정책 로직 훅으로 분리
H0ngJu Jan 27, 2026
c712c45
feat: 바텀시트 앨범 그리드 추가
H0ngJu Jan 27, 2026
b8bdf76
feat: input의 타입이 search 일 때 count 보이지 않도록 수정
H0ngJu Jan 27, 2026
34df7ca
refactor: mockData 분리 및 바텀시트 context 주입
H0ngJu Jan 27, 2026
0db583a
feat: 지도 화면 앨범 상세 및 핀 필터링 연결
H0ngJu Jan 28, 2026
a13054f
chore: generate apis
H0ngJu Jan 28, 2026
2016229
refactor: filterAlbum에 useMemo 추가
H0ngJu Jan 28, 2026
c44786e
chore: 불필요한 mockData 제거
H0ngJu Jan 28, 2026
3f7b8b2
refactor: sheetContextType 상수화
H0ngJu Jan 29, 2026
75c4446
refactor: 지도 위치, 줌레벨 상수화
H0ngJu Jan 29, 2026
f21d824
refactor: 타입단언 제거 & early-return
H0ngJu Jan 29, 2026
5f60db1
refactor: 타임아웃 상수화
H0ngJu Jan 29, 2026
03479c8
refactor: bottomSheet 훅 관련된 높이 상수화 분리
H0ngJu Jan 29, 2026
bafd712
feat: 모킹 핸들러 함수 및 데이터 추가
H0ngJu Jan 29, 2026
acdd243
feat: map api 데이터 훅 추가
H0ngJu Jan 29, 2026
54c2c00
Merge remote-tracking branch 'origin/develop' into feat/#25-map
H0ngJu Jan 29, 2026
825b2a8
chore: 폴더명 수정
H0ngJu Jan 29, 2026
8b50003
refactor: 수정된 input prop 적용
H0ngJu Jan 29, 2026
032926a
refactor: MSW handler 수정 (/selectable)
H0ngJu Jan 29, 2026
bad5d4a
chore: format:fix
H0ngJu Jan 29, 2026
fa9c484
refactor: SelectableAlbum 타입 임시 수정
H0ngJu Jan 29, 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 apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
"@repo/api-client": "workspace:*",
"@tanstack/react-query": "^5.64.0",
"@tanstack/react-query-devtools": "^5.64.0",
"mapbox-gl": "^3.18.1",
"next": "15.4.10",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"react-map-gl": "^8.1.0"
},
"devDependencies": {
"@netlify/plugin-nextjs": "^5.15.3",
Expand Down
206 changes: 206 additions & 0 deletions apps/web/src/app/map/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import type { AlbumWithPhotosResponse, PhotoListResponse } from '@repo/api-client';
import type { Album, AlbumDetailData } from '@/types/album.type';
import type { MapPin } from '@/types/map.type';

const center = { latitude: 37.5665, longitude: 126.978 };

const photoListResponseMock: PhotoListResponse = {
albums: [
{
id: 1,
title: '한강 라이딩',
photoCount: 5,
thumbnailUrl: 'https://picsum.photos/id/1018/300/300',
photos: [
{
id: 101,
url: 'https://picsum.photos/id/1018/300/300',
location: {
latitude: center.latitude + 0.003,
longitude: center.longitude + 0.008,
},
},
{
id: 102,
url: 'https://picsum.photos/id/1020/300/300',
location: {
latitude: center.latitude + 0.006,
longitude: center.longitude + 0.004,
},
},
{
id: 103,
url: 'https://picsum.photos/id/1024/300/300',
location: {
latitude: center.latitude - 0.004,
longitude: center.longitude + 0.002,
},
},
{
id: 104,
url: 'https://picsum.photos/id/1027/300/300',
location: {
latitude: center.latitude - 0.002,
longitude: center.longitude - 0.006,
},
},
{
id: 105,
url: 'https://picsum.photos/id/1035/300/300',
location: {
latitude: center.latitude + 0.004,
longitude: center.longitude - 0.004,
},
},
],
},
{
id: 2,
title: '카페 투어',
photoCount: 4,
thumbnailUrl: 'https://picsum.photos/id/1043/300/300',
photos: [
{
id: 201,
url: 'https://picsum.photos/id/1043/300/300',
location: {
latitude: center.latitude + 0.012,
longitude: center.longitude - 0.002,
},
},
{
id: 202,
url: 'https://picsum.photos/id/1050/300/300',
location: {
latitude: center.latitude + 0.009,
longitude: center.longitude - 0.007,
},
},
{
id: 203,
url: 'https://picsum.photos/id/1052/300/300',
location: {
latitude: center.latitude + 0.007,
longitude: center.longitude - 0.011,
},
},
{
id: 204,
url: 'https://picsum.photos/id/1060/300/300',
location: {
latitude: center.latitude + 0.011,
longitude: center.longitude - 0.009,
},
},
],
},
{
id: 3,
title: '야경 산책',
photoCount: 6,
thumbnailUrl: 'https://picsum.photos/id/1063/300/300',
photos: [
{
id: 301,
url: 'https://picsum.photos/id/1063/300/300',
location: {
latitude: center.latitude - 0.008,
longitude: center.longitude + 0.01,
},
},
{
id: 302,
url: 'https://picsum.photos/id/1067/300/300',
location: {
latitude: center.latitude - 0.01,
longitude: center.longitude + 0.014,
},
},
{
id: 303,
url: 'https://picsum.photos/id/1070/300/300',
location: {
latitude: center.latitude - 0.012,
longitude: center.longitude + 0.008,
},
},
{
id: 304,
url: 'https://picsum.photos/id/1074/300/300',
location: {
latitude: center.latitude - 0.006,
longitude: center.longitude + 0.004,
},
},
{
id: 305,
url: 'https://picsum.photos/id/1080/300/300',
location: {
latitude: center.latitude - 0.009,
longitude: center.longitude + 0.001,
},
},
{
id: 306,
url: 'https://picsum.photos/id/1084/300/300',
location: {
latitude: center.latitude - 0.004,
longitude: center.longitude + 0.013,
},
},
],
},
],
};

const safeAlbums: AlbumWithPhotosResponse[] = photoListResponseMock.albums ?? [];

export const albumList: Album[] = safeAlbums.map((album) => {
const photoList = (album.photos ?? [])
.filter((photo) => Boolean(photo?.url))
.map((photo) => ({
photoId: String(photo.id ?? ''),
src: photo.url ?? '',
}));

return {
id: album.id ?? 0,
title: album.title ?? '알 수 없는 앨범',
photoList,
photoCount: album.photoCount ?? photoList.length,
};
});

export const albumDetailById = safeAlbums.reduce<Record<number, AlbumDetailData>>(
(acc, album) => {
const albumId = album.id ?? 0;
acc[albumId] = {
id: albumId,
title: album.title ?? '알 수 없는 앨범',
photos: (album.photos ?? []).map((photo) => ({
id: photo.id ?? 0,
url: photo.url ?? '',
})),
};
return acc;
},
{},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

ID 기본값 0 사용 시 데이터 충돌 가능성

album.id ?? 0을 사용하면 여러 앨범의 ID가 undefined일 경우 모두 키 0으로 저장되어 데이터가 덮어씌워질 수 있습니다. 현재는 목 데이터라 문제가 없지만, 실제 API 연동 시 이 패턴이 그대로 사용되면 버그로 이어질 수 있습니다.

💡 고유 ID 생성 또는 필터링 제안
-export const albumDetailById = safeAlbums.reduce<Record<number, AlbumDetailData>>(
-  (acc, album) => {
-    const albumId = album.id ?? 0;
+export const albumDetailById = safeAlbums
+  .filter((album) => album.id != null)
+  .reduce<Record<number, AlbumDetailData>>((acc, album) => {
+    const albumId = album.id!;
     acc[albumId] = {
       id: albumId,
       title: album.title ?? '알 수 없는 앨범',
       photos: (album.photos ?? []).map((photo) => ({
         id: photo.id ?? 0,
         url: photo.url ?? '',
       })),
     };
     return acc;
-  },
-  {},
-);
+  }, {});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const albumDetailById = safeAlbums.reduce<Record<number, AlbumDetailData>>(
(acc, album) => {
const albumId = album.id ?? 0;
acc[albumId] = {
id: albumId,
title: album.title ?? '알 수 없는 앨범',
photos: (album.photos ?? []).map((photo) => ({
id: photo.id ?? 0,
url: photo.url ?? '',
})),
};
return acc;
},
{},
);
export const albumDetailById = safeAlbums
.filter((album) => album.id != null)
.reduce<Record<number, AlbumDetailData>>((acc, album) => {
const albumId = album.id!;
acc[albumId] = {
id: albumId,
title: album.title ?? '알 수 없는 앨범',
photos: (album.photos ?? []).map((photo) => ({
id: photo.id ?? 0,
url: photo.url ?? '',
})),
};
return acc;
}, {});
🤖 Prompt for AI Agents
In `@apps/web/src/app/map/mockData.ts` around lines 174 - 188, The reduce for
albumDetailById currently uses album.id ?? 0 which causes collisions when id is
undefined; update the logic to either skip albums with nullish ids or generate
safe unique keys instead. Specifically, before reducing safeAlbums, filter out
entries where album.id == null (so album.id is guaranteed) or, if you need to
keep them, derive a stable fallback key (e.g., a prefixed index or UUID) rather
than 0 to avoid overwriting; adjust references to albumDetailById, safeAlbums,
and AlbumDetailData accordingly so the resulting record keys are unique and
type-safe.


export const mapPins: MapPin[] = safeAlbums.flatMap((album) => {
const albumId = album.id ?? 0;
return (album.photos ?? [])
.filter((photo) => {
const latitude = photo?.location?.latitude;
const longitude = photo?.location?.longitude;
return typeof latitude === 'number' && typeof longitude === 'number';
})
.map((photo) => ({
id: photo.id ?? 0,
albumId,
latitude: photo.location?.latitude ?? center.latitude,
longitude: photo.location?.longitude ?? center.longitude,
imageUrl: photo.url ?? album.thumbnailUrl ?? 'https://picsum.photos/200/200',
imageCount: 1,
}));
});
14 changes: 14 additions & 0 deletions apps/web/src/app/map/page.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from '@emotion/styled';

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

export const HeaderContainer = styled.div`
width: 100%;
position: absolute;
top: 0;
z-index: 10;
`;
86 changes: 86 additions & 0 deletions apps/web/src/app/map/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use client';
import { ExploreHeader, MenuHeader } from '@/components/header';
import MapView from '@/components/map/MapView';
import * as S from './page.styles';
import { useEffect, useState } from 'react';
import { getCurrentPosition } from '@/utils/getCurrentPosition';
import { LocationState } from '@/types/map.type';
import BottomSheet from '@/components/bottomSheet/BottomSheet';
import { SheetContext } from '@/components/bottomSheet/_context/SheetContext';
import { albumDetailById, albumList, mapPins } from './mockData';

export default function MapPage() {
const [viewState, setViewState] = useState<LocationState | null>(null);
const [sheetContext, setSheetContext] = useState<SheetContext>({
type: 'home',
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 타입을 추후 관리하기 편하게 상수로 관리하면 어떨까요? 혹시나 이름 변경되면 코드 변경이 많아져서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

3f7b8b2

추가하였습니다!

});

useEffect(() => {
const init = async () => {
try {
const pos = await getCurrentPosition();

setViewState({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
zoom: 14,
});
} catch (err) {
console.log(err);
setViewState({
latitude: 37.5665,
Copy link
Contributor

Choose a reason for hiding this comment

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

(제안) 추후 변경하기 편하게 기본 위치도 상수화하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

75c4446

수정하였습니당 ~

longitude: 126.978,
zoom: 12,

Choose a reason for hiding this comment

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

medium

위치 정보 조회에 실패했을 때 사용되는 기본 위치(위도, 경도)와 줌 레벨이 하드코딩되어 있습니다. 이러한 '매직 넘버'는 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다.

**Frontend Design Guideline (Readability - Naming Magic Numbers)**에 따라, DEFAULT_LOCATION과 같은 명명된 상수로 추출하여 관리하는 것을 권장합니다.

References
  1. 의미를 알 수 없는 매직 넘버를 명명된 상수로 대체하여 코드의 명확성과 유지보수성을 향상시켜야 합니다. (link)

});
}
};

init();
}, []);

const selectedAlbumId =
sheetContext.type === 'albumDetail' ? sheetContext.albumId : null;
const selectedAlbumTitle =
selectedAlbumId !== null ? albumDetailById[selectedAlbumId]?.title : undefined;

const handleSelectAlbum = (albumId: number) => {
setSheetContext({ type: 'albumDetail', albumId });
};

const handleCloseAlbumDetail = () => {
setSheetContext({ type: 'albumList' });
};

return (
<S.Wrapper>
<S.HeaderContainer>
{sheetContext.type === 'albumDetail' ? (
<MenuHeader
title={selectedAlbumTitle ?? '앨범'}
onClickBack={handleCloseAlbumDetail}
/>
) : (
<ExploreHeader
title="서울특별시 마포구"
onClickProfile={() => {}}
onClickExplore={() => {}}
/>
)}
</S.HeaderContainer>
{viewState && (
<MapView
locationState={viewState}
pins={mapPins}
selectedAlbumId={selectedAlbumId}
/>
)}
<BottomSheet
context={sheetContext}
albums={albumList}
albumDetailById={albumDetailById}
onChangeContext={setSheetContext}
onSelectAlbum={handleSelectAlbum}
/>
</S.Wrapper>
);
}
9 changes: 8 additions & 1 deletion apps/web/src/app/photo/_contexts/PhotoContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
'use client';

import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from 'react';
import type { SelectedPhoto } from '../add/_types/photo';

interface PhotoContextValue {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/app/photo/add/_hooks/usePhotoSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const usePhotoSelect = (options?: UsePhotoSelectOptions): UsePhotoSelectR
setIsLoading(true);
const photoPromises = Array.from(files).map((file) => fileToSelectedPhoto(file));
const results = await Promise.all(photoPromises);
const newPhotos = results.filter((photo): photo is SelectedPhoto => photo !== null);
const newPhotos = results.filter(
(photo): photo is SelectedPhoto => photo !== null,
);

options?.onPhotosSelected?.(newPhotos);
setIsLoading(false);
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/app/photo/add/_utils/extractGpsFromExif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const findGpsIfdPointer = (
dataView: DataView,
tiffOffset: number,
ifdOffset: number,
littleEndian: boolean
littleEndian: boolean,
): number | null => {
const entries = dataView.getUint16(tiffOffset + ifdOffset, littleEndian);

Expand All @@ -106,7 +106,7 @@ const parseGpsIfd = (
dataView: DataView,
tiffOffset: number,
gpsIfdOffset: number,
littleEndian: boolean
littleEndian: boolean,
): GpsCoordinates | null => {
const entries = dataView.getUint16(tiffOffset + gpsIfdOffset, littleEndian);

Expand Down Expand Up @@ -154,7 +154,7 @@ const parseGpsIfd = (
const parseGpsCoordinate = (
dataView: DataView,
offset: number,
littleEndian: boolean
littleEndian: boolean,
): number => {
// GPS 좌표는 도/분/초 형식의 RATIONAL 3개로 저장됨
const degrees = readRational(dataView, offset, littleEndian);
Expand All @@ -164,7 +164,11 @@ const parseGpsCoordinate = (
return degrees + minutes / 60 + seconds / 3600;
};

const readRational = (dataView: DataView, offset: number, littleEndian: boolean): number => {
const readRational = (
dataView: DataView,
offset: number,
littleEndian: boolean,
): number => {
const numerator = dataView.getUint32(offset, littleEndian);
const denominator = dataView.getUint32(offset + 4, littleEndian);

Expand Down
Loading