diff --git a/src/apis/axiosClient.ts b/src/apis/axiosClient.ts
index 90cddad..5d02009 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, // 쿠키 인증이면 필수
});
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..f0de7e7 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,11 @@ const Routine = ({
{"이전"}