Skip to content
Open
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
15 changes: 15 additions & 0 deletions apps/web-user/src/app/auth/login/google/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { AUTH_ERROR_MESSAGES } from "@/apps/web-user/features/auth/constants/aut
import { PATHS } from "@/apps/web-user/common/constants/paths.constant";
import { useAlertStore } from "@/apps/web-user/common/store/alert.store";
import getApiMessage from "@/apps/web-user/common/utils/getApiMessage";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";
import { decodeJwtPayload } from "@/apps/web-user/features/auth/utils/jwt.util";
import { resolveSocialAuthFailReason } from "@/apps/web-user/features/auth/utils/social-auth-error.util";

/**
* 구글 OAuth 리다이렉트 콜백 — `code`로 `/v1/consumer/auth/google/login` 호출
Expand All @@ -22,13 +25,21 @@ function GoogleAuthCallbackContent() {
useEffect(() => {
const code = searchParams.get("code");
if (!code) {
// 구글 인증 화면에서 사용자가 취소한 경우 code 없이 리다이렉트됨
trackEvent("fail_social_auth", { provider: "google", fail_reason: "cancel" });
router.replace(PATHS.HOME);
return;
}

const run = async () => {
trackEvent("request_social_auth", { provider: "google" });

try {
const data = await authApi.googleLogin(code);
const userId = decodeJwtPayload<{ sub: string }>(data.accessToken)?.sub;
if (userId) {
trackEvent("success_login", { provider: "google", user_id: userId });
}
login(data.accessToken);
router.replace(PATHS.HOME);
} catch (error: unknown) {
Expand All @@ -49,6 +60,10 @@ function GoogleAuthCallbackContent() {
params.set("googleEmail", googleEmail);
router.replace(`${PATHS.AUTH.GOOGLE_REGISTER}?${params.toString()}`);
} else {
trackEvent("fail_social_auth", {
provider: "google",
fail_reason: resolveSocialAuthFailReason(error),
});
router.replace(PATHS.HOME);
showAlert({
type: "error",
Expand Down
15 changes: 15 additions & 0 deletions apps/web-user/src/app/auth/login/kakao/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { AUTH_ERROR_MESSAGES } from "@/apps/web-user/features/auth/constants/aut
import { PATHS } from "@/apps/web-user/common/constants/paths.constant";
import { useAlertStore } from "@/apps/web-user/common/store/alert.store";
import getApiMessage from "@/apps/web-user/common/utils/getApiMessage";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";
import { decodeJwtPayload } from "@/apps/web-user/features/auth/utils/jwt.util";
import { resolveSocialAuthFailReason } from "@/apps/web-user/features/auth/utils/social-auth-error.util";

function KakaoAuthCallbackContent() {
const router = useRouter();
Expand All @@ -18,13 +21,21 @@ function KakaoAuthCallbackContent() {
useEffect(() => {
const code = searchParams.get("code");
if (!code) {
// 카카오 인증 화면에서 사용자가 취소한 경우 code 없이 리다이렉트됨
trackEvent("fail_social_auth", { provider: "kakao", fail_reason: "cancel" });
router.replace(PATHS.HOME);
return;
}

const run = async () => {
trackEvent("request_social_auth", { provider: "kakao" });

try {
const data = await authApi.kakaoLogin(code);
const userId = decodeJwtPayload<{ sub: string }>(data.accessToken)?.sub;
if (userId) {
trackEvent("success_login", { provider: "kakao", user_id: userId });
}
login(data.accessToken);
router.replace(PATHS.HOME);
} catch (error: unknown) {
Expand All @@ -41,6 +52,10 @@ function KakaoAuthCallbackContent() {
params.set("kakaoEmail", kakaoEmail);
router.replace(`${PATHS.AUTH.KAKAO_REGISTER}?${params.toString()}`);
} else {
trackEvent("fail_social_auth", {
provider: "kakao",
fail_reason: resolveSocialAuthFailReason(error),
});
router.replace(PATHS.HOME);
showAlert({
type: "error",
Expand Down
68 changes: 65 additions & 3 deletions apps/web-user/src/app/map/MapPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import {
isStoreOpenOnSeoulCalendarDay,
storeCalendarOverlapsMapPickupHalfDay,
} from "@/apps/web-user/features/store/utils/store-business-calendar.util";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";
import type { MapAreaSearchTrigger } from "@/apps/web-user/common/types/analytics.type";

declare global {
interface Window {
Expand All @@ -86,6 +88,18 @@ export default function MapPageClient() {
const [pickupCalendarOpen, setPickupCalendarOpen] = useState(false);
const [mapListFilterPanelOpen, setMapListFilterPanelOpen] = useState(false);

// 지도 화면 노출
useEffect(() => {
trackEvent("view_map");
}, []);

// 마지막 지도 이동 트리거 (드래그/줌/현재위치) — success_map_area_search용
const lastMapGestureRef = useRef<MapAreaSearchTrigger | null>(null);
// 필터 적용 직후 다음 스토어 개수 갱신을 success_filter_apply로 처리하기 위한 플래그
const pendingFilterApplyRef = useRef(false);
// view_map_search_result 중복 발화 방지
const lastViewedMapSearchKeyRef = useRef<string | null>(null);

/** URL에 픽업이 있으면 상태에 반영 (검색 페이지 등에서 돌아올 때) */
useEffect(() => {
const hasPickupInUrl =
Expand All @@ -105,6 +119,10 @@ export default function MapPageClient() {

const handlePickupConfirm = useCallback(
(f: MapPickupFilter) => {
trackEvent("success_pickup_date_select", {
selected_date: f.date.toISOString(),
selected_time_slot: f.kind,
});
setPickupFilter(f);
applyPickupToUrl(f);
},
Expand Down Expand Up @@ -322,6 +340,7 @@ export default function MapPageClient() {
entries.set(store.id, entry);

window.kakao.maps.event.addListener(marker, "click", () => {
trackEvent("engage_store_pin", { store_id: entry.store.id });
if (markerImageRef.current)
kakaoMarkerEntriesRef.current.forEach((e) => e.marker.setImage(markerImageRef.current));
resetPlatformMarkerImages();
Expand Down Expand Up @@ -437,6 +456,7 @@ export default function MapPageClient() {
image: markerImageRef.current,
});
window.kakao.maps.event.addListener(marker, "click", () => {
trackEvent("engage_store_pin", { store_id: "none" });
setSelectedStore(null);
setSelectedUnenteredStore({
kakaoPlaceId: String(place.id ?? ""),
Expand Down Expand Up @@ -607,12 +627,14 @@ export default function MapPageClient() {

// 제스처(드래그·줌) 진행 여부 추적 — 제스처 중에는 마커 DOM 갱신을 전부 보류해 터치 끊김을 방지
window.kakao.maps.event.addListener(map, "dragstart", () => {
lastMapGestureRef.current = "drag";
isUserInteractingRef.current = true;
});
window.kakao.maps.event.addListener(map, "dragend", () => {
isUserInteractingRef.current = false;
});
window.kakao.maps.event.addListener(map, "zoom_start", () => {
lastMapGestureRef.current = "zoom";
isUserInteractingRef.current = true;
});
window.kakao.maps.event.addListener(map, "zoom_changed", () => {
Expand Down Expand Up @@ -640,6 +662,11 @@ export default function MapPageClient() {
applyKeywordSearchResultsRef.current(pending);
}
drawPlatformStoreMarkersRef.current();
trackEvent("success_map_area_search", {
trigger_type: lastMapGestureRef.current ?? "drag",
result_count: getStoresForListRef.current().length,
});
lastMapGestureRef.current = null;
const allowKakaoUnopened =
searchStoresRef.current === null &&
!searchQueryRef.current &&
Expand Down Expand Up @@ -730,9 +757,18 @@ export default function MapPageClient() {
) {
searchPlaces(map.getCenter());
}
// "내 위치로 이동" 버튼으로 명시적으로 재요청한 경우에만 재검색 완료 이벤트 전송
// (최초 진입 시 자동 중심 이동은 사용자 액션이 아니므로 제외)
if (lastMapGestureRef.current === "current_location") {
trackEvent("success_map_area_search", {
trigger_type: "current_location",
result_count: getStoresToShow(map).length,
});
lastMapGestureRef.current = null;
}
}
usedUserLocationForCenterRef.current = true;
}, [userLocation, searchQuery, pickupFilter, searchPlaces]);
}, [userLocation, searchQuery, pickupFilter, searchPlaces, getStoresToShow]);

// 현재위치 변경 또는 지도 준비 완료 시 현재위치 마커(점) 갱신
useEffect(() => {
Expand All @@ -756,6 +792,17 @@ export default function MapPageClient() {
if (listSheetPanelOffsetRef.current > 0) {
setListSheetStores(getStoresForList());
}
// 필터 적용 직후였다면, 새로 조회된 스토어 개수로 필터 적용 완료 이벤트 전송
if (pendingFilterApplyRef.current) {
pendingFilterApplyRef.current = false;
trackEvent("success_filter_apply", {
size_filter: listFilter.sizes?.join(",") || undefined,
price_min: listFilter.minPrice,
price_max: listFilter.maxPrice,
type_filter: listFilter.productCategoryTypes?.join(",") || undefined,
result_count: platformStores.length,
});
}
}, [
platformStores,
drawPlatformStoreMarkers,
Expand Down Expand Up @@ -793,6 +840,13 @@ export default function MapPageClient() {
const stores = filterStoresWithCoordinates(res.data ?? []);
if (cancelled) return;
searchStoresRef.current = stores;
if (lastViewedMapSearchKeyRef.current !== searchQuery) {
lastViewedMapSearchKeyRef.current = searchQuery;
trackEvent("view_map_search_result", {
keyword: searchQuery,
result_count: stores.length,
});
}
const map = mapInstanceRef.current;
if (!map || !window.kakao?.maps) return;
clearKakaoMarkers(); // 검색 모드 진입 시 기존 미입점 마커 제거
Expand Down Expand Up @@ -875,6 +929,8 @@ export default function MapPageClient() {
// ---- Handlers ----
/** 내 위치 버튼: 현재위치 재요청 후 다음 effect에서 지도 중심 이동 */
const handleRefreshLocation = () => {
trackEvent("engage_current_location");
lastMapGestureRef.current = "current_location";
usedUserLocationForCenterRef.current = false;
refreshUserLocation();
};
Expand Down Expand Up @@ -937,7 +993,10 @@ export default function MapPageClient() {
<MapTopSearchBar
searchQuery={searchQuery}
pickupFilter={pickupFilter}
onCalendarClick={() => setPickupCalendarOpen(true)}
onCalendarClick={() => {
trackEvent("engage_date_picker_open");
setPickupCalendarOpen(true);
}}
onPickupClear={handlePickupClear}
onSearchBackClick={() => router.push(buildMapSearchUrl(pickupFilter))}
onSearchEditClick={() =>
Expand Down Expand Up @@ -1039,7 +1098,10 @@ export default function MapPageClient() {
sortBy={listSortBy}
onSortByChange={setListSortBy}
listFilter={listFilter}
onListFilterChange={setListFilter}
onListFilterChange={(f) => {
pendingFilterApplyRef.current = true;
setListFilter(f);
}}
onFilterPanelOpenChange={setMapListFilterPanelOpen}
/>
)}
Expand Down
2 changes: 2 additions & 0 deletions apps/web-user/src/app/map/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
MAP_PICKUP_URL_PERIOD_KEY,
type MapPickupFilter,
} from "@/apps/web-user/features/store/utils/map.util";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";

const RECENT_SEARCHES_KEY = "recentSearches";
const MAX_RECENT = 10;
Expand Down Expand Up @@ -90,6 +91,7 @@ export default function MapSearchPage() {

const handleSearch = (term: string) => {
if (!term.trim()) return;
trackEvent("request_map_search", { keyword: term.trim() });
saveRecentSearch(term.trim());
setSearchTerm(term.trim());
router.push(buildMapPageUrl(term.trim(), pickupFilter));
Expand Down
11 changes: 11 additions & 0 deletions apps/web-user/src/app/mypage/order/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useEffect } from "react";
import Header from "@/apps/web-user/common/components/headers/Header";
import { Tabs } from "@/apps/web-user/common/components/tabs/Tabs";
import {
Expand All @@ -10,16 +11,26 @@ import {
PastOrderList,
usePastOrderCount,
} from "@/apps/web-user/features/mypage/order/components/PastOrderList";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";
import type { ReservationTabName } from "@/apps/web-user/common/types/analytics.type";

export default function MyOrdersPage() {
const upcomingCount = useUpcomingOrderCount();
const pastCount = usePastOrderCount();

// "내 예약" 메뉴 진입 - 전체 예약 목록 화면 노출
useEffect(() => {
trackEvent("view_reservation_list");
}, []);

return (
<div>
<Header variant="back-title" title="내 예약" />
<Tabs
defaultTab="upcoming"
onTabChange={(tabId) => {
trackEvent("engage_reservation_tab", { tab_name: tabId as ReservationTabName });
}}
tabs={[
{
id: "upcoming",
Expand Down
10 changes: 8 additions & 2 deletions apps/web-user/src/app/mypage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import { Icon } from "@/apps/web-user/common/components/icons";
import { PATHS } from "@/apps/web-user/common/constants/paths.constant";
Expand All @@ -16,6 +16,7 @@ import { useUpdateMypageProfile } from "@/apps/web-user/features/mypage/hooks/mu
import { Toast } from "@/apps/web-user/common/components/toast/Toast";
import { useLoginSheetStore } from "@/apps/web-user/common/store/login-sheet.store";
import { useScrollRestoration } from "@/apps/web-user/common/hooks/useScrollRestoration";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";

function getLoginInfo(user: {
googleId: string;
Expand Down Expand Up @@ -58,6 +59,11 @@ const TERMS_MENU = [
] as const;

export default function MypagePage() {
// 마이페이지 메인 화면 노출
useEffect(() => {
trackEvent("view_mypage");
}, []);

const { isAuthenticated } = useAuthStore();
const hasHydrated = useAuthHasHydrated();
// 메뉴 클릭 후 뒤로가기 시 클릭했던 스크롤 위치로 복원 (레이아웃이 정해지는 hydration 이후 활성화)
Expand Down Expand Up @@ -174,7 +180,7 @@ export default function MypagePage() {
<p className="text-sm text-gray-700">더욱 편리한 이용을 위해</p>
<button
type="button"
onClick={openLoginSheet}
onClick={() => openLoginSheet("mypage")}
className="py-[10px] px-5 text-sm font-bold text-white bg-primary rounded-lg"
>
로그인 / 회원가입
Expand Down
8 changes: 8 additions & 0 deletions apps/web-user/src/app/mypage/reviews/list/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import Header from "@/apps/web-user/common/components/headers/Header";
import { useWritableReviews } from "@/apps/web-user/features/review/hooks/queries/useWritableReviews";
import type { WritableReviewOrder } from "@/apps/web-user/features/review/types/review.type";
import { EmptyState } from "@/apps/web-user/common/components/fallbacks/EmptyState";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";

const DAY_NAMES = ["일", "월", "화", "수", "목", "금", "토"];

Expand All @@ -25,6 +27,11 @@ function getProductSummary(order: WritableReviewOrder) {
}

export default function ReviewListPage() {
// 내 후기 > 작성후기선택 페이지
useEffect(() => {
trackEvent("view_review_option_list");
}, []);

const { data } = useWritableReviews();
const orders = data?.data ?? [];

Expand Down Expand Up @@ -59,6 +66,7 @@ export default function ReviewListPage() {
{/* 후기 작성 버튼 */}
<Link
href={`/mypage/reviews/write?orderId=${order.id}`}
onClick={() => trackEvent("engage_review_write", { reservation_id: order.id })}
className="flex-shrink-0 flex items-center justify-center bg-primary text-white text-sm font-bold w-24 h-10 rounded-lg"
>
후기 작성
Expand Down
6 changes: 6 additions & 0 deletions apps/web-user/src/app/mypage/reviews/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import { Icon } from "@/apps/web-user/common/components/icons";
import { PATHS } from "@/apps/web-user/common/constants/paths.constant";
import { useWritableReviews } from "@/apps/web-user/features/review/hooks/queries/useWritableReviews";
import { EmptyState } from "@/apps/web-user/common/components/fallbacks/EmptyState";
import { trackEvent } from "@/apps/web-user/common/utils/analytics.util";

export default function MyReviewsPage() {
// 내 후기 페이지
useEffect(() => {
trackEvent("view_review_list");
}, []);

const [sortBy] = useState<ReviewSortBy>(ReviewSortBy.LATEST);
const { data } = useMyReviews({ sortBy });
const { mutate: deleteReview } = useDeleteMyReview();
Expand Down
Loading
Loading