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
16 changes: 15 additions & 1 deletion app/meeting/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { useToast } from '@/hooks/useToast';
import Toast from '@/components/ui/toast';
import { getMeetingUserId, removeMeetingUserId } from '@/lib/storage';
import { sendGAEvent } from '@next/third-parties/google';

interface StationInfo {
line: string;
name: string;
Expand Down Expand Up @@ -170,6 +169,21 @@ export default function Page() {
show();
return;
}

if (typeof window !== 'undefined') {
const calculationType = id ? 'recalculated' : 'first';
const isHost = localStorage.getItem(`is_host_${id}`) === 'true';
const userRole = isHost ? 'host' : 'participant';
const browserId = localStorage.getItem('browser_id');

sendGAEvent('event', 'midpoint_calculated', {
meeting_url_id: id,
browser_id: browserId,
role: userRole,
calculation_type: calculationType,
});
}
Comment on lines +173 to +185
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

calculation_type가 사실상 항상 recalculated로 전송됩니다.

Line 174는 id 존재 여부로 분기하고 있는데, /meeting/[id] 경로에서는 id가 항상 존재해 first가 전송되지 않습니다. 그리고 Line 177은 browser_id가 없을 때 null이 전송될 수 있습니다.

수정 제안
-    if (typeof window !== 'undefined') {
-      const calculationType = id ? 'recalculated' : 'first';
+    if (typeof window !== 'undefined' && id) {
+      const calcStateKey = `has_midpoint_calculated_${id}`;
+      const calculationType = sessionStorage.getItem(calcStateKey) ? 'recalculated' : 'first';
       const isHost = localStorage.getItem(`is_host_${id}`) === 'true';
       const userRole = isHost ? 'host' : 'participant';
-      const browserId = localStorage.getItem('browser_id');
+      let browserId = localStorage.getItem('browser_id');
+      if (!browserId) {
+        browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
+        localStorage.setItem('browser_id', browserId);
+      }

       sendGAEvent('event', 'midpoint_calculated', {
         meeting_url_id: id,
         browser_id: browserId,
         role: userRole,
         calculation_type: calculationType,
       });
+      sessionStorage.setItem(calcStateKey, '1');
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/meeting/`[id]/page.tsx around lines 173 - 185, The calculation_type logic
is wrong because id is always present on /meeting/[id], causing 'first' never to
be sent and browser_id can be null; update the code around sendGAEvent so
calculationType is determined by a persistent flag (e.g., check
localStorage.getItem(`midpoint_calculated_${id}`) or similar) rather than
presence of id, set calculationType to 'first' if that flag is absent and
'recalculated' if present, then after sending mark that flag in localStorage
(localStorage.setItem(`midpoint_calculated_${id}`, 'true')); also ensure
browser_id is not sent as null by reading localStorage.getItem('browser_id') and
if missing either generate/store a new id or omit the browser_id field when
calling sendGAEvent so it never transmits null.


router.push(`/result/${id}`);
};

Expand Down
41 changes: 38 additions & 3 deletions app/recommend/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Image from 'next/image';
import KakaoMapRecommend from '@/components/map/kakaoMapRecommend';
import { useRecommend } from '@/hooks/api/query/useRecommend';
import { useCheckMeeting } from '@/hooks/api/query/useCheckMeeting';
import { sendGAEvent } from '@next/third-parties/google';

function RecommendContent() {
const router = useRouter();
Expand Down Expand Up @@ -139,15 +140,49 @@ function RecommendContent() {
router.back();
};

const handleOpenKakaoMap = (e: React.MouseEvent, placeUrl?: string) => {
const handleOpenKakaoMap = (
e: React.MouseEvent,
placeUrl?: string,
place?: (typeof places)[0]
) => {
e.stopPropagation();

// 카카오맵에서 보기 클릭 시 GA 전송 (external_map_opened)
if (typeof window !== 'undefined' && meetingId && place) {
const browserId = localStorage.getItem('browser_id');
const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
const userRole = isHost ? 'host' : 'participant';
const candidateId = `place_${String(place.id).padStart(2, '0')}`;

sendGAEvent('event', 'external_map_opened', {
meeting_url_id: meetingId,
user_cookie_id: browserId,
role: userRole,
candidate_id: candidateId,
});
}

if (placeUrl) {
window.open(placeUrl, '_blank', 'noopener,noreferrer');
} else {
window.open('https://map.kakao.com', '_blank', 'noopener,noreferrer');
}
};

// 장소 리스트 중 하나 클릭 시 GA 전송 (place_list_viewed)
const handlePlaceClick = (place: (typeof places)[0]) => {
setSelectedPlaceId(place.id);
if (meetingId) {
const candidateId = `place_${String(place.id).padStart(2, '0')}`;
sendGAEvent('event', 'place_list_viewed', {
meeting_url_id: meetingId,
candidate_id: candidateId,
place_category: place.category,
rank_order: place.id,
});
}
};

return (
<div className="flex items-center justify-center p-0 md:min-h-[calc(100vh-200px)] md:py-25">
<div className="flex h-full w-full flex-col bg-white md:h-175 md:w-174 md:flex-row md:gap-2 lg:w-215">
Expand Down Expand Up @@ -194,7 +229,7 @@ function RecommendContent() {
{places.map((place) => (
<div
key={place.id}
onClick={() => setSelectedPlaceId(place.id)}
onClick={() => handlePlaceClick(place)}
className={`flex cursor-pointer flex-col gap-2 rounded border p-4 ${
selectedPlaceId === place.id
? 'border-blue-5 border-2'
Expand Down Expand Up @@ -247,7 +282,7 @@ function RecommendContent() {
{/* 하단 버튼은 조건부 렌더링 */}
{selectedPlaceId === place.id ? (
<button
onClick={(e) => handleOpenKakaoMap(e, place.placeUrl)}
onClick={(e) => handleOpenKakaoMap(e, place.placeUrl, place)}
className="bg-gray-8 w-full rounded py-2 text-[15px] text-white"
>
카카오맵에서 보기
Expand Down
40 changes: 39 additions & 1 deletion app/result/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,37 @@ export default function Page() {

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

// 중간지점 후보 조회 GA 이벤트
const trackMidpointCandidateViewed = useCallback(
(candidateRankOrder: number, candidateId: string) => {
if (typeof window === 'undefined' || !id) return;
const browserId = localStorage.getItem('browser_id');
const isHost = localStorage.getItem(`is_host_${id}`) === 'true';
const userRole = isHost ? 'host' : 'participant';

sendGAEvent('event', 'midpoint_candidate_viewed', {
meeting_url_id: id,
user_cookie_id: browserId,
role: userRole,
candidate_rank_order: candidateRankOrder,
candidate_id: candidateId,
});
},
[id]
);

// 장소 리스트에서 결과보기 페이지로 돌아왔을 때 midpoint_candidate_viewed 전송
useEffect(() => {
if (typeof window === 'undefined' || !id || locationResults.length === 0) return;
const fromRecommend = sessionStorage.getItem(`from_recommend_${id}`);
if (fromRecommend !== '1') return;

sessionStorage.removeItem(`from_recommend_${id}`);
const selected = locationResults.find((r) => r.id === selectedResultId) ?? locationResults[0];
const candidateId = `mid_${selected.endStation.replace(/\s+/g, '_')}`;
trackMidpointCandidateViewed(selected.id, candidateId);
}, [id, locationResults, selectedResultId, trackMidpointCandidateViewed]);
Comment on lines +171 to +181
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

추천 페이지 복귀 시 잘못된 후보가 집계될 수 있습니다.

Line 315에서 '1'만 저장하면, Line 178에서 selectedResultId 기본값(1)을 기준으로 후보를 선택하게 되어 실제로 추천 페이지로 이동했던 후보와 다른 candidate_id가 전송될 수 있습니다.

수정 제안
-    const fromRecommend = sessionStorage.getItem(`from_recommend_${id}`);
-    if (fromRecommend !== '1') return;
+    const fromRecommendRank = sessionStorage.getItem(`from_recommend_${id}`);
+    if (!fromRecommendRank) return;

-    sessionStorage.removeItem(`from_recommend_${id}`);
-    const selected = locationResults.find((r) => r.id === selectedResultId) ?? locationResults[0];
+    const parsedRank = Number(fromRecommendRank);
+    sessionStorage.removeItem(`from_recommend_${id}`);
+    const selected =
+      locationResults.find((r) => r.id === parsedRank) ??
+      locationResults.find((r) => r.id === selectedResultId) ??
+      locationResults[0];
     const candidateId = `mid_${selected.endStation.replace(/\s+/g, '_')}`;
     trackMidpointCandidateViewed(selected.id, candidateId);
-                          if (typeof window !== 'undefined') {
-                            sessionStorage.setItem(`from_recommend_${id}`, '1');
-                          }
+                          if (typeof window !== 'undefined') {
+                            sessionStorage.setItem(`from_recommend_${id}`, String(result.id));
+                          }

Also applies to: 314-316

🤖 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 171 - 181, The useEffect currently
treats sessionStorage.getItem(`from_recommend_${id}`) as a boolean flag ('1')
which can cause the wrong candidate to be picked; update the effect to read and
parse the stored value as a selectedResultId (supporting formats set by the
redirect logic: e.g. a plain numeric id or a prefix like "1:<id>"), then remove
the key and pick the candidate by that parsed id (falling back to the existing
selectedResultId or locationResults[0] if parsing fails) before computing
candidateId and calling trackMidpointCandidateViewed; reference useEffect,
sessionStorage.getItem(`from_recommend_${id}`), selectedResultId,
locationResults, and trackMidpointCandidateViewed to locate where to change.


// 뒤로 가기 클릭 시 캐시 데이터 무효화
const clearRelatedCache = useCallback(() => {
queryClient.removeQueries({ queryKey: ['midpoint', id] });
Expand Down Expand Up @@ -280,13 +311,20 @@ export default function Page() {
if (meetingType) params.append('meetingType', meetingType);
if (categoryParam) params.append('category', categoryParam);

if (typeof window !== 'undefined') {
sessionStorage.setItem(`from_recommend_${id}`, '1');
}
router.push(`/recommend?${params.toString()}`);
};

return (
<div
key={result.id}
onClick={() => setSelectedResultId(result.id)}
onClick={() => {
setSelectedResultId(result.id);
const candidateId = `mid_${result.endStation.replace(/\s+/g, '_')}`;
trackMidpointCandidateViewed(result.id, candidateId);
}}
className={`flex cursor-pointer flex-col gap-3.75 rounded border bg-white p-5 ${
selectedResultId === result.id
? 'border-blue-5 border-2'
Expand Down
Loading