diff --git a/src/frontend/.env.sample b/src/frontend/.env.sample new file mode 100644 index 00000000..2d3e9419 --- /dev/null +++ b/src/frontend/.env.sample @@ -0,0 +1,8 @@ +# Sample env file + +VITE_IS_PROD=false +VITE_API_URL=http://localhost:8000/api +VITE_WEBSOCKET_URL=${VITE_API_URL}/chat/ws +VITE_YOUTUBE_API_KEY=sample-youtube-api-key +VITE_SENTRY_DSN=sample-sentry-dsn +SENTRY_AUTH_TOKEN=sample-sentry-auth-token diff --git a/src/frontend/README.md b/src/frontend/README.md index 74872fd4..26fdfd54 100644 --- a/src/frontend/README.md +++ b/src/frontend/README.md @@ -1,50 +1,92 @@ -# React + TypeScript + Vite +# KickTube Frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +KickTube는 실시간 유튜브 영상 공유 및 채팅 플랫폼입니다. -Currently, two official plugins are available: +## 기술 스택 -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +### Core -## Expanding the ESLint configuration +- React 18.3 +- TypeScript 5.6 +- Vite 6.0 -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +### 상태 관리 -- Configure the top-level `parserOptions` property like this: +- Zustand 5.0 (전역 상태 관리) +- TanStack Query 5.64 (서버 상태 관리) -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) -``` +### 스타일링 + +- Styled Components 6.1 +- CSS Variables (Design Tokens) + +### 라우팅 + +- React Router 7.1 + +### 실시간 통신 + +- SockJS 1.5 +- StompJS 2.3 + +### 개발 도구 + +- ESLint 9.18 +- Prettier 3.4 +- Storybook 8.4 +- Vitest 2.1 (테스트) +- Sentry 8.48 + +## 환경 변수 + +프로젝트 실행을 위해 다음 환경 변수가 필요합니다: + +- `VITE_API_URL`: 백엔드 API 엔드포인트 +- `VITE_WEBSOCKET_URL`: 웹소켓 엔드포인트 +- `VITE_YOUTUBE_API_KEY`: 유튜브 API 키 +- `VITE_SENTRY_DSN`: Sentry DSN +- `VITE_SENTRY_AUTH_TOKEN`: Sentry 인증 토큰 +- `VITE_IS_PROD`: 프로덕션 환경 여부 +이러한 환경 변수는 프로젝트 루트에 `.env` 파일을 생성하여 설정할 수 있습니다. + +## 시작하기 -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from 'eslint-plugin-react' - -export default tseslint.config({ - // Set the react version - settings: { react: { version: '18.3' } }, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - }, -}) +```bash +# 의존성 설치 +pnpm install + +# 개발 서버 실행 +pnpm run dev + +# 빌드 +pnpm run build + +# 프로덕션 서버 실행 +pnpm run storybook ``` + +## 프로젝트 구조 + + +### 주요 기능 + +- **실시간 채팅**: WebSocket과 Stomp 프로토콜을 통한 실시간 채팅 기능 +- **유튜브 영상 공유**: 실시간 유튜브 영상 공유 및 동기화 +- **방 관리**: 공개/비공개 방 생성 및 관리 +- **사용자 관리**: 프로필 관리 및 친구 시스템 +- **검색**: Elasticsearch를 통한 유저, 방 검색 기능 + +### 상태 관리 + +- `useAuthStore`: 엑세스 토큰 및 리프레시 토큰 관리 +- `useUserStore`: 사용자 정보 관리 +- `useMyRoomsStore`: 사용자의 방 목록 관리 +- `useCurrentRoomStore`: 현재 접속한 방 정보 관리 +- `useWebSocketStore`: 웹소켓 연결 및 실시간 통신 관리 +- `useVideoStore`: 유튜브 영상 관리 + + +### 스타일 가이드 + +- CSS Variables를 통한 디자인 토큰 관리 +- Styled Components를 활용한 컴포넌트 스타일링 diff --git a/src/frontend/package.json b/src/frontend/package.json index b608f992..6aeb771c 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -14,6 +14,7 @@ "dependencies": { "@sentry/browser": "^8.48.0", "@sentry/react": "^8.48.0", + "@sentry/vite-plugin": "^3.2.0", "@tanstack/react-query": "^5.64.1", "@types/sockjs-client": "^1.5.4", "@types/stompjs": "^2.3.9", diff --git a/src/frontend/pnpm-lock.yaml b/src/frontend/pnpm-lock.yaml index 0bea3881..af1aa8b1 100644 --- a/src/frontend/pnpm-lock.yaml +++ b/src/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@sentry/react': specifier: ^8.48.0 version: 8.48.0(react@18.3.1) + '@sentry/vite-plugin': + specifier: ^3.2.0 + version: 3.2.0 '@tanstack/react-query': specifier: ^5.64.1 version: 5.64.1(react@18.3.1) @@ -805,10 +808,64 @@ packages: resolution: {integrity: sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==} engines: {node: '>=14.18'} + '@sentry/babel-plugin-component-annotate@3.2.0': + resolution: {integrity: sha512-Sg7nLRP1yiJYl/KdGGxYGbjvLq5rswyeB5yESgfWX34XUNZaFgmNvw4pU/QEKVeYgcPyOulgJ+y80ewujyffTA==} + engines: {node: '>= 14'} + '@sentry/browser@8.48.0': resolution: {integrity: sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==} engines: {node: '>=14.18'} + '@sentry/bundler-plugin-core@3.2.0': + resolution: {integrity: sha512-Q/ogVylue3XaFawyIxzuiic+7Dp4w63eJtRtVH8VBebNURyJ/re4GVoP1QNGccE1R243tXY1y2GiwqiJkAONOg==} + engines: {node: '>= 14'} + + '@sentry/cli-darwin@2.41.1': + resolution: {integrity: sha512-7pS3pu/SuhE6jOn3wptstAg6B5nUP878O6s+2svT7b5fKNfYUi/6NPK6dAveh2Ca0rwVq40TO4YFJabWMgTpdQ==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.41.1': + resolution: {integrity: sha512-EzYCEnnENBnS5kpNW+2dBcrPZn1MVfywh2joGVQZTpmgDL5YFJ59VOd+K0XuEwqgFI8BSNI14KXZ75s4DD1/Vw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd] + + '@sentry/cli-linux-arm@2.41.1': + resolution: {integrity: sha512-wNUvquD6qjOCczvuBGf9OiD29nuQ6yf8zzfyPJa5Bdx1QXuteKsKb6HBrMwuIR3liyuu0duzHd+H/+p1n541Hg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd] + + '@sentry/cli-linux-i686@2.41.1': + resolution: {integrity: sha512-urpQCWrdYnSAsZY3udttuMV88wTJzKZL10xsrp7sjD/Hd+O6qSLVLkxebIlxts70jMLLFHYrQ2bkRg5kKuX6Fg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd] + + '@sentry/cli-linux-x64@2.41.1': + resolution: {integrity: sha512-ZqpYwHXAaK4MMEFlyaLYr6mJTmpy9qP6n30jGhLTW7kHKS3s6GPLCSlNmIfeClrInEt0963fM633ZRnXa04VPw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd] + + '@sentry/cli-win32-i686@2.41.1': + resolution: {integrity: sha512-AuRimCeVsx99DIOr9cwdYBHk39tlmAuPDdy2r16iNzY0InXs4xOys4gGzM7N4vlFQvFkzuc778Su0HkfasgprA==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.41.1': + resolution: {integrity: sha512-6JcPvXGye61+wPp0xdzfc2YLE/Dcud8JdaK8VxLM3b/8+Em7E+UyliDu3uF8+YGUqizY5JYTd3fs17DC8DZhLw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.41.1': + resolution: {integrity: sha512-0GVmDiTV7R1492wkVY4bGcfC0fSmRmQjuxaaPI8CIV9B2VP9pBVCUizi1mevXaaE4I3fM60LI+XYrKFEneuVog==} + engines: {node: '>= 10'} + hasBin: true + '@sentry/core@8.48.0': resolution: {integrity: sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==} engines: {node: '>=14.18'} @@ -819,6 +876,10 @@ packages: peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x + '@sentry/vite-plugin@3.2.0': + resolution: {integrity: sha512-IVBoAzZmpoX9+mnmIMq2ndxlFPoWMuYSE5Mek5zOWpYh+GbPxvkrxvM+vg0HeLH4r5v9Tm0FWcEZDgDIZqtoSg==} + engines: {node: '>= 14'} + '@storybook/addon-actions@8.4.7': resolution: {integrity: sha512-mjtD5JxcPuW74T6h7nqMxWTvDneFtokg88p6kQ5OnC1M259iAXb//yiSZgu/quunMHPCXSiqn4FNOSgASTSbsA==} peerDependencies: @@ -1257,6 +1318,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -1280,6 +1345,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1315,6 +1384,10 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1379,6 +1452,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chromatic@11.24.0: resolution: {integrity: sha512-IocIxnxera27bRA51HHtfV9/Daqgj0E4BWYV12/nlf7qPJW1T82raQ5ypxZDJA4bXQywxOzpG2e6WWYvz/AwHA==} hasBin: true @@ -1506,6 +1583,10 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1746,6 +1827,9 @@ packages: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1778,6 +1862,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -1833,6 +1921,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1864,6 +1956,10 @@ packages: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -1987,6 +2083,9 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.0.2: resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} engines: {node: 20 || >=22} @@ -2010,6 +2109,10 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} @@ -2043,6 +2146,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2050,6 +2157,14 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -2067,6 +2182,15 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -2074,6 +2198,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + nwsapi@2.2.16: resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} @@ -2115,6 +2243,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2173,6 +2305,10 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2244,6 +2380,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + recast@0.23.9: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} @@ -2493,6 +2633,9 @@ packages: resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -2553,6 +2696,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unplugin@1.0.1: + resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + unplugin@1.16.1: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} @@ -2688,10 +2834,20 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2719,6 +2875,9 @@ packages: resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-typed-array@1.1.18: resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} engines: {node: '>= 0.4'} @@ -3304,6 +3463,8 @@ snapshots: '@sentry-internal/browser-utils': 8.48.0 '@sentry/core': 8.48.0 + '@sentry/babel-plugin-component-annotate@3.2.0': {} + '@sentry/browser@8.48.0': dependencies: '@sentry-internal/browser-utils': 8.48.0 @@ -3312,6 +3473,60 @@ snapshots: '@sentry-internal/replay-canvas': 8.48.0 '@sentry/core': 8.48.0 + '@sentry/bundler-plugin-core@3.2.0': + dependencies: + '@babel/core': 7.26.0 + '@sentry/babel-plugin-component-annotate': 3.2.0 + '@sentry/cli': 2.41.1 + dotenv: 16.4.7 + find-up: 5.0.0 + glob: 9.3.5 + magic-string: 0.30.8 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.41.1': + optional: true + + '@sentry/cli-linux-arm64@2.41.1': + optional: true + + '@sentry/cli-linux-arm@2.41.1': + optional: true + + '@sentry/cli-linux-i686@2.41.1': + optional: true + + '@sentry/cli-linux-x64@2.41.1': + optional: true + + '@sentry/cli-win32-i686@2.41.1': + optional: true + + '@sentry/cli-win32-x64@2.41.1': + optional: true + + '@sentry/cli@2.41.1': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.41.1 + '@sentry/cli-linux-arm': 2.41.1 + '@sentry/cli-linux-arm64': 2.41.1 + '@sentry/cli-linux-i686': 2.41.1 + '@sentry/cli-linux-x64': 2.41.1 + '@sentry/cli-win32-i686': 2.41.1 + '@sentry/cli-win32-x64': 2.41.1 + transitivePeerDependencies: + - encoding + - supports-color + '@sentry/core@8.48.0': {} '@sentry/react@8.48.0(react@18.3.1)': @@ -3321,6 +3536,14 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 + '@sentry/vite-plugin@3.2.0': + dependencies: + '@sentry/bundler-plugin-core': 3.2.0 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + '@storybook/addon-actions@8.4.7(storybook@8.4.7(bufferutil@4.0.9)(prettier@3.4.2)(utf-8-validate@5.0.10))': dependencies: '@storybook/global': 5.0.0 @@ -3953,6 +4176,12 @@ snapshots: acorn@8.14.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + agent-base@7.1.3: {} ajv@6.12.6: @@ -3972,6 +4201,11 @@ snapshots: ansi-styles@5.2.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@2.0.1: {} aria-query@5.3.0: @@ -4006,6 +4240,8 @@ snapshots: dependencies: open: 8.4.2 + binary-extensions@2.3.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -4078,6 +4314,18 @@ snapshots: check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chromatic@11.24.0: {} cliui@8.0.1: @@ -4175,6 +4423,8 @@ snapshots: dom-accessibility-api@0.6.3: {} + dotenv@16.4.7: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.1 @@ -4480,6 +4730,8 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -4515,6 +4767,13 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + globals@11.12.0: {} globals@14.0.0: {} @@ -4560,6 +4819,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 @@ -4589,6 +4855,10 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-callable@1.2.7: {} is-core-module@2.16.1: @@ -4713,6 +4983,8 @@ snapshots: loupe@3.1.2: {} + lru-cache@10.4.3: {} + lru-cache@11.0.2: {} lru-cache@5.1.1: @@ -4733,6 +5005,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.8: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + map-or-similar@1.5.0: {} math-intrinsics@1.1.0: {} @@ -4760,12 +5036,20 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@8.0.4: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 minimist@1.2.8: {} + minipass@4.2.8: {} + + minipass@7.1.2: {} + ms@2.0.0: optional: true @@ -4778,11 +5062,17 @@ snapshots: next-tick@1.1.0: optional: true + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-gyp-build@4.8.4: optional: true node-releases@2.0.19: {} + normalize-path@3.0.0: {} + nwsapi@2.2.16: {} object-inspect@1.13.3: {} @@ -4824,6 +5114,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + pathe@1.1.2: {} pathval@2.0.0: {} @@ -4870,6 +5165,8 @@ snapshots: process@0.11.10: {} + progress@2.0.3: {} + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -4940,6 +5237,10 @@ snapshots: dependencies: loose-envify: 1.4.0 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + recast@0.23.9: dependencies: ast-types: 0.16.1 @@ -5206,6 +5507,8 @@ snapshots: dependencies: tldts: 6.1.71 + tr46@0.0.3: {} + tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -5258,6 +5561,13 @@ snapshots: universalify@2.0.1: {} + unplugin@1.0.1: + dependencies: + acorn: 8.14.0 + chokidar: 3.6.0 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + unplugin@1.16.1: dependencies: acorn: 8.14.0 @@ -5371,8 +5681,14 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-sources@3.2.3: {} + + webpack-virtual-modules@0.5.0: {} + webpack-virtual-modules@0.6.2: {} websocket-driver@0.7.4: @@ -5406,6 +5722,11 @@ snapshots: tr46: 5.0.0 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-typed-array@1.1.18: dependencies: available-typed-arrays: 1.0.7 diff --git a/src/frontend/src/api/axios.instance.ts b/src/frontend/src/api/axios.instance.ts index 428c13db..5d4526e1 100644 --- a/src/frontend/src/api/axios.instance.ts +++ b/src/frontend/src/api/axios.instance.ts @@ -1,5 +1,7 @@ import { useAuthStore } from '@/stores/useAuthStore'; import axios from 'axios'; +import { logAxiosError } from './axios.log'; +import { ErrorType } from '@/types/enums/ErrorType'; const instance = axios.create({ baseURL: import.meta.env.VITE_API_URL, @@ -27,7 +29,7 @@ instance.interceptors.response.use( response => response, async error => { const originalRequest = error.config; - if (error.response.status === 401 && !originalRequest?._retry) { + if (error.response?.status === 401 && !originalRequest?._retry) { originalRequest._retry = true; try { const newAccessToken = await useAuthStore.getState().refreshAccessToken(); @@ -39,6 +41,8 @@ instance.interceptors.response.use( } catch (error) { return Promise.reject(error); } + } else { + logAxiosError(error, ErrorType.INTERNAL_SERVER_ERROR, error.message); } return Promise.reject(error); }, diff --git a/src/frontend/src/api/axios.log.ts b/src/frontend/src/api/axios.log.ts new file mode 100644 index 00000000..250d7371 --- /dev/null +++ b/src/frontend/src/api/axios.log.ts @@ -0,0 +1,21 @@ +import { captureException } from '@sentry/react'; +import { AxiosError } from 'axios'; +import { useUserStore } from '@/stores/useUserStore'; + +export const logAxiosError = (error: unknown, type: string, errorMsg: string) => { + if (error instanceof AxiosError && error.response?.status === 400) return; + + if (error instanceof AxiosError) { + error.message = errorMsg; + captureException(error, { + tags: { + userId: useUserStore.getState().user?.userId ?? 'anonymous', + api: error.response?.config.url, + httpMethod: error.config?.method?.toUpperCase(), + httpStatusCode: (error.response?.status ?? '').toString(), + }, + level: 'error', + extra: { type: type }, + }); + } +}; diff --git a/src/frontend/src/api/endpoints/auth/auth.api.ts b/src/frontend/src/api/endpoints/auth/auth.api.ts index 21e642d4..3283c4f7 100644 --- a/src/frontend/src/api/endpoints/auth/auth.api.ts +++ b/src/frontend/src/api/endpoints/auth/auth.api.ts @@ -2,6 +2,8 @@ import { useAuthStore } from '@/stores/useAuthStore'; import instance from '../../axios.instance'; import { LoginRequest, LoginResponseDto } from './auth.interface'; import axios from 'axios'; +import { ErrorType } from '@/types/enums/ErrorType'; +import { logAxiosError } from '@/api/axios.log'; const loginInstance = axios.create({ baseURL: import.meta.env.VITE_API_URL, @@ -27,9 +29,14 @@ export const authApi = { // 로그아웃 logout: async () => { - const { data } = await instance.post('/auth/logout'); - useAuthStore.getState().clear(); - return data; + try { + const { data } = await instance.post('/auth/logout'); + useAuthStore.getState().clear(); + return data; + } catch (error) { + logAxiosError(error, ErrorType.AUTH, '로그아웃 실패'); + throw error; + } }, // 토큰 갱신 diff --git a/src/frontend/src/api/endpoints/room/room.api.ts b/src/frontend/src/api/endpoints/room/room.api.ts index 5b77db6f..d06b422a 100644 --- a/src/frontend/src/api/endpoints/room/room.api.ts +++ b/src/frontend/src/api/endpoints/room/room.api.ts @@ -7,33 +7,54 @@ import { CurrentRoomDto, ReceiveMessageDto, } from './room.interface'; +import { logAxiosError } from '@/api/axios.log'; +import { ErrorType } from '@/types/enums/ErrorType'; export const roomApi = { // 방 생성 createRoom: async (roomInfo: RoomRequestDto) => { - const { data } = await instance.post<{ code: string }>('/rooms/create-room', roomInfo); - return data; + try { + const { data } = await instance.post<{ code: string }>('/rooms/create-room', roomInfo); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '방 생성 실패'); + throw error; + } }, // 내 방 조회 getMyRooms: async () => { - const { data } = await instance.get('/rooms/me'); - return data; + try { + const { data } = await instance.get('/rooms/me'); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '내 방 조회 실패'); + throw error; + } }, // 전체 방 조회 getRooms: async (page: number) => { - const response = await instance.get('/rooms/all', { - params: { page, size: 20 }, - }); - console.log(response.data); - return response.data; + try { + const response = await instance.get('/rooms/all', { + params: { page, size: 20 }, + }); + return response.data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '전체 방 조회 실패'); + throw error; + } }, // 방 입장 joinRoom: async (roomCode: string) => { - const { data } = await instance.post(`/rooms/join`, { roomCode }); - return data; + try { + const { data } = await instance.post(`/rooms/join`, { roomCode }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '방 입장 실패'); + throw error; + } }, // 방 나가기 @@ -50,25 +71,40 @@ export const roomApi = { // 메시지 조회 getMessages: async (roomId: number, cursor?: number, limit?: number) => { - const { data } = await instance.get(`/messages/${roomId}`, { - params: { cursor, limit: limit ?? 10 }, - }); - return data; + try { + const { data } = await instance.get(`/messages/${roomId}`, { + params: { cursor, limit: limit ?? 10 }, + }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '메시지 조회 실패'); + throw error; + } }, // 방 참여자 조회 getParticipants: async (roomId: string) => { - const { data } = await instance.get(`/rooms/participants`, { params: { roomId } }); - return data; + try { + const { data } = await instance.get(`/rooms/participants`, { params: { roomId } }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '방 참여자 조회 실패'); + throw error; + } }, // playlist 보내기(배열을 json 형식으로 보내기) sendPlaylist: async (roomId: number, playlist: PlaylistDto[]) => { - const { data } = await instance.post('/rooms/playlist', { - roomId, - playlist: playlist, - }); - return data; + try { + const { data } = await instance.post('/rooms/playlist', { + roomId, + playlist, + }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, 'playlist 보내기 실패'); + throw error; + } }, // 역할 변경 @@ -78,25 +114,46 @@ export const roomApi = { targetUserId: userId, newRole: role, }; - const { data } = await instance.patch(`/rooms/change-role`, body); - return data; + + try { + const { data } = await instance.patch(`/rooms/change-role`, body); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '역할 변경 실패'); + throw error; + } }, // 내 방 정보 수정 updateMyRoomTitle: async (roomId: number, title: string) => { - const { data } = await instance.patch(`/rooms/update`, { roomId, title }); - return data; + try { + const { data } = await instance.patch(`/rooms/update`, { roomId, title }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '내 방 제목 수정 실패'); + throw error; + } }, // 내 방 설명 수정 updateMyRoomDescription: async (roomId: number, description: string) => { - const { data } = await instance.patch(`/rooms/update`, { roomId, description }); - return data; + try { + const { data } = await instance.patch(`/rooms/update`, { roomId, description }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '내 방 설명 수정 실패'); + throw error; + } }, // 내 방 공개 여부 수정 updateMyRoomPublic: async (roomId: number, isPublic: boolean) => { - const { data } = await instance.patch(`/rooms/update`, { roomId, isPublic }); - return data; + try { + const { data } = await instance.patch(`/rooms/update`, { roomId, isPublic }); + return data; + } catch (error) { + logAxiosError(error, ErrorType.ROOM, '내 방 공개 여부 수정 실패'); + throw error; + } }, }; diff --git a/src/frontend/src/api/endpoints/user/user.api.ts b/src/frontend/src/api/endpoints/user/user.api.ts index 7664b72b..1a7baec8 100644 --- a/src/frontend/src/api/endpoints/user/user.api.ts +++ b/src/frontend/src/api/endpoints/user/user.api.ts @@ -1,49 +1,86 @@ import instance from '@/api/axios.instance'; import { RegisterDto, UpdateUserRequestDto, UserResponseDto } from './user.interface'; +import { logAxiosError } from '@/api/axios.log'; +import { ErrorType } from '@/types/enums/ErrorType'; export const userApi = { // 내 프로필 조회 getMyProfile: async () => { - const { data } = await instance.get('users/me'); - return data; + try { + const { data } = await instance.get('users/me'); + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '내 프로필 조회 실패'); + throw error; + } }, // 내 프로필 수정 updateMyProfile: async (updateUserRequestDto: UpdateUserRequestDto) => { - const { data } = await instance.patch(`users/me`, updateUserRequestDto); - return data; + try { + const { data } = await instance.patch(`users/me`, updateUserRequestDto); + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '내 프로필 수정 실패'); + throw error; + } }, // 전체 유저 조회 getUsers: async (page: number = 0, size: number = 10, nickname?: string) => { - const { data } = await instance.get<{ users: UserResponseDto[]; totalLength: number }>( + try { + const { data } = await instance.get<{ users: UserResponseDto[]; totalLength: number }>( `users`, { params: { page, size, nickname } }, ); - return data; + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '전체 유저 조회 실패'); + throw error; + } }, // 프로필 조회 getProfile: async (userId: string) => { - const { data } = await instance.get(`users/${userId}`); - return data; + try { + const { data } = await instance.get(`users/${userId}`); + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '타 유저 프로필 조회 실패'); + throw error; + } }, // 이메일 존재 여부 확인 checkEmailExists: async (email: string) => { - const { data } = await instance.get(`users/exists?email=${email}`); - return data; + try { + const { data } = await instance.get(`users/exists?email=${email}`); + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '이메일 존재 여부 확인 실패'); + throw error; + } }, // 닉네임 존재 여부 확인 checkNicknameExists: async (nickname: string) => { - const { data } = await instance.get(`users/exists?nickname=${nickname}`); - return data; + try { + const { data } = await instance.get(`users/exists?nickname=${nickname}`); + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '닉네임 존재 여부 확인 실패'); + throw error; + } }, // 회원가입 register: async (registerDto: RegisterDto) => { - const { data } = await instance.post(`users/register`, registerDto); - return data; + try { + const { data } = await instance.post(`users/register`, registerDto); + return data; + } catch (error) { + logAxiosError(error, ErrorType.USER, '회원가입 실패'); + throw error; + } }, }; diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx index e761c001..2d0fd6f3 100644 --- a/src/frontend/src/main.tsx +++ b/src/frontend/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App.tsx'; import { ToastProvider } from './components/common/Toast/index.tsx'; +import './sentry.ts'; createRoot(document.getElementById('root')!).render( // diff --git a/src/frontend/src/sentry.ts b/src/frontend/src/sentry.ts new file mode 100644 index 00000000..4c882924 --- /dev/null +++ b/src/frontend/src/sentry.ts @@ -0,0 +1,37 @@ +import * as Sentry from '@sentry/react'; +import { useEffect } from 'react'; +import { + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom'; + +Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + environment: import.meta.env.VITE_IS_PROD === 'true' ? 'production' : 'local', + release: '^8.18.0', + integrations: [ + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + Sentry.replayIntegration(), + ], + + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for tracing. + tracesSampleRate: 1.0, + + // Set `tracePropagationTargets` to control for which URLs trace propagation should be enabled + // tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/], + tracePropagationTargets: ['localhost'], + + // Capture Replay for 100% of all sessions, + // plus for 100% of sessions with an error + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, +}); diff --git a/src/frontend/src/stores/useUserStore.ts b/src/frontend/src/stores/useUserStore.ts index bcb7ec70..d83acb5d 100644 --- a/src/frontend/src/stores/useUserStore.ts +++ b/src/frontend/src/stores/useUserStore.ts @@ -20,13 +20,9 @@ export const useUserStore = create( roomId: null, setUser: (user: UserResponseDto) => set({ user }), fetchMyProfile: async () => { - try { - const data = await userApi.getMyProfile(); - set({ user: data }); - return data; - } catch (error) { - console.error(error); - } + const data = await userApi.getMyProfile(); + set({ user: data }); + return data; }, updateMyProfile: async (updateUserRequestDto: UpdateUserRequestDto) => { const data = await userApi.updateMyProfile(updateUserRequestDto); diff --git a/src/frontend/src/types/enums/ErrorType.ts b/src/frontend/src/types/enums/ErrorType.ts new file mode 100644 index 00000000..9a614c1c --- /dev/null +++ b/src/frontend/src/types/enums/ErrorType.ts @@ -0,0 +1,6 @@ +export enum ErrorType { + AUTH = 'AUTH', + USER = 'USER', + ROOM = 'ROOM', + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', +} diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index b90a6364..5aa53978 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -1,10 +1,4 @@ -// import { defineConfig } from 'vite' -// import react from '@vitejs/plugin-react' - -// // https://vite.dev/config/ -// export default defineConfig({ -// plugins: [react()], -// }) +import { sentryVitePlugin } from '@sentry/vite-plugin'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { visualizer } from 'rollup-plugin-visualizer'; @@ -14,14 +8,25 @@ export default defineConfig({ plugins: [ react(), visualizer({ open: true }), // 번들 분석 결과 자동 열기 + sentryVitePlugin({ + org: 'kickzo', + project: 'kicktube', + telemetry: false, + }), ], + resolve: { alias: [ { find: '@', replacement: path.resolve(__dirname, 'src') }, { find: '@components', replacement: path.resolve(__dirname, 'src/components') }, ], }, + define: { global: 'window', }, + + build: { + sourcemap: true, + }, }); diff --git a/src/infra/kong/kong.yml b/src/infra/kong/kong.yml index 2e791804..d36e0208 100644 --- a/src/infra/kong/kong.yml +++ b/src/infra/kong/kong.yml @@ -191,7 +191,11 @@ plugins: - Accept-Version - Content-Type - Authorization - exposed_headers: [] + - sentry-trace + - baggage + exposed_headers: + - sentry-trace + - baggage credentials: true max_age: 3600 preflight_continue: false