⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
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
69 changes: 34 additions & 35 deletions hooks/use-webview-message-handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { WebViewMessage, WebViewMessageEvent, WebViewMessageTypes } from '@/types/webview-message.types';
import { useCallback } from 'react';
import {
WebViewMessage,
WebViewMessageEvent,
WebViewMessageTypes,
} from "@/types/webview-message.types";
import { useCallback } from "react";

interface UseWebViewMessageHandlerOptions {
// 뒤로가기 요청 시 호출
Expand All @@ -8,50 +12,45 @@ interface UseWebViewMessageHandlerOptions {
onSubscribe?: (clubId: string, clubName?: string) => Promise<void> | void;
// 알림 구독 해제 요청 시 호출
onUnsubscribe?: (clubId: string) => Promise<void> | void;
// 공유하기 요청 시 호출
onShare?: (payload: { title: string; text: string; url: string }) => Promise<void> | void;
}

// WebView 메시지를 처리하는 Hook
export const useWebViewMessageHandler = ({
onNavigateBack,
onSubscribe,
onUnsubscribe,
onShare,
}: UseWebViewMessageHandlerOptions) => {
const handleMessage = useCallback((event: WebViewMessageEvent) => {
try {
const data = event.nativeEvent.data;
if (!data) return;
const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
try {
const data = event.nativeEvent.data;
if (!data) return;

const message: WebViewMessage = JSON.parse(data);
const message: WebViewMessage = JSON.parse(data);

switch (message.type) {
case WebViewMessageTypes.NAVIGATE_BACK:
onNavigateBack?.();
break;
case WebViewMessageTypes.NOTIFICATION_SUBSCRIBE:
if (message.payload?.clubId) {
onSubscribe?.(message.payload.clubId, message.payload.clubName);
}
break;
case WebViewMessageTypes.NOTIFICATION_UNSUBSCRIBE:
if (message.payload?.clubId) {
onUnsubscribe?.(message.payload.clubId);
}
break;
case WebViewMessageTypes.SHARE:
if (message.payload) {
onShare?.(message.payload);
}
break;
default:
console.warn('[WebViewHandler] 알 수 없는 메시지 타입:', message);
switch (message.type) {
case WebViewMessageTypes.NAVIGATE_BACK:
onNavigateBack?.();
break;
case WebViewMessageTypes.NOTIFICATION_SUBSCRIBE:
if (message.payload?.clubId) {
onSubscribe?.(message.payload.clubId, message.payload.clubName);
}
break;
case WebViewMessageTypes.NOTIFICATION_UNSUBSCRIBE:
if (message.payload?.clubId) {
onUnsubscribe?.(message.payload.clubId);
}
break;
default:
console.warn("[WebViewHandler] 알 수 없는 메시지 타입:", message);
}
} catch (error) {
console.error("[WebViewHandler] 메시지 파싱 오류:", error);
}
} catch (error) {
console.error('[WebViewHandler] 메시지 파싱 오류:', error);
}
}, [onNavigateBack, onSubscribe, onUnsubscribe, onShare]);
},
[onNavigateBack, onSubscribe, onUnsubscribe],
);

return { handleMessage };
};
21 changes: 11 additions & 10 deletions types/webview-message.types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@

//WebView 메시지 타입 상수
export const WebViewMessageTypes = {
NAVIGATE_BACK: 'NAVIGATE_BACK',
NOTIFICATION_SUBSCRIBE: 'NOTIFICATION_SUBSCRIBE',
NOTIFICATION_UNSUBSCRIBE: 'NOTIFICATION_UNSUBSCRIBE',
SHARE: 'SHARE',
NAVIGATE_BACK: "NAVIGATE_BACK",
NOTIFICATION_SUBSCRIBE: "NOTIFICATION_SUBSCRIBE",
NOTIFICATION_UNSUBSCRIBE: "NOTIFICATION_UNSUBSCRIBE",
SHARE: "SHARE",
} as const;
Comment on lines 2 to 7
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SHARE 상수 제거로 타입/정책 일관성 유지

SHARE 유니온이 제거됐는데 상수는 남아 있어 레거시 사용이 계속될 수 있습니다. 의도대로면 상수도 제거해 API 표면을 정리하는 편이 안전합니다.

🧹 Proposed fix
 export const WebViewMessageTypes = {
   NAVIGATE_BACK: "NAVIGATE_BACK",
   NOTIFICATION_SUBSCRIBE: "NOTIFICATION_SUBSCRIBE",
   NOTIFICATION_UNSUBSCRIBE: "NOTIFICATION_UNSUBSCRIBE",
-  SHARE: "SHARE",
 } as const;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const WebViewMessageTypes = {
NAVIGATE_BACK: 'NAVIGATE_BACK',
NOTIFICATION_SUBSCRIBE: 'NOTIFICATION_SUBSCRIBE',
NOTIFICATION_UNSUBSCRIBE: 'NOTIFICATION_UNSUBSCRIBE',
SHARE: 'SHARE',
NAVIGATE_BACK: "NAVIGATE_BACK",
NOTIFICATION_SUBSCRIBE: "NOTIFICATION_SUBSCRIBE",
NOTIFICATION_UNSUBSCRIBE: "NOTIFICATION_UNSUBSCRIBE",
SHARE: "SHARE",
} as const;
export const WebViewMessageTypes = {
NAVIGATE_BACK: "NAVIGATE_BACK",
NOTIFICATION_SUBSCRIBE: "NOTIFICATION_SUBSCRIBE",
NOTIFICATION_UNSUBSCRIBE: "NOTIFICATION_UNSUBSCRIBE",
} as const;
🤖 Prompt for AI Agents
In `@types/webview-message.types.ts` around lines 2 - 7, The WebViewMessageTypes
object still includes the obsolete SHARE entry; remove the SHARE: "SHARE"
property from the WebViewMessageTypes const (and any direct references to
WebViewMessageTypes.SHARE) so the runtime constants match the updated union/type
surface and prevent legacy usage—update or delete any code that still references
WebViewMessageTypes.SHARE to use the new approved message types (e.g.,
NAVIGATE_BACK, NOTIFICATION_SUBSCRIBE, NOTIFICATION_UNSUBSCRIBE) or refactor
callers to the new API.


// WebView 메시지 Discriminated Union 타입

export type WebViewMessage =
| { type: 'NAVIGATE_BACK' }
| { type: 'NOTIFICATION_SUBSCRIBE'; payload: { clubId: string; clubName?: string } }
| { type: 'NOTIFICATION_UNSUBSCRIBE'; payload: { clubId: string } }
| { type: 'SHARE'; payload: { title: string; text: string; url: string } };
| { type: "NAVIGATE_BACK" }
| {
type: "NOTIFICATION_SUBSCRIBE";
payload: { clubId: string; clubName?: string };
}
| { type: "NOTIFICATION_UNSUBSCRIBE"; payload: { clubId: string } };

// WebView 메시지 이벤트 타입 (react-native-webview)
export interface WebViewMessageEvent {
Expand Down
70 changes: 33 additions & 37 deletions ui/club-detail/club-detail-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { MoaImage } from '@/components/moa-image';
import { MoaText } from '@/components/moa-text';
import { PermissionDialog } from '@/components/permission-dialog';
import { USER_EVENT } from '@/constants/eventname';
import { useMixpanelContext } from '@/contexts';
import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context';
import { useMixpanelTrack, useWebViewMessageHandler } from '@/hooks';
import { Ionicons } from '@expo/vector-icons';
import Constants from 'expo-constants';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useMemo, useState } from 'react';
import { ActivityIndicator, Platform, Share, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView } from 'react-native-webview';
import styled from 'styled-components/native';
import { MoaImage } from "@/components/moa-image";
import { MoaText } from "@/components/moa-text";
import { PermissionDialog } from "@/components/permission-dialog";
import { USER_EVENT } from "@/constants/eventname";
import { useMixpanelContext } from "@/contexts";
import { useSubscribedClubsContext } from "@/contexts/subscribed-clubs-context";
import { useMixpanelTrack, useWebViewMessageHandler } from "@/hooks";
import { Ionicons } from "@expo/vector-icons";
import Constants from "expo-constants";
import { useLocalSearchParams, useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useMemo, useState } from "react";
import {
ActivityIndicator,
Platform,
TouchableOpacity
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { WebView } from "react-native-webview";
import styled from "styled-components/native";

export default function ClubWebViewScreen() {
const router = useRouter();
Expand All @@ -34,13 +38,13 @@ export default function ClubWebViewScreen() {

const cleanUrl = webviewUrl?.replace(/\/$/, "") || "";
const baseUrl = `${cleanUrl}/club/${id}`;

const params = new URLSearchParams();
if (sessionId) {
params.append('session_id', sessionId);
params.append("session_id", sessionId);
}
if (id && isSubscribed(id)) {
params.append('is_subscribed', 'true');
params.append("is_subscribed", "true");
}

const queryString = params.toString();
Expand Down Expand Up @@ -109,13 +113,13 @@ export default function ClubWebViewScreen() {
trackEvent(USER_EVENT.SUBSCRIBE_BUTTON_CLICKED, {
clubName: clubName || name,
subscribed: true,
from: 'club_detail',
url: 'app://moadong/club',
from: "club_detail",
url: "app://moadong/club",
});

// 이미 구독 중이면 무시
if (isSubscribed(targetId)) return;

const result = await toggleSubscribe(targetId);
if (result.needsPermission) {
setShowPermissionDialog(true);
Expand All @@ -124,27 +128,20 @@ export default function ClubWebViewScreen() {
onUnsubscribe: async (targetId: string) => {
// 구독 중이 아니면 무시
if (!isSubscribed(targetId)) return;

trackEvent(USER_EVENT.SUBSCRIBE_BUTTON_CLICKED, {
clubName: name,
subscribed: false,
from: 'club_detail',
url: 'app://moadong/club',
from: "club_detail",
url: "app://moadong/club",
});

await toggleSubscribe(targetId);
},
onShare: async ({ title, text, url }: { title: string; text: string; url: string }) => {
await Share.share({
title,
message: text,
url,
});
},
});

return (
<Container edges={['bottom']}>
<Container edges={["bottom"]}>
<StatusBar translucent style="dark" />
{hasError && (
<Header>
Expand All @@ -156,8 +153,8 @@ export default function ClubWebViewScreen() {
<MoaImage
source={
subscribed
? require('@/assets/icons/ic-subscribe-selected.png')
: require('@/assets/icons/ic-subscribe-unselected.png')
? require("@/assets/icons/ic-subscribe-selected.png")
: require("@/assets/icons/ic-subscribe-unselected.png")
}
style={{ width: 24, height: 24 }}
contentFit="contain"
Expand All @@ -180,7 +177,6 @@ export default function ClubWebViewScreen() {
showsVerticalScrollIndicator={false}
bounces={false}
overScrollMode="never"

/>
{isLoading && (
<LoadingContainer pointerEvents="none">
Expand Down