From e77e3598b26d85dc9a695cbc6f913e076f8414ce Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:01:51 +0900 Subject: [PATCH 1/7] =?UTF-8?q?chore:=20=EB=A9=94=ED=83=80=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/index.html b/index.html index 453075dd..9616d5f8 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,7 @@ THIP, 독서를 기록하는 가장 힙한 방법 + From d0f427a07e23ceb02cd5993c72968b72453d08f4 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:02:05 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=EC=84=BC=ED=84=B0?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=B0=94=20=ED=88=AC=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/notice/Notice.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 730257ad..0d41f10c 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -171,6 +171,12 @@ const NotificationList = styled.div` padding: 0 20px 20px 20px; width: 100%; overflow-y: auto; + /* Hide scrollbar but keep scroll */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + &::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ + } `; const NotificationCard = styled.div<{ read: boolean }>` From c9828fe99808415d3269960b8641620f2333138c Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:41:41 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20API=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notifications/postNotificationsCheck.ts | 25 ++++++ src/pages/notice/Notice.tsx | 86 +++++++++++++++++-- 2 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 src/api/notifications/postNotificationsCheck.ts diff --git a/src/api/notifications/postNotificationsCheck.ts b/src/api/notifications/postNotificationsCheck.ts new file mode 100644 index 00000000..cc575860 --- /dev/null +++ b/src/api/notifications/postNotificationsCheck.ts @@ -0,0 +1,25 @@ +import { apiClient } from '../index'; + +export interface PostNotificationsCheckRequest { + notificationId: number; +} + +export interface PostNotificationsCheckResponse> { + isSuccess: boolean; + code: number; + message: string; + data: { + route: string; // e.g., 'POST_DETAIL' + params?: Params; // e.g., { postId: 123 } + }; +} + +// 알림 확인(체크) 및 이동 정보 반환 API +export const postNotificationsCheck = async (notificationId: number) => { + const body: PostNotificationsCheckRequest = { notificationId }; + const response = await apiClient.post( + '/notifications/check', + body, + ); + return response.data; +}; diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 0d41f10c..9869a63d 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -5,6 +5,7 @@ import TitleHeader from '@/components/common/TitleHeader'; import leftArrow from '../../assets/common/leftArrow.svg'; import { colors, typography } from '@/styles/global/global'; import { getNotifications, type NotificationItem } from '@/api/notifications/getNotifications'; +import { postNotificationsCheck } from '@/api/notifications/postNotificationsCheck'; const Notice = () => { const [selected, setSelected] = useState(''); @@ -78,16 +79,87 @@ const Notice = () => { }; }, [isLoading, isLast, nextCursor, loadNotifications]); - // const handleReadNotification = (index: number) => { - // setNotifications(prev => - // prev.map((item, idx) => (idx === index ? { ...item, isChecked: true } : item)), - // ); - // }; - const filteredNotifications = notifications; const tabs = ['피드', '모임']; + const handleNotificationClick = async (notif: NotificationItem) => { + try { + const res = await postNotificationsCheck(notif.notificationId); + if (!res.isSuccess) return; + + // UI 즉시 반영: 읽음 처리 + // setNotifications(prev => + // prev.map(item => + // item.notificationId === notif.notificationId ? { ...item, isChecked: true } : item, + // ), + // ); + + const { route, params } = res.data as { route: string; params?: Record }; + + // 서버 라우팅 키 → 실제 앱 경로 매핑 + switch (route) { + // 이동 없음 + case 'NONE': + break; + + // 피드 1번 (해당유저 피드로 이동) + case 'FEED_USER': { + const userId = (params?.userId as number) ?? undefined; + if (userId !== undefined) { + navigate(`/otherfeed/${userId}`); + } + break; + } + + // 피드 2~6번 (피드상세페이지로 이동) + case 'FEED_DETAIL': { + const feedId = (params?.feedId as number) ?? undefined; + if (feedId !== undefined) { + navigate(`/feed/${feedId}`); + } + break; + } + + // 모임 (모집조기마감 or 모임시작) + case 'ROOM_MAIN': { + const roomId = (params?.roomId as number) ?? undefined; + if (roomId !== undefined) navigate(`/group/detail/joined/${roomId}`); + break; + } + + // host일때, 누군가 모임 참여를 눌렀을 때 + case 'ROOM_DETAIL': { + const roomId = (params?.roomId as number) ?? undefined; + if (roomId !== undefined) navigate(`/group/detail/${roomId}`); + break; + } + + // 모임방 -> 기록장 -> 해당 기록 필터링 화면으로 이동 + case 'ROOM_POST_DETAIL': + case 'ROOM_RECORD_DETAIL': + case 'ROOM_VOTE_DETAIL': { + const roomId = (params?.roomId as number) ?? undefined; + const postId = (params?.postId as number) ?? undefined; + const page = (params?.page as number) ?? undefined; + const postType = params?.postType as 'RECORD' | 'VOTE'; + if (roomId !== undefined) { + navigate(`/rooms/${roomId}/memory`, { + state: { focusPostId: postId, postType, page }, + }); + } + break; + } + + default: + break; + } + } catch (e) { + // noop: 실패 시 네비게이션 없이 무시 + console.error('알림 확인 처리 실패:', e); + } + }; + return ( { handleReadNotification(idx)} + onClick={() => handleNotificationClick(notif)} > {!notif.isChecked && } From 03dcdbaf5c2b9261c5aaccf22d0f6766876f3160 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Wed, 24 Sep 2025 22:54:29 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=EC=84=BC=ED=84=B0?= =?UTF-8?q?=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/memory/Memory.tsx | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/pages/memory/Memory.tsx b/src/pages/memory/Memory.tsx index 3f429032..881f9329 100644 --- a/src/pages/memory/Memory.tsx +++ b/src/pages/memory/Memory.tsx @@ -6,6 +6,7 @@ import MemoryContent from '../../components/memory/MemoryContent/MemoryContent'; import MemoryAddButton from '../../components/memory/MemoryAddButton/MemoryAddButton'; import Snackbar from '../../components/common/Modal/Snackbar'; import GlobalCommentBottomSheet from '../../components/common/CommentBottomSheet/GlobalCommentBottomSheet'; +import { useCommentBottomSheetStore } from '@/stores/useCommentBottomSheetStore'; import { Container, FixedHeader, ScrollableContent, FloatingElements } from './Memory.styled'; import { getMemoryPosts } from '../../api/memory/getMemoryPosts'; import type { GetMemoryPostsParams, Post, Record } from '../../types/memory'; @@ -31,7 +32,7 @@ const convertPostToRecord = (post: Post): Record => { isWriter: post.isWriter, isLiked: post.isLiked, isLocked: post.isLocked, // 블러 처리 여부 추가 - pollOptions: post.voteItems.map((item) => { + pollOptions: post.voteItems.map(item => { const maxCount = Math.max(...post.voteItems.map(v => v.count || 0)); return { id: item.voteItemId.toString(), @@ -50,6 +51,7 @@ const Memory = () => { const navigate = useNavigate(); const location = useLocation(); const { roomId } = useParams<{ roomId: string }>(); + const { openCommentBottomSheet } = useCommentBottomSheetStore(); // 상태 관리 const [activeTab, setActiveTab] = useState('group'); @@ -151,6 +153,30 @@ const Memory = () => { loadMemoryPosts(); }, [loadMemoryPosts]); + // Notice에서 넘어온 state(page, focusPostId 등)로 초기 필터 적용 + useEffect(() => { + type MemoryLocationState = { + page?: number; + focusPostId?: number; + postType?: 'RECORD' | 'VOTE'; + openComments?: boolean; + } | null; + const state = (location.state as MemoryLocationState) || null; + const initialPage = state?.page; + if (initialPage && !selectedPageRange) { + setSelectedPageRange({ start: initialPage, end: initialPage }); + setActiveFilter('page'); + } + + // 댓글 모달 자동 오픈 처리 + if (state?.openComments && state.focusPostId && state.postType) { + openCommentBottomSheet(state.focusPostId, state.postType); + // 동일 경로 재진입 시 중복 오픈 방지를 위해 state 제거 + navigate(location.pathname, { replace: true }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.state, roomId]); + // 새로운 기록이 추가되었을 때 처리 (작성 완료 후 돌아왔을 때) useEffect(() => { if (location.state?.newRecord) { From 0cc35c77ea49df93a5bca50226062c6e7e92ffd1 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:56:10 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=EC=84=BC=ED=84=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=9C,=20=EB=8C=93=EA=B8=80=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=EC=9D=B4=20=EC=97=B4=EB=A6=AC=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/notice/Notice.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 9869a63d..64c8ea0c 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -136,16 +136,20 @@ const Notice = () => { } // 모임방 -> 기록장 -> 해당 기록 필터링 화면으로 이동 - case 'ROOM_POST_DETAIL': - case 'ROOM_RECORD_DETAIL': - case 'ROOM_VOTE_DETAIL': { + case 'ROOM_POST_DETAIL': { const roomId = (params?.roomId as number) ?? undefined; const postId = (params?.postId as number) ?? undefined; const page = (params?.page as number) ?? undefined; const postType = params?.postType as 'RECORD' | 'VOTE'; + const shouldOpenComments = (params as { openComments?: boolean })?.openComments === true; if (roomId !== undefined) { navigate(`/rooms/${roomId}/memory`, { - state: { focusPostId: postId, postType, page }, + state: { + focusPostId: postId, + postType, + page, + ...(shouldOpenComments ? { openComments: true } : {}), + }, }); } break; From 3c3367c6b49be2ea2224ff6fca6bd96064712363 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:46:44 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20ga=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 8 +++++++ src/lib/ga.ts | 65 +++++++++++++++++++++++++++++++++----------------- src/main.tsx | 3 +-- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 9aaac5c5..da28e12c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react-cookie": "^8.0.1", "react-datepicker": "^8.4.0", "react-dom": "^19.1.0", + "react-ga4": "^2.1.0", "react-router-dom": "^7.6.0", "zustand": "^5.0.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b569676..fae23ebd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-ga4: + specifier: ^2.1.0 + version: 2.1.0 react-router-dom: specifier: ^7.6.0 version: 7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1404,6 +1407,9 @@ packages: peerDependencies: react: ^19.1.0 + react-ga4@2.1.0: + resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==} + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3026,6 +3032,8 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-ga4@2.1.0: {} + react-is@16.13.1: {} react-refresh@0.17.0: {} diff --git a/src/lib/ga.ts b/src/lib/ga.ts index b2db1e4f..cdccbcf3 100644 --- a/src/lib/ga.ts +++ b/src/lib/ga.ts @@ -1,3 +1,5 @@ +import ReactGA from 'react-ga4'; + declare global { interface Window { dataLayer: unknown[]; @@ -6,33 +8,52 @@ declare global { } export const GA_ID = import.meta.env.VITE_GA_MEASUREMENT_ID as string | undefined; +const GA_DEBUG = (import.meta.env.VITE_GA_DEBUG as string | undefined) === 'true'; + +let isInitialized = false; + +function isLocalhost(): boolean { + const hn = window.location.hostname; + return hn === 'localhost' || hn === '127.0.0.1' || hn === '::1'; +} export function initGA() { + if (isInitialized) return; if (!GA_ID) return; + if (isLocalhost()) return; + + ReactGA.initialize(GA_ID, { + gaOptions: { anonymizeIp: true }, + testMode: false, + }); + + if (GA_DEBUG) { + console.info('[GA4] initialized:', GA_ID); + } - // gtag.js 로더 주입 - const script = document.createElement('script'); - script.async = true; - script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`; - document.head.appendChild(script); - - // gtag 초기화 - window.dataLayer = window.dataLayer || []; - window.gtag = function gtag(...args: unknown[]) { - window.dataLayer.push(args); - }; - - window.gtag('js', new Date()); - // SPA라면 초기 자동 page_view는 끄고 필요 시 수동 전송 - window.gtag('config', GA_ID, { send_page_view: false }); + isInitialized = true; } -// SPA 라우팅 시 수동 전송용 export function sendPageView(path: string) { - if (!GA_ID || !window.gtag) return; - window.gtag('event', 'page_view', { - page_title: document.title, - page_location: window.location.href, - page_path: path, - }); + if (!GA_ID || !isInitialized) return; + if (GA_DEBUG) { + console.info('[GA4] page_view:', path); + } + ReactGA.send({ hitType: 'pageview', page: path, title: document.title }); +} + +type EventParams = Record & { category?: string }; + +export function trackEvent(eventName: string, params?: EventParams) { + if (!GA_ID || !isInitialized) return; + const category = params?.category ?? eventName; + const entries = Object.entries(params || {}).filter(([k]) => k !== 'category') as Array< + [string, string | number | boolean | undefined] + >; + const rest = Object.fromEntries(entries) as Record; + + if (GA_DEBUG) { + console.info('[GA4] event:', eventName, { category, ...rest }); + } + ReactGA.event({ category, action: eventName, ...rest }); } diff --git a/src/main.tsx b/src/main.tsx index 0ce356e5..967f983d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,8 @@ import { createRoot } from 'react-dom/client'; import './main.css'; import App from './App.tsx'; -import { initGA, sendPageView } from './lib/ga.ts'; +import { initGA } from './lib/ga.ts'; initGA(); -sendPageView(window.location.pathname); createRoot(document.getElementById('root')!).render(); From 5c90a7a432afce5093d355584a005dd922176851 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:31:54 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20ga4=20=EB=A1=9C=EC=A7=81=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/ga.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/ga.ts b/src/lib/ga.ts index cdccbcf3..313da230 100644 --- a/src/lib/ga.ts +++ b/src/lib/ga.ts @@ -52,8 +52,9 @@ export function trackEvent(eventName: string, params?: EventParams) { >; const rest = Object.fromEntries(entries) as Record; + const payload = { category, ...rest }; if (GA_DEBUG) { - console.info('[GA4] event:', eventName, { category, ...rest }); + console.info('[GA4] event:', eventName, payload); } - ReactGA.event({ category, action: eventName, ...rest }); + ReactGA.event(eventName, payload); }