diff --git a/.vscode/settings.json b/.vscode/settings.json index 97233c48..cd5ba4d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", + "source.fixAll.eslint": "always", "source.organizeImports": "always" }, "tailwindCSS.suggestions": true, diff --git a/apps/client/public/firebase-messaging-sw.js b/apps/client/public/firebase-messaging-sw.js index c5696972..e802dbd8 100644 --- a/apps/client/public/firebase-messaging-sw.js +++ b/apps/client/public/firebase-messaging-sw.js @@ -1,6 +1,25 @@ /* eslint-env serviceworker */ /* eslint-disable no-undef */ +const AMPLITUDE_API_KEY = 'bb48a29e445e2f350a1d23ad67f38d55'; + +const trackAmplitudeEvent = (eventType) => { + const isProd = self.location.hostname === 'pinback.today'; + if (!isProd) { + console.log('[Analytics] track', eventType); + return; + } + + fetch('https://api2.amplitude.com/2/httpapi', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + api_key: AMPLITUDE_API_KEY, + events: [{ device_id: 'serviceworker', event_type: eventType }], + }), + }).catch((err) => console.warn('[Amplitude] 이벤트 전송 실패', err)); +}; + const firebaseConfig = { apiKey: 'AIzaSyD3KM0IQ4Ro3Dd2fyAY8fnhE1bQ_NesrBc', authDomain: 'pinback-c55de.firebaseapp.com', @@ -35,6 +54,8 @@ firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(); messaging.onBackgroundMessage((payload) => { + trackAmplitudeEvent('Triggered_Reminder'); + const url = payload.data?.url || 'https://pinback.today'; const notificationTitle = payload.notification?.title || 'pinback'; const notificationOptions = { @@ -50,6 +71,8 @@ messaging.onBackgroundMessage((payload) => { self.addEventListener('notificationclick', (event) => { const targetUrl = event.notification.data?.url || 'https://pinback.today'; + trackAmplitudeEvent('Clicked_alarm'); + fetch( `https://www.google-analytics.com/mp/collect?measurement_id=G-847ZNSCC3J&api_secret=1hei57fPTKyGX5Cw73rwgA`, { diff --git a/apps/client/src/pages/jobPins/JobPins.tsx b/apps/client/src/pages/jobPins/JobPins.tsx index 53c946c8..3a58ccca 100644 --- a/apps/client/src/pages/jobPins/JobPins.tsx +++ b/apps/client/src/pages/jobPins/JobPins.tsx @@ -6,7 +6,9 @@ import JobPinsBottomNotice from '@pages/jobPins/components/JobPinsBottomNotice'; import MemoPopup from '@pages/jobPins/components/MemoPopup'; import { useJobPinsBottomNotice } from '@pages/jobPins/hooks/useJobPinsBottomNotice'; import Footer from '@pages/myBookmark/components/footer/Footer'; +import { analytics } from '@pinback/analytics'; import { Card } from '@pinback/design-system/ui'; +import AnalyticsCardWrapper from '@shared/components/analyticsCardWrapper/AnalyticsCardWrapper'; import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll'; const JobPins = () => { @@ -67,18 +69,30 @@ const JobPins = () => { const displayImageUrl = article.thumbnailUrl || undefined; return ( - getJobPinDetail(article.articleId)} - /> + bookmarkType="jobpin" + > + { + analytics.track('Clicked_Shared_Bookmark', { + article_id: String(article.articleId), + category_id: article.category + ? String(article.category.categoryId) + : undefined, + }); + getJobPinDetail(article.articleId); + }} + /> + ); })} diff --git a/apps/client/src/pages/jobPins/apis/axios.ts b/apps/client/src/pages/jobPins/apis/axios.ts index 292a3b53..85ccd5c2 100644 --- a/apps/client/src/pages/jobPins/apis/axios.ts +++ b/apps/client/src/pages/jobPins/apis/axios.ts @@ -1,4 +1,4 @@ -import { JobPinsResponse } from '@pages/jobPins/types/api'; +import { type JobPinsResponse } from '@pages/jobPins/types/api'; import apiRequest from '@shared/apis/setting/axiosInstance'; interface ApiResponse { diff --git a/apps/client/src/pages/jobPins/apis/queries.ts b/apps/client/src/pages/jobPins/apis/queries.ts index 533c6b91..90b162b6 100644 --- a/apps/client/src/pages/jobPins/apis/queries.ts +++ b/apps/client/src/pages/jobPins/apis/queries.ts @@ -1,9 +1,9 @@ -import { JobPinsResponse } from '@pages/jobPins/types/api'; +import { type JobPinsResponse } from '@pages/jobPins/types/api'; import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { getJobPinsArticleDetail, getJobPinsArticles, - JobPinsDetailResponse, + type JobPinsDetailResponse, } from './axios'; const PAGE_SIZE = 20; diff --git a/apps/client/src/pages/level/Level.tsx b/apps/client/src/pages/level/Level.tsx index e33a9628..c38ee9e2 100644 --- a/apps/client/src/pages/level/Level.tsx +++ b/apps/client/src/pages/level/Level.tsx @@ -1,5 +1,5 @@ import LevelScene from '@pages/level/components/LevelScene'; -import { TreeLevel } from '@pages/level/types/treeLevelType'; +import { type TreeLevel } from '@pages/level/types/treeLevelType'; import { Icon } from '@pinback/design-system/icons'; import { Badge } from '@pinback/design-system/ui'; import { cn } from '@pinback/design-system/utils'; diff --git a/apps/client/src/pages/level/components/LevelInfoCard.tsx b/apps/client/src/pages/level/components/LevelInfoCard.tsx index ba48bc71..0909a2d6 100644 --- a/apps/client/src/pages/level/components/LevelInfoCard.tsx +++ b/apps/client/src/pages/level/components/LevelInfoCard.tsx @@ -1,4 +1,4 @@ -import { TREE_LEVEL_TABLE, TreeLevel } from '@pages/level/types/treeLevelType'; +import { TREE_LEVEL_TABLE, type TreeLevel } from '@pages/level/types/treeLevelType'; import { Level } from '@pinback/design-system/ui'; import { cn } from '@pinback/design-system/utils'; diff --git a/apps/client/src/pages/level/components/LevelScene.tsx b/apps/client/src/pages/level/components/LevelScene.tsx index bbc4ed73..a54f800e 100644 --- a/apps/client/src/pages/level/components/LevelScene.tsx +++ b/apps/client/src/pages/level/components/LevelScene.tsx @@ -1,6 +1,6 @@ import { cn } from '@pinback/design-system/utils'; -import { TreeLevel } from '@pages/level/types/treeLevelType'; -import { HTMLAttributes } from 'react'; +import { type TreeLevel } from '@pages/level/types/treeLevelType'; +import { type HTMLAttributes } from 'react'; import chippi_level1 from '../../../assets/Lv.1.webp'; import chippi_level2 from '../../../assets/Lv.2.webp'; diff --git a/apps/client/src/pages/myBookmark/MyBookmark.tsx b/apps/client/src/pages/myBookmark/MyBookmark.tsx index f32efbc6..f2fe7dba 100644 --- a/apps/client/src/pages/myBookmark/MyBookmark.tsx +++ b/apps/client/src/pages/myBookmark/MyBookmark.tsx @@ -101,7 +101,6 @@ const MyBookmark = () => { onBadgeChange={setActiveBadge} updateToReadStatus={updateToReadStatus} openMenu={openMenu} - queryClient={queryClient} scrollContainerRef={scrollContainerRef} /> diff --git a/apps/client/src/pages/myBookmark/apis/axios.ts b/apps/client/src/pages/myBookmark/apis/axios.ts index 9c799bc1..fdacc9aa 100644 --- a/apps/client/src/pages/myBookmark/apis/axios.ts +++ b/apps/client/src/pages/myBookmark/apis/axios.ts @@ -1,7 +1,7 @@ import { - BookmarkArticlesCountResponse, - BookmarkArticlesResponse, - CategoryBookmarkArticleResponse, + type BookmarkArticlesCountResponse, + type BookmarkArticlesResponse, + type CategoryBookmarkArticleResponse, } from '@pages/myBookmark/types/api'; import apiRequest from '@shared/apis/setting/axiosInstance'; diff --git a/apps/client/src/pages/myBookmark/apis/queries.ts b/apps/client/src/pages/myBookmark/apis/queries.ts index 4863f399..9dae387c 100644 --- a/apps/client/src/pages/myBookmark/apis/queries.ts +++ b/apps/client/src/pages/myBookmark/apis/queries.ts @@ -3,7 +3,7 @@ import { useSuspenseInfiniteQuery, useSuspenseQuery, } from '@tanstack/react-query'; -import { CategoryBookmarkArticleResponse } from '../types/api'; +import { type CategoryBookmarkArticleResponse } from '../types/api'; import { getBookmarkArticles, getBookmarkArticlesCount, diff --git a/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx b/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx index 5378d387..c960c6a1 100644 --- a/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx +++ b/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx @@ -1,8 +1,11 @@ import NoArticles from '@pages/myBookmark/components/NoArticles/NoArticles'; import NoUnreadArticles from '@pages/myBookmark/components/noUnreadArticles/NoUnreadArticles'; import { useMyBookmarkContentData } from '@pages/myBookmark/hooks/useMyBookmarkContentData'; +import { analytics } from '@pinback/analytics'; import { Badge, Card } from '@pinback/design-system/ui'; -import { MutableRefObject } from 'react'; +import AnalyticsCardWrapper from '@shared/components/analyticsCardWrapper/AnalyticsCardWrapper'; +import { useQueryClient } from '@tanstack/react-query'; +import { type MutableRefObject } from 'react'; interface MyBookmarkContentProps { activeBadge: 'all' | 'notRead'; @@ -11,7 +14,6 @@ interface MyBookmarkContentProps { categoryId: string | null; updateToReadStatus: (id: number, options?: any) => void; openMenu: (id: number, anchor: HTMLElement) => void; - queryClient: any; scrollContainerRef: MutableRefObject; } @@ -22,9 +24,9 @@ const MyBookmarkContent = ({ categoryId, updateToReadStatus, openMenu, - queryClient, scrollContainerRef, }: MyBookmarkContentProps) => { + const queryClient = useQueryClient(); const { view, list, counts, pagination } = useMyBookmarkContentData({ activeBadge, category, @@ -65,50 +67,55 @@ const MyBookmarkContent = ({ className="scrollbar-hide mt-[2.6rem] flex h-screen flex-wrap content-start gap-[1.6rem] overflow-y-auto scroll-smooth" > {list.articles.map((article) => ( - { - window.open(article.url, '_blank'); - updateToReadStatus(article.articleId, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['bookmarkArticles'], - }); - queryClient.invalidateQueries({ - queryKey: ['bookmarkArticlesCount'], - }); - queryClient.invalidateQueries({ - queryKey: ['categoryBookmarkArticlesCount'], - }); - queryClient.invalidateQueries({ - queryKey: ['categoryBookmarkArticles'], - }); - }, - onError: (error: any) => { - console.error(error); - }, - }); - }} - onOptionsClick={(e) => { - e.stopPropagation(); - openMenu(article.articleId, e.currentTarget); - }} - /> + + { + analytics.track('Clicked_My_Bookmark', { + article_id: String(article.articleId), + category_id: article.category?.categoryId?.toString(), + }); + window.open(article.url, '_blank'); + updateToReadStatus(article.articleId, { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['bookmarkArticles'], + }); + queryClient.invalidateQueries({ + queryKey: ['bookmarkArticlesCount'], + }); + queryClient.invalidateQueries({ + queryKey: ['categoryBookmarkArticlesCount'], + }); + queryClient.invalidateQueries({ + queryKey: ['categoryBookmarkArticles'], + }); + }, + onError: (error: any) => { + console.error(error); + }, + }); + }} + onOptionsClick={(e) => { + e.stopPropagation(); + openMenu(article.articleId, e.currentTarget); + }} + /> + ))}
Array.from({ length }, (_, i) => { diff --git a/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts b/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts index 8888ea84..90003542 100644 --- a/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts +++ b/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts @@ -2,7 +2,7 @@ import { AlarmsType } from '@constants/alarms'; import { Step, stepOrder, - StepType, + type StepType, } from '@pages/onBoarding/constants/onboardingSteps'; import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime'; import { registerServiceWorker } from '@pages/onBoarding/utils/registerServiceWorker'; diff --git a/apps/client/src/pages/remind/Remind.tsx b/apps/client/src/pages/remind/Remind.tsx index de5e684d..63ce36a5 100644 --- a/apps/client/src/pages/remind/Remind.tsx +++ b/apps/client/src/pages/remind/Remind.tsx @@ -1,4 +1,6 @@ import { useGetRemindArticles } from '@pages/remind/apis/queries'; +import { analytics } from '@pinback/analytics'; +import AnalyticsCardWrapper from '@shared/components/analyticsCardWrapper/AnalyticsCardWrapper'; import NoReadArticles from '@pages/remind/components/noReadArticles/NoReadArticles'; import NoUnreadArticles from '@pages/remind/components/noUnreadArticles/NoUnreadArticles'; import { @@ -151,25 +153,28 @@ const Remind = () => { const displayImageUrl = article.thumbnailUrl || undefined; return ( - { - window.open(article.url, '_blank'); - updateToReadStatus(article.articleId); - }} - onOptionsClick={(e) => { - e.stopPropagation(); - - openMenu(article.articleId, e.currentTarget); - }} - /> + + { + analytics.track('Clicked_Reminder', { + article_id: String(article.articleId), + }); + window.open(article.url, '_blank'); + updateToReadStatus(article.articleId); + }} + onOptionsClick={(e) => { + e.stopPropagation(); + openMenu(article.articleId, e.currentTarget); + }} + /> + ); })} diff --git a/apps/client/src/shared/apis/axios.ts b/apps/client/src/shared/apis/axios.ts index 7c45e215..d6dbb410 100644 --- a/apps/client/src/shared/apis/axios.ts +++ b/apps/client/src/shared/apis/axios.ts @@ -1,9 +1,9 @@ import apiRequest from '@shared/apis/setting/axiosInstance'; import { - AmplitudeUserPropertiesResponse, - EditArticleRequest, - HasJobResponse, - JobsResponse, + type AmplitudeUserPropertiesResponse, + type EditArticleRequest, + type HasJobResponse, + type JobsResponse, } from '@shared/types/api'; import { formatLocalDateTime } from '@shared/utils/formatDateTime'; diff --git a/apps/client/src/shared/apis/queries.ts b/apps/client/src/shared/apis/queries.ts index 8b6619f9..7498dbb3 100644 --- a/apps/client/src/shared/apis/queries.ts +++ b/apps/client/src/shared/apis/queries.ts @@ -12,36 +12,36 @@ import { getMyProfile, patchCategory, patchUserJob, - patchUserJobRequest, + type patchUserJobRequest, postCategory, postSignUp, - postSignUpRequest, + type postSignUpRequest, putArticleReadStatus, putEditArticle, } from '@shared/apis/axios'; import { - AcornsResponse, - AmplitudeUserPropertiesResponse, - ArticleDetailResponse, - ArticleReadStatusResponse, - CategoryDetailResponse, - DashboardCategoriesResponse, - EditArticleRequest, - HasJobResponse, - JobsResponse, + type AcornsResponse, + type AmplitudeUserPropertiesResponse, + type ArticleDetailResponse, + type ArticleReadStatusResponse, + type CategoryDetailResponse, + type DashboardCategoriesResponse, + type EditArticleRequest, + type HasJobResponse, + type JobsResponse, } from '@shared/types/api'; import { fetchOGData } from '@shared/utils/fetchOgData'; import { authStorage } from '@shared/utils/authStorage'; import { extensionBridge } from '@shared/utils/extensionBridge'; import { useMutation, - UseMutationResult, + type UseMutationResult, useQuery, useQueryClient, - UseQueryResult, + type UseQueryResult, useSuspenseQuery, } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; +import { type AxiosError } from 'axios'; export const useGetDashboardCategories = (): UseQueryResult< DashboardCategoriesResponse, diff --git a/apps/client/src/shared/components/analyticsCardWrapper/AnalyticsCardWrapper.tsx b/apps/client/src/shared/components/analyticsCardWrapper/AnalyticsCardWrapper.tsx new file mode 100644 index 00000000..509aeab9 --- /dev/null +++ b/apps/client/src/shared/components/analyticsCardWrapper/AnalyticsCardWrapper.tsx @@ -0,0 +1,30 @@ +import { + analytics, + type ImpressionSavedContentProperties, +} from '@pinback/analytics'; +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; + +interface AnalyticsCardWrapperProps { + bookmarkType: ImpressionSavedContentProperties['bookmark_type']; + children: React.ReactNode; +} + +const AnalyticsCardWrapper = ({ + bookmarkType, + children, +}: AnalyticsCardWrapperProps) => { + const { ref, inView } = useInView({ threshold: 0.5, triggerOnce: true }); + + useEffect(() => { + if (inView) { + analytics.track('Impression_Saved_Content', { + bookmark_type: bookmarkType, + }); + } + }, [inView, bookmarkType]); + + return
{children}
; +}; + +export default AnalyticsCardWrapper; diff --git a/apps/client/src/shared/components/balloon/Balloon.tsx b/apps/client/src/shared/components/balloon/Balloon.tsx index f50fd155..216e14dc 100644 --- a/apps/client/src/shared/components/balloon/Balloon.tsx +++ b/apps/client/src/shared/components/balloon/Balloon.tsx @@ -1,6 +1,6 @@ import { Icon } from '@pinback/design-system/icons'; import { cn } from '@pinback/design-system/utils'; -import { ReactNode } from 'react'; +import { type ReactNode } from 'react'; type BalloonVariant = 'gray' | 'main'; diff --git a/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx b/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx index 495794ce..ae523f6f 100644 --- a/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx +++ b/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx @@ -18,7 +18,7 @@ import { useGetDashboardCategories, usePutEditArticle, } from '@shared/apis/queries'; -import { ArticleDetailResponse, EditArticleRequest } from '@shared/types/api'; +import { type ArticleDetailResponse, type EditArticleRequest } from '@shared/types/api'; import { combineDateTime } from '@shared/utils/datetime'; import { updateDate, updateTime } from '@shared/utils/formatDateTime'; import { useQueryClient } from '@tanstack/react-query'; diff --git a/apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx b/apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx index ba67e8f9..e23acea7 100644 --- a/apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx +++ b/apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx @@ -1,7 +1,7 @@ import { Suspense } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Checkbox } from '@pinback/design-system/ui'; -import { JobsResponse } from '@shared/types/api'; +import { type JobsResponse } from '@shared/types/api'; import JobCards from './JobCards'; import JobCardsSkeleton from './JobCardsSkeleton'; diff --git a/apps/client/src/shared/components/sidebar/AccordionItem.tsx b/apps/client/src/shared/components/sidebar/AccordionItem.tsx index c0696da4..e8e97649 100644 --- a/apps/client/src/shared/components/sidebar/AccordionItem.tsx +++ b/apps/client/src/shared/components/sidebar/AccordionItem.tsx @@ -1,7 +1,7 @@ import { useState, useId } from 'react'; import { cn } from '@pinback/design-system/utils'; import SideItem from './SideItem'; -import { IconToken } from './types/IconTokenType'; +import { type IconToken } from './types/IconTokenType'; interface AccordionItemProps extends Omit, 'children'> { diff --git a/apps/client/src/shared/components/sidebar/SideItem.tsx b/apps/client/src/shared/components/sidebar/SideItem.tsx index cd27430b..cba8a56a 100644 --- a/apps/client/src/shared/components/sidebar/SideItem.tsx +++ b/apps/client/src/shared/components/sidebar/SideItem.tsx @@ -1,6 +1,6 @@ import { Icon, type IconName } from '@pinback/design-system/icons'; import { cn } from '@pinback/design-system/utils'; -import { IconToken } from './types/IconTokenType'; +import { type IconToken } from './types/IconTokenType'; const ICON_MAP: Record = { clock: { on: 'ic_clock_active', off: 'ic_clock_disable' }, diff --git a/apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts b/apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts index 215e19d1..61bd1547 100644 --- a/apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts +++ b/apps/client/src/shared/components/sidebar/hooks/useCategoryActions.ts @@ -4,9 +4,9 @@ import { usePatchCategory, usePostCategory, } from '@shared/apis/queries'; -import { SidebarTab } from '@shared/hooks/useSidebarNav'; +import { type SidebarTab } from '@shared/hooks/useSidebarNav'; import { useQueryClient } from '@tanstack/react-query'; -import { Dispatch, SetStateAction, useState } from 'react'; +import { type Dispatch, type SetStateAction, useState } from 'react'; import { useNavigate } from 'react-router-dom'; interface CategoryActionsParams { diff --git a/apps/client/src/shared/utils/fetchOgData.ts b/apps/client/src/shared/utils/fetchOgData.ts index 94a599c5..a5841ab7 100644 --- a/apps/client/src/shared/utils/fetchOgData.ts +++ b/apps/client/src/shared/utils/fetchOgData.ts @@ -1,4 +1,4 @@ -import axios, { AxiosResponse } from 'axios'; +import axios, { type AxiosResponse } from 'axios'; export interface OGData { title: string; diff --git a/apps/client/src/shared/utils/treeLevel.ts b/apps/client/src/shared/utils/treeLevel.ts index 793f5d48..6af12256 100644 --- a/apps/client/src/shared/utils/treeLevel.ts +++ b/apps/client/src/shared/utils/treeLevel.ts @@ -1,7 +1,7 @@ import { TREE_LEVEL_TABLE, - TreeLevelResult, - TreeLevelRow, + type TreeLevelResult, + type TreeLevelRow, } from '@pages/level/types/treeLevelType'; function findLevelRow(count: number, rows: readonly TreeLevelRow[]) { diff --git a/apps/extension/src/pages/MainPop.tsx b/apps/extension/src/pages/MainPop.tsx index 73bf8441..0642c1cf 100644 --- a/apps/extension/src/pages/MainPop.tsx +++ b/apps/extension/src/pages/MainPop.tsx @@ -8,6 +8,7 @@ import thumbImg from '@assets/extension_thumb.svg'; import { useCategoryManager } from '@hooks/useCategoryManager'; import { usePageMeta } from '@hooks/usePageMeta'; import { useSaveBookmark } from '@hooks/useSaveBookmarks'; +import { analytics } from '@pinback/analytics'; import { Icon } from '@pinback/design-system/icons'; import { AutoDismissToast, @@ -22,7 +23,7 @@ import { validateDate, validateTime, } from '@pinback/design-system/ui'; -import { ArticleResponse } from '@shared-types/types'; +import { type ArticleResponse } from '@shared-types/types'; import Header from '@shared/components/Header'; import { combineDateTime, @@ -213,6 +214,14 @@ const MainPop = ({ type, savedData }: MainPopProps) => { }, { onSuccess: () => { + analytics.track('Saved_Article', { + // article_id: 응답 타입 미정의로 보류 + category_id: selected ?? undefined, + // is_first_save: 판단 불가로 보류 + page_domain: URL.canParse(url) + ? new URL(url).hostname + : undefined, + }); save({ url, title, diff --git a/packages/analytics/index.ts b/packages/analytics/index.ts index a5cd8f89..e940f80d 100644 --- a/packages/analytics/index.ts +++ b/packages/analytics/index.ts @@ -3,6 +3,7 @@ import { amplitudeProvider } from './src/providers/amplitude'; import { consoleProvider } from './src/providers/console'; export type { AnalyticsProvider, UserProperties } from './src/types'; +export type * from './src/ampli'; export { analytics }; interface InitAnalyticsOptions { diff --git a/packages/analytics/src/ampli/index.ts b/packages/analytics/src/ampli/index.ts index 012b4195..6c39f988 100644 --- a/packages/analytics/src/ampli/index.ts +++ b/packages/analytics/src/ampli/index.ts @@ -55,7 +55,7 @@ export interface IdentifyProperties { job_role?: string; } -export interface ClickedReminderProperties { +export interface ClickedAlarmProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ @@ -64,13 +64,9 @@ export interface ClickedReminderProperties { * 리마인드 아이디(보내고 클릭해졌는지 클릭률 계산 등) */ reminder_id?: string; - /** - * 리마인드 유형 - */ - reminder_type?: string; } -export interface ClickedSharedBookmarkProperties { +export interface ClickedMyBookmarkProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ @@ -81,22 +77,18 @@ export interface ClickedSharedBookmarkProperties { category_id?: string; } -export interface OpenedSavedContentProperties { +export interface ClickedReminderProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ article_id?: string; /** - * 카테고리 고유 ID (어디에 저장됐는지, 공유 여부 확인 등) - */ - category_id?: string; - /** - * 콘텐츠 유입 출처 + * 리마인드 아이디(보내고 클릭해졌는지 클릭률 계산 등) */ - source_type?: string; + reminder_id?: string; } -export interface SavedArticleProperties { +export interface ClickedSharedBookmarkProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ @@ -105,25 +97,28 @@ export interface SavedArticleProperties { * 카테고리 고유 ID (어디에 저장됐는지, 공유 여부 확인 등) */ category_id?: string; +} + +export interface ImpressionSavedContentProperties { /** - * 첫 저장인지 여부 - */ - is_first_save?: boolean; - /** - * 저장 대상 페이지 도메인 + * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ - page_domain?: string; + article_id?: string; /** - * 저장 방식 + * 콘텐츠 유입 출처 + * + * | Rule | Value | + * |---|---| + * | Enum Values | own, jobpin, remind | */ - save_method?: string; + bookmark_type?: "own" | "jobpin" | "remind"; /** - * 저장이 발생한 위치 + * 카테고리 고유 ID (어디에 저장됐는지, 공유 여부 확인 등) */ - save_source?: string; + category_id?: string; } -export interface SavedSharedBookmarkProperties { +export interface SavedArticleProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ @@ -133,31 +128,35 @@ export interface SavedSharedBookmarkProperties { */ category_id?: string; /** - * 저장이 발생한 위치 + * 첫 저장인지 여부 */ - save_source?: string; + is_first_save?: boolean; /** - * 콘텐츠 유입 출처 + * 저장 대상 페이지 도메인 */ - source_type?: string; + page_domain?: string; } -export interface TriggeredReminderProperties { +export interface SavedSharedBookmarkProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ article_id?: string; /** - * 카테고리 고유 ID (어디에 저장됐는지, 공유 여부 확인 등) + * 콘텐츠 유입 출처 + * + * | Rule | Value | + * |---|---| + * | Enum Values | own, jobpin, remind | */ - category_id?: string; + bookmark_type?: "own" | "jobpin" | "remind"; /** - * 리마인드 유형 + * 카테고리 고유 ID (어디에 저장됐는지, 공유 여부 확인 등) */ - reminder_type?: string; + category_id?: string; } -export interface ViewedSavedContentProperties { +export interface TriggeredReminderProperties { /** * 아티클 고유 ID (특정 아티클 재열람 확인 등) */ @@ -166,10 +165,6 @@ export interface ViewedSavedContentProperties { * 카테고리 고유 ID (어디에 저장됐는지, 공유 여부 확인 등) */ category_id?: string; - /** - * 콘텐츠 유입 출처 - */ - source_type?: string; } export class Identify implements BaseEvent { @@ -182,6 +177,26 @@ export class Identify implements BaseEvent { } } +export class ClickedAlarm implements BaseEvent { + event_type = 'Clicked_alarm'; + + constructor( + public event_properties?: ClickedAlarmProperties, + ) { + this.event_properties = event_properties; + } +} + +export class ClickedMyBookmark implements BaseEvent { + event_type = 'Clicked_My_Bookmark'; + + constructor( + public event_properties?: ClickedMyBookmarkProperties, + ) { + this.event_properties = event_properties; + } +} + export class ClickedReminder implements BaseEvent { event_type = 'Clicked_Reminder'; @@ -202,11 +217,11 @@ export class ClickedSharedBookmark implements BaseEvent { } } -export class OpenedSavedContent implements BaseEvent { - event_type = 'Opened_Saved_Content'; +export class ImpressionSavedContent implements BaseEvent { + event_type = 'Impression_Saved_Content'; constructor( - public event_properties?: OpenedSavedContentProperties, + public event_properties?: ImpressionSavedContentProperties, ) { this.event_properties = event_properties; } @@ -242,20 +257,6 @@ export class TriggeredReminder implements BaseEvent { } } -export class ViewedSavedContent implements BaseEvent { - event_type = 'Viewed_Saved_Content'; - - constructor( - public event_properties?: ViewedSavedContentProperties, - ) { - this.event_properties = event_properties; - } -} - -export class ViewedSharedBookmarkList implements BaseEvent { - event_type = 'Viewed_Shared_Bookmark_List'; -} - export type PromiseResult = { promise: Promise }; const getVoidPromiseResult = () => ({ promise: Promise.resolve() }); @@ -373,12 +374,46 @@ export class Ampli { return this.amplitude!.track(event, undefined, options); } + /** + * Clicked_alarm + * + * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Clicked_alarm) + * + * 알림을 클릭했을 때 발생 + * + * @param properties The event's properties (e.g. article_id) + * @param options Amplitude event options. + */ + clickedAlarm( + properties?: ClickedAlarmProperties, + options?: EventOptions, + ) { + return this.track(new ClickedAlarm(properties), options); + } + + /** + * Clicked_My_Bookmark + * + * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Clicked_My_Bookmark) + * + * 대시보드 나의 북마크 카드를 클릭했을 때 발생 (여기서 나의 북마크는 북마크 전체, 카테고리 등 전체를 포함함.) + * + * @param properties The event's properties (e.g. article_id) + * @param options Amplitude event options. + */ + clickedMyBookmark( + properties?: ClickedMyBookmarkProperties, + options?: EventOptions, + ) { + return this.track(new ClickedMyBookmark(properties), options); + } + /** * Clicked_Reminder * * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Clicked_Reminder) * - * 사용자가 리마인드를 클릭했을 때 발생 + * 대시보드 리마인드 카드를 클릭했을 때 발생 * * @param properties The event's properties (e.g. article_id) * @param options Amplitude event options. @@ -408,20 +443,20 @@ export class Ampli { } /** - * Opened_Saved_Content + * Impression_Saved_Content * - * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Opened_Saved_Content) + * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Impression_Saved_Content) * - * 저장한 콘텐츠를 다시 클릭해 열었을 때 발생 + * 저장한 콘텐츠 목록 또는 상세가 노출될 때 발생 * * @param properties The event's properties (e.g. article_id) * @param options Amplitude event options. */ - openedSavedContent( - properties?: OpenedSavedContentProperties, + impressionSavedContent( + properties?: ImpressionSavedContentProperties, options?: EventOptions, ) { - return this.track(new OpenedSavedContent(properties), options); + return this.track(new ImpressionSavedContent(properties), options); } /** @@ -474,38 +509,6 @@ export class Ampli { ) { return this.track(new TriggeredReminder(properties), options); } - - /** - * Viewed_Saved_Content - * - * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Viewed_Saved_Content) - * - * 저장한 콘텐츠 목록 또는 상세가 노출될 때 발생 - * - * @param properties The event's properties (e.g. article_id) - * @param options Amplitude event options. - */ - viewedSavedContent( - properties?: ViewedSavedContentProperties, - options?: EventOptions, - ) { - return this.track(new ViewedSavedContent(properties), options); - } - - /** - * Viewed_Shared_Bookmark_List - * - * [View in Tracking Plan](https://data.amplitude.com/pinback/default/events/main/latest/Viewed_Shared_Bookmark_List) - * - * 공유 북마크 목록을 조회했을 때 발생 - * - * @param options Amplitude event options. - */ - viewedSharedBookmarkList( - options?: EventOptions, - ) { - return this.track(new ViewedSharedBookmarkList(), options); - } } export const ampli = new Ampli(); diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js index 2226920d..4693f60b 100644 --- a/packages/eslint-config/react-internal.js +++ b/packages/eslint-config/react-internal.js @@ -34,6 +34,10 @@ const config = [ ...pluginReactHooks.configs.recommended.rules, // React scope no longer necessary with new JSX transform. 'react/react-in-jsx-scope': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], }, }, ];