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
2 changes: 1 addition & 1 deletion src/apis/axiosClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from "axios";
import { AuthError } from "./errors";

export const axiosClient = axios.create({
baseURL: "/api",
baseURL: "",
withCredentials: true, // 쿠키 인증이면 필수
});

Expand Down
4 changes: 3 additions & 1 deletion src/apis/createAxiosServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
10 changes: 8 additions & 2 deletions src/app/(plain)/exercise/class/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,29 @@ 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";

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;

Expand All @@ -40,6 +45,7 @@ const Page = () => {
duration={selectedProgram?.duration}
videos={selectedProgram?.videos}
initialId={selectedProgram?.videos[0]?.id}
type={type}
/>
);
};
Expand Down
5 changes: 5 additions & 0 deletions src/app/(plain)/exercise/class/[id]/panel/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -25,6 +29,7 @@ const Header = ({
if (isEnd) {
onEnd();
} else {
pushGtmEvent("click_classStop", getGtmClassType(type));
setOpenDialog(true);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +36,7 @@ export interface IWorkoutVideoPlaylistProps {
/** 인덱스 변경 콜백(옵션) */
onIndexChange?: (index: number, video: IWorkoutVideo) => void;
duration: number;
type: "customized" | "popular" | ["thematic", WorkoutKind] | null;
}

/** DOM 요소의 실시간 높이를 구하는 훅 */
Expand Down Expand Up @@ -68,6 +71,7 @@ export default function WorkoutVideoPlaylist({
assetBaseUrl,
onIndexChange,
duration,
type,
}: IWorkoutVideoPlaylistProps) {
const { isPhone } = useMedia();
const router = useRouter();
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -281,6 +294,7 @@ export default function WorkoutVideoPlaylist({
isEnd={index === videos.length - 1}
onEnd={notifyDone}
seconds={seconds}
type={type}
/>
</Box>

Expand Down
2 changes: 2 additions & 0 deletions src/app/(plain)/login/panel/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -57,6 +58,7 @@ export default function LoginForm() {
const onSubmit = async (data: LoginFormValues) => {
try {
await login(data);
pushGtmEvent("login_Success");
setToastOpen({
message: "로그인 성공! 오늘도 즐거운 시니핏 하세요!",
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -116,7 +117,11 @@ const Routine = ({
<Typography variant={"Heading1"}>{"이전"}</Typography>
</Button>
<Button
onClick={() => setOpenModal(true)}
id={"click_classStart"}
onClick={() => {
pushGtmEvent("click_classStart", getGtmClassType(type));
setOpenModal(true);
}}
variant={"contained"}
disableElevation
sx={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { IRoutineDetail } from "@/types/IRoutineDetail";
import useProgramStore from "@/states/useProgramStore";
import { useRouter } from "next/navigation";
import SenifitDialog from "@/components/SenifitDialog";
import { pushGtmEvent } from "@/utils/gtm";

const Page = () => {
const [openDialog, setOpenDialog] = useState(false);
Expand Down Expand Up @@ -106,6 +107,7 @@ const Page = () => {
<Stack
component={"form"}
onSubmit={handleSubmit(onSubmit, () => {
pushGtmEvent("customized_optionError");
setOpenDialog(true);
})}
direction={"column"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useMedia from "@/hooks/useMedia";
import Link from "next/link";
import { isAuthError } from "@/apis/errors";
import { useRouter } from "next/navigation";
import { getGtmClassType, pushGtmEvent } from "@/utils/gtm";

const Page = () => {
const { id } = useParams();
Expand All @@ -23,7 +24,7 @@ const Page = () => {

const searchParams = useSearchParams();
const seconds = Number(searchParams.get("seconds")) || 0;
const { selectedProgram } = useProgramStore();
const { selectedProgram, type } = useProgramStore();

const { mutate } = useMutation({
mutationFn: async () => {
Expand All @@ -38,7 +39,10 @@ const Page = () => {

useEffect(() => {
mutate();
}, [mutate]);
if (selectedProgram && seconds >= selectedProgram.duration * 60) {
pushGtmEvent("click_Finish", getGtmClassType(type));
}
}, [mutate, selectedProgram, seconds, type]);

return (
<Stack
Expand Down Expand Up @@ -123,6 +127,7 @@ const Page = () => {
gap={[1.25]}
>
<Button
id={"click_RecordQuick"}
component={Link}
href={`/record/write/${id}`} // 기록 페이지로 이동
variant={"contained"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import SenifitDialog from "@/components/SenifitDialog";

interface IVideoInfoProps extends IPopularRoutine {
onButtonClick: () => void;
gtmId?: string;
}

const VideoInfo = ({
Expand All @@ -23,6 +24,7 @@ const VideoInfo = ({
duration,
description,
onButtonClick,
gtmId,
}: IVideoInfoProps) => {
const [openDialog, setOpenDialog] = useState(false);

Expand Down Expand Up @@ -111,6 +113,7 @@ const VideoInfo = ({
)}

<Button
id={gtmId}
onClick={() => setOpenDialog(true)}
fullWidth
disableElevation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const Page = () => {
<VideoInfo
key={routine.id}
{...routine}
gtmId={`popular_Select${routine.duration}`}
onButtonClick={() => {
setType("popular");
setSelectedRoutineRecord({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const CustomizedRoutine = ({ authenticated }: { authenticated: boolean }) => {
const { isPhone } = useMedia();
return (
<Button
id={"click_Customized"}
component={Link}
href={
authenticated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const PopularRoutine = ({ authenticated }: { authenticated: boolean }) => {

return (
<Button
id={"popular_Click"}
fullWidth
component={Link}
href={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const ThematicRoutine = ({ authenticated }: { authenticated: boolean }) => {
const { isPhone } = useMedia();
return (
<Button
id={"Click_Topic"}
fullWidth
component={Link}
href={
Expand Down
2 changes: 2 additions & 0 deletions src/components/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const Carousel = <T,>({
<Box ref={containerRef} sx={{ position: "relative", overflow: "hidden" }}>
{/* 이전 버튼 */}
<IconButton
id={"click_click_Carousel"}
sx={{
...iconButtonStyle,
left: "1rem",
Expand All @@ -64,6 +65,7 @@ const Carousel = <T,>({

{/* 다음 버튼 */}
<IconButton
id={"click_click_Carousel"}
sx={{
...iconButtonStyle,
right: "1rem",
Expand Down
45 changes: 45 additions & 0 deletions src/utils/gtm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export type GTM_EVENT_TYPE =
| "click_classStart"
| "click_Start"
| "click_Progress"
| "click_classStop"
| "click_Finish"
| "record_Finish"
| "login_Success"
| "customized_optionError";

interface GtmPayload {
event: GTM_EVENT_TYPE;
classType?: string;
}

declare global {
interface Window {
dataLayer: GtmPayload[];
}
}

/**
* GTM dataLayer에 이벤트를 전송합니다.
* @param event 이벤트 명칭
* @param classType 클래스 유형 ("맞춤형", "인기", "주제별")
*/
export const pushGtmEvent = (event: GTM_EVENT_TYPE, classType?: string) => {
if (typeof window === "undefined") return;

window.dataLayer = window.dataLayer || [];

const payload: GtmPayload = { event };
if (classType) {
payload.classType = classType;
}

window.dataLayer.push(payload);
};

export const getGtmClassType = (type: unknown) => {
if (type === "customized") return "맞춤형";
if (type === "popular") return "인기";
if (Array.isArray(type) && type[0] === "thematic") return "주제별";
return undefined;
};
Loading