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
24 changes: 23 additions & 1 deletion app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCreateMeeting } from '@/hooks/api/mutation/useCreateMeeting';
import type { MeetingCreateRequest } from '@/types/api';
import { useToast } from '@/hooks/useToast';
import Toast from '@/components/ui/toast';
import { sendGAEvent } from '@next/third-parties/google';

export default function Page() {
const [meetingName, setMeetingName] = useState('');
Expand Down Expand Up @@ -123,7 +124,7 @@ export default function Page() {

const purposes = getPurposes();

// capacity 처리: "아직 안정해졌어요" 체크 시 30으로 설정
// capacity 처리: "아직 안정해졌어요" 체크 시 10으로 설정
const capacity = isParticipantUndecided ? 10 : participantCount || 1;

const requestData: MeetingCreateRequest = {
Expand All @@ -142,6 +143,27 @@ export default function Page() {
const { meetingId } = result.data;
console.log('생성된 ID:', meetingId);

// --- [GA4 이벤트 전송 로직 추가] ---
if (typeof window !== 'undefined') {
// 1. 브라우저 식별자(browser_id) 확인 및 생성 (Get or Create)
let browserId = localStorage.getItem('browser_id');
if (!browserId) {
// 없으면 새로 발급해서 브라우저에 각인!
const randomStr = Math.random().toString(36).substring(2, 15);
browserId = `bid_${randomStr}${Date.now().toString(36)}`;
localStorage.setItem('browser_id', browserId);
}

// 2. 방 만든 브라우저가 누구인지 식별자를 담아서 이벤트 전송
sendGAEvent('event', 'url_created', {
meeting_url_id: meetingId,
participant_count_expected: capacity,
browser_id: browserId,
entry_method: 'url_direct',
});
}
Comment on lines +147 to +164
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

browser_id 기반 추적 전 사용자 동의(GDPR/CCPA) 누락

browser_id를 사용자 동의 없이 localStorage에 저장하고 GA에 전송하는 것은 GDPR·CCPA 위반 소지가 있습니다. 이 식별자는 브라우저 수준에서 사용자를 추적하기 위한 목적으로 생성되므로, 쿠키/추적 동의와 동일하게 취급되어야 합니다.

사용자가 동의를 제공한 이후에만 browser_id를 생성·저장·전송하거나, 동의 관리 플랫폼(CMP)을 통해 GA 자체를 조건부로 로드하는 방식을 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/create/page.tsx` around lines 147 - 164, The current code creates and
stores browser_id in localStorage and calls sendGAEvent unconditionally; change
this so browser_id is only generated, saved, and sent after explicit tracking
consent is confirmed (e.g., check your CMP or a consent flag like
tracking_consent in localStorage/session) — update the logic around the
browser_id generation and sendGAEvent call to first verify consent (if consent
true then generate/set localStorage 'browser_id' and call sendGAEvent with
meetingId/capacity/browser_id; if consent false do nothing), and ensure that if
a browser_id exists but no consent is present you still do not send analytics
until consent is granted.

// -----------------------------------

// purposes를 localStorage에 저장 (장소 추천 카테고리로 사용)
const purposes = getPurposes();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

불필요한 purposes 변수 섀도잉 제거

Line 125에서 이미 const purposes = getPurposes()가 선언되어 있으며, if 블록 내부에서도 동일하게 접근 가능합니다. Line 168의 const purposes = getPurposes()는 동일한 함수를 다시 호출하는 불필요한 섀도잉으로, no-shadow ESLint 규칙이 활성화된 경우 에러가 됩니다.

🔧 제안하는 수정
-        // purposes를 localStorage에 저장 (장소 추천 카테고리로 사용)
-        const purposes = getPurposes();
-        if (purposes.length > 0) {
+        // purposes를 localStorage에 저장 (장소 추천 카테고리로 사용)
+        if (purposes.length > 0) {

Line 125의 purposes가 이미 if 블록 스코프에서 참조 가능하므로 별도 재선언이 필요 없습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/create/page.tsx` at line 168, Remove the redundant shadowed declaration
"const purposes = getPurposes()" and use the existing purposes variable already
declared earlier; in other words, delete the second call inside the if block so
the code references the outer "purposes" (from getPurposes()) rather than
redeclaring it and causing a no-shadow ESLint error.

if (purposes.length > 0) {
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Header from '../components/header';
import Footer from '../components/footer';
import GlobalModal from '@/components/modal/globalModal';
import QueryProvider from '@/components/providers/queryProvider';
import { GoogleAnalytics } from '@next/third-parties/google';

const pretendard = localFont({
src: [
Expand Down Expand Up @@ -49,6 +50,7 @@ export default function RootLayout({
strategy="beforeInteractive"
/>
</body>
<GoogleAnalytics gaId="G-3FN93H79SZ" />
</html>
);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"@next/third-parties": "^16.1.6",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-query": "^5.90.16",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading