From 1377627c14acf56a39eba17fb5ff96192fb5bbbd Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 00:48:27 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=9A=80=20sentry=20transpileClientSD?= =?UTF-8?q?K=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=EB=A1=9C=20=EB=B2=88?= =?UTF-8?q?=EB=93=A4=20=ED=81=AC=EA=B8=B0=20=EA=B0=90=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/next.config.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 1ec9bc2b..92e66201 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -75,8 +75,8 @@ export default withSentryConfig( // Upload a larger set of source maps for prettier stack traces (increases build time) widenClientFileUpload: true, - // Transpiles SDK to be compatible with IE11 (increases bundle size) - transpileClientSDK: true, + // IE11 지원 불필요 - 번들 사이즈 최적화를 위해 비활성화 + transpileClientSDK: false, // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. (increases server load) // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- From 8135f3923eb503e5efc1bbc0ba808e318d24e547 Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 00:49:02 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=9A=80=20sentry=20replayIntegration?= =?UTF-8?q?=20lazy=20load=EB=A1=9C=20=EC=B4=88=EA=B8=B0=20=EB=B2=88?= =?UTF-8?q?=EB=93=A4=20=ED=81=AC=EA=B8=B0=20=EC=A0=88=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/sentry.client.config.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index 855a2686..d8604a2a 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -29,12 +29,17 @@ if (process.env.NODE_ENV === "production") { // Web Vitals 자동 수집 활성화 enableInp: true, // Interaction to Next Paint (INP) 측정 }), + ], + }); - // Session Replay: 사용자 세션 녹화 (클라이언트 전용) - Sentry.replayIntegration({ + // Session Replay: 초기 번들에서 제외하고 lazy load (~30-40kB 절감) + // https://docs.sentry.io/platforms/javascript/session-replay/#lazy-loading-replay + Sentry.lazyLoadIntegration("replayIntegration").then((replay) => { + Sentry.addIntegration( + replay({ maskAllText: true, blockAllMedia: true, }), - ], + ); }); } From 7fc0a3c992eaac731d1af23d1d44a577a86e3a81 Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 00:49:53 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=94=A7=20@next/bundle-analyzer=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20analyze=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/next.config.mjs | 7 ++- apps/web/package.json | 4 +- pnpm-lock.yaml | 133 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 92e66201..e2e94092 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,5 +1,10 @@ // Injected content via Sentry wizard below import { withSentryConfig } from "@sentry/nextjs"; +import bundleAnalyzer from "@next/bundle-analyzer"; + +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); /** @type {import('next').NextConfig} */ const nextConfig = { @@ -57,7 +62,7 @@ const nextConfig = { }; export default withSentryConfig( - nextConfig, + withBundleAnalyzer(nextConfig), { // For all available options, see: // https://github.com/getsentry/sentry-webpack-plugin#options diff --git a/apps/web/package.json b/apps/web/package.json index 32c14923..ef303af3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,7 +11,8 @@ "format": "biome format --write .", "format:check": "biome format .", "typecheck": "tsc --noEmit", - "ci:check": "pnpm run lint:check && pnpm run typecheck" + "ci:check": "pnpm run lint:check && pnpm run typecheck", + "analyze": "ANALYZE=true next build" }, "dependencies": { "@hookform/resolvers": "^5.1.1", @@ -46,6 +47,7 @@ "zustand": "^5.0.7" }, "devDependencies": { + "@next/bundle-analyzer": "^16.1.6", "@svgr/webpack": "^8.1.0", "@types/node": "^20.11.19", "@types/react": "18.3.27", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7ff4448..703ccd3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,6 +226,9 @@ importers: specifier: ^5.0.7 version: 5.0.10(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) devDependencies: + '@next/bundle-analyzer': + specifier: ^16.1.6 + version: 16.1.6 '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0(typescript@5.9.3) @@ -1082,6 +1085,10 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -1375,6 +1382,9 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@next/bundle-analyzer@16.1.6': + resolution: {integrity: sha512-ee2kagdTaeEWPlotgdTOqFHYcD3e2m2bbE3I9Rq2i6ABYi5OgopmtEUe8NM23viaYxLV2tDH/2nd5+qKoEr6cw==} + '@next/env@14.2.35': resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} @@ -1898,6 +1908,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@prisma/instrumentation@6.19.0': resolution: {integrity: sha512-QcuYy25pkXM8BJ37wVFBO7Zh34nyRV1GOb2n3lPkkbRYfl4hWl3PTcImP41P0KrzVXfa/45p6eVCos27x3exIg==} peerDependencies: @@ -3265,6 +3278,10 @@ packages: peerDependencies: acorn: ^8.14.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -3667,6 +3684,9 @@ packages: sqlite3: optional: true + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -3751,6 +3771,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} @@ -3829,6 +3852,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -4056,6 +4083,10 @@ packages: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + h3@2.0.1-rc.11: resolution: {integrity: sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw==} engines: {node: '>=20.11.1'} @@ -4088,6 +4119,9 @@ packages: html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -4173,6 +4207,10 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -4532,6 +4570,10 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4634,6 +4676,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + oxc-minify@0.112.0: resolution: {integrity: sha512-rkVSeeIRSt+RYI9uX6xonBpLUpvZyegxIg0UL87ev7YAfUqp7IIZlRjkgQN5Us1lyXD//TOo0Dcuuro/TYOWoQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5057,6 +5103,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -5294,6 +5344,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -5636,6 +5690,11 @@ packages: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} + webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + webpack-sources@3.3.3: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} @@ -5705,6 +5764,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -6830,6 +6901,8 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@discoveryjs/json-ext@0.5.7': {} + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -7111,6 +7184,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@next/bundle-analyzer@16.1.6': + dependencies: + webpack-bundle-analyzer: 4.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@next/env@14.2.35': {} '@next/swc-darwin-arm64@14.2.33': @@ -7538,6 +7618,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.29': {} + '@prisma/instrumentation@6.19.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -9162,6 +9244,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} agent-base@6.0.2: @@ -9560,6 +9646,8 @@ snapshots: db0@0.3.4: {} + debounce@1.2.1: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -9625,6 +9713,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + duplexify@4.1.3: dependencies: end-of-stream: 1.4.5 @@ -9722,6 +9812,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -9993,6 +10085,10 @@ snapshots: - supports-color optional: true + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + h3@2.0.1-rc.11(crossws@0.4.4(srvx@0.10.1)): dependencies: rou3: 0.7.12 @@ -10021,6 +10117,8 @@ snapshots: html-entities@2.6.0: optional: true + html-escaper@2.0.2: {} + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -10117,6 +10215,8 @@ snapshots: is-obj@2.0.0: {} + is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-reference@1.2.1: @@ -10436,6 +10536,8 @@ snapshots: module-details-from-path@1.0.4: {} + mrmime@2.0.1: {} + ms@2.1.3: {} mz@2.7.0: @@ -10564,6 +10666,8 @@ snapshots: wrappy: 1.0.2 optional: true + opener@1.5.2: {} + oxc-minify@0.112.0: optionalDependencies: '@oxc-minify/binding-android-arm-eabi': 0.112.0 @@ -11016,6 +11120,12 @@ snapshots: signal-exit@4.1.0: {} + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -11266,6 +11376,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.21 @@ -11518,6 +11630,25 @@ snapshots: webidl-conversions@8.0.1: {} + webpack-bundle-analyzer@4.10.1: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.15.0 + acorn-walk: 8.3.4 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + webpack-sources@3.3.3: {} webpack-virtual-modules@0.5.0: {} @@ -11606,6 +11737,8 @@ snapshots: wrappy@1.0.2: optional: true + ws@7.5.10: {} + ws@8.19.0: {} xml-name-validator@5.0.0: {} From 19b6457778ed2ba0225cd01681c294fc36887cc8 Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 00:50:20 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=9A=80=20optimizePackageImports=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9C=BC=EB=A1=9C=20tree-shaking=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/next.config.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index e2e94092..e8f6a8a5 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -25,6 +25,19 @@ const nextConfig = { gzipSize: true, // Sentry instrumentation 활성화 (Web Vitals 수집에 필요) instrumentationHook: true, + optimizePackageImports: [ + "lucide-react", + "@radix-ui/react-select", + "@radix-ui/react-checkbox", + "@radix-ui/react-label", + "@radix-ui/react-progress", + "@tanstack/react-query", + "class-variance-authority", + "tailwind-merge", + "zod", + "react-hook-form", + "@hookform/resolvers", + ], }, eslint: { // Warning: This allows production builds to successfully complete even if From a776c39be1c5cf5b39fd8e5691819dd3c8dfd7dd Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 00:51:00 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=9A=80=20score/search=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20dynamic=20im?= =?UTF-8?q?port=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/university/score/page.tsx | 3 ++- apps/web/src/app/university/search/page.tsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/university/score/page.tsx b/apps/web/src/app/university/score/page.tsx index 2bc52798..f8cd1778 100644 --- a/apps/web/src/app/university/score/page.tsx +++ b/apps/web/src/app/university/score/page.tsx @@ -1,8 +1,9 @@ import type { Metadata } from "next"; +import dynamic from "next/dynamic"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import ScoreScreen from "./ScoreScreen"; +const ScoreScreen = dynamic(() => import("./ScoreScreen"), { ssr: false }); export const metadata: Metadata = { title: "성적 확인하기", diff --git a/apps/web/src/app/university/search/page.tsx b/apps/web/src/app/university/search/page.tsx index 54f15234..fb4333c6 100644 --- a/apps/web/src/app/university/search/page.tsx +++ b/apps/web/src/app/university/search/page.tsx @@ -1,9 +1,10 @@ import type { Metadata } from "next"; +import dynamic from "next/dynamic"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import SchoolSearchForm from "./PageContent"; -import SearchBar from "./SearchBar"; +const SearchBar = dynamic(() => import("./SearchBar"), { ssr: false }); +const SchoolSearchForm = dynamic(() => import("./PageContent"), { ssr: false }); export const metadata: Metadata = { title: "파견 학교 목록", From 87e2dbd1fa43aa1a5abae52bc3d419e6621c67b0 Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 01:01:35 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=90=9B=20admin=20biome=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20lint=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/.vscode/settings.json | 66 +-- apps/admin/biome.json | 80 ++-- .../features/scores/GpaScoreTable.tsx | 413 ++++++++-------- .../features/scores/LanguageScoreTable.tsx | 451 +++++++++--------- .../features/scores/ScoreVerifyButton.tsx | 112 ++--- .../features/scores/StatusBadge.tsx | 24 +- .../src/components/layout/AdminLayout.tsx | 12 +- apps/admin/src/components/ui/button.tsx | 60 +-- apps/admin/src/components/ui/card.tsx | 28 +- apps/admin/src/components/ui/input.tsx | 26 +- apps/admin/src/components/ui/label.tsx | 6 +- apps/admin/src/components/ui/table.tsx | 78 +-- apps/admin/src/components/ui/tabs.tsx | 60 +-- apps/admin/src/lib/api/auth.ts | 16 +- apps/admin/src/lib/api/client.ts | 124 ++--- apps/admin/src/lib/api/scores.ts | 72 +-- apps/admin/src/lib/utils/index.ts | 2 +- apps/admin/src/lib/utils/jwtUtils.ts | 34 +- apps/admin/src/lib/utils/localStorage.ts | 64 +-- apps/admin/src/router.tsx | 20 +- apps/admin/src/routes/__root.tsx | 88 ++-- apps/admin/src/routes/auth/login.tsx | 146 +++--- apps/admin/src/routes/index.tsx | 6 +- apps/admin/src/routes/scores/index.tsx | 78 +-- apps/admin/src/types/auth.ts | 6 +- apps/admin/src/types/scores.ts | 120 ++--- apps/admin/vite.config.ts | 53 +- 27 files changed, 1124 insertions(+), 1121 deletions(-) diff --git a/apps/admin/.vscode/settings.json b/apps/admin/.vscode/settings.json index 70dd163c..b001961b 100644 --- a/apps/admin/.vscode/settings.json +++ b/apps/admin/.vscode/settings.json @@ -1,35 +1,35 @@ { - "files.watcherExclude": { - "**/routeTree.gen.ts": true - }, - "search.exclude": { - "**/routeTree.gen.ts": true - }, - "files.readonlyInclude": { - "**/routeTree.gen.ts": true - }, - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[css]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "editor.codeActionsOnSave": { - "source.organizeImports.biome": "explicit" - } + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit" + } } diff --git a/apps/admin/biome.json b/apps/admin/biome.json index d12aad10..66d523ce 100644 --- a/apps/admin/biome.json +++ b/apps/admin/biome.json @@ -1,42 +1,42 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.12/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "includes": [ - "**/src/**/*", - "**/.vscode/**/*", - "**/index.html", - "**/vite.config.ts", - "!**/src/routeTree.gen.ts", - "!**/src/styles.css" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab", - "lineWidth": 120 - }, - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - } + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**/src/**/*", + "**/.vscode/**/*", + "**/index.html", + "**/vite.config.ts", + "!**/src/routeTree.gen.ts", + "!**/src/styles.css" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 120 + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } } diff --git a/apps/admin/src/components/features/scores/GpaScoreTable.tsx b/apps/admin/src/components/features/scores/GpaScoreTable.tsx index b6813bf7..d5728c8f 100644 --- a/apps/admin/src/components/features/scores/GpaScoreTable.tsx +++ b/apps/admin/src/components/features/scores/GpaScoreTable.tsx @@ -1,5 +1,5 @@ import { format } from "date-fns"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -9,221 +9,222 @@ import { ScoreVerifyButton } from "./ScoreVerifyButton"; import { StatusBadge } from "./StatusBadge"; interface Props { - verifyFilter: VerifyStatus; + verifyFilter: VerifyStatus; } const S3_BASE_URL = import.meta.env.VITE_S3_BASE_URL; export function GpaScoreTable({ verifyFilter }: Props) { - const [scores, setScores] = useState([]); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [loading, setLoading] = useState(false); - const [editingId, setEditingId] = useState(null); - const [editingGpa, setEditingGpa] = useState(0); - const [editingGpaCriteria, setEditingGpaCriteria] = useState(0); + const [scores, setScores] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [loading, setLoading] = useState(false); + const [editingId, setEditingId] = useState(null); + const [editingGpa, setEditingGpa] = useState(0); + const [editingGpaCriteria, setEditingGpaCriteria] = useState(0); - const fetchScores = async () => { - setLoading(true); - try { - const response = await scoreApi.getGpaScores({ verifyStatus: verifyFilter }, page); - setScores(response.content); - setTotalPages(response.totalPages); - } catch (error) { - console.error("Failed to fetch GPA scores:", error); - } finally { - setLoading(false); - } - }; + const fetchScores = useCallback(async () => { + setLoading(true); + try { + const response = await scoreApi.getGpaScores({ verifyStatus: verifyFilter }, page); + setScores(response.content); + setTotalPages(response.totalPages); + } catch (error) { + console.error("Failed to fetch GPA scores:", error); + } finally { + setLoading(false); + } + }, [verifyFilter, page]); - useEffect(() => { - fetchScores(); - }, [verifyFilter, page]); + useEffect(() => { + fetchScores(); + }, [fetchScores]); - const handleVerifyStatus = async (id: number, status: VerifyStatus, reason?: string) => { - try { - const score = scores.find((s) => s.gpaScoreStatusResponse.id === id); - await scoreApi.updateGpaScore(id, status, reason, score); - fetchScores(); - } catch (error) { - console.error("Failed to update GPA score:", error); - toast.error("성적 상태 업데이트에 실패했습니다"); - } - }; + const handleVerifyStatus = async (id: number, status: VerifyStatus, reason?: string) => { + try { + const score = scores.find((s) => s.gpaScoreStatusResponse.id === id); + await scoreApi.updateGpaScore(id, status, reason, score); + fetchScores(); + } catch (error) { + console.error("Failed to update GPA score:", error); + toast.error("성적 상태 업데이트에 실패했습니다"); + } + }; - const handleEdit = (score: GpaScoreWithUser) => { - setEditingId(score.gpaScoreStatusResponse.id); - setEditingGpa(score.gpaScoreStatusResponse.gpaResponse.gpa); - setEditingGpaCriteria(score.gpaScoreStatusResponse.gpaResponse.gpaCriteria); - }; + const handleEdit = (score: GpaScoreWithUser) => { + setEditingId(score.gpaScoreStatusResponse.id); + setEditingGpa(score.gpaScoreStatusResponse.gpaResponse.gpa); + setEditingGpaCriteria(score.gpaScoreStatusResponse.gpaResponse.gpaCriteria); + }; - const handleSave = async (score: GpaScoreWithUser) => { - try { - await scoreApi.updateGpaScore( - score.gpaScoreStatusResponse.id, - score.gpaScoreStatusResponse.verifyStatus, - score.gpaScoreStatusResponse.rejectedReason || undefined, - { - ...score, - gpaScoreStatusResponse: { - ...score.gpaScoreStatusResponse, - gpaResponse: { - ...score.gpaScoreStatusResponse.gpaResponse, - gpa: editingGpa, - gpaCriteria: editingGpaCriteria, - }, - }, - }, - ); - setEditingId(null); - fetchScores(); - toast.success("GPA가 수정되었습니다"); - } catch (error) { - console.error("Failed to update GPA:", error); - toast.error("GPA 수정에 실패했습니다"); - } - }; + const handleSave = async (score: GpaScoreWithUser) => { + try { + await scoreApi.updateGpaScore( + score.gpaScoreStatusResponse.id, + score.gpaScoreStatusResponse.verifyStatus, + score.gpaScoreStatusResponse.rejectedReason || undefined, + { + ...score, + gpaScoreStatusResponse: { + ...score.gpaScoreStatusResponse, + gpaResponse: { + ...score.gpaScoreStatusResponse.gpaResponse, + gpa: editingGpa, + gpaCriteria: editingGpaCriteria, + }, + }, + }, + ); + setEditingId(null); + fetchScores(); + toast.success("GPA가 수정되었습니다"); + } catch (error) { + console.error("Failed to update GPA:", error); + toast.error("GPA 수정에 실패했습니다"); + } + }; - const handlePageChange = (newPage: number) => { - if (newPage < 1 || newPage > totalPages) return; - setPage(newPage); - }; + const handlePageChange = (newPage: number) => { + if (newPage < 1 || newPage > totalPages) return; + setPage(newPage); + }; - return ( -
-
- - - - ID - 닉네임 - GPA - 기준점수 - 상태 - 제출일 - 거절사유 - 인증파일 - 작업 - - - - {loading ? ( - - -
-
- 로딩중... -
- - - ) : scores.length === 0 ? ( - - - 데이터가 없습니다 - - - ) : ( - scores.map((score) => ( - - {score.gpaScoreStatusResponse.id} - -
- 프로필 - {score.siteUserResponse.nickname} -
-
- - {editingId === score.gpaScoreStatusResponse.id ? ( -
- setEditingGpa(Number.parseFloat(e.target.value))} - className="w-20 rounded border px-2 py-1" - /> -
- ) : ( - score.gpaScoreStatusResponse.gpaResponse.gpa - )} -
- - {editingId === score.gpaScoreStatusResponse.id ? ( -
- setEditingGpaCriteria(Number.parseFloat(e.target.value))} - className="w-20 rounded border px-2 py-1" - /> - - -
- ) : ( -
- {score.gpaScoreStatusResponse.gpaResponse.gpaCriteria} - -
- )} -
- - - - {format(new Date(score.gpaScoreStatusResponse.createdAt), "yyyy-MM-dd HH:mm")} - {score.gpaScoreStatusResponse.rejectedReason || "-"} - - - 파일 보기 - - - - - handleVerifyStatus(score.gpaScoreStatusResponse.id, status, reason) - } - /> - -
- )) - )} - -
-
- {/* 페이지네이션 */} -
- - {Array.from({ length: totalPages }, (_, idx) => ( - - ))} - -
-
- ); + return ( +
+
+ + + + ID + 닉네임 + GPA + 기준점수 + 상태 + 제출일 + 거절사유 + 인증파일 + 작업 + + + + {loading ? ( + + +
+
+ 로딩중... +
+ + + ) : scores.length === 0 ? ( + + + 데이터가 없습니다 + + + ) : ( + scores.map((score) => ( + + {score.gpaScoreStatusResponse.id} + +
+ 프로필 + {score.siteUserResponse.nickname} +
+
+ + {editingId === score.gpaScoreStatusResponse.id ? ( +
+ setEditingGpa(Number.parseFloat(e.target.value))} + className="w-20 rounded border px-2 py-1" + /> +
+ ) : ( + score.gpaScoreStatusResponse.gpaResponse.gpa + )} +
+ + {editingId === score.gpaScoreStatusResponse.id ? ( +
+ setEditingGpaCriteria(Number.parseFloat(e.target.value))} + className="w-20 rounded border px-2 py-1" + /> + + +
+ ) : ( +
+ {score.gpaScoreStatusResponse.gpaResponse.gpaCriteria} + +
+ )} +
+ + + + {format(new Date(score.gpaScoreStatusResponse.createdAt), "yyyy-MM-dd HH:mm")} + {score.gpaScoreStatusResponse.rejectedReason || "-"} + + + 파일 보기 + + + + + handleVerifyStatus(score.gpaScoreStatusResponse.id, status, reason) + } + /> + +
+ )) + )} + +
+
+ {/* 페이지네이션 */} +
+ + {Array.from({ length: totalPages }, (_, idx) => ( + + ))} + +
+
+ ); } diff --git a/apps/admin/src/components/features/scores/LanguageScoreTable.tsx b/apps/admin/src/components/features/scores/LanguageScoreTable.tsx index 236dcc0c..8be2eeea 100644 --- a/apps/admin/src/components/features/scores/LanguageScoreTable.tsx +++ b/apps/admin/src/components/features/scores/LanguageScoreTable.tsx @@ -1,5 +1,5 @@ import { format } from "date-fns"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -9,243 +9,244 @@ import { ScoreVerifyButton } from "./ScoreVerifyButton"; import { StatusBadge } from "./StatusBadge"; interface Props { - verifyFilter: VerifyStatus; + verifyFilter: VerifyStatus; } const S3_BASE_URL = import.meta.env.VITE_S3_BASE_URL; const LANGUAGE_TEST_OPTIONS: { value: LanguageTestType; label: string }[] = [ - { value: "TOEIC", label: "TOEIC" }, - { value: "TOEFL_IBT", label: "TOEFL IBT" }, - { value: "TOEFL_ITP", label: "TOEFL ITP" }, - { value: "IELTS", label: "IELTS" }, - { value: "JLPT", label: "JLPT" }, - { value: "NEW_HSK", label: "NEW HSK" }, - { value: "DALF", label: "DALF" }, - { value: "CEFR", label: "CEFR" }, - { value: "TCF", label: "TCF" }, - { value: "TEF", label: "TEF" }, - { value: "DUOLINGO", label: "DUOLINGO" }, - { value: "ETC", label: "기타" }, + { value: "TOEIC", label: "TOEIC" }, + { value: "TOEFL_IBT", label: "TOEFL IBT" }, + { value: "TOEFL_ITP", label: "TOEFL ITP" }, + { value: "IELTS", label: "IELTS" }, + { value: "JLPT", label: "JLPT" }, + { value: "NEW_HSK", label: "NEW HSK" }, + { value: "DALF", label: "DALF" }, + { value: "CEFR", label: "CEFR" }, + { value: "TCF", label: "TCF" }, + { value: "TEF", label: "TEF" }, + { value: "DUOLINGO", label: "DUOLINGO" }, + { value: "ETC", label: "기타" }, ]; export function LanguageScoreTable({ verifyFilter }: Props) { - const [scores, setScores] = useState([]); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [loading, setLoading] = useState(false); - const [editingId, setEditingId] = useState(null); - const [editingScore, setEditingScore] = useState(""); - const [editingType, setEditingType] = useState("TOEIC"); + const [scores, setScores] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [loading, setLoading] = useState(false); + const [editingId, setEditingId] = useState(null); + const [editingScore, setEditingScore] = useState(""); + const [editingType, setEditingType] = useState("TOEIC"); - const fetchScores = async () => { - setLoading(true); - try { - const response = await scoreApi.getLanguageScores({ verifyStatus: verifyFilter }, page); - setScores(response.content); - setTotalPages(response.totalPages); - } catch (error) { - console.error("Failed to fetch Language scores:", error); - } finally { - setLoading(false); - } - }; + const fetchScores = useCallback(async () => { + setLoading(true); + try { + const response = await scoreApi.getLanguageScores({ verifyStatus: verifyFilter }, page); + setScores(response.content); + setTotalPages(response.totalPages); + } catch (error) { + console.error("Failed to fetch Language scores:", error); + } finally { + setLoading(false); + } + }, [verifyFilter, page]); - useEffect(() => { - fetchScores(); - }, [verifyFilter, page]); + useEffect(() => { + fetchScores(); + }, [fetchScores]); - const handleVerifyStatus = async (id: number, status: VerifyStatus, reason?: string) => { - try { - const score = scores.find((s) => s.languageTestScoreStatusResponse.id === id); - await scoreApi.updateLanguageScore(id, status, reason, score); - fetchScores(); - } catch (error) { - console.error("Failed to update Language score:", error); - toast.error("성적 상태 업데이트에 실패했습니다"); - } - }; + const handleVerifyStatus = async (id: number, status: VerifyStatus, reason?: string) => { + try { + const score = scores.find((s) => s.languageTestScoreStatusResponse.id === id); + await scoreApi.updateLanguageScore(id, status, reason, score); + fetchScores(); + } catch (error) { + console.error("Failed to update Language score:", error); + toast.error("성적 상태 업데이트에 실패했습니다"); + } + }; - const handleEdit = (score: LanguageScoreWithUser) => { - setEditingId(score.languageTestScoreStatusResponse.id); - setEditingScore(score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore); - setEditingType(score.languageTestScoreStatusResponse.languageTestResponse.languageTestType as LanguageTestType); - }; + const handleEdit = (score: LanguageScoreWithUser) => { + setEditingId(score.languageTestScoreStatusResponse.id); + setEditingScore(score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore); + setEditingType(score.languageTestScoreStatusResponse.languageTestResponse.languageTestType as LanguageTestType); + }; - const handleSave = async (score: LanguageScoreWithUser) => { - try { - await scoreApi.updateLanguageScore( - score.languageTestScoreStatusResponse.id, - score.languageTestScoreStatusResponse.verifyStatus, - score.languageTestScoreStatusResponse.rejectedReason || undefined, - { - ...score, - languageTestScoreStatusResponse: { - ...score.languageTestScoreStatusResponse, - languageTestResponse: { - ...score.languageTestScoreStatusResponse.languageTestResponse, - languageTestScore: editingScore, - languageTestType: editingType, - }, - }, - }, - ); - setEditingId(null); - fetchScores(); - toast.success("어학성적이 수정되었습니다"); - } catch (error) { - console.error("Failed to update language score:", error); - toast.error("어학성적 수정에 실패했습니다"); - } - }; + const handleSave = async (score: LanguageScoreWithUser) => { + try { + await scoreApi.updateLanguageScore( + score.languageTestScoreStatusResponse.id, + score.languageTestScoreStatusResponse.verifyStatus, + score.languageTestScoreStatusResponse.rejectedReason || undefined, + { + ...score, + languageTestScoreStatusResponse: { + ...score.languageTestScoreStatusResponse, + languageTestResponse: { + ...score.languageTestScoreStatusResponse.languageTestResponse, + languageTestScore: editingScore, + languageTestType: editingType, + }, + }, + }, + ); + setEditingId(null); + fetchScores(); + toast.success("어학성적이 수정되었습니다"); + } catch (error) { + console.error("Failed to update language score:", error); + toast.error("어학성적 수정에 실패했습니다"); + } + }; - const handlePageChange = (newPage: number) => { - if (newPage < 1 || newPage > totalPages) return; - setPage(newPage); - }; + const handlePageChange = (newPage: number) => { + if (newPage < 1 || newPage > totalPages) return; + setPage(newPage); + }; - return ( -
-
- - - - ID - 닉네임 - 시험종류 - 점수 - 상태 - 제출일 - 거절사유 - 인증파일 - 작업 - - - - {loading ? ( - - -
-
- 로딩중... -
- - - ) : scores.length === 0 ? ( - - - 데이터가 없습니다 - - - ) : ( - scores.map((score) => ( - - {score.languageTestScoreStatusResponse.id} - -
- 프로필 - {score.siteUserResponse.nickname} -
-
- - {editingId === score.languageTestScoreStatusResponse.id ? ( -
- -
- ) : ( - LANGUAGE_TEST_OPTIONS.find( - (option) => - option.value === score.languageTestScoreStatusResponse.languageTestResponse.languageTestType, - )?.label || score.languageTestScoreStatusResponse.languageTestResponse.languageTestType - )} -
- - {editingId === score.languageTestScoreStatusResponse.id ? ( -
- setEditingScore(e.target.value)} - className="w-20 rounded border px-2 py-1" - /> - - -
- ) : ( -
- {score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore} - -
- )} -
- - - - - {format(new Date(score.languageTestScoreStatusResponse.createdAt), "yyyy-MM-dd HH:mm")} - - {score.languageTestScoreStatusResponse.rejectedReason || "-"} - - - 파일 보기 - - - - - handleVerifyStatus(score.languageTestScoreStatusResponse.id, status, reason) - } - /> - -
- )) - )} - -
-
-
- - {Array.from({ length: totalPages }, (_, idx) => ( - - ))} - -
-
- ); + return ( +
+
+ + + + ID + 닉네임 + 시험종류 + 점수 + 상태 + 제출일 + 거절사유 + 인증파일 + 작업 + + + + {loading ? ( + + +
+
+ 로딩중... +
+ + + ) : scores.length === 0 ? ( + + + 데이터가 없습니다 + + + ) : ( + scores.map((score) => ( + + {score.languageTestScoreStatusResponse.id} + +
+ 프로필 + {score.siteUserResponse.nickname} +
+
+ + {editingId === score.languageTestScoreStatusResponse.id ? ( +
+ +
+ ) : ( + LANGUAGE_TEST_OPTIONS.find( + (option) => + option.value === score.languageTestScoreStatusResponse.languageTestResponse.languageTestType, + )?.label || score.languageTestScoreStatusResponse.languageTestResponse.languageTestType + )} +
+ + {editingId === score.languageTestScoreStatusResponse.id ? ( +
+ setEditingScore(e.target.value)} + className="w-20 rounded border px-2 py-1" + /> + + +
+ ) : ( +
+ {score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore} + +
+ )} +
+ + + + + {format(new Date(score.languageTestScoreStatusResponse.createdAt), "yyyy-MM-dd HH:mm")} + + {score.languageTestScoreStatusResponse.rejectedReason || "-"} + + + 파일 보기 + + + + + handleVerifyStatus(score.languageTestScoreStatusResponse.id, status, reason) + } + /> + +
+ )) + )} + +
+
+
+ + {Array.from({ length: totalPages }, (_, idx) => ( + + ))} + +
+
+ ); } diff --git a/apps/admin/src/components/features/scores/ScoreVerifyButton.tsx b/apps/admin/src/components/features/scores/ScoreVerifyButton.tsx index 5169dce5..583be920 100644 --- a/apps/admin/src/components/features/scores/ScoreVerifyButton.tsx +++ b/apps/admin/src/components/features/scores/ScoreVerifyButton.tsx @@ -2,68 +2,68 @@ import { useState } from "react"; import type { VerifyStatus } from "@/types/scores"; interface Props { - currentStatus: VerifyStatus; - onVerifyChange: (status: VerifyStatus, reason?: string) => void; + currentStatus: VerifyStatus; + onVerifyChange: (status: VerifyStatus, reason?: string) => void; } export function ScoreVerifyButton({ currentStatus, onVerifyChange }: Props) { - const [showRejectInput, setShowRejectInput] = useState(false); - const [rejectReason, setRejectReason] = useState(""); + const [showRejectInput, setShowRejectInput] = useState(false); + const [rejectReason, setRejectReason] = useState(""); - const handleApprove = () => { - onVerifyChange("APPROVED"); - }; + const handleApprove = () => { + onVerifyChange("APPROVED"); + }; - const handleReject = () => { - if (showRejectInput) { - onVerifyChange("REJECTED", rejectReason); - setShowRejectInput(false); - setRejectReason(""); - } else { - setShowRejectInput(true); - } - }; + const handleReject = () => { + if (showRejectInput) { + onVerifyChange("REJECTED", rejectReason); + setShowRejectInput(false); + setRejectReason(""); + } else { + setShowRejectInput(true); + } + }; - if (currentStatus !== "PENDING") { - return null; - } + if (currentStatus !== "PENDING") { + return null; + } - return ( -
- + return ( +
+ - {showRejectInput ? ( -
- setRejectReason(e.target.value)} - placeholder="거절 사유" - className="rounded border px-2 py-1" - /> - -
- ) : ( - - )} -
- ); + {showRejectInput ? ( +
+ setRejectReason(e.target.value)} + placeholder="거절 사유" + className="rounded border px-2 py-1" + /> + +
+ ) : ( + + )} +
+ ); } diff --git a/apps/admin/src/components/features/scores/StatusBadge.tsx b/apps/admin/src/components/features/scores/StatusBadge.tsx index 51cce272..434871d5 100644 --- a/apps/admin/src/components/features/scores/StatusBadge.tsx +++ b/apps/admin/src/components/features/scores/StatusBadge.tsx @@ -1,25 +1,25 @@ import type { VerifyStatus } from "@/types/scores"; const statusStyles = { - PENDING: "bg-yellow-100 text-yellow-800", - APPROVED: "bg-green-100 text-green-800", - REJECTED: "bg-red-100 text-red-800", + PENDING: "bg-yellow-100 text-yellow-800", + APPROVED: "bg-green-100 text-green-800", + REJECTED: "bg-red-100 text-red-800", }; const statusLabels = { - PENDING: "대기중", - APPROVED: "승인됨", - REJECTED: "거절됨", + PENDING: "대기중", + APPROVED: "승인됨", + REJECTED: "거절됨", }; interface StatusBadgeProps { - status: VerifyStatus; + status: VerifyStatus; } export function StatusBadge({ status }: StatusBadgeProps) { - return ( - - {statusLabels[status]} - - ); + return ( + + {statusLabels[status]} + + ); } diff --git a/apps/admin/src/components/layout/AdminLayout.tsx b/apps/admin/src/components/layout/AdminLayout.tsx index e15bb4cc..636cdc12 100644 --- a/apps/admin/src/components/layout/AdminLayout.tsx +++ b/apps/admin/src/components/layout/AdminLayout.tsx @@ -1,11 +1,11 @@ interface AdminLayoutProps { - children: React.ReactNode; + children: React.ReactNode; } export function AdminLayout({ children }: AdminLayoutProps) { - return ( -
-
{children}
-
- ); + return ( +
+
{children}
+
+ ); } diff --git a/apps/admin/src/components/ui/button.tsx b/apps/admin/src/components/ui/button.tsx index 54c8c57b..502bf39e 100644 --- a/apps/admin/src/components/ui/button.tsx +++ b/apps/admin/src/components/ui/button.tsx @@ -5,42 +5,42 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, ); export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ; - }, + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; + }, ); Button.displayName = "Button"; diff --git a/apps/admin/src/components/ui/card.tsx b/apps/admin/src/components/ui/card.tsx index 82099ea7..63ba9666 100644 --- a/apps/admin/src/components/ui/card.tsx +++ b/apps/admin/src/components/ui/card.tsx @@ -3,40 +3,40 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const Card = React.forwardRef>(({ className, ...props }, ref) => ( -
+
)); Card.displayName = "Card"; const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), + ({ className, ...props }, ref) => ( +
+ ), ); CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), + ({ className, ...props }, ref) => ( +
+ ), ); CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), + ({ className, ...props }, ref) => ( +
+ ), ); CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef>( - ({ className, ...props }, ref) =>
, + ({ className, ...props }, ref) =>
, ); CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), + ({ className, ...props }, ref) => ( +
+ ), ); CardFooter.displayName = "CardFooter"; diff --git a/apps/admin/src/components/ui/input.tsx b/apps/admin/src/components/ui/input.tsx index 7db52411..875315a0 100644 --- a/apps/admin/src/components/ui/input.tsx +++ b/apps/admin/src/components/ui/input.tsx @@ -3,19 +3,19 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { - return ( - - ); - }, + ({ className, type, ...props }, ref) => { + return ( + + ); + }, ); Input.displayName = "Input"; diff --git a/apps/admin/src/components/ui/label.tsx b/apps/admin/src/components/ui/label.tsx index a37a1edf..17727c08 100644 --- a/apps/admin/src/components/ui/label.tsx +++ b/apps/admin/src/components/ui/label.tsx @@ -7,10 +7,10 @@ import { cn } from "@/lib/utils"; const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); const Label = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef & VariantProps + React.ComponentRef, + React.ComponentPropsWithoutRef & VariantProps >(({ className, ...props }, ref) => ( - + )); Label.displayName = LabelPrimitive.Root.displayName; diff --git a/apps/admin/src/components/ui/table.tsx b/apps/admin/src/components/ui/table.tsx index 1f70268e..c2bf6d74 100644 --- a/apps/admin/src/components/ui/table.tsx +++ b/apps/admin/src/components/ui/table.tsx @@ -3,73 +3,73 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const Table = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- - - ), + ({ className, ...props }, ref) => ( +
+
+ + ), ); Table.displayName = "Table"; const TableHeader = React.forwardRef>( - ({ className, ...props }, ref) => , + ({ className, ...props }, ref) => , ); TableHeader.displayName = "TableHeader"; const TableBody = React.forwardRef>( - ({ className, ...props }, ref) => ( - - ), + ({ className, ...props }, ref) => ( + + ), ); TableBody.displayName = "TableBody"; const TableFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( - tr]:last:border-b-0", className)} {...props} /> - ), + ({ className, ...props }, ref) => ( + tr]:last:border-b-0", className)} {...props} /> + ), ); TableFooter.displayName = "TableFooter"; const TableRow = React.forwardRef>( - ({ className, ...props }, ref) => ( - - ), + ({ className, ...props }, ref) => ( + + ), ); TableRow.displayName = "TableRow"; const TableHead = React.forwardRef>( - ({ className, ...props }, ref) => ( -
[role=checkbox]]:translate-y-[2px]", - className, - )} - {...props} - /> - ), + ({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ), ); TableHead.displayName = "TableHead"; const TableCell = React.forwardRef>( - ({ className, ...props }, ref) => ( - [role=checkbox]]:translate-y-[2px]", className)} - {...props} - /> - ), + ({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", className)} + {...props} + /> + ), ); TableCell.displayName = "TableCell"; const TableCaption = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), + ({ className, ...props }, ref) => ( + + ), ); TableCaption.displayName = "TableCaption"; diff --git a/apps/admin/src/components/ui/tabs.tsx b/apps/admin/src/components/ui/tabs.tsx index b9fced1a..6dcfd29e 100644 --- a/apps/admin/src/components/ui/tabs.tsx +++ b/apps/admin/src/components/ui/tabs.tsx @@ -6,47 +6,47 @@ import { cn } from "@/lib/utils"; const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef + React.ComponentRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef + React.ComponentRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef + React.ComponentRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); TabsContent.displayName = TabsPrimitive.Content.displayName; diff --git a/apps/admin/src/lib/api/auth.ts b/apps/admin/src/lib/api/auth.ts index 49648bba..21c56dc3 100644 --- a/apps/admin/src/lib/api/auth.ts +++ b/apps/admin/src/lib/api/auth.ts @@ -3,13 +3,13 @@ import { publicAxiosInstance } from "@/lib/api/client"; import type { AdminSignInResponse, ReissueAccessTokenResponse } from "@/types/auth"; export const adminSignInApi = (email: string, password: string): Promise> => - publicAxiosInstance.post("/auth/email/sign-in", { email, password }); + publicAxiosInstance.post("/auth/email/sign-in", { email, password }); export const reissueAccessTokenApi = (refreshToken: string): Promise> => - publicAxiosInstance.post( - "/admin/auth/reissue", - {}, - { - headers: { Authorization: `Bearer ${refreshToken}` }, - }, - ); + publicAxiosInstance.post( + "/admin/auth/reissue", + {}, + { + headers: { Authorization: `Bearer ${refreshToken}` }, + }, + ); diff --git a/apps/admin/src/lib/api/client.ts b/apps/admin/src/lib/api/client.ts index 1efcf4d3..ea475449 100644 --- a/apps/admin/src/lib/api/client.ts +++ b/apps/admin/src/lib/api/client.ts @@ -2,87 +2,87 @@ import axios, { type AxiosInstance } from "axios"; import { reissueAccessTokenApi } from "@/lib/api/auth"; import { isTokenExpired } from "@/lib/utils/jwtUtils"; import { - loadAccessToken, - loadRefreshToken, - removeAccessToken, - removeRefreshToken, - saveAccessToken, + loadAccessToken, + loadRefreshToken, + removeAccessToken, + removeRefreshToken, + saveAccessToken, } from "@/lib/utils/localStorage"; const convertToBearer = (token: string) => `Bearer ${token}`; export const axiosInstance: AxiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_SERVER_URL, - withCredentials: true, + baseURL: import.meta.env.VITE_API_SERVER_URL, + withCredentials: true, }); axiosInstance.interceptors.request.use( - async (config) => { - const newConfig = { ...config }; - let accessToken: string | null = loadAccessToken(); + async (config) => { + const newConfig = { ...config }; + let accessToken: string | null = loadAccessToken(); - if (accessToken === null || isTokenExpired(accessToken)) { - const refreshToken = loadRefreshToken(); - if (refreshToken === null || isTokenExpired(refreshToken)) { - removeAccessToken(); - removeRefreshToken(); - return config; - } + if (accessToken === null || isTokenExpired(accessToken)) { + const refreshToken = loadRefreshToken(); + if (refreshToken === null || isTokenExpired(refreshToken)) { + removeAccessToken(); + removeRefreshToken(); + return config; + } - await reissueAccessTokenApi(refreshToken) - .then((res) => { - accessToken = res.data.accessToken; - saveAccessToken(accessToken); - }) - .catch((err) => { - removeAccessToken(); - removeRefreshToken(); - console.error("인증 토큰 갱신중 오류가 발생했습니다", err); - }); - } + await reissueAccessTokenApi(refreshToken) + .then((res) => { + accessToken = res.data.accessToken; + saveAccessToken(accessToken); + }) + .catch((err) => { + removeAccessToken(); + removeRefreshToken(); + console.error("인증 토큰 갱신중 오류가 발생했습니다", err); + }); + } - if (accessToken !== null) { - newConfig.headers.Authorization = convertToBearer(accessToken); - } - return newConfig; - }, - (error) => Promise.reject(error), + if (accessToken !== null) { + newConfig.headers.Authorization = convertToBearer(accessToken); + } + return newConfig; + }, + (error) => Promise.reject(error), ); axiosInstance.interceptors.response.use( - (response) => response, - async (error) => { - const newError = { ...error }; - if (error.response?.status === 401 || error.response?.status === 403) { - const refreshToken = loadRefreshToken(); + (response) => response, + async (error) => { + const newError = { ...error }; + if (error.response?.status === 401 || error.response?.status === 403) { + const refreshToken = loadRefreshToken(); - if (refreshToken === null || isTokenExpired(refreshToken)) { - removeAccessToken(); - removeRefreshToken(); - throw newError; - } + if (refreshToken === null || isTokenExpired(refreshToken)) { + removeAccessToken(); + removeRefreshToken(); + throw newError; + } - try { - const newAccessToken = await reissueAccessTokenApi(refreshToken).then((res) => res.data.accessToken); - saveAccessToken(newAccessToken); + try { + const newAccessToken = await reissueAccessTokenApi(refreshToken).then((res) => res.data.accessToken); + saveAccessToken(newAccessToken); - if (error?.config.headers === undefined) { - newError.config.headers = {}; - } - newError.config.headers.Authorization = convertToBearer(newAccessToken); + if (error?.config.headers === undefined) { + newError.config.headers = {}; + } + newError.config.headers.Authorization = convertToBearer(newAccessToken); - return await axios.request(newError.config); - } catch (err) { - removeAccessToken(); - removeRefreshToken(); - throw Error("로그인이 필요합니다"); - } - } else { - throw newError; - } - }, + return await axios.request(newError.config); + } catch (_err) { + removeAccessToken(); + removeRefreshToken(); + throw Error("로그인이 필요합니다"); + } + } else { + throw newError; + } + }, ); export const publicAxiosInstance: AxiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_SERVER_URL, + baseURL: import.meta.env.VITE_API_SERVER_URL, }); diff --git a/apps/admin/src/lib/api/scores.ts b/apps/admin/src/lib/api/scores.ts index d7775e3a..83b3be8e 100644 --- a/apps/admin/src/lib/api/scores.ts +++ b/apps/admin/src/lib/api/scores.ts @@ -1,45 +1,45 @@ import { axiosInstance } from "@/lib/api/client"; import type { - GpaScoreUpdateRequest, - GpaScoreWithUser, - LanguageScoreWithUser, - LanguageTestScoreUpdateRequest, - LanguageTestType, - PageResponse, - ScoreSearchCondition, - VerifyStatus, + GpaScoreUpdateRequest, + GpaScoreWithUser, + LanguageScoreWithUser, + LanguageTestScoreUpdateRequest, + LanguageTestType, + PageResponse, + ScoreSearchCondition, + VerifyStatus, } from "@/types/scores"; export const scoreApi = { - // GPA 성적 조회 - getGpaScores: (condition: ScoreSearchCondition, page: number): Promise> => - axiosInstance.get("/admin/scores/gpas", { params: { ...condition, page } }).then((res) => res.data), + // GPA 성적 조회 + getGpaScores: (condition: ScoreSearchCondition, page: number): Promise> => + axiosInstance.get("/admin/scores/gpas", { params: { ...condition, page } }).then((res) => res.data), - // GPA 성적 수정 - updateGpaScore: (id: number, status: VerifyStatus, reason?: string, score?: GpaScoreWithUser) => { - if (!score) throw new Error("Score data is required"); - const request: GpaScoreUpdateRequest = { - gpa: score.gpaScoreStatusResponse.gpaResponse.gpa, - gpaCriteria: score.gpaScoreStatusResponse.gpaResponse.gpaCriteria, - verifyStatus: status, - rejectedReason: reason, - }; - return axiosInstance.put(`/admin/scores/gpas/${id}`, request); - }, + // GPA 성적 수정 + updateGpaScore: (id: number, status: VerifyStatus, reason?: string, score?: GpaScoreWithUser) => { + if (!score) throw new Error("Score data is required"); + const request: GpaScoreUpdateRequest = { + gpa: score.gpaScoreStatusResponse.gpaResponse.gpa, + gpaCriteria: score.gpaScoreStatusResponse.gpaResponse.gpaCriteria, + verifyStatus: status, + rejectedReason: reason, + }; + return axiosInstance.put(`/admin/scores/gpas/${id}`, request); + }, - // 어학성적 조회 - getLanguageScores: (condition: ScoreSearchCondition, page: number): Promise> => - axiosInstance.get("/admin/scores/language-tests", { params: { ...condition, page } }).then((res) => res.data), + // 어학성적 조회 + getLanguageScores: (condition: ScoreSearchCondition, page: number): Promise> => + axiosInstance.get("/admin/scores/language-tests", { params: { ...condition, page } }).then((res) => res.data), - // 어학성적 수정 - updateLanguageScore: (id: number, status: VerifyStatus, reason?: string, score?: LanguageScoreWithUser) => { - if (!score) throw new Error("Score data is required"); - const request: LanguageTestScoreUpdateRequest = { - languageTestType: score.languageTestScoreStatusResponse.languageTestResponse.languageTestType as LanguageTestType, - languageTestScore: score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore, - verifyStatus: status, - rejectedReason: reason, - }; - return axiosInstance.put(`/admin/scores/language-tests/${id}`, request); - }, + // 어학성적 수정 + updateLanguageScore: (id: number, status: VerifyStatus, reason?: string, score?: LanguageScoreWithUser) => { + if (!score) throw new Error("Score data is required"); + const request: LanguageTestScoreUpdateRequest = { + languageTestType: score.languageTestScoreStatusResponse.languageTestResponse.languageTestType as LanguageTestType, + languageTestScore: score.languageTestScoreStatusResponse.languageTestResponse.languageTestScore, + verifyStatus: status, + rejectedReason: reason, + }; + return axiosInstance.put(`/admin/scores/language-tests/${id}`, request); + }, }; diff --git a/apps/admin/src/lib/utils/index.ts b/apps/admin/src/lib/utils/index.ts index 365058ce..ac680b30 100644 --- a/apps/admin/src/lib/utils/index.ts +++ b/apps/admin/src/lib/utils/index.ts @@ -2,5 +2,5 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } diff --git a/apps/admin/src/lib/utils/jwtUtils.ts b/apps/admin/src/lib/utils/jwtUtils.ts index cbe30350..981a78e5 100644 --- a/apps/admin/src/lib/utils/jwtUtils.ts +++ b/apps/admin/src/lib/utils/jwtUtils.ts @@ -1,22 +1,22 @@ export const isTokenExpired = (token: string): boolean => { - if (!token) { - return true; - } - try { - // JWT의 payload 부분을 디코딩합니다 (Base64URL 디코딩) - const payload = JSON.parse(atob(token.split(".")[1])); + if (!token) { + return true; + } + try { + // JWT의 payload 부분을 디코딩합니다 (Base64URL 디코딩) + const payload = JSON.parse(atob(token.split(".")[1])); - // 현재 시간 (초 단위로) - const currentTime = Math.floor(Date.now() / 1000); + // 현재 시간 (초 단위로) + const currentTime = Math.floor(Date.now() / 1000); - // 토큰의 만료 시간 (`exp` 클레임) - const { exp } = payload; + // 토큰의 만료 시간 (`exp` 클레임) + const { exp } = payload; - // 토큰이 만료되었는지 확인 - return exp < currentTime; - } catch (error) { - console.error("인증 토큰에 문제가 있습니다:", error); - // 토큰이 잘못된 경우 만료된 것으로 간주 - return true; - } + // 토큰이 만료되었는지 확인 + return exp < currentTime; + } catch (error) { + console.error("인증 토큰에 문제가 있습니다:", error); + // 토큰이 잘못된 경우 만료된 것으로 간주 + return true; + } }; diff --git a/apps/admin/src/lib/utils/localStorage.ts b/apps/admin/src/lib/utils/localStorage.ts index fc0660a1..1e055473 100644 --- a/apps/admin/src/lib/utils/localStorage.ts +++ b/apps/admin/src/lib/utils/localStorage.ts @@ -1,49 +1,49 @@ export const loadRefreshToken = () => { - try { - return localStorage.getItem("refreshToken"); - } catch (err) { - console.error("Could not load refresh token", err); - return null; - } + try { + return localStorage.getItem("refreshToken"); + } catch (err) { + console.error("Could not load refresh token", err); + return null; + } }; export const saveRefreshToken = (token: string) => { - try { - localStorage.setItem("refreshToken", token); - } catch (err) { - console.error("Could not save refresh token", err); - } + try { + localStorage.setItem("refreshToken", token); + } catch (err) { + console.error("Could not save refresh token", err); + } }; export const removeRefreshToken = () => { - try { - localStorage.removeItem("refreshToken"); - } catch (err) { - console.error("Could not remove refresh token", err); - } + try { + localStorage.removeItem("refreshToken"); + } catch (err) { + console.error("Could not remove refresh token", err); + } }; export const loadAccessToken = () => { - try { - return localStorage.getItem("accessToken"); - } catch (err) { - console.error("Could not load access token", err); - return null; - } + try { + return localStorage.getItem("accessToken"); + } catch (err) { + console.error("Could not load access token", err); + return null; + } }; export const saveAccessToken = (token: string) => { - try { - localStorage.setItem("accessToken", token); - } catch (err) { - console.error("Could not save access token", err); - } + try { + localStorage.setItem("accessToken", token); + } catch (err) { + console.error("Could not save access token", err); + } }; export const removeAccessToken = () => { - try { - localStorage.removeItem("accessToken"); - } catch (err) { - console.error("Could not remove access token", err); - } + try { + localStorage.removeItem("accessToken"); + } catch (err) { + console.error("Could not remove access token", err); + } }; diff --git a/apps/admin/src/router.tsx b/apps/admin/src/router.tsx index 5c708369..765dbe78 100644 --- a/apps/admin/src/router.tsx +++ b/apps/admin/src/router.tsx @@ -1,17 +1,17 @@ -import { createRouter } from '@tanstack/react-router' +import { createRouter } from "@tanstack/react-router"; // Import the generated route tree -import { routeTree } from './routeTree.gen' +import { routeTree } from "./routeTree.gen"; // Create a new router instance export const getRouter = () => { - const router = createRouter({ - routeTree, - context: {}, + const router = createRouter({ + routeTree, + context: {}, - scrollRestoration: true, - defaultPreloadStaleTime: 0, - }) + scrollRestoration: true, + defaultPreloadStaleTime: 0, + }); - return router -} + return router; +}; diff --git a/apps/admin/src/routes/__root.tsx b/apps/admin/src/routes/__root.tsx index 89a63691..23718ea8 100644 --- a/apps/admin/src/routes/__root.tsx +++ b/apps/admin/src/routes/__root.tsx @@ -8,52 +8,52 @@ import { AdminLayout } from "@/components/layout/AdminLayout"; import appCss from "../styles.css?url"; export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charSet: "utf-8", - }, - { - name: "viewport", - content: "width=device-width, initial-scale=1", - }, - { - title: "Solid Connection Admin", - }, - ], - links: [ - { - rel: "stylesheet", - href: appCss, - }, - ], - }), + head: () => ({ + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title: "Solid Connection Admin", + }, + ], + links: [ + { + rel: "stylesheet", + href: appCss, + }, + ], + }), - shellComponent: RootDocument, + shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - - - - - {children} - - , - }, - ]} - /> - - - - ); + return ( + + + + + + {children} + + , + }, + ]} + /> + + + + ); } diff --git a/apps/admin/src/routes/auth/login.tsx b/apps/admin/src/routes/auth/login.tsx index 8e779a90..d04c3481 100644 --- a/apps/admin/src/routes/auth/login.tsx +++ b/apps/admin/src/routes/auth/login.tsx @@ -9,85 +9,87 @@ import { adminSignInApi } from "@/lib/api/auth"; import { saveAccessToken, saveRefreshToken } from "@/lib/utils/localStorage"; export const Route = createFileRoute("/auth/login")({ - component: LoginPage, + component: LoginPage, }); function LoginPage() { - const navigate = useNavigate(); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsLoading(true); - try { - const response = await adminSignInApi(email, password); - const { accessToken, refreshToken } = response.data; + try { + const response = await adminSignInApi(email, password); + const { accessToken, refreshToken } = response.data; - saveAccessToken(accessToken); - saveRefreshToken(refreshToken); + saveAccessToken(accessToken); + saveRefreshToken(refreshToken); - toast("로그인 성공", { - description: "관리자 페이지로 이동합니다.", - }); + toast("로그인 성공", { + description: "관리자 페이지로 이동합니다.", + }); - navigate({ to: "/scores" }); - } catch (err: unknown) { - const error = err as { response?: { data?: { message?: string } } }; - toast.error("로그인 실패", { - description: error.response?.data?.message || "로그인에 실패했습니다.", - }); - } finally { - setIsLoading(false); - } - }; + navigate({ to: "/scores" }); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + toast.error("로그인 실패", { + description: error.response?.data?.message || "로그인에 실패했습니다.", + }); + } finally { + setIsLoading(false); + } + }; - return ( -
- - - 관리자 로그인 - 솔리드 커넥션 관리자 페이지입니다 - - -
-
- - setEmail(e.target.value)} - disabled={isLoading} - required - className="h-9" - /> -
-
- - setPassword(e.target.value)} - disabled={isLoading} - required - className="h-9" - /> -
- -
-
-
-
- ); + return ( +
+ + + 관리자 로그인 + 솔리드 커넥션 관리자 페이지입니다 + + +
+
+ + {/* biome-ignore lint/correctness/useUniqueElementIds: login form is singleton */} + setEmail(e.target.value)} + disabled={isLoading} + required + className="h-9" + /> +
+
+ + {/* biome-ignore lint/correctness/useUniqueElementIds: login form is singleton */} + setPassword(e.target.value)} + disabled={isLoading} + required + className="h-9" + /> +
+ +
+
+
+
+ ); } diff --git a/apps/admin/src/routes/index.tsx b/apps/admin/src/routes/index.tsx index c2316528..45d3c3eb 100644 --- a/apps/admin/src/routes/index.tsx +++ b/apps/admin/src/routes/index.tsx @@ -1,7 +1,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ - beforeLoad: () => { - throw redirect({ to: "/scores" }); - }, + beforeLoad: () => { + throw redirect({ to: "/scores" }); + }, }); diff --git a/apps/admin/src/routes/scores/index.tsx b/apps/admin/src/routes/scores/index.tsx index 287c3fef..2fc550f1 100644 --- a/apps/admin/src/routes/scores/index.tsx +++ b/apps/admin/src/routes/scores/index.tsx @@ -8,51 +8,51 @@ import { loadAccessToken } from "@/lib/utils/localStorage"; import type { VerifyStatus } from "@/types/scores"; export const Route = createFileRoute("/scores/")({ - beforeLoad: () => { - // 클라이언트 사이드에서만 인증 체크 - if (typeof window !== "undefined") { - const token = loadAccessToken(); - if (!token || isTokenExpired(token)) { - throw redirect({ to: "/auth/login" }); - } - } - }, - component: ScoresPage, + beforeLoad: () => { + // 클라이언트 사이드에서만 인증 체크 + if (typeof window !== "undefined") { + const token = loadAccessToken(); + if (!token || isTokenExpired(token)) { + throw redirect({ to: "/auth/login" }); + } + } + }, + component: ScoresPage, }); function ScoresPage() { - const [verifyFilter, setVerifyFilter] = useState("PENDING"); + const [verifyFilter, setVerifyFilter] = useState("PENDING"); - return ( -
-

성적 관리

+ return ( +
+

성적 관리

-
- -
+
+ +
- - - GPA 성적 - 어학성적 - + + + GPA 성적 + 어학성적 + - - - + + + - - - - -
- ); + + + + +
+ ); } diff --git a/apps/admin/src/types/auth.ts b/apps/admin/src/types/auth.ts index d90ff077..73034795 100644 --- a/apps/admin/src/types/auth.ts +++ b/apps/admin/src/types/auth.ts @@ -1,8 +1,8 @@ export interface AdminSignInResponse { - accessToken: string; - refreshToken: string; + accessToken: string; + refreshToken: string; } export interface ReissueAccessTokenResponse { - accessToken: string; + accessToken: string; } diff --git a/apps/admin/src/types/scores.ts b/apps/admin/src/types/scores.ts index 81de579f..9e95bc1b 100644 --- a/apps/admin/src/types/scores.ts +++ b/apps/admin/src/types/scores.ts @@ -1,105 +1,105 @@ export type VerifyStatus = "PENDING" | "APPROVED" | "REJECTED"; export interface ScoreSearchCondition { - verifyStatus?: VerifyStatus; + verifyStatus?: VerifyStatus; } export interface GpaResponse { - gpa: number; - gpaCriteria: number; - gpaReportUrl: string; + gpa: number; + gpaCriteria: number; + gpaReportUrl: string; } export interface GpaScore { - verifyStatus: VerifyStatus; - rejectedReason?: string; + verifyStatus: VerifyStatus; + rejectedReason?: string; } export interface GpaScoreStatusResponse { - id: number; - gpaResponse: GpaResponse; - verifyStatus: VerifyStatus; - rejectedReason: string | null; - createdAt: string; - updatedAt: string; + id: number; + gpaResponse: GpaResponse; + verifyStatus: VerifyStatus; + rejectedReason: string | null; + createdAt: string; + updatedAt: string; } export interface SiteUserResponse { - id: number; - nickname: string; - profileImageUrl: string; + id: number; + nickname: string; + profileImageUrl: string; } export interface GpaScoreWithUser { - gpaScoreStatusResponse: GpaScoreStatusResponse; - siteUserResponse: SiteUserResponse; + gpaScoreStatusResponse: GpaScoreStatusResponse; + siteUserResponse: SiteUserResponse; } export interface PageResponse { - content: T[]; - pageNumber: number; - pageSize: number; - totalElements: number; - totalPages: number; + content: T[]; + pageNumber: number; + pageSize: number; + totalElements: number; + totalPages: number; } export interface LanguageResponse { - languageType: string; - score: number; - testDate: string; - expireDate: string; - languageReportUrl: string; + languageType: string; + score: number; + testDate: string; + expireDate: string; + languageReportUrl: string; } export interface LanguageTestResponse { - languageTestType: string; - languageTestScore: string; - languageTestReportUrl: string; + languageTestType: string; + languageTestScore: string; + languageTestReportUrl: string; } export interface LanguageTestScore { - verifyStatus: VerifyStatus; - rejectedReason?: string; + verifyStatus: VerifyStatus; + rejectedReason?: string; } export interface LanguageTestScoreStatusResponse { - id: number; - languageTestResponse: LanguageTestResponse; - verifyStatus: VerifyStatus; - rejectedReason: string | null; - createdAt: string; - updatedAt: string; + id: number; + languageTestResponse: LanguageTestResponse; + verifyStatus: VerifyStatus; + rejectedReason: string | null; + createdAt: string; + updatedAt: string; } export interface LanguageScoreWithUser { - languageTestScoreStatusResponse: LanguageTestScoreStatusResponse; - siteUserResponse: SiteUserResponse; + languageTestScoreStatusResponse: LanguageTestScoreStatusResponse; + siteUserResponse: SiteUserResponse; } export type LanguageTestType = - | "TOEIC" - | "TOEFL_IBT" - | "TOEFL_ITP" - | "IELTS" - | "JLPT" - | "NEW_HSK" - | "ETC" - | "DALF" - | "CEFR" - | "TCF" - | "TEF" - | "DUOLINGO"; + | "TOEIC" + | "TOEFL_IBT" + | "TOEFL_ITP" + | "IELTS" + | "JLPT" + | "NEW_HSK" + | "ETC" + | "DALF" + | "CEFR" + | "TCF" + | "TEF" + | "DUOLINGO"; export interface GpaScoreUpdateRequest { - gpa: number; - gpaCriteria: number; - verifyStatus: VerifyStatus; - rejectedReason?: string; + gpa: number; + gpaCriteria: number; + verifyStatus: VerifyStatus; + rejectedReason?: string; } export interface LanguageTestScoreUpdateRequest { - languageTestType: LanguageTestType; - languageTestScore: string; - verifyStatus: VerifyStatus; - rejectedReason?: string; + languageTestType: LanguageTestType; + languageTestScore: string; + verifyStatus: VerifyStatus; + rejectedReason?: string; } diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index b4b93964..ccfdd640 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -1,30 +1,29 @@ -import { defineConfig } from 'vite' -import { devtools } from '@tanstack/devtools-vite' -import { tanstackStart } from '@tanstack/react-start/plugin/vite' -import viteReact from '@vitejs/plugin-react' -import viteTsConfigPaths from 'vite-tsconfig-paths' -import { fileURLToPath, URL } from 'url' - -import tailwindcss from '@tailwindcss/vite' -import { nitro } from 'nitro/vite' +import tailwindcss from "@tailwindcss/vite"; +import { devtools } from "@tanstack/devtools-vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import viteReact from "@vitejs/plugin-react"; +import { nitro } from "nitro/vite"; +import { fileURLToPath, URL } from "node:url"; +import { defineConfig } from "vite"; +import viteTsConfigPaths from "vite-tsconfig-paths"; const config = defineConfig({ - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - plugins: [ - devtools(), - nitro(), - // this is the plugin that enables path aliases - viteTsConfigPaths({ - projects: ['./tsconfig.json'], - }), - tailwindcss(), - tanstackStart(), - viteReact(), - ], -}) + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + plugins: [ + devtools(), + nitro(), + // this is the plugin that enables path aliases + viteTsConfigPaths({ + projects: ["./tsconfig.json"], + }), + tailwindcss(), + tanstackStart(), + viteReact(), + ], +}); -export default config +export default config; From 4e38ec8bce3da8661a46a470d8eb942c039c373d Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 01:01:42 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=90=9B=20web=20lint=20=EB=B0=8F=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/next.config.mjs | 3 +- .../NewsSection/_hooks/useSectionHadnler.ts | 2 +- apps/web/src/app/layout.tsx | 2 +- .../_hooks/useSelectedTab.ts | 2 +- .../_hooks/useImageInputHandler.ts | 2 +- .../application/ScorePageContent.tsx | 2 +- .../university/application/ScoreSearchBar.tsx | 2 +- .../app/university/application/ScoreSheet.tsx | 4 +- .../ChannelSelct/_hooks/useSelectHandler.ts | 2 +- .../ui/BottomSheet/hooks/useHandleModal.ts | 10 +- .../hooks/useFloatingUpHandler.ts | 12 +-- .../components/ui/UniverSityCard/index.tsx | 2 +- .../lib/react-query/useMutationWithFailure.ts | 99 +++++++++++++++++++ 13 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 apps/web/src/lib/react-query/useMutationWithFailure.ts diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index e8f6a8a5..44217529 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,6 +1,7 @@ // Injected content via Sentry wizard below -import { withSentryConfig } from "@sentry/nextjs"; + import bundleAnalyzer from "@next/bundle-analyzer"; +import { withSentryConfig } from "@sentry/nextjs"; const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === "true", diff --git a/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts b/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts index 7613dedb..947cbc65 100644 --- a/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts +++ b/apps/web/src/app/(home)/_ui/NewsSection/_hooks/useSectionHadnler.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, type RefObject } from "react"; +import { type RefObject, useEffect, useRef, useState } from "react"; interface UseSectionHandlerReturn { sectionRef: RefObject; diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 47c53c49..507a71ae 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata, Viewport } from "next"; -import type { ReactNode } from "react"; import dynamic from "next/dynamic"; import localFont from "next/font/local"; +import type { ReactNode } from "react"; import GlobalLayout from "@/components/layout/GlobalLayout"; import ToastContainer from "@/components/ui/Toast"; diff --git a/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts b/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts index e5477135..8f1295d8 100644 --- a/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts +++ b/apps/web/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/useSelectedTab.ts @@ -1,4 +1,4 @@ -import { useRef, useState, type RefObject } from "react"; +import { type RefObject, useRef, useState } from "react"; import { FilterTab } from "@/types/mentor"; diff --git a/apps/web/src/app/my/modify/_ui/ModifyContent/_ui/ImageInputFiled/_hooks/useImageInputHandler.ts b/apps/web/src/app/my/modify/_ui/ModifyContent/_ui/ImageInputFiled/_hooks/useImageInputHandler.ts index 812cb209..f01c2cfd 100644 --- a/apps/web/src/app/my/modify/_ui/ModifyContent/_ui/ImageInputFiled/_hooks/useImageInputHandler.ts +++ b/apps/web/src/app/my/modify/_ui/ModifyContent/_ui/ImageInputFiled/_hooks/useImageInputHandler.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, type ChangeEvent, type RefObject } from "react"; +import { type ChangeEvent, type RefObject, useEffect, useRef, useState } from "react"; import { useController, useFormContext } from "react-hook-form"; import { convertUploadedImageUrl } from "@/utils/fileUtils"; diff --git a/apps/web/src/app/university/application/ScorePageContent.tsx b/apps/web/src/app/university/application/ScorePageContent.tsx index 79fd5766..78c53cea 100644 --- a/apps/web/src/app/university/application/ScorePageContent.tsx +++ b/apps/web/src/app/university/application/ScorePageContent.tsx @@ -7,7 +7,7 @@ import ConfirmCancelModal from "@/components/modal/ConfirmCancelModal"; import ButtonTab from "@/components/ui/ButtonTab"; import Tab from "@/components/ui/Tab"; import { REGIONS_KO } from "@/constants/university"; -import { toast } from "@/lib/zustand/useToastStore"; + import type { ScoreSheet as ScoreSheetType } from "@/types/application"; import type { RegionKo } from "@/types/university"; import ScoreSearchBar from "./ScoreSearchBar"; diff --git a/apps/web/src/app/university/application/ScoreSearchBar.tsx b/apps/web/src/app/university/application/ScoreSearchBar.tsx index 65c068a7..2f402fe0 100644 --- a/apps/web/src/app/university/application/ScoreSearchBar.tsx +++ b/apps/web/src/app/university/application/ScoreSearchBar.tsx @@ -1,5 +1,5 @@ -import { IconSearchFilled } from "@/public/svgs"; import type { RefObject } from "react"; +import { IconSearchFilled } from "@/public/svgs"; type ScoreSearchBarProps = { onClick: () => void; diff --git a/apps/web/src/app/university/application/ScoreSheet.tsx b/apps/web/src/app/university/application/ScoreSheet.tsx index 4ff3285f..3e373eb1 100644 --- a/apps/web/src/app/university/application/ScoreSheet.tsx +++ b/apps/web/src/app/university/application/ScoreSheet.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { IconExpandMoreFilled } from "@/public/svgs/community"; -import type { ScoreSheet } from "@/types/application"; +import type { ScoreSheet as ScoreSheetType } from "@/types/application"; import { languageTestMapping } from "@/types/score"; -const ScoreSheet = ({ scoreSheet }: { scoreSheet: ScoreSheet }) => { +const ScoreSheet = ({ scoreSheet }: { scoreSheet: ScoreSheetType }) => { const [tableOpened, setTableOpened] = useState(false); return ( diff --git a/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts b/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts index 76f9dca8..8f03f1d1 100644 --- a/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts +++ b/apps/web/src/components/mentor/ChannelSelct/_hooks/useSelectHandler.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, type RefObject } from "react"; +import { type RefObject, useEffect, useRef, useState } from "react"; import { type Control, type FieldValues, useController } from "react-hook-form"; import type { ChannelType } from "@/types/mentor"; diff --git a/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts b/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts index bdf8b0ed..3b0e5f6f 100644 --- a/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts +++ b/apps/web/src/components/ui/BottomSheet/hooks/useHandleModal.ts @@ -1,12 +1,4 @@ -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, - type MutableRefObject, - type RefObject, -} from "react"; +import { type MutableRefObject, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; // 드래그 핸들에서 제외해야 하는 인터랙티브 엘리먼트 판별 const isInteractiveElement = (el: EventTarget | null): boolean => { diff --git a/apps/web/src/components/ui/FloatingUpBtn/hooks/useFloatingUpHandler.ts b/apps/web/src/components/ui/FloatingUpBtn/hooks/useFloatingUpHandler.ts index 204a0f6e..e2a28e92 100644 --- a/apps/web/src/components/ui/FloatingUpBtn/hooks/useFloatingUpHandler.ts +++ b/apps/web/src/components/ui/FloatingUpBtn/hooks/useFloatingUpHandler.ts @@ -1,15 +1,11 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; const useFloatingUpHandler = (scrollYThreshold: number = 400) => { const [isVisible, setIsVisible] = useState(false); - const handleScroll = () => { - if (window.scrollY > scrollYThreshold) { - setIsVisible(true); - } else { - setIsVisible(false); - } - }; + const handleScroll = useCallback(() => { + setIsVisible(window.scrollY > scrollYThreshold); + }, [scrollYThreshold]); const handleClick = () => { window.scrollTo({ diff --git a/apps/web/src/components/ui/UniverSityCard/index.tsx b/apps/web/src/components/ui/UniverSityCard/index.tsx index 48e6c814..56a1381f 100644 --- a/apps/web/src/components/ui/UniverSityCard/index.tsx +++ b/apps/web/src/components/ui/UniverSityCard/index.tsx @@ -39,7 +39,7 @@ const UniversityCard = ({ university, showCapacity = true }: UniversityCardProps {convertedKoreanName} diff --git a/apps/web/src/lib/react-query/useMutationWithFailure.ts b/apps/web/src/lib/react-query/useMutationWithFailure.ts new file mode 100644 index 00000000..24283b24 --- /dev/null +++ b/apps/web/src/lib/react-query/useMutationWithFailure.ts @@ -0,0 +1,99 @@ +"use client"; + +import { + type MutateOptions, + type UseMutationOptions, + type UseMutationResult, + useMutation as useReactQueryMutation, +} from "@tanstack/react-query"; + +export type { MutateOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; + +export type MutationFailureHandler = { + onFailure?: (error: TError, variables: TVariables, context: TContext | undefined) => void; +}; + +export type MutationOptionsWithFailure = UseMutationOptions< + TData, + TError, + TVariables, + TContext +> & + MutationFailureHandler; + +export type MutateOptionsWithFailure = MutateOptions< + TData, + TError, + TVariables, + TContext +> & + MutationFailureHandler; + +const mergeOnError = ( + onError?: UseMutationOptions["onError"], + onFailure?: (error: TError, variables: TVariables, context: TContext | undefined) => void, +): UseMutationOptions["onError"] => { + if (!onError && !onFailure) return undefined; + return (...args: Parameters["onError"]>>) => { + onError?.(...args); + onFailure?.(args[0], args[1], args[2]); + }; +}; + +const normalizeMutationOptions = ( + options: MutationOptionsWithFailure, +): UseMutationOptions => { + const { onFailure, onError, ...rest } = options; + const mergedOnError = mergeOnError(onError, onFailure); + return mergedOnError + ? { ...rest, onError: mergedOnError as UseMutationOptions["onError"] } + : rest; +}; + +const normalizeMutateOptions = ( + options?: MutateOptionsWithFailure, +): MutateOptions | undefined => { + if (!options) return undefined; + const { onFailure, onError, ...rest } = options; + const mergedOnError = mergeOnError(onError, onFailure); + return mergedOnError + ? { ...rest, onError: mergedOnError as MutateOptions["onError"] } + : rest; +}; + +export type MutationResultWithFailure = UseMutationResult< + TData, + TError, + TVariables, + TContext +> & { + mutate: (variables: TVariables, options?: MutateOptionsWithFailure) => void; + mutateAsync: ( + variables: TVariables, + options?: MutateOptionsWithFailure, + ) => Promise; +}; + +export const useMutation = ( + options: MutationOptionsWithFailure, +): MutationResultWithFailure => { + const mutation = useReactQueryMutation(normalizeMutationOptions(options)); + + const mutate: MutationResultWithFailure["mutate"] = ( + variables, + mutateOptions, + ) => { + mutation.mutate(variables, normalizeMutateOptions(mutateOptions)); + }; + + const mutateAsync: MutationResultWithFailure["mutateAsync"] = ( + variables, + mutateOptions, + ) => mutation.mutateAsync(variables, normalizeMutateOptions(mutateOptions)); + + return { + ...mutation, + mutate, + mutateAsync, + }; +}; From da2a12c8da067ed65017b8ed312438fb33b063bf Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 02:16:34 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=94=A7=20@types/react=20hoist=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .npmrc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.npmrc b/.npmrc index 6bcbcd4e..847bb359 100644 --- a/.npmrc +++ b/.npmrc @@ -1,5 +1,8 @@ # Next.js 호환성을 위한 설정 shamefully-hoist=true +public-hoist-pattern[]='*' +public-hoist-pattern[]='!@types/react' +public-hoist-pattern[]='!@types/react-dom' auto-install-peers=true strict-peer-dependencies=false From 1fcb90193c4a2d32c325cb49cf1f5949b683e4fd Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 9 Feb 2026 02:16:52 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=90=9B=20admin=20=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=20=EC=B2=B4=ED=81=AC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../features/scores/GpaScoreTable.tsx | 8 +++--- .../features/scores/LanguageScoreTable.tsx | 8 +++--- apps/admin/src/routes/auth/login.tsx | 28 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/admin/src/components/features/scores/GpaScoreTable.tsx b/apps/admin/src/components/features/scores/GpaScoreTable.tsx index d5728c8f..6d7a9bfd 100644 --- a/apps/admin/src/components/features/scores/GpaScoreTable.tsx +++ b/apps/admin/src/components/features/scores/GpaScoreTable.tsx @@ -211,10 +211,10 @@ export function GpaScoreTable({ verifyFilter }: Props) { - {Array.from({ length: totalPages }, (_, idx) => ( - - {Array.from({ length: totalPages }, (_, idx) => ( -