Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
214acf6
setting: @amplitude/unified 의쑴 μΆ”κ°€
constantly-dev Mar 28, 2026
c833133
setting: analytics 이벀트 및 νƒ€μž… μ •μ˜
constantly-dev Mar 31, 2026
712a3f7
setting: amplitude/console 컨트둀러 및 AnalyticsProvider κ΅¬ν˜„
constantly-dev Mar 31, 2026
32daf9a
setting: AnalyticsProvider 앱에 μ—°κ²°
constantly-dev Mar 31, 2026
abc5827
fix: storybook header μ»΄ν¬λ„ŒνŠΈ ci issue ν•΄κ²°
constantly-dev Apr 14, 2026
7c981f8
setting: analytics controller/event νƒ€μž… μˆ˜μ • 및 user property μ„ΈνŒ… μ€€λΉ„
constantly-dev Apr 14, 2026
b6913e1
feat: class νŽ˜μ΄μ§€ amplitude 이벀트 μ—°κ²°
constantly-dev Apr 14, 2026
941663e
feat: dancer νŽ˜μ΄μ§€ amplitude 이벀트 μ—°κ²°
constantly-dev Apr 14, 2026
ecc4538
feat: my νŽ˜μ΄μ§€ amplitude 이벀트 μ—°κ²°
constantly-dev Apr 14, 2026
5203e29
feat: search νŽ˜μ΄μ§€ amplitude 이벀트 μ—°κ²°
constantly-dev Apr 14, 2026
32bc986
chore: header storyμ—μ„œ play μ£Όμ„μ²˜λ¦¬
constantly-dev Apr 14, 2026
a91851f
fix: 좩돌 ν•΄κ²°
constantly-dev Apr 14, 2026
d0ced7b
feat: λ°±μ—”λ“œ API 응닡 μŠ€νŽ™ 반영
constantly-dev Apr 16, 2026
1188706
feat: Amplitude μœ μ € ν”„λ‘œνΌν‹° μ„€μ • ν™œμ„±ν™”
constantly-dev Apr 16, 2026
f942eaf
chore: Header storyμ—μ„œ λΆˆν•„μš”ν•œ import 제거
constantly-dev Apr 16, 2026
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"svgr": "npx @svgr/cli -d src/shared/assets/svg --no-prettier --ignore-existing --typescript --no-dimensions --no-index --jsx-runtime automatic public/svg"
},
"dependencies": {
"@amplitude/unified": "^1.0.7",
"@hookform/resolvers": "^5.0.1",
"@lukemorales/query-key-factory": "^1.3.4",
"@sentry/react": "^9.39.0",
Expand Down
353 changes: 353 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/app/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect } from 'react';
import { Toaster } from 'react-hot-toast';
import queryClient from '@/app/queryClient';
import ModalProvider from '@/common/components/Modal/ModalProvider';
import { AnalyticsProvider } from '@/lib/analytics';

const setScreenSize = () => {
// vh κ΄€λ ¨
Expand All @@ -31,9 +32,12 @@ export default function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
{children}
<ModalProvider />
<Toaster containerStyle={{ margin: '0 auto' }} />
{/* TODO-userproperty: AnalyticsProviderκ°€ useGetMe(React Query)λ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ QueryClientProvider μ•ˆμ— μœ„μΉ˜ν•΄μ•Ό 함 */}
<AnalyticsProvider>
{children}
<ModalProvider />
<Toaster containerStyle={{ margin: '0 auto' }} />
</AnalyticsProvider>
</QueryClientProvider>
);
}
12 changes: 9 additions & 3 deletions src/app/auth/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { kakaoLogin, postLogout, postReissue } from '@/app/auth/apis/ky';
import type { loginTypes } from '@/app/auth/types/api';
import { useEventLogger } from '@/lib/analytics';
import { authKeys } from '@/shared/constants/queryKey';
import type { ApiError } from '@/shared/types/ApiError';
import { clearStorage } from '@/shared/utils/handleToken';

export const useLoginMutation = () => {
const router = useRouter();
const { logSubmitEvent } = useEventLogger();

return useMutation({
mutationFn: ({ redirectUrl, code }: loginTypes) => kakaoLogin(redirectUrl, code),

onSuccess: ({ data: { isOnboarded, isDeleted } }) => {
console.log('성곡은 ν•˜λ‹ˆ??');
onSuccess: ({ data: { isOnboarded, isDeleted, role } }) => {
logSubmitEvent('login_success', { user_type: role });

if (!isOnboarded || isDeleted) {
console.log('μ—¬κΈ°μ•Ό?');
clearStorage();
router.push(isDeleted ? '/onboarding?isDeleted=true' : '/onboarding');
return;
Expand All @@ -32,9 +34,13 @@ export const useLoginMutation = () => {

// λ‘œκ·Έμ•„μ›ƒ
export const usePostLogout = () => {
const { reset } = useEventLogger();

return useMutation({
mutationFn: postLogout,
onSuccess: () => {
// TODO-userproperty: λ‘œκ·Έμ•„μ›ƒ μ‹œ Amplitude μœ μ € 식별 μ΄ˆκΈ°ν™”
reset();
clearStorage();
window.location.reload();
},
Expand Down
1 change: 1 addition & 0 deletions src/app/auth/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface loginTypes {
export interface LoginResponseTypes {
isOnboarded: boolean;
isDeleted: boolean;
role: 'MEMBER' | 'TEACHER';
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useHeartToggle } from '@/app/class/[id]/hooks/useHeartToggle';
import type { LessonDetailResponseTypes } from '@/app/class/[id]/types/api';
import BlurButton from '@/common/components/BlurButton/BlurButton';
import BoxButton from '@/common/components/BoxButton/BoxButton';
import { useEventLogger } from '@/lib/analytics';
import IcHeartFilledGray07 from '@/shared/assets/svg/IcHeartFilledGray07';
import IcHeartOutlinedGray07 from '@/shared/assets/svg/IcHeartOutlinedGray07';
import { WITHDRAW_USER_NAME } from '@/shared/constants/withdrawUser';
Expand All @@ -25,8 +26,19 @@ const ClassButtonWrapper = ({ lessonData }: { lessonData: LessonDetailResponseTy
const finalButtonText = isDeletedTeacher ? 'μ‹ μ²­λΆˆκ°€' : buttonText;
const finalIsDisabled = isDeletedTeacher || isDisabled || isMyLesson;

const { logClickEvent } = useEventLogger();

const handleApplyClick = () => {
if (!isDisabled && id) {
logClickEvent('lesson_reservation_start', {
lesson_id: Number(id),
lesson_name: lessonData.name,
teacher_name: lessonData.teacherNickname,
teacher_id: lessonData.teacherId,
lesson_price: lessonData.price,
lesson_session_count: lessonData.lessonRound.lessonRounds.length,
lesson_capacity: lessonData.maxReservationCount,
});
router.push(`/class/${id}/register`);
}
};
Expand Down
20 changes: 18 additions & 2 deletions src/app/class/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { use } from 'react';
import { use, useEffect } from 'react';
import { useGetLessonDetail } from '@/app/class/[id]/apis/queries';
import ClassButtonWrapper from '@/app/class/[id]/components/ClassButtonWrapper/ClassButtonWrapper';
import ClassInfoWrapper from '@/app/class/[id]/components/ClassInfoWrapper/ClassInfoWrapper';
Expand All @@ -10,18 +10,34 @@ import { LOW_SEAT_THRESHOLD } from '@/app/class/[id]/constants';
import { chipWrapperStyle, topImgStyle, withdrawIconStyle, withdrawImgStyle } from '@/app/class/[id]/index.css';
import Divider from '@/common/components/Divider/Divider';
import Text from '@/common/components/Text/Text';
import { useEventLogger } from '@/lib/analytics';
import IcCircleCautionFilled from '@/shared/assets/svg/IcCircleCautionFilled';

export default function Page({ params }: { params: Promise<{ id: string }> }) {
const { logPageViewEvent } = useEventLogger();
const { id } = use(params);
const lessonId = Number(id);

const isValidLessonId = Number.isInteger(lessonId) && lessonId > 0;

const { data, isPending, isError } = useGetLessonDetail(lessonId, {
enabled: Boolean(isValidLessonId),
});

useEffect(() => {
if (!data) return;
logPageViewEvent('lesson_view', {
lesson_id: lessonId,
lesson_name: data.name,
teacher_name: data.teacherNickname,
teacher_id: data.teacherId,
lesson_price: data.price,
lesson_session_count: data.lessonRound.lessonRounds.length,
lesson_capacity: data.maxReservationCount,
referrer_page: document.referrer,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);

if (!isValidLessonId) {
throw new Error('Invalid lesson id');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import Divider from '@/common/components/Divider/Divider';
import Head from '@/common/components/Head/Head';
import Text from '@/common/components/Text/Text';
import { notify } from '@/common/components/Toast/Toast';
import { useEventLogger } from '@/lib/analytics';
import IcCheckcircleGray0524 from '@/shared/assets/svg/IcCheckcircleGray0524';
import IcCheckcircleMain0324 from '@/shared/assets/svg/IcCheckcircleMain0324';
import { vars } from '@/shared/styles/theme.css';
Expand All @@ -44,8 +45,9 @@ const ReservationStep = ({ onNext }: ReservationStepPropTypes) => {
const [isAllChecked, setIsAllChecked] = useState(false);
const [agreements, setAgreements] = useState(new Array(AGREEMENT_TERMS.length).fill(false));

const { logSubmitEvent } = useEventLogger();
const params = useParams<{ id: string }>();
const id = params.id;
const id = params?.id;
const { data, isError, isLoading } = useGetReservation(Number(id));
const { mutate: postReservation, isPending } = usePostReservation();

Expand All @@ -58,7 +60,21 @@ const ReservationStep = ({ onNext }: ReservationStepPropTypes) => {
postReservation(
{ lessonId: id },
{
onSuccess: onNext,
onSuccess: (detail) => {
logSubmitEvent('lesson_reservation_complete', {
lesson_id: Number(id),
lesson_name: data.name,
teacher_name: data.teacherNickname,
// TODO: (reservation API에 μ—†λŠ” ν•„λ“œ)
teacher_id: 0,
lesson_price: data.price,
lesson_session_count: data.lessonRound.lessonRounds.length,
// TODO: (reservation API에 μ—†λŠ” ν•„λ“œ)
lesson_capacity: 0,
reservation_status: 'μŠΉμΈλŒ€κΈ°',
});
onNext(detail);
},
onError: (error: ApiError<{ message: string }>) => {
const message = error?.response?.data?.message || 'μ˜ˆμ•½ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμ–΄μš”. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.';
notify({
Expand Down
2 changes: 2 additions & 0 deletions src/app/class/[id]/register/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ export interface ReservationDetailResponseTypes {
imageUrl: string;
name: string;
teacherNickname: string;
teacherId: number;
price: number;
detail: string;
level: string;
location: string;
locationDetail: string;
maxReservationCount: number;
lessonRound: {
lessonRounds: {
startDateTime: string;
Expand Down
32 changes: 26 additions & 6 deletions src/app/dancer/[id]/components/DancerInfo/DancerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import { expandInstagramUrl, expandYouTubeUrl } from '@/app/dancer/[id]/utils/ur
import Divider from '@/common/components/Divider/Divider';
import Head from '@/common/components/Head/Head';
import Text from '@/common/components/Text/Text';
import { useEventLogger } from '@/lib/analytics';
import IcInstagram20 from '@/shared/assets/svg/IcInstagram20';
import IcYoutube20 from '@/shared/assets/svg/IcYoutube20';

const DancerInfo = ({ dancerData }: { dancerData: DancerDetailResponseTypes }) => {
const DancerInfo = ({ dancerData, id }: { dancerData: DancerDetailResponseTypes; id: number }) => {
const { instagram, youtube, detail, nickname, lessons } = dancerData;

const { logClickEvent } = useEventLogger();
const router = useRouter();

const handleClassClick = (lessonId: number) => {
Expand All @@ -39,7 +41,16 @@ const DancerInfo = ({ dancerData }: { dancerData: DancerDetailResponseTypes }) =
{(instagram || youtube) && (
<div className={socialLinksStyle}>
{instagram && (
<a href={expandInstagramUrl(instagram)} target="_blank" rel="noopener noreferrer">
<a
href={expandInstagramUrl(instagram)}
target="_blank"
rel="noopener noreferrer"
onClick={() =>
logClickEvent('external_link_click', {
teacher_name: nickname,
teacher_id: id,
})
}>
<div className={socialLinkStyle}>
<IcInstagram20 width="2rem" />
<Text tag="b2_m" color="gray5" className={linkStyle}>
Expand All @@ -50,7 +61,16 @@ const DancerInfo = ({ dancerData }: { dancerData: DancerDetailResponseTypes }) =
)}

{youtube && (
<a href={expandYouTubeUrl(youtube)} target="_blank" rel="noopener noreferrer">
<a
href={expandYouTubeUrl(youtube)}
target="_blank"
rel="noopener noreferrer"
onClick={() =>
logClickEvent('external_link_click', {
teacher_name: nickname,
teacher_id: id,
})
}>
<div className={socialLinkStyle}>
<IcYoutube20 width="2rem" height="2rem" />
<Text tag="b2_m" color="gray5" className={linkStyle}>
Expand Down Expand Up @@ -78,9 +98,9 @@ const DancerInfo = ({ dancerData }: { dancerData: DancerDetailResponseTypes }) =
</Head>
) : (
<div className={rowScrollStyle}>
{lessons.map((data, id) => {
const isFirst = id === 0;
const isLast = id === lessons.length - 1;
{lessons.map((data, index) => {
const isFirst = index === 0;
const isLast = index === lessons.length - 1;

return (
<div
Expand Down
19 changes: 17 additions & 2 deletions src/app/dancer/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
'use client';

import { use } from 'react';
import { use, useEffect } from 'react';
import { useGetDancerDetail } from '@/app/dancer/[id]/apis/queries';
import DancerInfo from '@/app/dancer/[id]/components/DancerInfo/DancerInfo';
import TabWrapper from '@/app/dancer/[id]/components/TabWrapper/TabWrapper';
import { genresWrapperStyle, gradientOverlayStyle, textWrapperStyle, topImgStyle } from '@/app/dancer/[id]/index.css';
import Head from '@/common/components/Head/Head';
import Tag from '@/common/components/Tag/Tag';
import Text from '@/common/components/Text/Text';
import { useEventLogger } from '@/lib/analytics';
import { genreMapping } from '@/shared/constants/index';

export default function Page({ params }: { params: Promise<{ id: string }> }) {
const { logPageViewEvent } = useEventLogger();
const { id } = use(params);
const dancerId = Number(id);

Expand All @@ -20,6 +22,19 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
enabled: Boolean(isValidDancerId),
});

useEffect(() => {
if (!data) return;
logPageViewEvent('teacher_view', {
teacher_name: data.nickname,
teacher_id: dancerId,
referrer_page: document.referrer,
has_sns: data.instagram || data.youtube ? true : false,
has_video: data.videoUrls ? data.videoUrls.length > 0 : false,
has_experience: data.experiences ? data.experiences.length > 0 : false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);

if (!isValidDancerId) {
throw new Error('Invalid dancer id');
}
Expand Down Expand Up @@ -62,7 +77,7 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
</Head>
</div>
</div>
<DancerInfo dancerData={data} />
<DancerInfo dancerData={data} id={dancerId} />
<TabWrapper colorScheme="primary" dancerData={data} />
</>
);
Expand Down
9 changes: 8 additions & 1 deletion src/app/my/(instructor)/create-class/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import Modal from '@/common/components/Modal/Modal';
import { notify } from '@/common/components/Toast/Toast';
import useDebounce from '@/common/hooks/useDebounce';
import { useModalStore } from '@/common/stores/modal';
import { useEventLogger } from '@/lib/analytics';
import { genreEngMapping, levelEngMapping } from '@/shared/constants';
import { lessonKeys, memberKeys, teacherKeys } from '@/shared/constants/queryKey';
import useBlockBackWithUnsavedChanges from '@/shared/hooks/useBlockBackWithUnsavedChanges';
Expand All @@ -43,9 +44,10 @@ import useImageUploader from '@/shared/hooks/useImageUploader';

export default function Page() {
const params = useParams<{ id: string }>();
const id = params.id;
const id = params?.id;
const router = useRouter();
const { openModal } = useModalStore();
const { logSubmitEvent } = useEventLogger();

const lessonId = Number(id);
const isValidId = !isNaN(lessonId) && lessonId > 0;
Expand Down Expand Up @@ -225,6 +227,11 @@ export default function Page() {
{ lessonId, infoData: updatedInfo },
{
onSuccess: () => {
logSubmitEvent('lesson_create_done', {
lesson_id: lessonId,
lesson_name: updatedInfo.name,
is_edit: true,
});
queryClient.invalidateQueries({ queryKey: memberKeys.me.queryKey });
queryClient.invalidateQueries({ queryKey: lessonKeys.list.queryKey });
queryClient.invalidateQueries({ queryKey: lessonKeys.detail(lessonId).queryKey });
Expand Down
8 changes: 6 additions & 2 deletions src/app/my/(instructor)/create-class/apis/ky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ export const getLocationList = async (query: string): Promise<LocationsData> =>
return data;
};

export const postClassRegisterInfo = async (infoData: ClassRegisterInfoTypes) => {
const data = await kyInstance.post(API_URL.LESSONS, { json: infoData }).json();
export interface ClassRegisterResponseTypes {
lessonId: number;
}

export const postClassRegisterInfo = async (infoData: ClassRegisterInfoTypes): Promise<ClassRegisterResponseTypes> => {
const data = await kyInstance.post(API_URL.LESSONS, { json: infoData }).json<ClassRegisterResponseTypes>();
return data;
};

Expand Down
8 changes: 8 additions & 0 deletions src/app/my/(instructor)/create-class/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import Modal from '@/common/components/Modal/Modal';
import { notify } from '@/common/components/Toast/Toast';
import useDebounce from '@/common/hooks/useDebounce';
import { useModalStore } from '@/common/stores/modal';
import { useEventLogger } from '@/lib/analytics';
import { genreEngMapping, levelEngMapping } from '@/shared/constants';
import { lessonKeys, memberKeys, teacherKeys } from '@/shared/constants/queryKey';
import useBlockBackWithUnsavedChanges from '@/shared/hooks/useBlockBackWithUnsavedChanges';
Expand All @@ -44,6 +45,7 @@ import useImageUploader from '@/shared/hooks/useImageUploader';
export default function Page() {
const router = useRouter();
const { openModal } = useModalStore();
const { logSubmitEvent } = useEventLogger();

// 클래슀 등둝 νŽ˜μ΄μ§€λŠ” μ‹ κ·œ λ“±λ‘μš©μ΄λ―€λ‘œ lessonIdκ°€ μ—†μŒ
const lessonId = null;
Expand Down Expand Up @@ -236,6 +238,12 @@ export default function Page() {
} else {
classRegisterMutate(updatedInfo, {
onSuccess: () => {
logSubmitEvent('lesson_create_done', {
// TODO: 등둝 API 응닡에 lesson_id μ—†μŒ
lesson_id: 0,
lesson_name: updatedInfo.name,
is_edit: false,
});
queryClient.invalidateQueries({ queryKey: memberKeys.me.queryKey });
queryClient.invalidateQueries({ queryKey: lessonKeys.list.queryKey });
router.push('/my/create-class/completion');
Expand Down
Loading
Loading