Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
147 changes: 54 additions & 93 deletions app/result/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState, useMemo } from 'react';
import { useState, useMemo, useEffect, useCallback } from 'react';
import Image from 'next/image';
import { useOpenModal } from '@/hooks/useOpenModal';
import { useParams, useRouter } from 'next/navigation';
Expand Down Expand Up @@ -44,10 +44,13 @@ export default function Page() {

const myRoute = routesWithColor.find((route) => route.nickname === myNickname);
const travelTime = myRoute?.travelTime || 0;


const totalTravelTime = routesWithColor.reduce((sum, route) => sum + (route.travelTime || 0), 0);
const averageTravelTime = routesWithColor.length > 0 ? Math.round(totalTravelTime / routesWithColor.length) : 0;

const totalTravelTime = routesWithColor.reduce(
(sum, route) => sum + (route.travelTime || 0),
0
);
const averageTravelTime =
routesWithColor.length > 0 ? Math.round(totalTravelTime / routesWithColor.length) : 0;

const extractLineNumber = (linenumber: string): string => {
if (!linenumber) return '';
Expand Down Expand Up @@ -107,82 +110,36 @@ export default function Page() {
};
});
}, [midpointData, myNickname, id]);

// 카테고리 텍스트 생성 함수
const getCategoryText = (category: string | undefined): string => {
if (!category) return '밍글링 추천 1위';
return `${category}이 많은 장소`;

// 카테고리 종성에 따라 "이/가"를 다르게 렌더링
const lastChar = category.charCodeAt(category.length - 1);
const hasJongseong = (lastChar - 0xac00) % 28 !== 0;
return `${category}${hasJongseong ? '이' : '가'} 많은 장소`;
Comment on lines +117 to +121
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

한글 종성 판별 시 비한글 문자에 대한 가드 누락

category의 마지막 글자가 한글 음절(U+AC00~U+D7AF) 범위가 아닌 경우(숫자, 영문, 특수문자 등), (lastChar - 0xAC00) % 28 계산이 잘못된 결과를 반환합니다. 예를 들어 카테고리가 "카페2"일 경우 "이"가 잘못 붙을 수 있습니다.

🛡️ 한글 범위 체크 추가 제안
     const lastChar = category.charCodeAt(category.length - 1);
-    const hasJongseong = (lastChar - 0xac00) % 28 !== 0;
+    const isKoreanSyllable = lastChar >= 0xac00 && lastChar <= 0xd7af;
+    const hasJongseong = isKoreanSyllable && (lastChar - 0xac00) % 28 !== 0;
     return `${category}${hasJongseong ? '이' : '가'} 많은 장소`;
📝 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
// 카테고리 종성에 따라 "이/가"를 다르게 렌더링
const lastChar = category.charCodeAt(category.length - 1);
const hasJongseong = (lastChar - 0xac00) % 28 !== 0;
return `${category}${hasJongseong ? '이' : '가'} 많은 장소`;
// 카테고리 종성에 따라 "이/가"를 다르게 렌더링
const lastChar = category.charCodeAt(category.length - 1);
const isKoreanSyllable = lastChar >= 0xac00 && lastChar <= 0xd7af;
const hasJongseong = isKoreanSyllable && (lastChar - 0xac00) % 28 !== 0;
return `${category}${hasJongseong ? '이' : '가'} 많은 장소`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/result/`[id]/page.tsx around lines 117 - 121, The current jongseong check
using lastChar/hasJongseong can produce wrong results for non-Hangul characters;
update the logic around category, lastChar and hasJongseong to: obtain the final
code point with category.codePointAt(category.length - 1), check it falls in the
Hangul Syllables range (0xAC00..0xD7AF) before performing (codePoint - 0xAC00) %
28, and default hasJongseong to false for non-Hangul so the fallback renders '가'
correctly; update the variables lastChar and hasJongseong in this block
accordingly (use codePointAt and an explicit range check).

};

const [selectedResultId, setSelectedResultId] = useState<number>(1);

const handleModifyStart = () => {
// 뒤로 가기 클릭 시 캐시 데이터 무효화
const clearRelatedCache = useCallback(() => {
queryClient.removeQueries({ queryKey: ['midpoint', id] });
queryClient.removeQueries({ queryKey: ['recommend', id] });
router.back();
};
}, [id, queryClient]);

const getLineColor = (fullLineName: string) => {
const cleaned = fullLineName.replace('호선', '').trim();
if (/^\d+$/.test(cleaned)) {
switch (cleaned) {
case '1':
return 'bg-[#004A85]';
case '2':
return 'bg-[#00A23F]';
case '3':
return 'bg-[#ED6C00]';
case '4':
return 'bg-[#009BCE]';
case '5':
return 'bg-[#794698]';
case '6':
return 'bg-[#7C4932]';
case '7':
return 'bg-[#6E7E31]';
case '8':
return 'bg-[#D11D70]';
case '9':
return 'bg-[#A49D87]';
default:
return 'bg-gray-400';
}
}
switch (fullLineName) {
case '우이신설선':
return 'bg-[#B0CE18]';
case '신림선':
return 'bg-[#5E7DBB]';
case '의정부경전철':
return 'bg-[#F0831E]';
case '용인에버라인':
return 'bg-[#44A436]';
case '인천2호선':
return 'bg-[#F4A462]';
case '김포골드라인':
return 'bg-[#F4A462]';
case '경의선':
case '경의중앙선':
return 'bg-[#6AC2B3]';
case '수인분당선':
return 'bg-[#ECA300]';
case '신분당선':
return 'bg-[#B81B30]';
case '인천1호선':
return 'bg-[#B4C7E7]';
case '공항철도':
return 'bg-[#0079AC]';
case '경춘선':
return 'bg-[#007A62]';
case '경강산':
return 'bg-[#0B318F]';
case '서해선':
return 'bg-[#5EAC41]';
default:
return 'bg-gray-400';
}
const handleModifyStart = () => {
clearRelatedCache();
router.replace(`/meeting/${id}`);
};

useEffect(() => {
clearRelatedCache();

return () => clearRelatedCache();
}, [clearRelatedCache]);
Comment on lines +137 to +141
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

마운트 시 removeQueries가 현재 컴포넌트의 데이터를 제거하여 불필요한 리패치 및 로딩 깜빡임 발생

clearRelatedCache가 마운트 시점에도 호출되면서 ['midpoint', id] 쿼리 데이터를 제거합니다. 이 컴포넌트가 useMidpoint(id)로 같은 쿼리를 구독 중이므로, 마운트 직후 데이터가 사라지고 → 로딩 상태 전환 → 리패치가 발생합니다. 정상적인 네비게이션(최초 진입)에서도 불필요한 리패치와 로딩 UI 깜빡임이 생깁니다.

뒤로가기 시 캐시 무효화가 목적이라면, 언마운트 시에만 캐시를 제거하면 충분합니다. 마운트 시에는 invalidateQueries를 사용하면 기존 데이터를 보여주면서 백그라운드 리패치가 가능합니다.

🔧 수정 제안: 마운트 시 invalidate, 언마운트 시 remove
  useEffect(() => {
-   clearRelatedCache();
-
-   return () => clearRelatedCache();
- }, [clearRelatedCache]);
+   // 마운트 시에는 백그라운드 리패치 (기존 데이터 유지)
+   queryClient.invalidateQueries({ queryKey: ['midpoint', id] });
+   queryClient.invalidateQueries({ queryKey: ['recommend', id] });
+
+   // 언마운트 시에는 캐시 완전 제거 (뒤로가기 캐시 무효화)
+   return () => {
+     queryClient.removeQueries({ queryKey: ['midpoint', id] });
+     queryClient.removeQueries({ queryKey: ['recommend', id] });
+   };
+ }, [id, queryClient]);

또는 마운트 시 캐시 무효화가 아예 필요 없다면 언마운트 cleanup만 남기세요:

useEffect(() => {
  return () => clearRelatedCache();
}, [clearRelatedCache]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/result/`[id]/page.tsx around lines 137 - 141, The effect currently calls
clearRelatedCache() on mount and unmount which removes the ['midpoint', id]
query and causes an immediate data drop and refetch; change the useEffect so
that on mount it calls queryClient.invalidateQueries(['midpoint', id]) (or a new
helper that calls invalidateQueries) to trigger a background refetch without
clearing existing data, and keep clearRelatedCache() (which performs
removeQueries) only in the cleanup function so removal happens only on unmount;
update the useEffect surrounding clearRelatedCache, and ensure this aligns with
the useMidpoint(id) subscription.


return (
<div className="flex items-center justify-center p-0 md:min-h-[calc(100vh-200px)] md:py-20">
<div className="flex h-full w-full flex-col bg-white md:h-175 md:w-174 md:flex-row md:gap-4 lg:w-215">
Expand All @@ -196,7 +153,7 @@ export default function Page() {
<div className="px-5 pt-5 md:p-0">
<div className="flex items-center justify-between">
<button
onClick={() => router.back()}
onClick={handleModifyStart}
className="flex items-center justify-center"
type="button"
>
Expand Down Expand Up @@ -248,41 +205,46 @@ export default function Page() {
</div>
) : (
locationResults.map((result) => {
const category = meetingData?.data?.purposes?.[meetingData.data.purposes.length - 1];
const category =
meetingData?.data?.purposes?.[meetingData.data.purposes.length - 1];
const categoryText = getCategoryText(category);

const handleRecommendClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (!id || !result.endStation) return;

let meetingType = '';
let categoryParam = '';

if (typeof window !== 'undefined') {
meetingType = localStorage.getItem(`meeting_${id}_meetingType`) || '';
categoryParam = localStorage.getItem(`meeting_${id}_category`) || '';
}

if (!meetingType && meetingData?.data?.purposes && meetingData.data.purposes.length > 0) {

if (
!meetingType &&
meetingData?.data?.purposes &&
meetingData.data.purposes.length > 0
) {
meetingType = meetingData.data.purposes[0];
}
if (!categoryParam && category) {
categoryParam = category;
}

const params = new URLSearchParams({
meetingId: id,
midPlace: result.endStation,
lat: result.latitude.toString(),
lng: result.longitude.toString(),
});

if (meetingType) params.append('meetingType', meetingType);
if (categoryParam) params.append('category', categoryParam);

router.push(`/recommend?${params.toString()}`);
};

return (
<div
key={result.id}
Expand All @@ -293,33 +255,31 @@ export default function Page() {
: 'border-gray-2 hover:bg-gray-1'
}`}
>

<div className="flex items-center gap-1.5">
<Image src='/icon/stars.svg' alt='stars' width={16} height={16} />
<span className="text-blue-5 text-sm font-medium">{categoryText}</span>
<Image src="/icon/stars.svg" alt="stars" width={16} height={16} />
<span className="text-blue-5 text-sm font-medium">
{categoryText}
</span>
</div>



<div className="flex items-center justify-between">
<span className="text-gray-10 text-xl font-bold">
{result.endStation}역
</span>
<div className="flex flex-col items-end ">
<span className="text-gray-6 text-[13px] font-regular">
<div className="flex flex-col items-end">
<span className="text-gray-6 text-[13px] font-normal">
평균 이동시간{' '}
<span className="text-blue-5 text-[18px] font-bold">
{result.averageTravelTime}분
</span>
</span>

</div>
</div>



<div className="flex gap-2">
<button
onClick={handleRecommendClick}
className="bg-gray-8 hover:bg-gray-9 flex-1 h-10 cursor-pointer rounded-[4px] text-[15px] font-normal text-white transition-colors"
className="bg-gray-8 hover:bg-gray-9 h-10 flex-1 cursor-pointer rounded-[4px] text-[15px] font-normal text-white transition-colors"
type="button"
>
주변 장소 추천
Expand All @@ -329,14 +289,15 @@ export default function Page() {
e.stopPropagation();
openModal(
'TRANSFER',
{ meetingId: id,
{
meetingId: id,
userRoutes: result.userRoutes,
endStation: result.endStation,
},
e
);
}}
className="bg-gray-1 hover:bg-gray-2 flex-1 h-10 cursor-pointer rounded-[4px] text-[15px] font-normal text-blue-5 transition-colors"
className="bg-gray-1 hover:bg-gray-2 text-blue-5 h-10 flex-1 cursor-pointer rounded-[4px] text-[15px] font-normal transition-colors"
type="button"
>
환승 경로 보기
Expand Down
3 changes: 1 addition & 2 deletions components/map/kakaoMapLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
map.setBounds(bounds);
}, [map, endStation, userRoutes]);

const handleRecommendClick = () => {

Check warning on line 78 in components/map/kakaoMapLine.tsx

View workflow job for this annotation

GitHub Actions / build

'handleRecommendClick' is assigned a value but never used
if (!meetingId || !endStation) {
router.push('/recommend');
return;
Expand Down Expand Up @@ -176,7 +176,7 @@
<CustomOverlayMap position={markerPosition} yAnchor={1} zIndex={30}>
<div className="flex flex-col items-center">
{/* 1. 상단 정보 말풍선 */}
<div className="relative mb-2 flex min-w-[80px] flex-col items-center justify-center rounded bg-[#2C2F36] px-3 py-2 shadow-lg">
<div className="relative mb-2 flex min-w-20 flex-col items-center justify-center rounded bg-[#2C2F36] px-3 py-2 shadow-lg">
<span className="text-[11px] leading-tight whitespace-nowrap text-white">
{userRoute.startStation}역에서
</span>
Expand All @@ -202,7 +202,6 @@
})}
</Map>


<ZoomControl map={map} />
</div>
);
Expand Down
44 changes: 17 additions & 27 deletions components/modal/transferModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,14 @@ export default function TransferModal({
// stations 배열에서 호선별 경로 추출 함수
const extractRouteSteps = (route: UserRoute) => {
const steps: Array<{ linenumber: string; station: string; isLast: boolean }> = [];

if (!route.stations || route.stations.length === 0) {
return steps;
}


let currentLine = route.stations[0]?.linenumber || '';

route.stations.forEach((station, index) => {

if (station.linenumber !== currentLine || index === route.stations.length - 1) {
if (currentLine) {
steps.push({
Expand All @@ -141,7 +139,6 @@ export default function TransferModal({
}
});


if (route.stations.length > 0) {
const lastStation = route.stations[route.stations.length - 1];
if (steps.length === 0 || steps[steps.length - 1].station !== lastStation.station) {
Expand All @@ -164,15 +161,14 @@ export default function TransferModal({
>
{/* 헤더 영역 */}
<DialogHeader className="gap-6 text-left">
<DialogTitle className="text-gray-10 text-[22px] font-semibold tracking-[-1.94px]">
<DialogTitle className="text-gray-10 text-[22px] font-semibold tracking-[-1.94%]">
모임원 환승경로 보기
</DialogTitle>
<DialogDescription className="text-blue-5 text-[20px] font-semibold tracking-[-1.2px]">
<DialogDescription className="text-blue-5 text-[20px] font-semibold tracking-[-1.2%]">
{endStation ? `${endStation}역 도착` : '도착역'}
</DialogDescription>
</DialogHeader>


<div className="flex flex-col gap-[12px] overflow-y-scroll">
{userRoutes.length === 0 ? (
<div className="flex items-center justify-center py-8">
Expand All @@ -187,58 +183,52 @@ export default function TransferModal({
key={index}
className="border-gray-2 relative flex flex-col rounded-[4px] border bg-white"
>

<div className="flex items-center justify-between py-3 border-b-gray-1 border-b mx-5">
<span className="text-gray-7 text-[14px] font-semibold leading-[1.571] tracking-[0.203px]">
<div className="border-b-gray-1 mx-5 flex items-center justify-between border-b py-3">
<span className="text-gray-7 text-[14px] leading-[1.571] font-semibold tracking-[0.203px]">
{route.nickname}
</span>
<div className="flex items-center gap-[7px]">
<span className="text-gray-6 text-[13px] font-normal leading-[1.385] tracking-[0.252px]">
<span className="text-gray-6 text-[13px] leading-[1.385] font-normal tracking-[0.252px]">
이동시간
</span>
<span className="text-blue-5 text-[18px] font-semibold leading-[1.445] tracking-[-0.0036px]">
<span className="text-blue-5 text-[18px] leading-[1.445] font-semibold tracking-[-0.0036px]">
{route.travelTime}분
</span>
</div>
</div>



{/* 하단: 환승 경로 (세로 배치) */}
<div className="relative flex gap-4 p-5">
<div className="flex flex-col gap-[10px] items-center">
<div className="flex flex-col items-center gap-[10px]">
{routeSteps.map((step, idx) => (
<div key={idx} className="flex flex-col items-center gap-[10px]">
<div
className={`flex min-w-[60px] items-center justify-center gap-1 rounded-[5px] px-[7px] py-[2px] text-[13px] font-normal leading-[1.385] tracking-[0.252px] text-white ${getLineBadgeStyle(
className={`flex min-w-[60px] items-center justify-center gap-1 rounded-[5px] px-[7px] py-[2px] text-[13px] leading-[1.385] font-normal tracking-[0.252px] text-white ${getLineBadgeStyle(
step.linenumber
)}`}
>
<Image src='/icon/train.svg' alt='train' width={12} height={12} />
<Image src="/icon/train.svg" alt="train" width={12} height={12} />
<span>{step.linenumber}</span>
</div>

<div className="flex items-center justify-center">
<Image src='/icon/down.svg' alt='arrow-down' width={12} height={12} />
<Image src="/icon/down.svg" alt="arrow-down" width={12} height={12} />
</div>
</div>
))}
<div className="bg-gray-8 flex min-w-[60px] items-center justify-center rounded-[5px] px-[7px] py-[2px] text-[13px] font-normal leading-[1.385] tracking-[0.252px] text-white">

<div className="bg-gray-8 flex min-w-[60px] items-center justify-center rounded-[5px] px-[7px] py-[2px] text-[13px] leading-[1.385] font-normal tracking-[0.252px] text-white">
하차
</div>
</div>


<div className="flex flex-col gap-[30px] text-gray-8 text-[13px] font-normal leading-[1.385] tracking-[0.252px]">
<div className="text-gray-8 flex flex-col gap-[30px] text-[13px] leading-[1.385] font-normal tracking-[0.252px]">
{routeSteps.map((step, idx) => (
<span key={idx}>{step.station}역</span>
))}

<span>{endStation || routeSteps[routeSteps.length - 1]?.station}역</span>
</div>


</div>
</div>
);
Expand Down
Loading