[feat]카카오맵 feature 모듈 분리 및 장소 선택 모달 디자인 변경#77
Conversation
- meetings feature에 산재된 카카오맵 관련 코드를 kakaomap feature로 분리. - Map, MapMarker, Marker, ZoomControl 컴포넌트와 useKakaoLoader, useKakaoMap, useKakaoPlaceSearch 훅을 독립 모듈로 구성. - PlaceListSkeleton 컴포넌트와 usePlaceSearch 훅을 추가하여 장소 검색 UX 개선. - 장소 검색 모달 디자인 수정
WalkthroughKakao Maps 기능을 독립 feature로 분리해 SDK 로더·컨텍스트·타입·컴포넌트(Map, Marker, MapMarker, ZoomControl)·훅(useKakaoLoader, useKakaoPlaceSearch, usePlaceSearch 등)을 추가하고, meetings 쪽 기존 Kakao 관련 훅·타입 구현을 삭제·마이그레이션했습니다. Changes
Sequence DiagramsequenceDiagram
participant User
participant Map as "Map Component"
participant Loader as "KakaoMapApiLoader"
participant SDK as "Kakao SDK (window.kakao.maps)"
participant Context as "KakaoMapContext"
participant Marker as "Marker Component"
User->>Map: Mount (props: center, level)
Map->>Loader: getInstance(options).load()
Loader->>SDK: inject script & wait
SDK-->>Loader: kakao.maps ready
Loader-->>Map: resolve
Map->>SDK: new kakao.maps.Map(container, options)
Map->>Context: provide map instance
Map->>User: render children (markers, controls)
User->>Marker: Mount (position, props)
Marker->>Context: useKakaoMapContext()
Context-->>Marker: return map instance
Marker->>SDK: new kakao.maps.Marker({ map, position })
SDK-->>Marker: marker created
Marker->>SDK: kakao.maps.event.addListener(marker, 'click', handler)
SDK-->>User: events invoke handlers
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@src/features/kakaomap/components/Marker.tsx`:
- Line 107: The useImperativeHandle call currently uses an empty deps array so
it runs before markerRef.current is set, causing the forwarded ref to be null;
update useImperativeHandle(ref, () => markerRef.current as KakaoMarker,
[markerRef.current]) or bind it to the marker instance state so the effect
re-runs when the marker is created (reference symbols: useImperativeHandle,
markerRef, KakaoMarker, marker creation effect).
In `@src/features/kakaomap/components/ZoomControl.tsx`:
- Around line 32-35: The code computes position from _position and instantiates
new window.kakao.maps.ZoomControl() eagerly, which throws if the Kakao SDK isn't
loaded or map is null; update ZoomControl and position calculations to run
lazily and only when the SDK and map are available: move the position logic into
the same useMemo (or a guarded block) that depends on map/_position and check
for window.kakao?.maps before accessing ControlPosition or constructing
ZoomControl, returning null/undefined until the SDK is present; reference
symbols: _position, position, useMemo, zoomControl (new
window.kakao.maps.ZoomControl()), and ensure the component early-returns or
renders nothing when map is null or window.kakao?.maps is falsy.
In `@src/features/kakaomap/hooks/useKakaoLoader.ts`:
- Around line 24-27: The code passes import.meta.env.VITE_KAKAO_MAP_KEY to
Loader.getInstance using a blind "as string" cast which will convert undefined
to the literal "undefined"; change this to validate the env var before calling
Loader.getInstance: read import.meta.env.VITE_KAKAO_MAP_KEY into a variable,
check if it's truthy (throw an Error or fail-fast with a clear message if
missing), then pass that validated value to Loader.getInstance (reference:
Loader.getInstance and import.meta.env.VITE_KAKAO_MAP_KEY in useKakaoLoader.ts).
In `@src/features/kakaomap/hooks/useKakaoMap.ts`:
- Around line 113-118: initializeMap 함수 내부에서 map.addControl(new
kakao.maps.MapTypeControl(), ...) 및 map.addControl(new kakao.maps.ZoomControl(),
...)를 직접 추가해 ZoomControl 컴포넌트와 중복될 위험이 있으니, initializeMap에서 해당 하드코딩된 컨트롤 추가를
제거하거나 제어 옵션을 도입해 중복을 방지하세요; 예를 들어 initializeMap(mapOptions, { addControls:
boolean }) 같은 플래그를 받아 true일 때만 map.addControl을 호출하거나 아예 remove한 뒤 선언적 컴포넌트인
ZoomControl 및 MapTypeControl 컴포넌트로 컨트롤을 일원화하도록 수정하고, 관련 코드 호출부(또는 컴포넌트 트리)에서 적절히
플래그를 설정하거나 컴포넌트를 사용하도록 변경하세요.
In `@src/features/kakaomap/hooks/useKakaoPlaceSearch.ts`:
- Around line 30-31: The validation uses searchKeyword.trim() but the code calls
keywordSearch with the untrimmed original; change to compute a trimmed value
(e.g., const trimmed = searchKeyword.trim()), use that for the emptiness check
and pass trimmed into keywordSearch (or consistently use the untrimmed value
everywhere if trimming is not desired) so that useKakaoPlaceSearch's call to
keywordSearch and its validation are consistent; update references to
searchKeyword.trim() and keywordSearch accordingly.
In `@src/features/meetings/components/index.ts`:
- Line 1: The current export in meetings/components/index.ts directly references
kakaomap's internal path (MapModal) which couples to internal structure;
instead, add a re-export of MapModal from the kakaomap public entry (e.g., in
the kakaomap index.ts export MapModal: export { default as MapModal } from
'./components/MapModal'), then update meetings/components/index.ts to re-export
from the public API (e.g., export { MapModal } from '@/features/kakaomap') so
meetings no longer depends on kakaomap internals.
In `@src/features/meetings/hooks/usePlaceSearch.ts`:
- Around line 64-71: In usePlaceSearch.ts inside the useEffect that depends on
mapInstance and places, change the forEach callback from an expression-bodied
arrow (places.forEach((p) => bounds.extend(...))) to a block-bodied arrow that
explicitly calls bounds.extend without implicitly returning (e.g.
places.forEach((p) => { bounds.extend(new kakao.maps.LatLng(Number(p.y),
Number(p.x))); })), keeping kakao, bounds, kakao.maps.LatLng and
mapInstance.setBounds intact so the lint error about implicit return in the
forEach callback is resolved.
- Around line 73-78: resetState currently resets search-related state but leaves
mapInstance intact; update the useCallback handler resetState to call
setMapInstance(null) so mapInstance is cleared when resetting, and add
setMapInstance to the resetState dependency array so the callback stays correct
(locate the resetState function and modify its body and dependencies
accordingly).
🧹 Nitpick comments (11)
src/features/kakaomap/hooks/useKakaoMap.ts (2)
81-86: API 키가 훅 내부에 하드코딩됨
import.meta.env.VITE_KAKAO_MAP_KEY를 훅 내부에서 직접 참조하고 있어, 훅의 재사용성이 환경변수에 결합됩니다.Loader는 싱글톤이므로 상위 레벨(앱 초기화 시점)에서 한 번만Loader.getInstance를 호출하고, 훅에서는Loader.getInstance()(옵션 없이)로 기존 인스턴스를 가져오는 구조가 더 깔끔합니다.
145-158: 마커 이벤트 리스너 명시적 제거 누락
kakao.maps.event.addListener로 등록한mouseover/mouseout리스너가clearMarkers()에서removeListener없이setMap(null)만으로 정리됩니다. 마커 객체가 GC되면 문제없지만, 외부에서 마커 참조를 유지하는 경우 메모리 누수 가능성이 있습니다.src/features/kakaomap/hooks/useKakaoLoader.ts (1)
29-37: 컴포넌트 언마운트 시 비동기 상태 업데이트 가능성
loader.load()가 진행 중일 때 컴포넌트가 언마운트되면setLoading/setError가 호출됩니다. React 18에서 경고는 발생하지 않지만, abort 패턴을 적용하면 더 안전합니다.♻️ 옵션: abort flag 적용
useEffect(() => { + let cancelled = false const loader = Loader.getInstance({ appkey: import.meta.env.VITE_KAKAO_MAP_KEY as string, libraries: ['services'], }) loader .load() .then(() => { - setLoading(false) + if (!cancelled) setLoading(false) }) .catch((err: unknown) => { - setLoading(false) - setError(err instanceof Error ? err : new Error('카카오 지도 SDK 로드에 실패했습니다.')) + if (!cancelled) { + setLoading(false) + setError(err instanceof Error ? err : new Error('카카오 지도 SDK 로드에 실패했습니다.')) + } }) + + return () => { cancelled = true } }, [])src/features/kakaomap/lib/useKakaoEvent.ts (1)
26-38:handler가 처음에undefined이다가 이후 값이 설정되면 이벤트가 등록되지 않습니다.
target/type이 변경되지 않는 한 effect가 재실행되지 않으므로, 초기undefined→ 이후 유효 핸들러 전환 시 이벤트 바인딩이 누락됩니다.현재 사용처(Marker.tsx)에서 문제가 없다면 괜찮지만, 범용 훅이라면
handler존재 여부를 deps에 포함하거나 문서에 제약을 명시하는 것이 좋습니다.src/features/kakaomap/components/Map.tsx (2)
44-44: Biome: 글로벌Map섀도잉 경고
Map이라는 이름이globalThis.Map을 섀도잉합니다. 맵 라이브러리에서 흔한 패턴이므로 의도적이라면 Biome 룰을 해당 라인에서 suppress하는 것이 좋습니다. 그렇지 않으면KakaoMap등으로 컴포넌트 이름 변경을 고려해보세요.
54-70: 마운트 시 생성 → 바로 center/level sync effect가 중복 실행됩니다.
useLayoutEffect([], ...)에서setMap(kakaoMap)호출 후 Line 73-76, 79-82의 effect가 동일 값으로setCenter/setLevel을 한 번 더 호출합니다. 성능 영향은 미미하지만, 초기값 중복 적용을 방지하려면 이전 값을 ref로 추적하는 방법이 있습니다.src/features/kakaomap/hooks/useKakaoPlaceSearch.ts (1)
29-65:onSearchSuccess/onSearchError가 inline 함수일 경우search가 매 렌더마다 재생성됩니다.
search를 다른 effect의 deps로 사용하면 불필요한 재실행이 발생할 수 있습니다. Map.tsx의onCreateRef패턴처럼 콜백을 ref로 안정화하면 해결됩니다.♻️ ref 안정화 예시
+ const onSuccessRef = useRef(onSearchSuccess) + onSuccessRef.current = onSearchSuccess + const onErrorRef = useRef(onSearchError) + onErrorRef.current = onSearchError + const search = useCallback( (searchKeyword: string) => { // ... - onSearchSuccess?.(data) + onSuccessRef.current?.(data) // ... - onSearchError?.(message) + onErrorRef.current?.(message) // ... }, - [onSearchSuccess, onSearchError] + [] )src/features/meetings/components/PlaceSearchModal.tsx (1)
68-105:hidden클래스는display: none을 적용합니다 — 의도와 일치하는지 확인하세요.주석에 "CSS로만 숨김"이라고 되어있는데,
hidden은 레이아웃에서 완전히 제거합니다. Map 인스턴스는 React 트리에 남아있으므로 목적(인스턴스 유지)은 달성되지만, 만약 Map이 컨테이너 크기에 의존하는relayout()같은 동작이 필요하다면invisible(visibility: hidden)이 더 안전할 수 있습니다.src/features/meetings/components/PlaceList.tsx (1)
6-48: 깔끔한 프레젠테이셔널 컴포넌트입니다.UI 전용으로 잘 분리되어 있고
stopPropagation처리도 적절합니다. 한 가지 참고:road_address_name이 빈 문자열인 장소가 있을 수 있는데, Line 31에서address_name으로 폴백하면 더 안전합니다.💡 도로명 주소 폴백 제안
- <p className="typo-body4 text-grey-700 mb-xsmall">{place.road_address_name}</p> + <p className="typo-body4 text-grey-700 mb-xsmall">{place.road_address_name || place.address_name}</p>src/features/kakaomap/lib/kakaoMapApiLoader.ts (2)
74-85:Array.sort()가 기존 인스턴스의libraries배열을 직접 변경합니다
Loader.instance.libraries.sort()는 원본 배열을 in-place로 정렬합니다. 기능적 버그는 아니지만, 인스턴스의 내부 데이터가 외부 호출마다 변경되는 부작용이 있습니다.♻️ 비파괴적 정렬로 변경
- const isSameLibs = - JSON.stringify(Loader.instance.libraries.sort()) === - JSON.stringify((options.libraries ?? []).sort()) + const isSameLibs = + JSON.stringify([...Loader.instance.libraries].sort()) === + JSON.stringify([...(options.libraries ?? [])].sort())
109-148: 실패한 script 태그가 DOM에 누적됩니다
tryLoad()호출마다document.head.appendChild(script)로 새 스크립트를 추가하지만, 실패 시 제거하지 않습니다. 재시도 횟수만큼 실패한<script>태그가<head>에 남게 됩니다.🧹 실패 시 script 태그 정리
script.onerror = () => { + script.remove() // fetch로 실제 HTTP 상태 코드 확인 fetch(url)
- useKakaoMapContext가 null 대신 Error를 throw하도록 변경하여 Map 컴포넌트 외부 사용 시 명확한 에러 발생. - Marker 인스턴스 관리를 useRef에서 useMemo로 전환하여 타이밍 문제 해결. - components/context/hooks/lib 각 디렉토리에 barrel export index 추가. - useKakaoMap, MapModal 불필요 파일 삭제. - useKakaoLoader 환경변수 사전 체크 로직 개선. - usePlaceSearch 모달 닫기 시 mapInstance null 초기화.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/features/meetings/hooks/usePlaceSearch.ts`:
- Around line 50-57: The hook call to useKakaoPlaceSearch is not destructuring
the returned error value, so on API failures you only ever surface a generic
fallback message; update the destructuring to include error (e.g., const {
places, search, reset, error } = useKakaoPlaceSearch(...)) and then change the
error handler (onSearchError or wherever sdkError is used) to use that error
object when setting the search error state or message (use error.message or
error.toString() as appropriate) so SDK-success + API-failure cases show the
real API error rather than only sdkError or a generic string; apply the same
change at the other occurrence around lines 137-138.
🧹 Nitpick comments (5)
src/features/kakaomap/components/Marker.tsx (1)
143-146:image객체 참조 비교로 불필요한setImage호출 가능
image는 매 렌더마다 새 객체 참조가 될 수 있어useLayoutEffect가 매번 실행됩니다. → 마커 이미지가 불필요하게 재설정될 수 있음.부모에서
useMemo로image를 안정화하거나, 내부에서 얕은 비교(shallow compare)로 변경 여부를 판단하는 방법이 있습니다.src/features/kakaomap/index.ts (1)
1-31: 타입은 명시적, 하위 모듈은export *— 일관성 참고타입(Line 2-19)은 개별 이름으로 export하고, 하위 모듈(Line 22-31)은
export *로 전달하고 있습니다. 각 하위 모듈의index.ts가 이미 공개 API를 관리하고 있어 현재 구조에서는 문제없지만, 하위 모듈 간 동일 이름 export가 생기면 충돌 가능성이 있습니다.지금은 괜찮지만 모듈이 커지면 명시적 re-export 전환을 고려해볼 수 있습니다.
src/features/meetings/hooks/usePlaceSearch.ts (2)
64-71:window.kakao접근 시 방어 코드 부재SDK 로딩이 완료된 후 실행되는 것이 의도이지만,
effectiveSearchState가'error'가 아닌 상태에서mapInstance가 존재하더라도window.kakao.maps가 없을 수 있는 edge case가 있습니다. 동일 패턴이handlePlaceClick(Line 109),handlePlaceFocus(Line 127)에도 존재합니다.🛡️ 수정 제안 (bounds effect)
useEffect(() => { if (!mapInstance || places.length === 0) return + if (!window.kakao?.maps) return const { kakao } = window const bounds = new kakao.maps.LatLngBounds()
106-121:handlePlaceClick에서 불필요한setCenter호출
mapInstance.setCenter()직후onOpenChange(false)+resetState()로 모달을 닫고 mapInstance를 null로 초기화합니다. 지도 센터 이동이 사용자에게 보이지 않으므로 해당 호출은 무의미합니다.♻️ 수정 제안
const handlePlaceClick = useCallback( (place: KakaoPlace) => { - if (mapInstance) { - mapInstance.setCenter(new window.kakao.maps.LatLng(Number(place.y), Number(place.x))) - } onSelectPlace({ name: place.place_name, address: place.road_address_name || place.address_name, latitude: Number(place.y), longitude: Number(place.x), }) onOpenChange(false) resetState() }, - [mapInstance, onSelectPlace, onOpenChange, resetState] + [onSelectPlace, onOpenChange, resetState] )src/features/meetings/components/PlaceSearchModal.tsx (1)
9-9:Mapimport가 전역Map을 shadow함 (Biome lint)
Map이름이 JavaScript 전역Map생성자를 가립니다. 실제 이 파일에서 전역Map을 사용하지 않으므로 기능적 문제는 없지만, 향후 혼동 가능성이 있습니다.♻️ 대안: alias import
-import { Map, MapMarker, ZoomControl } from '@/features/kakaomap' +import { Map as KakaoMap, MapMarker, ZoomControl } from '@/features/kakaomap'사용 시
<KakaoMap ...>으로 변경하면 됩니다.
|
@coderabbitai 리뷰반영된거 확인해서 resolve 해주시죠 |
|
✅ Actions performedComments resolved. Auto-approval is disabled; enable |
|
@coderabbitai 반영확인하고 resolve하신건가요? 아니면 그냥 다 닫으셨나요 |
|
Tip For best results, initiate chat on the files or code changes.
혹시 특정 변경사항이나 리뷰 코멘트에 대해 제대로 반영되었는지 확인이 필요하시면 말씀해 주세요. 개별적으로 검토해 드리겠습니다. |
|
@coderabbitai 확인해주세요~ 잘 반영 된건지 하나씩!!!!!!!! 잘 반영 된거면 닫고 아니면 열고 피드백주세여 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 19131 🏁 Script executed: Length of output: 4671
✅ 반영 완료 (7개)1. Marker.tsx - useImperativeHandle deps반영됨 ✅ 2. useKakaoLoader.ts - 환경변수 체크반영됨 ✅ 3. useKakaoMap.ts - 컨트롤 중복반영됨 ✅ 4. kakaomap/hooks/useKakaoPlaceSearch.ts - trim 불일치반영됨 ✅ 5. meetings/components/index.ts - MapModal cross-feature import반영됨 ✅ 6. usePlaceSearch.ts - forEach 콜백문제 없음 ✅ 7. usePlaceSearch.ts - resetState mapInstance 초기화반영됨 ✅ ❌ 미반영 (2개)8. ZoomControl.tsx - SDK 체크 누락 (Line 32-35)const position =
typeof _position === 'string' ? window.kakao.maps.ControlPosition[_position] : _position
const zoomControl = useMemo(() => new window.kakao.maps.ZoomControl(), [])문제: SDK 미로드 시 제안: const position = useMemo(() => {
if (!window.kakao?.maps) return 0
return typeof _position === 'string'
? window.kakao.maps.ControlPosition[_position]
: _position
}, [_position])
const zoomControl = useMemo(() => {
if (!window.kakao?.maps) return null
return new window.kakao.maps.ZoomControl()
}, [])9. usePlaceSearch.ts - 검색 API 오류 메시지 누락 (Line 50, 138)const { places, search, reset } = useKakaoPlaceSearch({ // error 미사용
// ...
})
const errorMessage = sdkError?.message ?? '오류가 발생했습니다. 다시 시도해주세요.'
// searchError가 없어서 검색 API 오류 시 제네릭 메시지만 노출문제: SDK 정상 + 검색 API 오류 시 구체적 에러 메시지 전달 불가. 제안: const { places, error: searchError, search, reset } = useKakaoPlaceSearch({
// ...
})
const errorMessage = sdkError?.message ?? searchError ?? '오류가 발생했습니다. 다시 시도해주세요.'미반영된 2개는 현재 comment를 그대로 열어두시면 될 것 같습니다. 나머지는 잘 반영하셨습니다! 👍 |
|
@coderabbitai 9번은 수정했습니다 다시 리뷰봐주세요 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 3042
✅ 8. ZoomControl.tsx - SDK 체크문제 없습니다! 👍 이유:
✅ 9. usePlaceSearch.ts - error 처리반영 완료! 🎉 const { places, error: searchError, search, reset } = useKakaoPlaceSearch({
// ...
})
const errorMessage = sdkError?.message ?? searchError ?? '오류가 발생했습니다. 다시 시도해주세요.'
모든 리뷰 이슈가 해결되었습니다! 🎊 If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/features/meetings/hooks/usePlaceSearch.ts`:
- Around line 1-163: The file fails Prettier formatting; run the formatter (e.g.
prettier --write) on this file to fix code style so CI passes. Specifically
reformat the exported hook usePlaceSearch and its helpers (handleKeyDown,
handlePlaceClick, handlePlaceFocus, resetState, etc.), then commit the updated
file; ensure your editor/commit hook uses the project's Prettier config so
future changes follow the same rules.
🧹 Nitpick comments (1)
src/features/meetings/hooks/usePlaceSearch.ts (1)
64-71:window.kakao직접 접근 — SDK 언로드 시 런타임 에러 가능성.
mapInstance가 존재하면 SDK도 로드된 상태일 가능성이 높지만,window.kakao.maps에 대한 방어 코드가 없습니다. 옵셔널 체이닝이나 가드를 추가하면 더 안전합니다.현재는
mapInstance존재 = SDK 로드 완료라는 암묵적 보장에 의존하고 있어 실질적 위험은 낮지만, 109번·127번 라인에서도 동일 패턴이 반복됩니다.🛡️ 방어 코드 예시
useEffect(() => { if (!mapInstance || places.length === 0) return + if (!window.kakao?.maps) return const { kakao } = window const bounds = new kakao.maps.LatLngBounds() places.forEach((p) => bounds.extend(new kakao.maps.LatLng(Number(p.y), Number(p.x)))) mapInstance.setBounds(bounds) }, [mapInstance, places])
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/features/kakaomap/lib/kakaoMapApiLoader.ts (2)
69-79:this.libraries.sort()가 내부 배열을 직접 변경(mutate)합니다.
Array.sort()는 원본 배열을 변경하므로,getInstance가 옵션과 함께 반복 호출될 때마다this.libraries가 in-place로 정렬됩니다.readonly필드 선언은 재할당만 막을 뿐, 배열 내부 변경은 막지 않습니다.동작 결과는 동일하지만(이미 정렬된 배열을 다시 정렬), 의도와 불일치합니다.
🔧 스프레드 복사 후 비교
const isSameLibs = - JSON.stringify(KakaoMapApiLoader.instance.libraries.sort()) === - JSON.stringify((options.libraries ?? []).sort()) + JSON.stringify([...KakaoMapApiLoader.instance.libraries].sort()) === + JSON.stringify([...(options.libraries ?? [])].sort())
120-141:onerror시 동일 URL로fetch재요청 → 429 상황에서 부담 가중 가능스크립트 로드 실패 시 HTTP 상태 코드를 얻기 위해 같은 URL로
fetch를 보내고 있습니다. 이미 429(쿼터 초과)인 경우 불필요한 요청이 추가로 발생하고, 재시도 루프와 결합하면maxRetries × 2만큼 요청이 나갑니다.대안:
fetch대신 제네릭 에러 메시지로 대체하거나,fetch에 별도AbortController+ 짧은 타임아웃을 걸어 부작용을 줄이는 방법이 있습니다.
🚀 풀 리퀘스트 제안
meetingsfeature에 흩어져 있던 카카오맵 관련 코드를kakaomap독립 feature 모듈로 분리usePlaceSearch훅으로 분리하여PlaceSearchModal컴포넌트 단순화PlaceListSkeleton) 추가📋 작업 내용
신규:
src/features/kakaomap/components/Map.tsxcomponents/MapMarker.tsxcomponents/Marker.tsxcomponents/ZoomControl.tsxcontext/KakaoMapContext.tshooks/useKakaoLoader.tshooks/useKakaoPlaceSearch.tslib/kakaoMapApiLoader.tslib/useKakaoEvent.tskakaoMap.types.ts수정:
src/features/meetings/hooks/usePlaceSearch.ts(신규): 장소 검색 상태(idle/searching/success/noResults/error) 및 비동기 로직 캡슐화components/PlaceListSkeleton.tsx(신규): 검색 중 로딩 스켈레톤 UIcomponents/PlaceSearchModal.tsx: 상태 관리 로직을usePlaceSearch로 위임, UI만 담당하도록 단순화components/PlaceList.tsx:kakaomapfeature 타입 참조로 변경hooks/useKakaoPlaceSearch.ts삭제 → kakaomap으로 이동kakaoMap.types.ts,loadKakaoSdk.ts삭제 → kakaomap으로 이동Summary by CodeRabbit
새로운 기능
리팩토링