From b7ee94921e707719ea12e810d3ff5b75ac86a036 Mon Sep 17 00:00:00 2001 From: tmin002 Date: Tue, 3 Feb 2026 17:01:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20baseurl=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/axiosClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apis/axiosClient.ts b/src/apis/axiosClient.ts index 280ae31..22c10f5 100644 --- a/src/apis/axiosClient.ts +++ b/src/apis/axiosClient.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { AuthError } from "./errors"; export const axiosClient = axios.create({ - baseURL: "/api", + baseURL: "", withCredentials: true, // 쿠키 인증이면 필수 }); From 87bd80abf08b388677c173435eb2abf12f7a33b7 Mon Sep 17 00:00:00 2001 From: HYUN-SIUU Date: Wed, 4 Feb 2026 21:47:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=0Dfeat:=20GTM=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/createAxiosServer.ts | 4 +- src/app/(plain)/exercise/class/[id]/page.tsx | 10 ++++- .../exercise/class/[id]/panel/Header.tsx | 5 +++ .../class/[id]/panel/WorkoutVideoPlayer.tsx | 14 ++++++ src/app/(plain)/login/panel/LoginForm.tsx | 2 + .../exercise/check-selected/panel/Routine.tsx | 6 ++- .../(exercise)/exercise/customized/page.tsx | 2 + .../(exercise)/exercise/done/[id]/page.tsx | 8 +++- src/utils/gtm.ts | 45 +++++++++++++++++++ 9 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 src/utils/gtm.ts diff --git a/src/apis/createAxiosServer.ts b/src/apis/createAxiosServer.ts index 363cd70..4cc9b34 100644 --- a/src/apis/createAxiosServer.ts +++ b/src/apis/createAxiosServer.ts @@ -5,7 +5,9 @@ import { headers as nextHeaders } from "next/headers"; import { AuthError } from "./errors"; const API_PREFIX = normalizePrefix(process.env.NEXT_PUBLIC_API_BASE ?? "/api"); -const API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL || "").trim().replace(/\/+$/, ""); +const API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL || "") + .trim() + .replace(/\/+$/, ""); const ENV_SITE_URL = ensureOrigin(process.env.NEXT_PUBLIC_SITE_URL); const devHttpsAgent = diff --git a/src/app/(plain)/exercise/class/[id]/page.tsx b/src/app/(plain)/exercise/class/[id]/page.tsx index 296d7bf..93d36a2 100644 --- a/src/app/(plain)/exercise/class/[id]/page.tsx +++ b/src/app/(plain)/exercise/class/[id]/page.tsx @@ -5,6 +5,7 @@ import useProgramStore from "@/states/useProgramStore"; import WorkoutVideoPlaylist from "./panel/WorkoutVideoPlayer"; import { useRouter } from "next/navigation"; import { useClientReady } from "@/hooks/useClientReady"; +import { getGtmClassType, pushGtmEvent } from "@/utils/gtm"; // import { useDeadlineTrigger } from "@/hooks/useDeadlineTrigger"; // import dayjs from "dayjs"; // import { useToastStore } from "@/states/useToastStore"; @@ -12,17 +13,21 @@ import { useClientReady } from "@/hooks/useClientReady"; const Page = () => { const isClientReady = useClientReady(); const router = useRouter(); - const { selectedProgram } = useProgramStore(); + const { selectedProgram, type } = useProgramStore(); // const { setToastOpen } = useToastStore(); + const startFired = React.useRef(false); useEffect(() => { if (!isClientReady) return; else if (!selectedProgram) { router.push("/"); + } else if (!startFired.current) { + pushGtmEvent("click_Start", getGtmClassType(type)); + startFired.current = true; } return () => {}; - }, [selectedProgram, router, isClientReady]); + }, [selectedProgram, router, isClientReady, type]); if (!selectedProgram) return null; @@ -40,6 +45,7 @@ const Page = () => { duration={selectedProgram?.duration} videos={selectedProgram?.videos} initialId={selectedProgram?.videos[0]?.id} + type={type} /> ); }; diff --git a/src/app/(plain)/exercise/class/[id]/panel/Header.tsx b/src/app/(plain)/exercise/class/[id]/panel/Header.tsx index c45d42c..95c7a32 100644 --- a/src/app/(plain)/exercise/class/[id]/panel/Header.tsx +++ b/src/app/(plain)/exercise/class/[id]/panel/Header.tsx @@ -5,17 +5,21 @@ import React, { useState } from "react"; import Timer from "./Timer"; import useMedia from "@/hooks/useMedia"; import SenifitDialog from "@/components/SenifitDialog"; +import { WorkoutKind } from "@/types/IRoutine"; +import { getGtmClassType, pushGtmEvent } from "@/utils/gtm"; const Header = ({ seconds, duration, onEnd, isEnd, + type, }: { duration: number; isEnd: boolean; onEnd: () => void; seconds: number; + type: "customized" | "popular" | ["thematic", WorkoutKind] | null; }) => { const [openDialog, setOpenDialog] = useState(false); @@ -25,6 +29,7 @@ const Header = ({ if (isEnd) { onEnd(); } else { + pushGtmEvent("click_classStop", getGtmClassType(type)); setOpenDialog(true); } }; diff --git a/src/app/(plain)/exercise/class/[id]/panel/WorkoutVideoPlayer.tsx b/src/app/(plain)/exercise/class/[id]/panel/WorkoutVideoPlayer.tsx index 70f97e7..4ecef8e 100644 --- a/src/app/(plain)/exercise/class/[id]/panel/WorkoutVideoPlayer.tsx +++ b/src/app/(plain)/exercise/class/[id]/panel/WorkoutVideoPlayer.tsx @@ -11,6 +11,8 @@ import { axiosClient } from "@/apis/axiosClient"; import { isAuthError } from "@/apis/errors"; // import { useToastStore } from "@/states/useToastStore"; import SenifitDialog from "@/components/SenifitDialog"; +import { getGtmClassType, pushGtmEvent } from "@/utils/gtm"; +import { WorkoutKind } from "@/types/IRoutine"; export interface IWorkoutVideo { id: number; @@ -34,6 +36,7 @@ export interface IWorkoutVideoPlaylistProps { /** 인덱스 변경 콜백(옵션) */ onIndexChange?: (index: number, video: IWorkoutVideo) => void; duration: number; + type: "customized" | "popular" | ["thematic", WorkoutKind] | null; } /** DOM 요소의 실시간 높이를 구하는 훅 */ @@ -68,6 +71,7 @@ export default function WorkoutVideoPlaylist({ assetBaseUrl, onIndexChange, duration, + type, }: IWorkoutVideoPlaylistProps) { const { isPhone } = useMedia(); const router = useRouter(); @@ -160,6 +164,15 @@ export default function WorkoutVideoPlaylist({ }; }, [recordId]); + const progressFired = useRef(false); + useEffect(() => { + const halfDuration = Math.floor(duration * 30); + if (!progressFired.current && seconds >= halfDuration && halfDuration > 0) { + pushGtmEvent("click_Progress", getGtmClassType(type)); + progressFired.current = true; + } + }, [seconds, duration, type]); + // 이탈 방지 bypass 플래그 (앱 내부 이동 시 사용) const shouldBypassUnload = useRef(false); @@ -281,6 +294,7 @@ export default function WorkoutVideoPlaylist({ isEnd={index === videos.length - 1} onEnd={notifyDone} seconds={seconds} + type={type} /> diff --git a/src/app/(plain)/login/panel/LoginForm.tsx b/src/app/(plain)/login/panel/LoginForm.tsx index 15108b2..ff2f84c 100644 --- a/src/app/(plain)/login/panel/LoginForm.tsx +++ b/src/app/(plain)/login/panel/LoginForm.tsx @@ -25,6 +25,7 @@ import EyeOffIcon from "@/components/icons/EyeOffIcon"; import InquiryButton from "@/components/InquiryButton"; import BackgroundImage from "@/assets/images/login-background.png"; import { signupGoogleForm } from "@/constants/signupGF"; +import { pushGtmEvent } from "@/utils/gtm"; type LoginFormValues = { id: string; password: string }; @@ -57,6 +58,7 @@ export default function LoginForm() { const onSubmit = async (data: LoginFormValues) => { try { await login(data); + pushGtmEvent("login_Success"); setToastOpen({ message: "로그인 성공! 오늘도 즐거운 시니핏 하세요!", }); diff --git a/src/app/(with-container)/(exercise)/exercise/check-selected/panel/Routine.tsx b/src/app/(with-container)/(exercise)/exercise/check-selected/panel/Routine.tsx index 69a5348..ed92b71 100644 --- a/src/app/(with-container)/(exercise)/exercise/check-selected/panel/Routine.tsx +++ b/src/app/(with-container)/(exercise)/exercise/check-selected/panel/Routine.tsx @@ -14,6 +14,7 @@ import { IRoutineDetail } from "@/types/IRoutineDetail"; import { Button, Divider, Stack, Typography } from "@mui/material"; import Link from "next/link"; import React from "react"; +import { getGtmClassType, pushGtmEvent } from "@/utils/gtm"; const Routine = ({ type, @@ -116,7 +117,10 @@ const Routine = ({ {"이전"}