diff --git a/package.json b/package.json
index 25f66ab..11dfa02 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8689848..0f97393 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,12 @@ importers:
'@mui/material':
specifier: ^6.4.4
version: 6.4.4(@emotion/react@11.14.0(@types/react@19.0.8)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.8)(react@18.3.1))(@types/react@19.0.8)(react@18.3.1))(@types/react@19.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@sentry/react':
+ specifier: ^9.11.0
+ version: 9.11.0(react@18.3.1)
+ '@sentry/tracing':
+ specifier: ^7.120.3
+ version: 7.120.3
'@tailwindcss/vite':
specifier: ^4.0.6
version: 4.0.6(vite@6.1.0(jiti@2.4.2)(lightningcss@1.29.1))
@@ -708,6 +714,56 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@sentry-internal/browser-utils@9.11.0':
+ resolution: {integrity: sha512-XS71kRf7lw5St/Jc9G2Viy1cKgqGoPHqUAykXEtFt38JVXdf1TY/dSbKv/PAgNqMvC1xvdTsN0HF/81o7DNUEA==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/feedback@9.11.0':
+ resolution: {integrity: sha512-50KiRmrF1Ldr+KoRawqcCYVk7TAVP8K/I81Jk9YWwlp1+Pu1ArpYDmTNCLXTgoyiyO38aHefKGZJX6AKFuSsUQ==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/replay-canvas@9.11.0':
+ resolution: {integrity: sha512-ZcRg8TWfF0ucjK2i+4TY/blRNJ7YKrgMpx19pFj6eCOJ1K8geSkAFPIfDHcQEwIU1ZTN+HiCwx0JvTI9YZxjfg==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/replay@9.11.0':
+ resolution: {integrity: sha512-0k24h58O/2VQw1dwT/zQiWvUzLNQxpxbrVN/MYPT7czSEhI+1bX8fxMHXZORl2JqhetImMXzxH3XkuHQPEqQMg==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/tracing@7.120.3':
+ resolution: {integrity: sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==}
+ engines: {node: '>=8'}
+
+ '@sentry/browser@9.11.0':
+ resolution: {integrity: sha512-DSDj8wQJoiLqqOcntl+7phjd8l8KN9A0vaV7mZNHWbrHU3MVwXqTyLyERRLC6wi0t7F5kqczqa3xLmKjK/fMZg==}
+ engines: {node: '>=18'}
+
+ '@sentry/core@7.120.3':
+ resolution: {integrity: sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==}
+ engines: {node: '>=8'}
+
+ '@sentry/core@9.11.0':
+ resolution: {integrity: sha512-qfb4ahGZubbrNh1MnbEqyHFp87rIwQIZapyQLCaYpudXrP1biEpLOV3mMDvDJWCdX460hoOwQ3SkwipV3We/7w==}
+ engines: {node: '>=18'}
+
+ '@sentry/react@9.11.0':
+ resolution: {integrity: sha512-sH/3KnDsLxBFRoxPyIpab7OewkfStdZQQwgpfv8R0yDKpGg4lU7KdTccryFvWL123UpHC7ydPWjdcfC8YV/EPQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.14.0 || 17.x || 18.x || 19.x
+
+ '@sentry/tracing@7.120.3':
+ resolution: {integrity: sha512-B7bqyYFgHuab1Pn7w5KXsZP/nfFo4VDBDdSXDSWYk5+TYJ3IDruO3eJFhOrircfsz4YwazWm9kbeZhkpsHDyHg==}
+ engines: {node: '>=8'}
+
+ '@sentry/types@7.120.3':
+ resolution: {integrity: sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==}
+ engines: {node: '>=8'}
+
+ '@sentry/utils@7.120.3':
+ resolution: {integrity: sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==}
+ engines: {node: '>=8'}
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
engines: {node: '>=14'}
@@ -2946,6 +3002,62 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@sentry-internal/browser-utils@9.11.0':
+ dependencies:
+ '@sentry/core': 9.11.0
+
+ '@sentry-internal/feedback@9.11.0':
+ dependencies:
+ '@sentry/core': 9.11.0
+
+ '@sentry-internal/replay-canvas@9.11.0':
+ dependencies:
+ '@sentry-internal/replay': 9.11.0
+ '@sentry/core': 9.11.0
+
+ '@sentry-internal/replay@9.11.0':
+ dependencies:
+ '@sentry-internal/browser-utils': 9.11.0
+ '@sentry/core': 9.11.0
+
+ '@sentry-internal/tracing@7.120.3':
+ dependencies:
+ '@sentry/core': 7.120.3
+ '@sentry/types': 7.120.3
+ '@sentry/utils': 7.120.3
+
+ '@sentry/browser@9.11.0':
+ dependencies:
+ '@sentry-internal/browser-utils': 9.11.0
+ '@sentry-internal/feedback': 9.11.0
+ '@sentry-internal/replay': 9.11.0
+ '@sentry-internal/replay-canvas': 9.11.0
+ '@sentry/core': 9.11.0
+
+ '@sentry/core@7.120.3':
+ dependencies:
+ '@sentry/types': 7.120.3
+ '@sentry/utils': 7.120.3
+
+ '@sentry/core@9.11.0': {}
+
+ '@sentry/react@9.11.0(react@18.3.1)':
+ dependencies:
+ '@sentry/browser': 9.11.0
+ '@sentry/core': 9.11.0
+ hoist-non-react-statics: 3.3.2
+ react: 18.3.1
+
+ '@sentry/tracing@7.120.3':
+ dependencies:
+ '@sentry-internal/tracing': 7.120.3
+
+ '@sentry/types@7.120.3': {}
+
+ '@sentry/utils@7.120.3':
+ dependencies:
+ '@sentry/types': 7.120.3
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.9)':
dependencies:
'@babel/core': 7.26.9
@@ -3910,7 +4022,6 @@ snapshots:
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
- optional: true
ignore@5.3.2: {}
diff --git a/src/Sentry/instrument.ts b/src/Sentry/instrument.ts
new file mode 100644
index 0000000..8c14e4b
--- /dev/null
+++ b/src/Sentry/instrument.ts
@@ -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;
diff --git a/src/apis/client.ts b/src/apis/client.ts
index 69fe0dd..bcd976e 100644
--- a/src/apis/client.ts
+++ b/src/apis/client.ts
@@ -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,
@@ -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;
@@ -42,6 +49,7 @@ client.interceptors.response.use(
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
} catch (e) {
+ // 센트리 에러 전송
return Promise.reject(e);
}
}
diff --git a/src/components/ResultLetter.tsx b/src/components/ResultLetter.tsx
index c150e45..8db0a2e 100644
--- a/src/components/ResultLetter.tsx
+++ b/src/components/ResultLetter.tsx
@@ -1,6 +1,7 @@
import { CATEGORYS } from '../pages/Write/constants';
import LetterWrapper from './LetterWrapper';
+import WebpImage from './WebpImage';
export default function ResultLetter({
categoryName = 'CONSOLATION',
@@ -24,7 +25,9 @@ export default function ResultLetter({
따숨이님께
{title}
-
+
+
*/}