refactor: 8호선 데이터, 이용약관/개인정보처리방침 추가 및 지도 컴포넌트 리팩토링#33
Conversation
- api 응답 대기 시간을 2분으로 연장 - 지도 이미지가 여전히 화면을 뚫고 나가는 문제 해결
- 이동시간 렌더링 문제 해결 - 일직선이던 지도 경로를 꺾은선으로 수정
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. Walkthrough약관 및 개인정보처리방침 모달을 추가하고 global modal 타입/핸들링을 확장했으며, 추천·결과 페이지와 카카오맵 라인 컴포넌트의 UI·렌더링 로직을 일부 리팩토링하고 메타데이터와 정거장 데이터, Next 설정을 업데이트했습니다. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (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 `@app/recommend/page.tsx`:
- Around line 140-142: The share button currently rendered as <button
className="text-gray-5 shrink-0 cursor-pointer"> with the Image child has no
onClick handler so it does nothing; add a click handler (e.g., implement a
handleShare function in app/recommend/page.tsx and assign it to the button's
onClick) that performs the share logic (or, if the feature isn't ready, set the
button to disabled and add aria-disabled and a tooltip/visually-hidden label or
hide the button entirely) and ensure accessibility attributes (aria-label) are
present to describe the action.
- Around line 120-128: The current card toggles between 'border-2' and 'border'
causing a 1px layout shift; update the class logic in the clickable card (the
element using key={place.id}, onClick={() => setSelectedPlaceId(place.id)} and
reading selectedPlaceId === place.id) so that it always includes 'border-2' and
only switches border color classes (e.g., from 'border-gray-2' to a selected
color like 'border-blue-5') or alternatively remove border thickness changes and
apply a non-layout ring for selection; adjust the conditional to change
color/ring classes rather than toggling between 'border-2' and 'border'.
In `@components/footer.tsx`:
- Around line 19-32: The two buttons that call openModal('TERMS', ...) and
openModal('POLICY', ...) in components/footer.tsx are missing the cursor-pointer
class; update their className strings (the button elements using onClick={(e) =>
openModal('TERMS', ...)} and onClick={(e) => openModal('POLICY', ...)}) to
include "cursor-pointer" so they visually indicate clickability (match the
commented feedback button's behavior).
In `@components/map/kakaoMapLine.tsx`:
- Around line 139-141: The comment and the actual stroke opacity disagree:
update the strokeOpacity prop used in the kakaoMap Line/Polyline render (the JSX
prop named strokeOpacity in components/map/kakaoMapLine.tsx) to the intended
value of 0.8 and/or adjust the comment so both match; specifically set
strokeOpacity={0.8} where the prop is currently strokeOpacity={1} (or change the
comment to reflect 1) so comment and implementation are consistent.
- Around line 151-167: The tooltip only toggles via onMouseEnter/onMouseLeave so
touch devices never open it; update the component that renders the group (the
div with onMouseEnter/onMouseLeave) to also handle touch/click by adding an
onClick and onTouchStart handler that toggles the hovered state via
setHoveredUserId (use userRoute.nickname to open and null to close), ensure the
handlers respect the existing isHovered logic (so the className conditional
still works) and debounce or ignore redundant events if needed to avoid
immediately closing on mobile; keep tooltip markup (userRoute.startStation and
userRoute.travelTime) unchanged so it displays when setHoveredUserId matches.
- Around line 115-130: When building pathCoordinates, if userRoute.stations
exists you currently map only the stations and omit endStation, causing the
polyline to stop short; update the logic in the pathCoordinates construction
(the branch that maps userRoute.stations) to append the endStation coordinate as
the final point (apply offsetVal to endStation.latitude and endStation.longitude
for consistency with the other branch), so pathCoordinates becomes
[...mappedStations, { lat: endStation.latitude + offsetVal, lng:
endStation.longitude + offsetVal }]; markerPosition
(userRoute.latitude/longitude + offsetVal) can remain unchanged.
In `@database/stations_info.json`:
- Around line 2132-2155: 데이터 일관성 문제: stations_info.json에 추가된 역 객체들("암사역사공원",
"장자호수공원", "동구릉", "다산")에 "역" 접미사가 누락되어 있습니다; 각 해당 객체의 name 값을 공식 명칭으로 수정하여
"암사역사공원역", "장자호수공원역", "동구릉역", "다산역"으로 바꾸세요 (해당 JSON 객체들을 찾아 name 필드만 업데이트).
In `@next.config.ts`:
- Around line 5-8: Remove the unused experimental.proxyTimeout setting from
next.config (the experimental.proxyTimeout key is ignored in current Next.js
versions) and instead implement one of the supported approaches: control request
timeouts inside your Route Handlers / API Routes by adding timeout logic around
fetch calls, or set the Node HTTP server timeout globally via
NODE_OPTIONS='--http-server-default-timeout=120000'; update any comments to
explain the chosen approach and delete the proxyTimeout property from the
config.
🧹 Nitpick comments (6)
components/map/kakaoMapLine.tsx (2)
133-133: 배열 인덱스 대신 안정적인 키 사용을 권장합니다.
nickname이 이미 고유 식별자로 사용되고 있으므로(hoveredUserId), 인덱스 대신nickname을 키로 사용하면 리스트 순서 변경 시 React 재조정이 더 정확해집니다.♻️ 수정 제안
- <React.Fragment key={`user-route-${index}`}> + <React.Fragment key={`user-route-${userRoute.nickname}`}>
193-200:meetingId도encodeURIComponent로 인코딩하는 것이 안전합니다.
endStation.name은 인코딩하면서meetingId는 하지 않는 것이 일관성이 없습니다. 예상치 못한 값이 들어올 경우를 대비해 모든 쿼리 파라미터를 인코딩하는 것을 권장합니다.♻️ 수정 제안
- `/recommend?meetingId=${meetingId}&midPlace=${encodeURIComponent(endStation.name)}&lat=${endStation.latitude}&lng=${endStation.longitude}` + `/recommend?meetingId=${encodeURIComponent(meetingId)}&midPlace=${encodeURIComponent(endStation.name)}&lat=${encodeURIComponent(String(endStation.latitude))}&lng=${encodeURIComponent(String(endStation.longitude))}`components/modal/policyModal.tsx (2)
12-15: 인터페이스 이름TermsModalProps가PolicyModal에서 사용되고 있습니다.이 파일은
PolicyModal컴포넌트인데, props 인터페이스 이름이TermsModalProps로 되어 있어 혼동을 줍니다.termsModal.tsx에도 동일한 인터페이스가 정의되어 있으므로, 공통 타입을 추출하거나 이 파일에서는 이름을 변경하는 것이 좋습니다.♻️ 리팩토링 제안
옵션 1: 이 파일에서 이름 변경
-interface TermsModalProps { +interface PolicyModalProps { isOpen: boolean; onClose: () => void; } -export default function PolicyModal({ isOpen, onClose }: TermsModalProps) { +export default function PolicyModal({ isOpen, onClose }: PolicyModalProps) {옵션 2: 공통 타입 추출 (두 모달 모두에 적용)
// types/modal.ts export interface BaseModalProps { isOpen: boolean; onClose: () => void; }
36-305:termsModal.tsx와policyModal.tsx의 구조가 거의 동일합니다.두 모달은 Dialog 구조, 헤더, 스크롤 영역, 푸터 버튼이 동일한 패턴으로 반복되고 있습니다(Lines 17-318 전체). 본문 내용만 다르므로, 공통 레이아웃을 추출하여 재사용하면 유지보수가 용이해집니다.
♻️ 공통 레이아웃 컴포넌트 제안
// components/modal/legalDocumentModal.tsx interface LegalDocumentModalProps { isOpen: boolean; onClose: () => void; title: string; description: string; children: React.ReactNode; } export default function LegalDocumentModal({ isOpen, onClose, title, description, children }: LegalDocumentModalProps) { return ( <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> <DialogContent onOpenAutoFocus={(e) => e.preventDefault()} className="flex max-h-[85vh] w-[calc(100%-40px)] max-w-lg flex-col gap-6 rounded bg-white px-6 py-5 md:max-w-xl" > <DialogHeader className="shrink-0 text-left"> <DialogTitle className="text-[20px] font-bold text-black md:text-[22px]">{title}</DialogTitle> <DialogDescription className="sr-only">{description}</DialogDescription> </DialogHeader> <div className="flex-1 overflow-y-auto border-y border-gray-100 py-4 pr-2 text-sm leading-relaxed text-gray-600"> <div className="space-y-6">{children}</div> </div> <DialogFooter className="shrink-0"> <button type="button" onClick={onClose} className="bg-blue-5 hover:bg-blue-8 w-full cursor-pointer rounded-sm py-3 text-sm font-semibold text-white transition-colors md:w-auto md:px-6"> 확인 </button> </DialogFooter> </DialogContent> </Dialog> ); }그러면 각 모달은 내용만 전달하면 됩니다:
export default function PolicyModal({ isOpen, onClose }: PolicyModalProps) { return ( <LegalDocumentModal isOpen={isOpen} onClose={onClose} title="개인정보처리방침" description="밍글링 개인정보처리방침 전문입니다."> {/* 본문 섹션들만 여기에 */} </LegalDocumentModal> ); }app/result/[id]/page.tsx (2)
205-221:selectedResult계산이 모바일/데스크탑 섹션에서 중복됩니다.동일한
locationResults.find(...)로직이 IIFE 내에서 두 번 실행되고 있습니다.selectedResult를 컴포넌트 상단에서 한 번만 계산하면 중복 제거와 가독성 개선이 됩니다.♻️ selectedResult를 한 번만 계산하도록 리팩토링
useMemo블록 아래에 다음과 같이 추출하고, 두 IIFE를 제거합니다:+ const selectedResult = useMemo(() => { + if (locationResults.length === 0) return null; + return locationResults.find((r) => r.id === selectedResultId) || locationResults[0]; + }, [locationResults, selectedResultId]);그리고 모바일/데스크탑 렌더링을 단순화합니다:
{/* 모바일 전용 지도 영역 */} - {locationResults.length > 0 && - (() => { - const selectedResult = - locationResults.find((r) => r.id === selectedResultId) || locationResults[0]; - return ( - <KakaoMapLine - className="bg-gray-1 relative block h-93.5 md:hidden" - endStation={{ - name: selectedResult.endStation, - latitude: selectedResult.latitude, - longitude: selectedResult.longitude, - }} - userRoutes={selectedResult.userRoutes} - meetingId={id} - /> - ); - })()} + {selectedResult && ( + <KakaoMapLine + className="bg-gray-1 relative block h-93.5 md:hidden" + endStation={{ + name: selectedResult.endStation, + latitude: selectedResult.latitude, + longitude: selectedResult.longitude, + }} + userRoutes={selectedResult.userRoutes} + meetingId={id} + /> + )}데스크탑 섹션도 동일하게 적용합니다.
Also applies to: 328-344
36-36:travelTime의 falsy 기본값 처리 — 현재는 문제없으나??사용을 권장합니다.
|| 0은travelTime이0일 때도0을 반환하므로 현재 동작에는 문제가 없습니다. 다만 의미적으로 nullish 병합(??)이 의도를 더 명확히 표현합니다.travelTime이 빈 문자열 등 다른 falsy 값일 가능성이 있다면??가 다른 동작을 할 수 있으니 데이터 타입도 확인해 주세요.♻️ 제안
- const travelTime = myRoute?.travelTime || 0; + const travelTime = myRoute?.travelTime ?? 0;
| <div | ||
| key={place.id} | ||
| onClick={() => setSelectedPlaceId(place.id)} | ||
| className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${ | ||
| selectedPlaceId === place.id | ||
| ? 'border-blue-5 border-2' // 선택 시 파란 테두리 | ||
| : 'border-gray-2 hover:bg-gray-1 bg-white' | ||
| }`} | ||
| > |
There was a problem hiding this comment.
선택/비선택 시 border 두께 변경으로 레이아웃 쉬프트 발생 가능.
선택된 카드는 border-2 (2px), 비선택 카드는 border (1px)로 렌더링됩니다. 1px 차이로 인해 카드 선택 시 미세한 레이아웃 점프가 발생할 수 있습니다.
🔧 수정 제안: 항상 border-2를 유지하고 색상만 변경
className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${
selectedPlaceId === place.id
- ? 'border-blue-5 border-2' // 선택 시 파란 테두리
- : 'border-gray-2 hover:bg-gray-1 bg-white'
+ ? 'border-blue-5 border-2'
+ : 'border-gray-2 hover:bg-gray-1 border-2 border-transparent bg-white'
}`}또는 ring을 사용하여 레이아웃에 영향을 주지 않는 방식도 고려해 볼 수 있습니다.
📝 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.
| <div | |
| key={place.id} | |
| onClick={() => setSelectedPlaceId(place.id)} | |
| className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${ | |
| selectedPlaceId === place.id | |
| ? 'border-blue-5 border-2' // 선택 시 파란 테두리 | |
| : 'border-gray-2 hover:bg-gray-1 bg-white' | |
| }`} | |
| > | |
| <div | |
| key={place.id} | |
| onClick={() => setSelectedPlaceId(place.id)} | |
| className={`flex cursor-pointer flex-col gap-2 rounded border-2 p-4 ${ | |
| selectedPlaceId === place.id | |
| ? 'border-blue-5' // 선택 시 파란 테두리 | |
| : 'border-gray-2 hover:bg-gray-1 bg-white' | |
| }`} | |
| > |
🤖 Prompt for AI Agents
In `@app/recommend/page.tsx` around lines 120 - 128, The current card toggles
between 'border-2' and 'border' causing a 1px layout shift; update the class
logic in the clickable card (the element using key={place.id}, onClick={() =>
setSelectedPlaceId(place.id)} and reading selectedPlaceId === place.id) so that
it always includes 'border-2' and only switches border color classes (e.g., from
'border-gray-2' to a selected color like 'border-blue-5') or alternatively
remove border thickness changes and apply a non-layout ring for selection;
adjust the conditional to change color/ring classes rather than toggling between
'border-2' and 'border'.
| <button className="text-gray-5 shrink-0 cursor-pointer"> | ||
| <Image src="/icon/gray_share.svg" alt="공유" width={24} height={24} /> | ||
| </button> |
There was a problem hiding this comment.
공유 버튼에 onClick 핸들러가 없습니다.
공유 아이콘 버튼이 렌더링되지만 클릭 시 아무 동작도 하지 않습니다. 기능이 아직 구현되지 않았다면 비활성화 상태로 표시하거나 숨기는 것이 좋습니다.
🤖 Prompt for AI Agents
In `@app/recommend/page.tsx` around lines 140 - 142, The share button currently
rendered as <button className="text-gray-5 shrink-0 cursor-pointer"> with the
Image child has no onClick handler so it does nothing; add a click handler
(e.g., implement a handleShare function in app/recommend/page.tsx and assign it
to the button's onClick) that performs the share logic (or, if the feature isn't
ready, set the button to disabled and add aria-disabled and a
tooltip/visually-hidden label or hide the button entirely) and ensure
accessibility attributes (aria-label) are present to describe the action.
| <button | ||
| type="button" | ||
| onClick={(e) => openModal('TERMS', undefined, e)} | ||
| className="text-gray-7 text-[16px]" | ||
| > | ||
| 이용약관 | ||
| </Link> | ||
| <Link href="/" className="text-gray-7 text-[16px]"> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={(e) => openModal('POLICY', undefined, e)} | ||
| className="text-gray-7 text-[16px]" | ||
| > | ||
| 개인정보 처리방침 | ||
| </Link> | ||
| </button> |
There was a problem hiding this comment.
버튼에 cursor-pointer 클래스 누락.
같은 파일의 주석 처리된 피드백 버튼(Line 41)에는 cursor-pointer가 있지만, 이 두 버튼에는 없습니다. <Link>에서 <button>으로 변경되면서 기본 커서가 화살표로 바뀌어 클릭 가능 요소임을 시각적으로 인지하기 어렵습니다.
🔧 수정 제안
<button
type="button"
onClick={(e) => openModal('TERMS', undefined, e)}
- className="text-gray-7 text-[16px]"
+ className="text-gray-7 cursor-pointer text-[16px]"
>
이용약관
</button>
<button
type="button"
onClick={(e) => openModal('POLICY', undefined, e)}
- className="text-gray-7 text-[16px]"
+ className="text-gray-7 cursor-pointer text-[16px]"
>
개인정보 처리방침
</button>📝 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.
| <button | |
| type="button" | |
| onClick={(e) => openModal('TERMS', undefined, e)} | |
| className="text-gray-7 text-[16px]" | |
| > | |
| 이용약관 | |
| </Link> | |
| <Link href="/" className="text-gray-7 text-[16px]"> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={(e) => openModal('POLICY', undefined, e)} | |
| className="text-gray-7 text-[16px]" | |
| > | |
| 개인정보 처리방침 | |
| </Link> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={(e) => openModal('TERMS', undefined, e)} | |
| className="text-gray-7 cursor-pointer text-[16px]" | |
| > | |
| 이용약관 | |
| </button> | |
| <button | |
| type="button" | |
| onClick={(e) => openModal('POLICY', undefined, e)} | |
| className="text-gray-7 cursor-pointer text-[16px]" | |
| > | |
| 개인정보 처리방침 | |
| </button> |
🤖 Prompt for AI Agents
In `@components/footer.tsx` around lines 19 - 32, The two buttons that call
openModal('TERMS', ...) and openModal('POLICY', ...) in components/footer.tsx
are missing the cursor-pointer class; update their className strings (the button
elements using onClick={(e) => openModal('TERMS', ...)} and onClick={(e) =>
openModal('POLICY', ...)}) to include "cursor-pointer" so they visually indicate
clickability (match the commented feedback button's behavior).
| const pathCoordinates = | ||
| userRoute.stations && userRoute.stations.length > 0 | ||
| ? userRoute.stations.map((station) => ({ | ||
| lat: station.latitude + offsetVal, // 위도 이동 | ||
| lng: station.longitude + offsetVal, // 경도 이동 | ||
| })) | ||
| : [ | ||
| { lat: userRoute.latitude + offsetVal, lng: userRoute.longitude + offsetVal }, | ||
| { lat: endStation.latitude, lng: endStation.longitude }, | ||
| ]; | ||
|
|
||
| // 2. 출발 마커 좌표에도 오프셋 적용 | ||
| const markerPosition = { | ||
| lat: userRoute.latitude + offsetVal, | ||
| lng: userRoute.longitude + offsetVal, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the file and understand its structure
echo "=== File location and size ==="
find . -name "kakaoMapLine.tsx" -type f 2>/dev/null
# Show the full file to understand context
echo "=== Full file content ==="
cat -n components/map/kakaoMapLine.tsx | head -150Repository: SWYP-mingling/Frontend
Length of output: 6141
🏁 Script executed:
#!/bin/bash
# Search for UserRoute and endStation type definitions and usage
echo "=== Search for UserRoute type definition ==="
rg -n "interface UserRoute|type UserRoute" --type=ts --type=tsx -A10 2>/dev/null
echo "=== Search for endStation usage in the file ==="
rg -n "endStation" components/map/kakaoMapLine.tsx -B2 -A2 2>/dev/null
echo "=== Search for stations array usage patterns ==="
rg -n "\.stations" components/map/kakaoMapLine.tsx -B2 -A2 2>/dev/nullRepository: SWYP-mingling/Frontend
Length of output: 3287
🏁 Script executed:
#!/bin/bash
# Search for data sources and how userRoute and endStation are passed to this component
echo "=== Search for component props ==="
rg -n "interface.*Props|type.*Props" components/map/kakaoMapLine.tsx -A10 2>/dev/null
echo "=== Search for where this component is used ==="
rg -n "kakaoMapLine\|KakaoMapLine" -g "!node_modules" --type=ts --type=tsx -B2 -A2 2>/dev/null | head -50Repository: SWYP-mingling/Frontend
Length of output: 412
🏁 Script executed:
#!/bin/bash
# Search for where userRoutes data comes from and how stations is populated
echo "=== Search for userRoutes data source and stations population ==="
rg -n "stations" -g "!node_modules" --type=ts --type=tsx -B5 -A5 | grep -E "(stations|\.map\(|\.filter\(|API|fetch|response)" | head -80
echo "=== Search for UserRoute or route data construction ==="
rg -n "UserRoute" -g "!node_modules" --type=ts --type=tsx -B3 -A3 | head -100Repository: SWYP-mingling/Frontend
Length of output: 260
🏁 Script executed:
#!/bin/bash
# Look for API calls or data transformation related to routes
echo "=== Search for API endpoints returning routes ==="
rg -n "routes|stations" -g "!node_modules" -g "!*.test.*" --type=ts --type=tsx lib/ api/ 2>/dev/null | head -60
# Search for type definitions across the codebase
echo "=== Search for all UserRoute definitions ==="
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "UserRoute" | grep -v node_modules | head -10Repository: SWYP-mingling/Frontend
Length of output: 231
🏁 Script executed:
#!/bin/bash
# Find where kakaoMapLine is imported/used
echo "=== Components importing kakaoMapLine ==="
rg -l "kakaoMapLine\|KakaoMapLine" --type-list | head -20
find . -name "*.tsx" -o -name "*.ts" | xargs grep -l "kakaoMapLine\|KakaoMapLine" | grep -v node_modules
echo "=== Check transferModal to understand UserRoute usage ==="
cat -n ./components/modal/transferModal.tsx | head -100Repository: SWYP-mingling/Frontend
Length of output: 3726
🏁 Script executed:
#!/bin/bash
# Search for API calls and data fetching
echo "=== Search for API calls with route/station keywords ==="
find . -path ./node_modules -prune -o -name "*.ts" -o -name "*.tsx" | xargs grep -i "api.*route\|fetch.*route\|get.*route" 2>/dev/null | head -40
echo "=== Search for endpoint definitions ==="
find . -name "*.ts" -o -name "*.tsx" | xargs grep -E "(endpoint|url).*route\|stations" 2>/dev/null | grep -v node_modules | head -30Repository: SWYP-mingling/Frontend
Length of output: 263
🏁 Script executed:
#!/bin/bash
# Look at the structure more - search for pages using the map component
echo "=== Search for pages directory ==="
find . -type d -name "pages" -o -name "app" | grep -v node_modules
# Search for server actions or data fetching
echo "=== Search for userAction or route-related server functions ==="
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "getUserRoute\|fetchRoute\|route" 2>/dev/null | grep -v node_modules | head -15Repository: SWYP-mingling/Frontend
Length of output: 395
🏁 Script executed:
#!/bin/bash
# Check the result page where kakaoMapLine is used
echo "=== Result page component ==="
cat -n ./app/result/[id]/page.tsx | head -200Repository: SWYP-mingling/Frontend
Length of output: 7773
🏁 Script executed:
#!/bin/bash
# Search for where stations data comes from - look for API calls
echo "=== Search for fetch/axios calls that return stations ==="
rg -n "fetch\|axios" app/result/[id]/page.tsx -B2 -A10
echo "=== Search for any data transformation related to routes ==="
rg -n "\.stations\|stations\s*=" app/ -B3 -A3Repository: SWYP-mingling/Frontend
Length of output: 184
🏁 Script executed:
#!/bin/bash
# Find the useMidpoint hook to understand data source
echo "=== Find useMidpoint hook ==="
find . -name "*useMidpoint*" -o -name "*midpoint*" | grep -v node_modules
echo "=== Check the useMidpoint implementation ==="
cat -n ./hooks/api/query/useMidpoint.ts 2>/dev/null || cat -n ./hooks/api/query/useMidpoint.jsRepository: SWYP-mingling/Frontend
Length of output: 661
🏁 Script executed:
#!/bin/bash
# Search for the API endpoint that returns midpoint data
echo "=== Search for midpoint API ==="
rg -n "midpoint" hooks/ -B2 -A8 --type=ts --type=tsx
echo "=== Look for API service files ==="
find . -path ./node_modules -prune -o -name "*api*" -o -name "*service*" -type f -print | grep -v node_modules | head -20Repository: SWYP-mingling/Frontend
Length of output: 177
🏁 Script executed:
#!/bin/bash
# Find MidpointResponse type definition
echo "=== Search for MidpointResponse type ==="
find . -name "*.ts" | xargs grep -l "MidpointResponse" | grep -v node_modules
echo "=== Check types/api.ts ==="
cat -n ./types/api.ts | head -150Repository: SWYP-mingling/Frontend
Length of output: 3673
🏁 Script executed:
#!/bin/bash
# Also search for stations array definition in the API response
echo "=== Search for stations in types ==="
grep -r "stations" types/ 2>/dev/null || echo "No types directory found"
echo "=== Look for any README or documentation about the API ==="
find . -name "README*" -o -name "*.md" | grep -v node_modules | head -5Repository: SWYP-mingling/Frontend
Length of output: 237
stations 배열 사용 시 도착역이 polyline 경로에서 누락되는 버그.
stations 배열이 존재할 때와 없을 때 경로 구성이 불일치합니다:
- stations 배열이 있을 때 (현재): pathCoordinates가 stations만 포함 → endStation 누락
- stations 배열이 없을 때: pathCoordinates에 시작점과 endStation 모두 포함
결과적으로 경로 전환이 있는 경우(stations 배열 존재), polyline이 도착역 마커까지 도달하지 않아 시각적 간격이 생깁니다.
🔧 endStation을 pathCoordinates 끝에 추가하는 수정안
const pathCoordinates =
userRoute.stations && userRoute.stations.length > 0
? userRoute.stations.map((station) => ({
lat: station.latitude + offsetVal,
lng: station.longitude + offsetVal,
}))
+ .concat([{ lat: endStation.latitude, lng: endStation.longitude }])
: [
{ lat: userRoute.latitude + offsetVal, lng: userRoute.longitude + offsetVal },
{ lat: endStation.latitude, lng: endStation.longitude },
];📝 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.
| const pathCoordinates = | |
| userRoute.stations && userRoute.stations.length > 0 | |
| ? userRoute.stations.map((station) => ({ | |
| lat: station.latitude + offsetVal, // 위도 이동 | |
| lng: station.longitude + offsetVal, // 경도 이동 | |
| })) | |
| : [ | |
| { lat: userRoute.latitude + offsetVal, lng: userRoute.longitude + offsetVal }, | |
| { lat: endStation.latitude, lng: endStation.longitude }, | |
| ]; | |
| // 2. 출발 마커 좌표에도 오프셋 적용 | |
| const markerPosition = { | |
| lat: userRoute.latitude + offsetVal, | |
| lng: userRoute.longitude + offsetVal, | |
| }; | |
| const pathCoordinates = | |
| userRoute.stations && userRoute.stations.length > 0 | |
| ? userRoute.stations.map((station) => ({ | |
| lat: station.latitude + offsetVal, // 위도 이동 | |
| lng: station.longitude + offsetVal, // 경도 이동 | |
| })) | |
| .concat([{ lat: endStation.latitude, lng: endStation.longitude }]) | |
| : [ | |
| { lat: userRoute.latitude + offsetVal, lng: userRoute.longitude + offsetVal }, | |
| { lat: endStation.latitude, lng: endStation.longitude }, | |
| ]; | |
| // 2. 출발 마커 좌표에도 오프셋 적용 | |
| const markerPosition = { | |
| lat: userRoute.latitude + offsetVal, | |
| lng: userRoute.longitude + offsetVal, | |
| }; |
🤖 Prompt for AI Agents
In `@components/map/kakaoMapLine.tsx` around lines 115 - 130, When building
pathCoordinates, if userRoute.stations exists you currently map only the
stations and omit endStation, causing the polyline to stop short; update the
logic in the pathCoordinates construction (the branch that maps
userRoute.stations) to append the endStation coordinate as the final point
(apply offsetVal to endStation.latitude and endStation.longitude for consistency
with the other branch), so pathCoordinates becomes [...mappedStations, { lat:
endStation.latitude + offsetVal, lng: endStation.longitude + offsetVal }];
markerPosition (userRoute.latitude/longitude + offsetVal) can remain unchanged.
| // 겹침 방지를 위해 평소에는 불투명하게, | ||
| // 그래도 겹친다면 구분되도록 0.8 정도로 설정 | ||
| strokeOpacity={1} |
There was a problem hiding this comment.
주석과 실제 strokeOpacity 값이 불일치합니다.
주석에는 "0.8 정도로 설정"이라고 되어 있지만 실제 값은 1입니다. 의도한 값으로 통일해 주세요.
🤖 Prompt for AI Agents
In `@components/map/kakaoMapLine.tsx` around lines 139 - 141, The comment and the
actual stroke opacity disagree: update the strokeOpacity prop used in the
kakaoMap Line/Polyline render (the JSX prop named strokeOpacity in
components/map/kakaoMapLine.tsx) to the intended value of 0.8 and/or adjust the
comment so both match; specifically set strokeOpacity={0.8} where the prop is
currently strokeOpacity={1} (or change the comment to reflect 1) so comment and
implementation are consistent.
| <div | ||
| className="group flex cursor-pointer flex-col items-center" | ||
| style={{ transform: 'translateY(-30px)' }} | ||
| className="group relative flex cursor-pointer flex-col items-center" | ||
| onMouseEnter={() => setHoveredUserId(userRoute.nickname)} | ||
| onMouseLeave={() => setHoveredUserId(null)} | ||
| > | ||
| {/* 말풍선 */} | ||
| <div className="bg-gray-9 relative mb-1 flex flex-col items-center rounded px-4.5 py-1.25 shadow-lg transition-transform group-hover:-translate-y-1"> | ||
| <span className="mb-1 text-[11px] whitespace-nowrap text-white"> | ||
| {userRoute.startStation}에서 | ||
| </span> | ||
| <span className="text-blue-2 text-sm font-semibold whitespace-nowrap"> | ||
| {userRoute.travelTime}분 | ||
| <div | ||
| className={`absolute bottom-full mb-2 flex flex-col items-center rounded bg-gray-900 px-3 py-1 shadow-lg transition-all duration-200 ${ | ||
| isHovered | ||
| ? 'translate-y-0 opacity-100' | ||
| : 'pointer-events-none translate-y-2 opacity-0' | ||
| }`} | ||
| > | ||
| <span className="text-xs whitespace-nowrap text-white"> | ||
| {userRoute.startStation} ({userRoute.travelTime}분) | ||
| </span> | ||
| <div className="bg-gray-9 absolute -bottom-1 h-2 w-2 rotate-45 transform"></div> | ||
| <div className="absolute -bottom-1 h-2 w-2 rotate-45 bg-gray-900"></div> | ||
| </div> |
There was a problem hiding this comment.
모바일에서 호버 툴팁이 표시되지 않습니다.
onMouseEnter/onMouseLeave는 터치 디바이스에서 안정적으로 동작하지 않아, 모바일 사용자는 출발역과 이동시간 정보를 확인할 수 없습니다. onClick 또는 onTouchStart로 토글하는 방식을 추가하면 모바일 접근성이 개선됩니다.
🤖 Prompt for AI Agents
In `@components/map/kakaoMapLine.tsx` around lines 151 - 167, The tooltip only
toggles via onMouseEnter/onMouseLeave so touch devices never open it; update the
component that renders the group (the div with onMouseEnter/onMouseLeave) to
also handle touch/click by adding an onClick and onTouchStart handler that
toggles the hovered state via setHoveredUserId (use userRoute.nickname to open
and null to close), ensure the handlers respect the existing isHovered logic (so
the className conditional still works) and debounce or ignore redundant events
if needed to avoid immediately closing on mobile; keep tooltip markup
(userRoute.startStation and userRoute.travelTime) unchanged so it displays when
setHoveredUserId matches.
| experimental: { | ||
| // 기본값이 30초~60초 정도인데, 이를 120초(120,000ms)로 늘립니다. | ||
| proxyTimeout: 120000, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Next.js version and verify proxyTimeout option exists
echo "=== Checking package.json for Next.js version ==="
if [ -f "package.json" ]; then
cat package.json | grep -A 2 '"next"'
else
echo "package.json not found"
fi
echo ""
echo "=== Searching for proxyTimeout in Next.js configuration types ==="
# Look for type definitions
if [ -d "node_modules/next" ]; then
echo "Checking dist files..."
find node_modules/next -type f \( -name "*.d.ts" -o -name "config*.js" \) 2>/dev/null | head -20
echo ""
echo "Searching for 'proxyTimeout' in Next.js files..."
rg "proxyTimeout" node_modules/next --max-count=20 2>/dev/null || echo "No matches found for proxyTimeout"
else
echo "node_modules/next not found"
fi
echo ""
echo "=== Checking Next.js version file ==="
if [ -f "node_modules/next/package.json" ]; then
cat node_modules/next/package.json | grep -E '"version"|"name"'
fiRepository: SWYP-mingling/Frontend
Length of output: 50380
🌐 Web query:
Next.js 16.1.6 experimental proxyTimeout configuration option
💡 Result:
In Next.js 16.1.6, there is no supported experimental.proxyTimeout option in next.config.(js|ts).
- The
experimental.proxyTimeoutconfig did exist historically (added in PR #40289) as a number (ms) underexperimental.proxyTimeout. [1] - That option was removed later (the Next.js team/community notes it was “previously possible” but “has been removed in subsequent changes”). [2]
- Today, when Next.js performs rewrite proxying, it uses an internal
http-proxytimeout that’s hard-limited to ~30 seconds (and WebSocket proxying is not timed out). [3][4]
If you need a different timeout in 16.1.6, the practical workaround is to avoid rewrite-based proxying for that path and instead proxy via your own server/API route (where you control timeouts) rather than relying on Next.js’s built-in rewrite proxy. [3][4]
Sources: [1] [2] [3] [4]
🌐 Web query:
Next.js proxyTimeout removed issue #62869``
💡 Result:
Issue vercel/next.js #62869 reports that when you use rewrites via Middleware/Proxy and the upstream stalls, Next.js will keep the proxied request open for a relatively long time (commonly observed as ~30s), which can cause resource buildup/OOM under load. The issue notes that a configurable proxyTimeout existed briefly (added as an experimental option in #40289) but was removed later, leaving no supported way to tune this timeout via next.config.* today. [1]
Related context:
- A long-running request timing out around 30 seconds when using
rewrites()has been a known limitation/complaint since at least Discussion#36598. [2] - Next.js’ proxy implementation has been documented/quoted in discussions as defaulting to a 30s
proxyTimeoutbehavior (with special-casing for websockets). [3] - Current Next.js Proxy-related config docs list things like
experimental.proxyClientMaxBodySize, but do not document anyproxyTimeoutoption, consistent with it having been removed. [4]
What to do instead (practical workaround): if you need strict upstream timeouts, put a real reverse proxy in front (nginx/Envoy/HAProxy/Vercel/Cloudflare rules) or avoid rewrites-proxying for slow/unstable upstreams and call the upstream from your own handler (where you can enforce timeouts) rather than relying on Next’s internal proxy. [1][2]
Sources: [1] #62869, [2] #36598, [3] #49546, [4] Next.js docs experimental.proxyClientMaxBodySize
experimental.proxyTimeout은 Next.js 16.1.6에서 작동하지 않습니다.
proxyTimeout은 타입 정의에는 존재하지만, PR #40289 이후 기능이 제거되어 현재 Next.js에서는 지원되지 않습니다. 이 설정은 무시되며 rewrites를 통한 프록시 요청은 여전히 기본 ~30초 타임아웃이 적용됩니다.
긴 API 호출의 타임아웃을 늘리려면 다음 중 하나를 사용하세요:
- Next.js API Route (Route Handler)에서 직접
fetchtimeout 제어 - Node.js 수준에서
NODE_OPTIONS='--http-server-default-timeout=120000'설정
🤖 Prompt for AI Agents
In `@next.config.ts` around lines 5 - 8, Remove the unused
experimental.proxyTimeout setting from next.config (the
experimental.proxyTimeout key is ignored in current Next.js versions) and
instead implement one of the supported approaches: control request timeouts
inside your Route Handlers / API Routes by adding timeout logic around fetch
calls, or set the Node HTTP server timeout globally via
NODE_OPTIONS='--http-server-default-timeout=120000'; update any comments to
explain the chosen approach and delete the proxyTimeout property from the
config.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/recommend/page.tsx`:
- Around line 24-32: The current useMemo block reading localStorage
(meetingCategory) is SSR-unsafe and misuses useMemo; replace it with a
client-only state pattern: create a useState for meetingCategory and populate it
inside a useEffect that reads
localStorage.getItem(`meeting_${meetingId}_category`) (guarding on meetingId) so
the read happens only on mount, and ensure that when no key exists you set a
sensible fallback (e.g., undefined or null) rather than '', so the dependent
hook useRecommend (its enabled check !!category) can behave correctly; update
any references to meetingCategory accordingly.
🧹 Nitpick comments (1)
app/recommend/page.tsx (1)
12-13: 문자열 리터럴 스타일이 인접 라인과 불일치합니다.Line 12는 템플릿 리터럴(
`)을, Line 13은 단일 따옴표(')를 사용합니다. 보간이 필요 없는 단순 문자열이므로 스타일을 통일하는 것이 좋습니다.- const meetingId = searchParams.get(`meetingId`) || ''; + const meetingId = searchParams.get('meetingId') || '';
| // 모임 카테고리 가져오기 (localStorage에서 캐싱된 값 사용) | ||
| const meetingCategory = useMemo(() => { | ||
| const cachedCategory = localStorage.getItem(`meeting_${meetingId}_category`); | ||
| return cachedCategory || ''; | ||
| }); | ||
| if (cachedCategory) { | ||
| return cachedCategory; | ||
| } | ||
|
|
||
| // 카테고리 변경 핸들러 | ||
| const handleCategoryChange = (category: string) => { | ||
| setSelectedCategory(category); | ||
| setSelectedPlaceId(1); // 카테고리 변경 시 첫 번째 장소 선택 | ||
| }; | ||
| return ''; | ||
| }, [meetingId]); |
There was a problem hiding this comment.
useMemo 내에서 localStorage 접근은 SSR 안전성 문제가 있습니다.
localStorage는 브라우저 전용 API입니다. 현재 Suspense + useSearchParams 조합으로 클라이언트 렌더링이 보장될 수 있지만, 이 의존성은 암묵적이고 깨지기 쉽습니다. 컴포넌트가 다른 곳으로 이동하거나 Suspense 래퍼가 제거되면 SSR 시 ReferenceError가 발생합니다.
또한 useMemo는 순수 계산용이며, 외부 저장소 읽기에는 useState + useEffect 또는 useSyncExternalStore가 더 적절합니다.
추가로, localStorage에 해당 키가 없으면 meetingCategory가 빈 문자열이 되어 useRecommend의 enabled 조건(!!category)이 false가 됩니다. 이 경우 사용자는 "추천 장소가 없습니다"를 보게 되며 복구 방법이 없습니다.
🔧 useState + useEffect 패턴으로 수정 제안
- // 모임 카테고리 가져오기 (localStorage에서 캐싱된 값 사용)
- const meetingCategory = useMemo(() => {
- const cachedCategory = localStorage.getItem(`meeting_${meetingId}_category`);
- if (cachedCategory) {
- return cachedCategory;
- }
-
- return '';
- }, [meetingId]);
+ // 모임 카테고리 가져오기 (localStorage에서 캐싱된 값 사용)
+ const [meetingCategory, setMeetingCategory] = useState('');
+
+ useEffect(() => {
+ const cachedCategory = localStorage.getItem(`meeting_${meetingId}_category`);
+ if (cachedCategory) {
+ setMeetingCategory(cachedCategory);
+ }
+ }, [meetingId]);useEffect를 사용하면 SSR 안전성이 보장되고, localStorage 접근이 클라이언트 마운트 이후에만 발생합니다.
📝 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.
| // 모임 카테고리 가져오기 (localStorage에서 캐싱된 값 사용) | |
| const meetingCategory = useMemo(() => { | |
| const cachedCategory = localStorage.getItem(`meeting_${meetingId}_category`); | |
| return cachedCategory || ''; | |
| }); | |
| if (cachedCategory) { | |
| return cachedCategory; | |
| } | |
| // 카테고리 변경 핸들러 | |
| const handleCategoryChange = (category: string) => { | |
| setSelectedCategory(category); | |
| setSelectedPlaceId(1); // 카테고리 변경 시 첫 번째 장소 선택 | |
| }; | |
| return ''; | |
| }, [meetingId]); | |
| // 모임 카테고리 가져오기 (localStorage에서 캐싱된 값 사용) | |
| const [meetingCategory, setMeetingCategory] = useState(''); | |
| useEffect(() => { | |
| const cachedCategory = localStorage.getItem(`meeting_${meetingId}_category`); | |
| if (cachedCategory) { | |
| setMeetingCategory(cachedCategory); | |
| } | |
| }, [meetingId]); |
🤖 Prompt for AI Agents
In `@app/recommend/page.tsx` around lines 24 - 32, The current useMemo block
reading localStorage (meetingCategory) is SSR-unsafe and misuses useMemo;
replace it with a client-only state pattern: create a useState for
meetingCategory and populate it inside a useEffect that reads
localStorage.getItem(`meeting_${meetingId}_category`) (guarding on meetingId) so
the read happens only on mount, and ensure that when no key exists you set a
sensible fallback (e.g., undefined or null) rather than '', so the dependent
hook useRecommend (its enabled check !!category) can behave correctly; update
any references to meetingCategory accordingly.
…ing/Frontend into refactor/station-list
🚀 refactor: 8호선 데이터, 이용약관/개인정보처리방침 추가 및 지도 컴포넌트 리팩토링
📝 변경사항
지하철 8호선 연장 데이터 추가: 별내선 등 8호선 연장 구간에 대한 역 데이터를 추가했습니다.
이용약관 및 개인정보처리방침 추가: 모달 내에 서비스 이용약관 및 개인정보처리방침 전문을 추가했습니다.
지도 경로 시각화 개선: 출발지에서 도착지까지 일직선으로 그어지던 경로를, 경유하는 모든 역 좌표를 잇는 꺾은선(Polyline) 형태로 수정하여 실제 이동 경로처럼 보이도록 개선했습니다.
지도 레이아웃 버그 수정: 모바일 환경에서 지도 컴포넌트가 화면 너비를 초과하여 레이아웃이 깨지는(가로 스크롤 발생) 문제를 해결했습니다.
이동시간 렌더링 수정: 결과 리스트에서 특정 사용자의 travelTime이 올바르게 표시되지 않던 로직을 수정했습니다.
API 타임아웃 연장: 중간 지점 계산 로직의 소요 시간을 고려하여, 프론트엔드 프록시 및 API 응답 대기 시간을 기존 30초에서 **2분(120초)**으로 연장했습니다.
✅ 체크리스트
📸 스크린샷
💬 리뷰어 전달사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항