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);
}