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
6 changes: 5 additions & 1 deletion src/apis/chat/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ export interface ChatMessage {
isMine: boolean;
}

export interface SendChatMessageRequest {
chatRoomId: number;
content: string;
}

export interface ChatMessageRequestParam extends PaginationParams {
chatRoomId: number;
type: 'DIRECT' | 'GROUP';
}

export interface ChatMessagesResponse extends PaginationResponse {
Expand Down
7 changes: 3 additions & 4 deletions src/apis/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ChatMessagesResponse,
ChatRoomsResponse,
CreateChatRoomResponse,
SendChatMessageRequest,
} from './entity';

export const getChatRooms = async () => {
Expand All @@ -22,9 +23,8 @@ export const postChatRooms = async (userId: number) => {
return response;
};

export const postChatMessage = async (chatRoomId: number, type: 'DIRECT' | 'GROUP', content: string) => {
export const postChatMessage = async ({ chatRoomId, content }: SendChatMessageRequest) => {
return apiClient.post<ChatMessage>(`chats/rooms/${chatRoomId}/messages`, {
params: { type },
body: { content },
requiresAuth: true,
});
Expand All @@ -37,9 +37,8 @@ export const getChatMessages = async ({ chatRoomId, ...query }: ChatMessageReque
});
};

export const postChatMute = async (chatRoomId: number, type: 'DIRECT' | 'GROUP') => {
export const postChatMute = async (chatRoomId: number) => {
return apiClient.post<{ isMuted: boolean }>(`chats/rooms/${chatRoomId}/mute`, {
params: { type },
requiresAuth: true,
});
};
Expand Down
4 changes: 2 additions & 2 deletions src/config/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function initSentry() {
matchRoutes,
}),
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
maskAllText: false,
blockAllMedia: false,
}),
Comment on lines 38 to 41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

개인정보 노출 위험 확인 필요

maskAllText: falseblockAllMedia: false 설정으로 Sentry 리플레이에서 사용자 텍스트와 미디어가 그대로 녹화됩니다. 채팅 내용 등 민감한 정보가 Sentry에 전송될 수 있어 GDPR/개인정보보호 관점에서 검토가 필요합니다.

의도된 변경이라면 특정 요소만 마스킹하는 mask 옵션 사용을 권장합니다.

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

In `@src/config/sentry.ts` around lines 38 - 41, Sentry.replayIntegration 설정에서
maskAllText: false 및 blockAllMedia: false로 인해 사용자 채팅·미디어 등 민감한 정보가 전송될 위험이 있으니
Sentry.replayIntegration 호출(Sentry.replayIntegration)에서 민감 데이터가 캡처되지 않도록
maskAllText와 blockAllMedia를 true로 변경하거나, 의도된 경우 특정 요소만 마스킹하도록 mask 옵션을 사용해 민감한
셀렉터(예: 채팅 입력, 프로필 이미지 등)를 명시적으로 제외하도록 설정을 업데이트하세요.

],
tracePropagationTargets: ['localhost', ...(import.meta.env.VITE_API_PATH ? [import.meta.env.VITE_API_PATH] : [])],
Expand Down
45 changes: 10 additions & 35 deletions src/pages/Chat/hooks/useChat.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { useMemo } from 'react';
import { useMutation, useSuspenseQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { getChatMessages, getChatRooms, postChatMessage, postChatRooms, postChatMute } from '@/apis/chat';
import { useGetClubMembers } from '@/pages/Club/ClubDetail/hooks/useGetClubMembers';

type ChatType = 'DIRECT' | 'GROUP';

export const chatQueryKeys = {
all: ['chat'] as const,
rooms: () => [...chatQueryKeys.all, 'rooms'] as const,
messages: (chatRoomId: number, type: ChatType) => [...chatQueryKeys.all, 'messages', chatRoomId, type] as const,
messages: (chatRoomId: number) => [...chatQueryKeys.all, 'messages', chatRoomId] as const,
};

const useChat = (chatRoomId?: number) => {
Expand All @@ -20,11 +17,6 @@ const useChat = (chatRoomId?: number) => {
refetchInterval: 5000,
});

const currentRoomType: ChatType | undefined = useMemo(() => {
if (!chatRoomId) return undefined;
return chatRoomList.rooms.find((room) => room.roomId === chatRoomId)?.chatType;
}, [chatRoomId, chatRoomList.rooms]);

const { mutateAsync: createChatRoom } = useMutation({
mutationKey: ['createChatRoom'],
mutationFn: (userId: number) => postChatRooms(userId),
Expand All @@ -36,17 +28,13 @@ const useChat = (chatRoomId?: number) => {
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey:
chatRoomId && currentRoomType
? chatQueryKeys.messages(chatRoomId, currentRoomType)
: ['chat', 'messages', 'disabled'],
queryKey: chatRoomId ? chatQueryKeys.messages(chatRoomId) : ['chat', 'messages', 'disabled'],

enabled: !!chatRoomId && !!currentRoomType,
enabled: !!chatRoomId,

queryFn: ({ pageParam }) =>
getChatMessages({
chatRoomId: chatRoomId!,
type: currentRoomType!,
page: pageParam,
limit: 20,
}),
Expand All @@ -62,28 +50,15 @@ const useChat = (chatRoomId?: number) => {

const totalUnreadCount = chatRoomList.rooms.reduce((sum, room) => sum + room.unreadCount, 0);

const { mutateAsync: sendMessage } = useMutation<
Awaited<ReturnType<typeof postChatMessage>>,
Error,
{ chatRoomId: number; content: string }
>({
const { mutate: sendMessage } = useMutation({
mutationKey: ['sendMessage', chatRoomId],

mutationFn: async ({ chatRoomId, content }) => {
if (!currentRoomType) {
throw new Error('chatType is missing');
}

return postChatMessage(chatRoomId, currentRoomType, content);
},
mutationFn: postChatMessage,

onSuccess: () => {
if (!chatRoomId || !currentRoomType) return;

if (!chatRoomId) return;
queryClient.invalidateQueries({
queryKey: chatQueryKeys.messages(chatRoomId, currentRoomType),
queryKey: chatQueryKeys.messages(chatRoomId),
});

queryClient.invalidateQueries({
queryKey: chatQueryKeys.rooms(),
});
Expand All @@ -97,11 +72,11 @@ const useChat = (chatRoomId?: number) => {
const { mutateAsync: toggleMute } = useMutation({
mutationKey: ['toggleMute', chatRoomId],
mutationFn: async () => {
if (!chatRoomId || !currentRoomType) {
throw new Error('chatRoomId or type missing');
if (!chatRoomId) {
throw new Error('chatRoomId is missing');
}

return postChatMute(chatRoomId, currentRoomType);
return postChatMute(chatRoomId);
},
onSuccess: () => {
queryClient.invalidateQueries({
Expand Down
14 changes: 8 additions & 6 deletions src/pages/Guide/hooks/useGuideSlider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ export function useGuideSlider(total: number) {
const navigate = useNavigate();
const [index, setIndex] = useState(0);

const isLast = index === total - 1;
const isLast = total > 0 && index === total - 1;

const next = useCallback(() => {
if (total <= 0) return;

if (isLast) {
navigate('/home');
return;
}
setIndex((prev) => prev + 1);
}, [isLast, navigate]);

setIndex((prev) => Math.min(prev + 1, total - 1));
}, [isLast, navigate, total]);

const prev = useCallback(() => {
if (index === 0) return;
setIndex((prev) => prev - 1);
}, [index]);
setIndex((prev) => Math.max(prev - 1, 0));
}, []);

return {
index,
Expand Down
28 changes: 3 additions & 25 deletions src/pages/Guide/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import { useEffect, useRef } from 'react';
import { GUIDE_ITEMS } from './guideData';
import GuideProgressBar from './GuideProgressBar';
import { useGuideSlider } from './hooks/useGuideSlider';
import { useSwipe } from './hooks/useSwipe';

function GuidePage() {
const timerRef = useRef<number | null>(null);

const { index, next, prev } = useGuideSlider(GUIDE_ITEMS.length);
const { onTouchStart, onTouchEnd } = useSwipe(next, prev);

const item = GUIDE_ITEMS[index];

useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}

timerRef.current = window.setTimeout(() => {
next();
}, item.duration ?? 3000);

return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [index, item.duration, next]);
if (!item) return null;
const duration = item.duration ?? 3000;

return (
<div
Expand All @@ -36,12 +19,7 @@ function GuidePage() {
>
<div className="relative flex-1 overflow-hidden">
<div className="absolute top-0 right-0 left-0 z-30">
<GuideProgressBar
total={GUIDE_ITEMS.length}
current={index}
duration={item.duration ?? 3000}
onComplete={next}
/>
<GuideProgressBar total={GUIDE_ITEMS.length} current={index} duration={duration} onComplete={next} />
</div>

<img
Expand Down