-
Notifications
You must be signed in to change notification settings - Fork 4
feat(admin): migrate admin app to TanStack Start #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough새로운 관리자(Admin) 애플리케이션을 구축하기 위한 완전한 프로젝트 구조가 추가되었습니다. 주요 구성은 다음과 같습니다:
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
🤖 Fix all issues with AI agents
In `@apps/admin/package.json`:
- Line 30: The package.json currently pins the Nitro dependency to the nightly
channel ("nitro": "npm:nitro-nightly@latest"); replace this with a stable,
specific version to avoid unpredictable builds—e.g., change the "nitro" entry to
a fixed release like "nitropack": "2.13.1" or set "nitro": "2.13.1" (whichever
matches your package naming), ensuring the dependency field is version-pinned
rather than pointing to npm:nitro-nightly@latest.
In `@apps/admin/public/robots.txt`:
- Around line 1-3: The current robots.txt contains "User-agent: *" with an empty
"Disallow:" which allows crawlers; confirm whether the admin site should indeed
be publicly indexable, and if it should be blocked change the "Disallow:"
directive to block everything (e.g., set Disallow to "/" for "User-agent: *") or
alternatively serve an environment-specific robots.txt so the admin app uses a
blocking file in production; update the robots.txt accordingly and ensure
deployment serves the correct file for the admin frontend.
In `@apps/admin/src/components/features/scores/GpaScoreTable.tsx`:
- Around line 39-41: 필터(verifyFilter) 변경 시 현재 page 상태가 유지되어 범위를 벗어난 빈 결과가 나오는
문제를 수정하려면 GpaScoreTable 컴포넌트에 verifyFilter 전용 useEffect를 추가해 page 상태를 1로 리셋하도록
하세요; 즉 기존 useEffect([verifyFilter, page])에서 fetchScores 호출은 유지하되, 별도
useEffect(() => setPage(1), [verifyFilter])를 만들어 verifyFilter가 바뀔 때마다 page를 1로
설정해 fetchScores 호출 시 올바른 페이지가 사용되도록 합니다 (참조: verifyFilter, page, fetchScores,
GpaScoreTable).
- Around line 140-163: The onChange handlers for the GPA inputs currently call
Number.parseFloat(e.target.value) which yields NaN for an empty string; update
both handlers (the one setting editingGpa via setEditingGpa and the one setting
editingGpaCriteria via setEditingGpaCriteria) to first read e.target.value and,
if it's empty (""), set the state to undefined (or null) instead of parsing,
otherwise set Number.parseFloat(value); also ensure the input value props use a
safe expression like editingGpa ?? "" and editingGpaCriteria ?? "" so the fields
stay controlled and you never store NaN in editingGpa or editingGpaCriteria
before sending to the API.
In `@apps/admin/src/components/features/scores/LanguageScoreTable.tsx`:
- Around line 54-56: When verifyFilter changes we must reset pagination to avoid
requesting a non-existent page; add a useEffect that watches verifyFilter and
calls setPage(1) (or the component's page state setter) so the component (where
fetchScores and state variables page, verifyFilter, and setPage are defined)
resets to page 1 before fetchScores runs; ensure this new effect does not
include page in its dependency array to avoid loops and keep the existing
useEffect([verifyFilter, page]) that calls fetchScores.
In `@apps/admin/src/components/features/scores/ScoreVerifyButton.tsx`:
- Around line 1-69: The reject flow currently allows submitting an empty
rejectReason; update the ScoreVerifyButton so when handleReject is invoked while
showRejectInput is true it validates rejectReason.trim() is non-empty and
prevents calling onVerifyChange("REJECTED", ...) if empty (instead keep the
input open and do not clear rejectReason), and also disable the confirm button
(the button that calls handleReject when showRejectInput is true) unless
rejectReason.trim().length > 0 so users cannot submit an empty reason; reference
handleReject, rejectReason, showRejectInput, and onVerifyChange to locate where
to add the validation and button disabled state.
In `@apps/admin/src/components/layout/AdminLayout.tsx`:
- Around line 1-3: The AdminLayoutProps interface references React.ReactNode but
React is not imported; add an import for React (import React from 'react') or
import the type directly (import { ReactNode } from 'react') and update
AdminLayoutProps to use ReactNode if you choose the latter; ensure the import is
placed at the top of the file so TypeScript recognizes the type used by
AdminLayoutProps.
In `@apps/admin/src/components/ui/button.tsx`:
- Around line 39-43: The Button component currently doesn't extract or set a
default for the HTML button type, causing buttons to default to "submit"; update
the React.forwardRef callback (the Button component) to destructure type from
props, then compute a safeType = asChild ? undefined : (type ?? "button"), and
pass that safeType explicitly along with className, ref, and ...props to Comp
(use Slot when asChild is true, otherwise "button") so non-child buttons default
to type="button" while preserving behavior when asChild is true.
In `@apps/admin/src/lib/api/client.ts`:
- Around line 24-47: The access token can be reused after a failed refresh
because the catch block removes stored tokens but does not set the local
accessToken to null, and newConfig.headers may be undefined before assigning
Authorization. In the reissueAccessTokenApi(...) .catch handler (referencing
removeAccessToken, removeRefreshToken, accessToken, saveAccessToken), set
accessToken = null after removing persisted tokens; and before assigning
Authorization in the later block ensure newConfig.headers is initialized (e.g.,
newConfig.headers = newConfig.headers || {}) so convertToBearer(accessToken) is
only used when accessToken is non-null.
In `@apps/admin/src/lib/utils/jwtUtils.ts`:
- Around line 1-21: The isTokenExpired function incorrectly treats tokens with
exp === currentTime as valid and does not handle missing/invalid exp; update
isTokenExpired to parse the payload as before but then validate that payload.exp
is a finite number (e.g., typeof exp === "number" and isFinite(exp)); if exp is
missing or not a valid number, return true (treat as expired); finally use the
RFC-compliant comparison exp <= currentTime to determine expiration and keep the
existing try/catch logging behavior around JSON parsing/atob to return true on
any error.
In `@apps/admin/src/lib/utils/localStorage.ts`:
- Around line 1-49: The refresh token helpers (saveRefreshToken,
loadRefreshToken, removeRefreshToken) currently persist secrets to localStorage
and must be removed or refactored so refresh tokens are stored as httpOnly,
Secure cookies set from the server; stop writing/reading refresh tokens from
localStorage and ensure server endpoints set the cookie (axios already has
withCredentials enabled). Keep access token usage limited to short-lived tokens
in localStorage (saveAccessToken, loadAccessToken, removeAccessToken) only if
necessary, or prefer storing access tokens in memory; update any client code
that calls saveRefreshToken/loadRefreshToken/removeRefreshToken to instead rely
on server-set cookies and cookie-based refresh endpoints. Also update error
handling around saveAccessToken/loadAccessToken/removeAccessToken to reflect
shorter TTL and ensure removeAccessToken is called on logout/refresh failure.
In `@apps/admin/src/routes/index.tsx`:
- Around line 1-6: The redirect in the Route's beforeLoad uses "/scores" but
other routes are defined with a trailing slash; update the redirect call inside
beforeLoad to use "/scores/" so it matches the createFileRoute("/scores/")
definition and maintains consistent routing (refer to Route, createFileRoute,
beforeLoad, and redirect).
In `@apps/admin/src/routes/scores/index.tsx`:
- Around line 11-17: The route guard in beforeLoad currently only checks
loadAccessToken() and uses isTokenExpired() to redirect to "/auth/login",
causing unnecessary redirects when a valid refresh token exists; change
beforeLoad to also call loadRefreshToken() and isRefreshTokenExpired() (or reuse
the API client's token validation helper) and only throw redirect({ to:
"/auth/login" }) when both access and refresh tokens are missing/expired so the
guard matches the API client's refresh-capable behavior.
In `@apps/admin/src/types/scores.ts`:
- Around line 13-25: The local GpaScore interface's rejectedReason is currently
optional string (rejectedReason?: string) but the response type
GpaScoreStatusResponse uses string | null, causing mapping mismatches; update
the GpaScore interface (rejectedReason) to allow null (e.g., rejectedReason:
string | null or rejectedReason?: string | null) so it matches
GpaScoreStatusResponse and avoids conversion code—change the property on the
GpaScore interface rather than the response type.
- Around line 60-72: The local LanguageTestScore interface's rejectedReason is
currently optional string and must accept null to match API responses; update
the LanguageTestScore definition (and any related uses) so rejectedReason is
typed as string | null (e.g., rejectedReason?: string | null) and adjust any
code that assumes only undefined to handle null values as well, referencing the
LanguageTestScore interface to locate the change.
🧹 Nitpick comments (5)
apps/admin/vite.config.ts (1)
12-23: 1. 중복된 경로 별칭 설정 제거를 권장드립니다.
viteTsConfigPaths플러그인이 이미tsconfig.json의paths설정(@/*→./src/*)을 읽어오므로,resolve.alias에서@별칭을 별도로 정의할 필요가 없습니다.
- 현재 상태: 두 곳에서 동일한 별칭을 설정하고 있어 유지보수 시 불일치 위험이 있습니다.
- 권장 조치:
resolve.alias블록을 제거하고viteTsConfigPaths에만 의존하세요.♻️ 중복 제거 제안
-import { fileURLToPath, URL } from 'url' - import tailwindcss from '@tailwindcss/vite' import { nitro } from 'nitro/vite' const config = defineConfig({ - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, plugins: [ devtools(), nitro(),apps/admin/src/lib/utils/localStorage.ts (1)
1-49: 중복된 try/catch 패턴은 공통 헬퍼로 묶는 편이 유지보수에 유리합니다.
키 상수화까지 함께 하면 오타 위험도 줄어들어요.♻️ 리팩터링 예시
+const ACCESS_TOKEN_KEY = "accessToken"; +const REFRESH_TOKEN_KEY = "refreshToken"; + +const safeGet = (key: string) => { + try { + return localStorage.getItem(key); + } catch (err) { + console.error(`Could not load ${key}`, err); + return null; + } +}; + +const safeSet = (key: string, value: string) => { + try { + localStorage.setItem(key, value); + } catch (err) { + console.error(`Could not save ${key}`, err); + } +}; + +const safeRemove = (key: string) => { + try { + localStorage.removeItem(key); + } catch (err) { + console.error(`Could not remove ${key}`, err); + } +}; + -export const loadRefreshToken = () => { - try { - return localStorage.getItem("refreshToken"); - } catch (err) { - console.error("Could not load refresh token", err); - return null; - } -}; +export const loadRefreshToken = () => safeGet(REFRESH_TOKEN_KEY); + +export const saveRefreshToken = (token: string) => safeSet(REFRESH_TOKEN_KEY, token); + +export const removeRefreshToken = () => safeRemove(REFRESH_TOKEN_KEY);apps/admin/src/routes/__root.tsx (1)
41-54: 1. 프로덕션에서 Devtools 렌더링 차단 권장
- 현재
TanStackDevtools가 항상 렌더링되어 운영 번들에도 포함됩니다.- 운영 환경에서는 제외해 번들 크기와 노출을 줄이는 편이 안전합니다.
변경 제안
- <TanStackDevtools - config={{ - position: "bottom-right", - }} - plugins={[ - { - name: "Tanstack Router", - render: <TanStackRouterDevtoolsPanel />, - }, - ]} - /> + {import.meta.env.DEV ? ( + <TanStackDevtools + config={{ + position: "bottom-right", + }} + plugins={[ + { + name: "Tanstack Router", + render: <TanStackRouterDevtoolsPanel />, + }, + ]} + /> + ) : null}apps/admin/src/types/scores.ts (2)
54-58: languageTestType는 union 타입을 쓰는 게 안전합니다.
- 허용 값 제한으로 안정성↑
이미LanguageTestType이 있으니 연결해 두면 오타 방지가 됩니다.
타입 체계도 더 일관돼 보여요.🔧 제안 변경
export interface LanguageTestResponse { - languageTestType: string; + languageTestType: LanguageTestType; languageTestScore: string; languageTestReportUrl: string; }
93-105: REJECTED 상태의 rejectedReason 필수 여부를 타입으로 보장할지 확인해 주세요.
- API 규칙이 있다면 타입으로 묶기
백엔드가 REJECTED 시 사유를 필수로 요구한다면, 타입에서 강제하는 편이 안전합니다.
규칙이 없다면 지금 형태도 괜찮아요.🔧 예시 (규칙이 있을 때)
-export interface GpaScoreUpdateRequest { - gpa: number; - gpaCriteria: number; - verifyStatus: VerifyStatus; - rejectedReason?: string; -} +export type GpaScoreUpdateRequest = + | { + gpa: number; + gpaCriteria: number; + verifyStatus: "REJECTED"; + rejectedReason: string; + } + | { + gpa: number; + gpaCriteria: number; + verifyStatus: "PENDING" | "APPROVED"; + rejectedReason?: string | null; + };
| "clsx": "^2.1.1", | ||
| "date-fns": "^4.1.0", | ||
| "lucide-react": "^0.561.0", | ||
| "nitro": "npm:nitro-nightly@latest", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Nitro npm stable version 2026 current release
💡 Result:
As of February 1, 2026, the current stable (“latest” dist-tag) npm release of Nitro (package: nitropack) is 2.13.1, published January 15, 2026. [1]
(For reference, 2.13.1 is also listed among published versions on package trackers.) [2]
Sources: [1] [2]
🌐 Web query:
Solid Start Nitro recommended version stable
💡 Result:
For SolidStart (stable 1.x), the recommended “stable Nitro” is whatever SolidStart pulls in via Vinxi—i.e., Nitro v2 (the nitropack package), not Nitro v3.
- Use the latest stable SolidStart:
@solidjs/start@1.2.1(current “latest” tag). [1] @solidjs/start@1.2.1depends on Vinxi^0.5.7. [1]- Current Vinxi (0.5.x) depends on
nitropack^2.11.10(Nitro v2), which is the stable Nitro line SolidStart 1.x is effectively built on. [2] - SolidStart’s roadmap indicates Nitro v3 is a future goal tied to SolidStart 2.0 work, not the current stable line. [3]
Recommendation (stable): stay on SolidStart 1.2.1 and let it manage Nitro via Vinxi (Nitro v2 / nitropack 2.x). [1] [2]
Sources:
[1] (cdn.jsdelivr.net)
[2] (cdn.jsdelivr.net)
[3] (github.com)
Citations:
- 1: https://cdn.jsdelivr.net/npm/%40solidjs/start%401.2.1/package.json
- 2: https://cdn.jsdelivr.net/npm/vinxi%400.5.10/package.json
- 3: SolidStart: Public Roadmap - DeVinxi and Beyond solidjs/solid-start#1960
🌐 Web query:
nitro-nightly vs nitro npm differences 2025 2026
💡 Result:
What “nightly” means (Nitro / Nitropack)
- Nightly packages (
nitro-nightly,nitropack-nightly) are auto-published builds from every commit tomainso you can try changes immediately, before a stable release. (nitro.build) - Stable packages (
nitro/nitropackon npmlatest) are the tested, versioned releases intended for most production use. (npmjs.com)
Key differences (esp. relevant in 2025–2026)
-
Stability & breaking changes
- Nightly can include unreleased features, regressions, or breaking changes because it tracks
maincommit-by-commit. (nitro.build)
- Nightly can include unreleased features, regressions, or breaking changes because it tracks
-
Versioning
- Stable uses normal semver (example shown on npm/yarn:
nitropack2.13.1 published Jan 15, 2026). (classic.yarnpkg.com) - Nightly uses timestamp/commit-suffixed versions (example:
nitro-nightly3.0.1-20260109-…). (jsdelivr.com)
- Stable uses normal semver (example shown on npm/yarn:
-
Major-line difference (common in 2025–2026)
nitro-nightlyis commonly associated with Nitro v3 (alpha/beta) development, while stable channels are often still v2.x for many users. (socket.dev)
-
How you should install
- Recommended: use an npm alias in
package.json(don’tnpm install nitro-nightlydirectly). (v3.nitro.build)
Examples from the docs:- Nitro:
"nitro": "npm:nitro-nightly@latest"(v3.nitro.build) - Nitropack:
"nitropack": "npm:nitropack-nightly@latest"(nitro.build)
- Nitro:
- Docs also recommend removing lockfiles and reinstalling when switching to nightly. (v3.nitro.build)
- Recommended: use an npm alias in
-
Nuxt note
- If you’re on Nuxt, Nitro docs suggest using the Nuxt nightly channel, since it already includes the Nitro nightly dependency. (nitro.build)
Citations:
- 1: https://nitro.build/guide/nightly?utm_source=openai
- 2: https://www.npmjs.com/package/nitropack?utm_source=openai
- 3: https://nitro.build/guide/nightly?utm_source=openai
- 4: https://classic.yarnpkg.com/en/package/nitropack?utm_source=openai
- 5: https://www.jsdelivr.com/package/npm/nitro-nightly?utm_source=openai
- 6: https://socket.dev/npm/package/nitro-nightly?utm_source=openai
- 7: https://v3.nitro.build/docs/nightly?utm_source=openai
- 8: https://v3.nitro.build/docs/nightly?utm_source=openai
- 9: https://nitro.build/guide/nightly?utm_source=openai
- 10: https://v3.nitro.build/docs/nightly?utm_source=openai
- 11: https://nitro.build/guide/nightly?utm_source=openai
Nitro 안정 버전 사용을 권장합니다.
"nitro": "npm:nitro-nightly@latest"는 main 브랜치의 최신 커밋이 자동 배포되는 빌드로, 프로덕션 환경에서는 예측 불가능한 오류나 breaking change가 발생할 수 있습니다.
- 현재 상태: 개발 단계에서는 최신 기능을 빠르게 테스트할 수 있는 장점이 있습니다.
- 권장 조치: 프로덕션 배포 전에 안정 버전(현재
nitropack@2.13.1, 2026년 1월 15일 배포)으로 전환하거나, 최소한 특정 버전을 고정하는 것을 고려해 주세요. - SolidStart 프로젝트 기준: SolidStart 1.x의 안정 라인은 Nitro v2를 사용하므로, 장기 지원과 안정성이 필요한 경우 이를 따르는 것이 좋습니다.
🤖 Prompt for AI Agents
In `@apps/admin/package.json` at line 30, The package.json currently pins the
Nitro dependency to the nightly channel ("nitro": "npm:nitro-nightly@latest");
replace this with a stable, specific version to avoid unpredictable builds—e.g.,
change the "nitro" entry to a fixed release like "nitropack": "2.13.1" or set
"nitro": "2.13.1" (whichever matches your package naming), ensuring the
dependency field is version-pinned rather than pointing to
npm:nitro-nightly@latest.
| # https://www.robotstxt.org/robotstxt.html | ||
| User-agent: * | ||
| Disallow: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 1) 크롤링 허용 의도 확인
어드민이라면 색인 노출을 막는 편이 일반적이니, 공개 의도가 맞는지 확인 부탁드려요. 필요 시 아래처럼 차단하는 쪽을 고려해 주세요.
🔧 제안 변경
User-agent: *
-Disallow:
+Disallow: /📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # https://www.robotstxt.org/robotstxt.html | |
| User-agent: * | |
| Disallow: | |
| # https://www.robotstxt.org/robotstxt.html | |
| User-agent: * | |
| Disallow: / |
🤖 Prompt for AI Agents
In `@apps/admin/public/robots.txt` around lines 1 - 3, The current robots.txt
contains "User-agent: *" with an empty "Disallow:" which allows crawlers;
confirm whether the admin site should indeed be publicly indexable, and if it
should be blocked change the "Disallow:" directive to block everything (e.g.,
set Disallow to "/" for "User-agent: *") or alternatively serve an
environment-specific robots.txt so the admin app uses a blocking file in
production; update the robots.txt accordingly and ensure deployment serves the
correct file for the admin frontend.
| useEffect(() => { | ||
| fetchScores(); | ||
| }, [verifyFilter, page]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/admin/src/components/features/scores/GpaScoreTable.tsx | head -100Repository: solid-connection/solid-connect-web
Length of output: 4133
🏁 Script executed:
cat -n apps/admin/src/components/features/scores/GpaScoreTable.tsx | tail -n +100Repository: solid-connection/solid-connect-web
Length of output: 6501
필터 변경 시 페이지 상태가 유지되어 빈 결과가 표시될 수 있습니다.
현재 코드에서 verifyFilter가 변경되면 useEffect가 실행되지만, page 상태가 초기화되지 않고 그대로 유지됩니다. 예를 들어 첫 번째 필터에서 5페이지에 있다가 두 번째 필터(2페이지만 있음)로 바꾸면, 페이지 5는 범위를 벗어나 빈 화면이 나타날 수 있습니다.
다음과 같이 필터 변경 시 페이지를 자동으로 1로 리셋하는 흐름을 추가하기를 권장합니다.
- 필터 변경 감지 시 페이지 초기화
verifyFilter변경만 감지하는 별도의useEffect추가page를 1로 리셋
변경 제안
+ useEffect(() => {
+ setPage(1);
+ }, [verifyFilter]);
+
useEffect(() => {
fetchScores();
}, [verifyFilter, page]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| fetchScores(); | |
| }, [verifyFilter, page]); | |
| useEffect(() => { | |
| setPage(1); | |
| }, [verifyFilter]); | |
| useEffect(() => { | |
| fetchScores(); | |
| }, [verifyFilter, page]); |
🤖 Prompt for AI Agents
In `@apps/admin/src/components/features/scores/GpaScoreTable.tsx` around lines 39
- 41, 필터(verifyFilter) 변경 시 현재 page 상태가 유지되어 범위를 벗어난 빈 결과가 나오는 문제를 수정하려면
GpaScoreTable 컴포넌트에 verifyFilter 전용 useEffect를 추가해 page 상태를 1로 리셋하도록 하세요; 즉 기존
useEffect([verifyFilter, page])에서 fetchScores 호출은 유지하되, 별도 useEffect(() =>
setPage(1), [verifyFilter])를 만들어 verifyFilter가 바뀔 때마다 page를 1로 설정해 fetchScores
호출 시 올바른 페이지가 사용되도록 합니다 (참조: verifyFilter, page, fetchScores, GpaScoreTable).
| {editingId === score.gpaScoreStatusResponse.id ? ( | ||
| <div className="flex gap-2"> | ||
| <input | ||
| type="number" | ||
| step="0.01" | ||
| value={editingGpa} | ||
| onChange={(e) => setEditingGpa(Number.parseFloat(e.target.value))} | ||
| className="w-20 rounded border px-2 py-1" | ||
| /> | ||
| </div> | ||
| ) : ( | ||
| score.gpaScoreStatusResponse.gpaResponse.gpa | ||
| )} | ||
| </TableCell> | ||
| <TableCell> | ||
| {editingId === score.gpaScoreStatusResponse.id ? ( | ||
| <div className="flex gap-2"> | ||
| <input | ||
| type="number" | ||
| step="0.01" | ||
| value={editingGpaCriteria} | ||
| onChange={(e) => setEditingGpaCriteria(Number.parseFloat(e.target.value))} | ||
| className="w-20 rounded border px-2 py-1" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd "GpaScoreTable.tsx"Repository: solid-connection/solid-connect-web
Length of output: 137
🏁 Script executed:
# Read the specific file to check the code around lines 140-163
cat -n apps/admin/src/components/features/scores/GpaScoreTable.tsx | sed -n '130,170p'Repository: solid-connection/solid-connect-web
Length of output: 2259
🏁 Script executed:
# Check the broader context to understand how editingGpa and editingGpaCriteria are used
cat -n apps/admin/src/components/features/scores/GpaScoreTable.tsx | head -n 250 | tail -n 150Repository: solid-connection/solid-connect-web
Length of output: 7184
🏁 Script executed:
# Get the beginning of the file to see state declarations and handleEdit function
cat -n apps/admin/src/components/features/scores/GpaScoreTable.tsx | head -n 100Repository: solid-connection/solid-connect-web
Length of output: 4133
🏁 Script executed:
# Get the handleEdit and handleSave functions completely
cat -n apps/admin/src/components/features/scores/GpaScoreTable.tsx | sed -n '50,90p'Repository: solid-connection/solid-connect-web
Length of output: 1596
🏁 Script executed:
# Let me verify the behavior of Number.parseFloat with empty string
python3 << 'EOF'
# Simulating JavaScript behavior
import json
# Test Number.parseFloat("") equivalent in Python
try:
result = float("")
except ValueError as e:
result = float('nan')
print(f"float('') would be: {result}")
print(f"Is it NaN? {result != result}") # NaN != NaN is True in JavaScript and Python
# Check what happens when NaN is serialized to JSON
import math
test_obj = {"gpa": float('nan')}
try:
json_str = json.dumps(test_obj)
print(f"JSON serialization: {json_str}")
except ValueError as e:
print(f"JSON serialization error: {e}")
EOFRepository: solid-connection/solid-connect-web
Length of output: 150
🏁 Script executed:
# Check if there are any input validation or constraints in the component
grep -n "validate\|check\|guard\|min\|max" apps/admin/src/components/features/scores/GpaScoreTable.tsxRepository: solid-connection/solid-connect-web
Length of output: 60
🏁 Script executed:
# Look for any error handling or validation in the scoreApi
fd "scoreApi\|scores\.ts\|scores\.js" --type fRepository: solid-connection/solid-connect-web
Length of output: 60
🏁 Script executed:
# Search for scoreApi definition
rg "scoreApi\s*=" --type ts --type tsx | head -20Repository: solid-connection/solid-connect-web
Length of output: 105
🏁 Script executed:
# Find files with "scores" in the path
fd -e ts -e tsx | grep -i scoreRepository: solid-connection/solid-connect-web
Length of output: 1815
🏁 Script executed:
# Check the API implementation
find . -path "*/api/scores*" -o -path "*/lib/api*"Repository: solid-connection/solid-connect-web
Length of output: 205
🏁 Script executed:
# Check the scoreApi implementation
cat apps/admin/src/lib/api/scores.tsRepository: solid-connection/solid-connect-web
Length of output: 1948
🏁 Script executed:
# Also check if there's any validation in the number input itself
# Test what happens when we clear a number input field in HTML/React
python3 << 'EOF'
# In HTML/JavaScript, when a number input is cleared, e.target.value returns ""
# Number.parseFloat("") returns NaN in JavaScript
# Let's verify this behavior
# Simulate the exact code from the review
test_value = ""
parsed = float(test_value) if test_value else float('nan')
print(f"parseFloat('') returns: NaN")
print(f"NaN is a real problem when sent to API: YES")
print(f"When editingGpa is NaN and passed to handleSave, it becomes part of the payload: YES")
print()
print(f"Current behavior:")
print(f"1. User clears input field -> e.target.value = ''")
print(f"2. Number.parseFloat('') -> NaN")
print(f"3. setEditingGpa(NaN)")
print(f"4. handleSave sends editingGpa: NaN in the request body")
print()
print(f"Proposed fix:")
print(f"1. User clears input field -> e.target.value = ''")
print(f"2. Check: value === '' ? 0 : Number.parseFloat(value)")
print(f"3. setEditingGpa(0)")
print(f"4. handleSave sends editingGpa: 0 in the request body")
EOFRepository: solid-connection/solid-connect-web
Length of output: 606
숫자 입력 필드를 비웠을 때 NaN이 API 요청에 포함될 수 있음
Number.parseFloat("")는 NaN을 반환하므로, 입력 필드를 비운 후 저장하면 NaN 값이 서버로 전송됩니다. 이를 방지하려면 빈 값에 대한 처리가 필요합니다.
-
첫 번째 입력 필드 (GPA)의 onChange 핸들러 수정
onChange={(e) => { + const value = e.target.value; + setEditingGpa(value === "" ? 0 : Number.parseFloat(value)); - setEditingGpa(Number.parseFloat(e.target.value)) }} -
두 번째 입력 필드 (기준점수)의 onChange 핸들러 동일하게 수정
onChange={(e) => { + const value = e.target.value; + setEditingGpaCriteria(value === "" ? 0 : Number.parseFloat(value)); - setEditingGpaCriteria(Number.parseFloat(e.target.value)) }}
🤖 Prompt for AI Agents
In `@apps/admin/src/components/features/scores/GpaScoreTable.tsx` around lines 140
- 163, The onChange handlers for the GPA inputs currently call
Number.parseFloat(e.target.value) which yields NaN for an empty string; update
both handlers (the one setting editingGpa via setEditingGpa and the one setting
editingGpaCriteria via setEditingGpaCriteria) to first read e.target.value and,
if it's empty (""), set the state to undefined (or null) instead of parsing,
otherwise set Number.parseFloat(value); also ensure the input value props use a
safe expression like editingGpa ?? "" and editingGpaCriteria ?? "" so the fields
stay controlled and you never store NaN in editingGpa or editingGpaCriteria
before sending to the API.
| useEffect(() => { | ||
| fetchScores(); | ||
| }, [verifyFilter, page]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/admin/src/components/features/scores/LanguageScoreTable.tsx | head -100Repository: solid-connection/solid-connect-web
Length of output: 4516
필터 변경 시 페이지 상태가 유지되어 빈 화면이 나타날 수 있습니다
필터를 변경할 때 현재 페이지 번호가 초기화되지 않으면, 높은 페이지에서 필터를 바꿀 경우 존재하지 않는 페이지를 요청하게 됩니다. 예를 들어 3페이지에 있을 때 필터를 변경하면, 새 필터의 결과가 2페이지 이하라고 해도 여전히 3페이지를 요청하여 빈 결과가 표시됩니다.
verifyFilter변경 시page를 1로 초기화하는 useEffect를 추가해주세요- 사용자 경험을 개선하고 예상치 못한 빈 화면을 방지할 수 있습니다.
변경 제안
+ useEffect(() => {
+ setPage(1);
+ }, [verifyFilter]);
+
useEffect(() => {
fetchScores();
}, [verifyFilter, page]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| fetchScores(); | |
| }, [verifyFilter, page]); | |
| useEffect(() => { | |
| setPage(1); | |
| }, [verifyFilter]); | |
| useEffect(() => { | |
| fetchScores(); | |
| }, [verifyFilter, page]); |
🤖 Prompt for AI Agents
In `@apps/admin/src/components/features/scores/LanguageScoreTable.tsx` around
lines 54 - 56, When verifyFilter changes we must reset pagination to avoid
requesting a non-existent page; add a useEffect that watches verifyFilter and
calls setPage(1) (or the component's page state setter) so the component (where
fetchScores and state variables page, verifyFilter, and setPage are defined)
resets to page 1 before fetchScores runs; ensure this new effect does not
include page in its dependency array to avoid loops and keep the existing
useEffect([verifyFilter, page]) that calls fetchScores.
| export const loadRefreshToken = () => { | ||
| 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); | ||
| } | ||
| }; | ||
|
|
||
| export const removeRefreshToken = () => { | ||
| 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; | ||
| } | ||
| }; | ||
|
|
||
| export const saveAccessToken = (token: string) => { | ||
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's understand the codebase structure
git ls-files | head -20Repository: solid-connection/solid-connect-web
Length of output: 568
🏁 Script executed:
# Check if the file exists and explore the directory structure
find apps/admin/src -type f -name "*.ts" -o -name "*.tsx" | grep -E "(auth|token|storage)" | head -20Repository: solid-connection/solid-connect-web
Length of output: 174
🏁 Script executed:
# Search for usages of the token storage functions
rg "loadRefreshToken|saveRefreshToken|loadAccessToken|saveAccessToken" --type ts --type tsx -lRepository: solid-connection/solid-connect-web
Length of output: 105
🏁 Script executed:
# Check for authentication setup and middleware
find apps -type f \( -name "*auth*" -o -name "*middleware*" -o -name "*.config.*" \) | head -20Repository: solid-connection/solid-connect-web
Length of output: 464
🏁 Script executed:
# Search for usages of loadRefreshToken and saveRefreshToken with correct syntax
rg "loadRefreshToken|saveRefreshToken|loadAccessToken|saveAccessToken" -t ts -A 2 -B 2Repository: solid-connection/solid-connect-web
Length of output: 6025
🏁 Script executed:
# Check the authCookieUtils to understand how cookies are being used in this project
cat -n apps/web/src/utils/authCookieUtils.tsRepository: solid-connection/solid-connect-web
Length of output: 558
🏁 Script executed:
# Check auth implementation in admin
cat -n apps/admin/src/lib/api/auth.tsRepository: solid-connection/solid-connect-web
Length of output: 809
🏁 Script executed:
# Check the authCookieUtils to see how cookies are being used
cat -n apps/web/src/utils/authCookieUtils.tsRepository: solid-connection/solid-connect-web
Length of output: 558
🏁 Script executed:
# Check if admin app has middleware or server setup
find apps/admin -name "middleware.*" -o -name "*.config.*" | head -10Repository: solid-connection/solid-connect-web
Length of output: 103
🏁 Script executed:
# Check admin vite config to understand the setup
cat -n apps/admin/vite.config.tsRepository: solid-connection/solid-connect-web
Length of output: 1039
🏁 Script executed:
# Check the auth types to understand token structure
cat -n apps/admin/src/types/auth.tsRepository: solid-connection/solid-connect-web
Length of output: 293
🏁 Script executed:
# Check how the public axios instance is configured
cat -n apps/admin/src/lib/api/client.tsRepository: solid-connection/solid-connect-web
Length of output: 3229
🏁 Script executed:
# Search for any cookie handling in admin app
rg "cookie" -i -t ts --type tsx apps/admin/Repository: solid-connection/solid-connect-web
Length of output: 105
🏁 Script executed:
# Check the main app entry point to understand full structure
cat -n apps/admin/src/main.tsx 2>/dev/null || cat -n apps/admin/src/root.tsx 2>/dev/null || find apps/admin/src -name "*.tsx" -o -name "*.ts" | grep -E "(main|root|app)" | head -5Repository: solid-connection/solid-connect-web
Length of output: 373
🏁 Script executed:
# Check if there's any server-side code or API handlers in admin
find apps/admin -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "httpOnly\|cookie\|Set-Cookie" 2>/dev/null | head -10Repository: solid-connection/solid-connect-web
Length of output: 60
🏁 Script executed:
# Verify the exact location where refreshToken is used and exposed
rg "refreshToken" apps/admin/src -t ts -B 2 -A 2 | head -40Repository: solid-connection/solid-connect-web
Length of output: 2468
🏁 Script executed:
# Check if there are any environment-specific auth strategies
rg "Authorization|Bearer" apps/admin/src -t ts | head -15Repository: solid-connection/solid-connect-web
Length of output: 480
localStorage의 토큰 저장소는 XSS 공격에 노출되어 있습니다.
현재 코드에서 accessToken과 refreshToken 모두 localStorage에 저장되고 있으며, refreshToken은 인증 갱신 시 Authorization 헤더를 통해 직접 사용됩니다. 임의의 JavaScript가 주입될 경우 이 토큰들에 즉시 접근할 수 있는 상태입니다. 다음 항목들을 검토해 주세요:
-
저장 위치 재검토: 리프레시 토큰을 httpOnly/Secure 쿠키로 이동하세요. 현재 axios 설정에
withCredentials: true가 이미 적용되어 있고, TanStack Start + Nitro 아키텍처가 서버 사이드 쿠키 처리를 지원하므로 구현이 가능합니다. -
필요 범위 최소화: 리프레시 토큰을 httpOnly 쿠키로 관리하게 되면, localStorage에는 짧은 만료 시간의 액세스 토큰만 저장하도록 범위를 줄일 수 있습니다.
🤖 Prompt for AI Agents
In `@apps/admin/src/lib/utils/localStorage.ts` around lines 1 - 49, The refresh
token helpers (saveRefreshToken, loadRefreshToken, removeRefreshToken) currently
persist secrets to localStorage and must be removed or refactored so refresh
tokens are stored as httpOnly, Secure cookies set from the server; stop
writing/reading refresh tokens from localStorage and ensure server endpoints set
the cookie (axios already has withCredentials enabled). Keep access token usage
limited to short-lived tokens in localStorage (saveAccessToken, loadAccessToken,
removeAccessToken) only if necessary, or prefer storing access tokens in memory;
update any client code that calls
saveRefreshToken/loadRefreshToken/removeRefreshToken to instead rely on
server-set cookies and cookie-based refresh endpoints. Also update error
handling around saveAccessToken/loadAccessToken/removeAccessToken to reflect
shorter TTL and ensure removeAccessToken is called on logout/refresh failure.
| import { createFileRoute, redirect } from "@tanstack/react-router"; | ||
|
|
||
| export const Route = createFileRoute("/")({ | ||
| beforeLoad: () => { | ||
| throw redirect({ to: "/scores" }); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# /scores 라우트 정의 확인
rg -n "createFileRoute\(\"/scores" -C3 --type tsxRepository: solid-connection/solid-connect-web
Length of output: 105
🏁 Script executed:
#!/bin/bash
# /scores 라우트 정의 확인 (파일 타입 없이 검색)
rg -n "createFileRoute.*scores" -C3Repository: solid-connection/solid-connect-web
Length of output: 621
🏁 Script executed:
#!/bin/bash
# 다른 리다이렉트 패턴 확인
rg -n "redirect.*to:" apps/admin/src -C2Repository: solid-connection/solid-connect-web
Length of output: 719
🏁 Script executed:
#!/bin/bash
# /auth/login 라우트 정의 확인
rg -n "createFileRoute.*auth.*login" -C3Repository: solid-connection/solid-connect-web
Length of output: 566
🏁 Script executed:
#!/bin/bash
# 모든 라우트 정의 확인해서 패턴 파악
rg -n "export const Route = createFileRoute" apps/admin/src/routes -C1Repository: solid-connection/solid-connect-web
Length of output: 650
1) 리다이렉트 경로를 "/scores/"로 맞춰주세요.
현재 코드에서는 createFileRoute("/scores/")로 정의된 라우트로 리다이렉트할 때 "/scores" (슬래시 없음)를 사용하고 있어요. 다른 라우트 정의들과 일관성을 맞추기 위해 슬래시를 포함시켜 주세요.
🧭 제안 패치
export const Route = createFileRoute("/")({
beforeLoad: () => {
- throw redirect({ to: "/scores" });
+ throw redirect({ to: "/scores/" });
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createFileRoute, redirect } from "@tanstack/react-router"; | |
| export const Route = createFileRoute("/")({ | |
| beforeLoad: () => { | |
| throw redirect({ to: "/scores" }); | |
| }, | |
| import { createFileRoute, redirect } from "@tanstack/react-router"; | |
| export const Route = createFileRoute("/")({ | |
| beforeLoad: () => { | |
| throw redirect({ to: "/scores/" }); | |
| }, |
🤖 Prompt for AI Agents
In `@apps/admin/src/routes/index.tsx` around lines 1 - 6, The redirect in the
Route's beforeLoad uses "/scores" but other routes are defined with a trailing
slash; update the redirect call inside beforeLoad to use "/scores/" so it
matches the createFileRoute("/scores/") definition and maintains consistent
routing (refer to Route, createFileRoute, beforeLoad, and redirect).
| beforeLoad: () => { | ||
| // 클라이언트 사이드에서만 인증 체크 | ||
| if (typeof window !== "undefined") { | ||
| const token = loadAccessToken(); | ||
| if (!token || isTokenExpired(token)) { | ||
| throw redirect({ to: "/auth/login" }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the actual file to see the code at lines 11-17
cat -n apps/admin/src/routes/scores/index.tsx | head -30Repository: solid-connection/solid-connect-web
Length of output: 1365
🏁 Script executed:
# Check if loadRefreshToken exists in the localStorage utils
find . -name "localStorage*" -type f | head -20Repository: solid-connection/solid-connect-web
Length of output: 162
🏁 Script executed:
# Search for loadRefreshToken and loadAccessToken definitions
rg "loadRefreshToken|loadAccessToken" --type typescript --type tsx -A 3 -B 1Repository: solid-connection/solid-connect-web
Length of output: 112
🏁 Script executed:
# Look for isTokenExpired implementation
rg "isTokenExpired" --type typescript --type tsx -A 5Repository: solid-connection/solid-connect-web
Length of output: 112
🏁 Script executed:
# Read the localStorage.ts file to see available functions
cat -n ./apps/admin/src/lib/utils/localStorage.tsRepository: solid-connection/solid-connect-web
Length of output: 1544
🏁 Script executed:
# Search for jwtUtils and isTokenExpired implementation
cat -n ./apps/admin/src/lib/utils/jwtUtils.ts 2>/dev/null || find . -name "jwtUtils*" -type fRepository: solid-connection/solid-connect-web
Length of output: 749
🏁 Script executed:
# Search for refresh token usage in the codebase
rg "refreshToken" -A 3 -B 1 | head -100Repository: solid-connection/solid-connect-web
Length of output: 5510
🏁 Script executed:
# Look for auth-related hooks or functions that might handle token refresh
rg "refresh|token" apps/admin/src --max-count=50 | grep -i "refresh\|auth" | head -40Repository: solid-connection/solid-connect-web
Length of output: 1775
리프레시 토큰까지 확인하여 불필요한 로그인 리다이렉트 방지하기
현재 이 라우트 가드는 액세스 토큰만 체크한 후 만료되면 즉시 로그인으로 보냅니다. 하지만 리프레시 토큰이 여전히 유효하면 API 호출 시 자동으로 토큰을 갱신할 수 있기 때문에, 사용자가 불필요하게 로그인 페이지로 리다이렉트됩니다.
-
현재 문제점
- 액세스 토큰이 만료되었지만 리프레시 토큰은 유효한 상태에서도 로그인으로 강제 이동
- API 클라이언트(
apps/admin/src/lib/api/client.ts)는 이미 리프레시 토큰까지 체크하는 로직이 구현되어 있음 - 라우트 가드와 API 클라이언트의 토큰 검증 기준이 불일치
-
제안하는 변경
- 리프레시 토큰을 함께 로드하여 둘 다 만료된 경우에만 로그인으로 리다이렉트
- 기존 API 클라이언트와 동일한 검증 기준으로 통일
변경 제안
-import { loadAccessToken } from "@/lib/utils/localStorage";
+import { loadAccessToken, loadRefreshToken } from "@/lib/utils/localStorage";
@@
- const token = loadAccessToken();
- if (!token || isTokenExpired(token)) {
+ const token = loadAccessToken();
+ const refreshToken = loadRefreshToken();
+ if (
+ (!token || isTokenExpired(token)) &&
+ (!refreshToken || isTokenExpired(refreshToken))
+ ) {
throw redirect({ to: "/auth/login" });
}🤖 Prompt for AI Agents
In `@apps/admin/src/routes/scores/index.tsx` around lines 11 - 17, The route guard
in beforeLoad currently only checks loadAccessToken() and uses isTokenExpired()
to redirect to "/auth/login", causing unnecessary redirects when a valid refresh
token exists; change beforeLoad to also call loadRefreshToken() and
isRefreshTokenExpired() (or reuse the API client's token validation helper) and
only throw redirect({ to: "/auth/login" }) when both access and refresh tokens
are missing/expired so the guard matches the API client's refresh-capable
behavior.
| export interface GpaScore { | ||
| verifyStatus: VerifyStatus; | ||
| rejectedReason?: string; | ||
| } | ||
|
|
||
| export interface GpaScoreStatusResponse { | ||
| id: number; | ||
| gpaResponse: GpaResponse; | ||
| verifyStatus: VerifyStatus; | ||
| rejectedReason: string | null; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rejectedReason의 null 처리 타입을 맞춰 주세요.
-
- 응답 vs 로컬 타입 불일치
응답은string | null인데 로컬은string?라 매핑 시 충돌이 날 수 있어요.
null까지 허용해 주면 변환 코드가 줄어듭니다.
- 응답 vs 로컬 타입 불일치
🔧 제안 변경
export interface GpaScore {
verifyStatus: VerifyStatus;
- rejectedReason?: string;
+ rejectedReason?: string | null;
}🤖 Prompt for AI Agents
In `@apps/admin/src/types/scores.ts` around lines 13 - 25, The local GpaScore
interface's rejectedReason is currently optional string (rejectedReason?:
string) but the response type GpaScoreStatusResponse uses string | null, causing
mapping mismatches; update the GpaScore interface (rejectedReason) to allow null
(e.g., rejectedReason: string | null or rejectedReason?: string | null) so it
matches GpaScoreStatusResponse and avoids conversion code—change the property on
the GpaScore interface rather than the response type.
| export interface LanguageTestScore { | ||
| verifyStatus: VerifyStatus; | ||
| rejectedReason?: string; | ||
| } | ||
|
|
||
| export interface LanguageTestScoreStatusResponse { | ||
| id: number; | ||
| languageTestResponse: LanguageTestResponse; | ||
| verifyStatus: VerifyStatus; | ||
| rejectedReason: string | null; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
언어 점수 rejectedReason도 null 호환이 필요합니다.
-
- 응답 타입과의 간극
응답은string | null인데 로컬은string?라 맞지 않을 수 있어요.
null 허용을 추가하면 변환이 매끄럽습니다.
- 응답 타입과의 간극
🔧 제안 변경
export interface LanguageTestScore {
verifyStatus: VerifyStatus;
- rejectedReason?: string;
+ rejectedReason?: string | null;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface LanguageTestScore { | |
| verifyStatus: VerifyStatus; | |
| rejectedReason?: string; | |
| } | |
| export interface LanguageTestScoreStatusResponse { | |
| id: number; | |
| languageTestResponse: LanguageTestResponse; | |
| verifyStatus: VerifyStatus; | |
| rejectedReason: string | null; | |
| createdAt: string; | |
| updatedAt: string; | |
| } | |
| export interface LanguageTestScore { | |
| verifyStatus: VerifyStatus; | |
| rejectedReason?: string | null; | |
| } | |
| export interface LanguageTestScoreStatusResponse { | |
| id: number; | |
| languageTestResponse: LanguageTestResponse; | |
| verifyStatus: VerifyStatus; | |
| rejectedReason: string | null; | |
| createdAt: string; | |
| updatedAt: string; | |
| } |
🤖 Prompt for AI Agents
In `@apps/admin/src/types/scores.ts` around lines 60 - 72, The local
LanguageTestScore interface's rejectedReason is currently optional string and
must accept null to match API responses; update the LanguageTestScore definition
(and any related uses) so rejectedReason is typed as string | null (e.g.,
rejectedReason?: string | null) and adjust any code that assumes only undefined
to handle null values as well, referencing the LanguageTestScore interface to
locate the change.
구현 완료 내용
기존
solid-connection-admin프로젝트를 TanStack Start 기반으로apps/admin폴더에 마이그레이션했습니다.주요 변경 사항
/->/scores리다이렉트, 로그인 및 성적 관리 페이지 구현apps/admin위치로 이동 및turbo.json빌드 파이프라인 설정실행 방법