Skip to content
Open
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"@egjs/react-infinitegrid": "^4.12.0",
"@mui/icons-material": "^6.4.4",
"@mui/material": "^6.4.4",
"@sentry/react": "^9.11.0",
"@sentry/tracing": "^7.120.3",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
Expand Down
113 changes: 112 additions & 1 deletion pnpm-lock.yaml

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

44 changes: 44 additions & 0 deletions src/Sentry/instrument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import useAuthStore from '@/stores/authStore';
import * as Sentry from '@sentry/react';

Sentry.init({
dsn: import.meta.env.VITE_SENTRY_URL,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
// 모든 텍스트, input 마스킹을 뺄 수 있지만 보안상으로 위험할 수 있으니 특정 클래스나 속성이 있는 요소에 마스킹을 걸 수 있는 mask 속성을 같이 활용하자!
// 제일 베스트는 전체를 마스킹 처리 하고 보여줄 몇몇 공간만 unmask속성으로 클래스를 따로 지정해줘 마스킹을 풀어주는게 베스트!
maskAllText: false,
maskAllInputs: false,

// mask 속성으로 마스킹 할 부분에만 클래스 부여해서 마스킹 하기(아래 코드처럼 input에 type이 password인 경우만 마스킹 걸 수도 있음)
mask: ['.secret', 'input[type="password"]'],
}),
],

// 아래 옵션은 사용자에게 발생하는 오류에 대해 모니터링 서비스를 선택하는 코드들임
// 0~1의 수치는 사용자 발생 오류의 몇 %를 저장할것인지 정하는 값
// 0.1당 10%임
// 0.1이니깐 사용자 100명중 10명의 오류 세션을 저장하는 형태()
tracesSampleRate: 0.1,
// tracesSampleRate는 사용자가 이용하는 서비스의 성능을 기록하는 속성
replaysOnErrorSampleRate: 0.1,
// replaysOnErrorSampleRate는 세션을 버퍼라는 휘발성 메모리 공간에 보관해두다가 오류가 발생한 시점에 세션만 저장하는 속성

// replaysSessionSampleRate: 0.1,
// replaysSessionSampleRate는 사용자의 서비스 이용 처음부터 끝까지 모든 세션을 저장해두는 속성
// 오류가 없어도 서비스 이용 사항을 세부적으로 추적 가능하지만 비용이 어마무시해서(0.1이 에러샘플녹화 1보다 더 많이 듬) 실배포에선 돈 진짜 많으면 켜두자

// tracePropagationTargets: ["localhost", '백엔드 도메인 주소']
// tracePropagationTargets는 프론트엔드와 백엔드의 퍼포먼스 추적을 연결할때 사용하는 속성으로 백엔드도 Sentry를 사용해야 서로 연결 가능하다.
});

// 오류가 발생한 유저의 username(36.5에선 zipcode가 닉네임임), email 등을 추가해서 사이트에서 오류 필터링이 가능하다.
// 이외에도 id 등의 필터링 속성이 있지만 현재 백엔드 로직상 로그인시에 유저 id에 대한 정보를 안 받아오기 때문에 추가를 못한다... 나중에 요청 드려봐야할듯

Sentry.setUser({
username: useAuthStore.getState().zipCode,
email: useAuthStore.getState().email,
});

export default Sentry;
8 changes: 8 additions & 0 deletions src/apis/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import axios from 'axios';

import useAuthStore from '@/stores/authStore';
import Sentry from '@/Sentry/instrument';
import useToastStore from '@/stores/toastStore';

const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
Expand All @@ -23,6 +25,11 @@ client.interceptors.response.use(
async (error) => {
const logout = useAuthStore.getState().logout;
const isLoggedIn = useAuthStore.getState().isLoggedIn;
Sentry.captureException(error);
useToastStore.getState().setToastActive({
title: '서버에 오류가 발생했습니다.',
toastType: 'Error',
});

const originalRequest = error.config;

Expand All @@ -42,6 +49,7 @@ client.interceptors.response.use(
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
} catch (e) {
// 센트리 에러 전송
return Promise.reject(e);
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/ResultLetter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CATEGORYS } from '../pages/Write/constants';

import LetterWrapper from './LetterWrapper';
import WebpImage from './WebpImage';

export default function ResultLetter({
categoryName = 'CONSOLATION',
Expand All @@ -24,7 +25,9 @@ export default function ResultLetter({
<span className="caption-b text-gray-60">따숨이님께</span>
<span className="caption-r text-gray-80 line-clamp-3 break-all">{title}</span>
</div>
<img src={CATEGORYS[categoryName]} alt="우표" />

<WebpImage src={CATEGORYS[categoryName]} alt="우표" />
{/* <img src={CATEGORYS[categoryName]} alt="우표" /> */}
</div>
<div className="flex flex-col gap-[5px]">
<span className="caption-sb text-gray-60">{today}</span>
Expand Down
34 changes: 34 additions & 0 deletions src/components/WebpImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';

function checkWebpSupport() {
return new Promise((resolve) => {
const webp = new Image();
// 이미지 로드에 따른 이벤트핸들러 부여
webp.onload = () => resolve(webp.width > 0 && webp.height > 0);
webp.onerror = () => resolve(false);
// 이미지 로드 시도
webp.src =
'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
});
}

export default function WebpImage({
src,
alt = '',
className = '',
}: {
src: string;
alt: string;
className?: string;
}) {
const [imageSrc, setImageSrc] = useState(src);

useEffect(() => {
checkWebpSupport().then((isSupported) => {
console.log(isSupported);
setImageSrc(isSupported ? src : src.replace(/\.\w+$/, '.png'));
});
}, [src]);

return <img src={imageSrc} alt={alt} className={className} />;
}
2 changes: 2 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { BrowserRouter } from 'react-router';
import App from './App';
import './styles/index.css';

import './Sentry/instrument';

const queryClient = new QueryClient();
queryClient.setDefaultOptions({
queries: {
Expand Down
2 changes: 2 additions & 0 deletions src/pages/Auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const AuthCallbackPage = () => {
const logout = useAuthStore((state) => state.logout);
const setAccessToken = useAuthStore((state) => state.setAccessToken);
const setZipCode = useAuthStore((state) => state.setZipCode);
const setEmail = useAuthStore((state) => state.setEmail);
const setIsAdmin = useAuthStore((state) => state.setIsAdmin);
const navigate = useNavigate();
let accessToken = '';
Expand Down Expand Up @@ -42,6 +43,7 @@ const AuthCallbackPage = () => {
const zipCodeResponse = await getMydata();
if (!zipCodeResponse) throw new Error('Error fetching user data');
setZipCode(zipCodeResponse.data.data.zipCode);
setEmail(zipCodeResponse.data.data.email);
}
break;

Expand Down
11 changes: 10 additions & 1 deletion src/pages/Landing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useAuthStore from '@/stores/authStore';
import useThemeStore from '@/stores/themeStore';

import { STYLE_CLASS } from './constants';
import WebpImage from '@/components/WebpImage';

const Landing = () => {
const [step, setStep] = useState(0);
Expand All @@ -23,14 +24,22 @@ const Landing = () => {

return (
<main className="relative flex grow justify-center" onClick={() => setStep((prev) => prev + 1)}>
<img
<WebpImage
src={theme === 'light' ? LandingImg : LandingImgDark}
alt="서비스 소개 이미지"
className={twMerge(
'fixed bottom-0 h-70 w-auto max-w-none -translate-x-1/2 transition-all duration-200',
STYLE_CLASS[step].imagePosition,
)}
/>
{/* <img
src={theme === 'light' ? LandingImg : LandingImgDark}
alt="서비스 소개 이미지"
className={twMerge(
'fixed bottom-0 h-70 w-auto max-w-none -translate-x-1/2 transition-all duration-200',
STYLE_CLASS[step].imagePosition,
)}
/> */}
<section
className={twMerge(
'fixed z-1 -translate-x-1/2 transition-all duration-200',
Expand Down
3 changes: 0 additions & 3 deletions src/pages/Write/CategorySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,8 @@ export default function CategorySelect({
setSend(true);
setToastActive({ title: '편지 전송을 완료했습니다.', toastType: 'Success' });
} else if (res?.status === 400) {
// 일단 에러 발생하면 무조건 검열단어라고 토스트를 띄웠는데 후에 에러 처리 수정해야함
setToastActive({ title: '편지에 검열 단어가 포함되어있습니다.', toastType: 'Error' });
setStep('edit');
} else {
setToastActive({ title: '편지 전송과정에 오류가 발생했습니다.', toastType: 'Error' });
}
};

Expand Down
11 changes: 10 additions & 1 deletion src/pages/Write/components/ThemeOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { twMerge } from 'tailwind-merge';
import useWrite from '@/stores/writeStore';

import { CATEGORY_LIST } from '../constants';
import WebpImage from '@/components/WebpImage';

export default function ThemeOption() {
const letterRequest = useWrite((state) => state.letterRequest);
Expand All @@ -20,14 +21,22 @@ export default function ThemeOption() {
aria-label="편지 테마 설정하기"
>
<span className="caption-m">{target.name}</span>
<img
<WebpImage
src={target.src}
alt="테마 이미지"
className={twMerge(
'w-full',
letterRequest.paperType === target.paperType && 'border-primary-1-hover border-2',
)}
/>
{/* <img
src={target.src}
alt="테마 이미지"
className={twMerge(
'w-full',
letterRequest.paperType === target.paperType && 'border-primary-1-hover border-2',
)}
/> */}
</button>
);
})}
Expand Down
Loading