Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# next.js
**/.next/
**/out/
**/.vinext/

# production
**/build
Expand Down
56 changes: 56 additions & 0 deletions apps/admin/VINEXT_MIGRATION_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Vinext Migration Report

## 변경 요약

- `apps/admin`의 TanStack Start 라우팅 구조를 Vinext 호환 App Router 구조로 변경했다.
- `src/routes` 기반 라우트를 `src/app`으로 이동하고, 라우트 파일에 있던 UI는 feature 컴포넌트로 분리했다.
- TanStack Router navigation 의존을 일반 anchor 및 브라우저 이동 흐름으로 교체했다.
- Vinext `dev`, `build`, `start` script를 적용하고 Vite 설정을 Vinext 플러그인 중심으로 정리했다.
- TanStack Start, TanStack Router, route tree, Nitro 기반 진입점을 제거했다.

## 라우트 매핑

| Before | After |
| --- | --- |
| `src/routes/__root.tsx` | `src/app/layout.tsx`, `src/app/providers.tsx` |
| `src/routes/index.tsx` | `src/app/page.tsx` |
| `src/routes/login.tsx` | `src/app/login/page.tsx` |
| `src/routes/auth/login.tsx` | `src/app/auth/login/page.tsx`, `src/components/features/auth/AdminLoginPage.tsx` |
| `src/routes/scores/index.tsx` | `src/app/scores/page.tsx`, `src/components/features/scores/ScoresPageContent.tsx` |
| `src/routes/bruno/index.tsx` | `src/app/bruno/page.tsx`, `src/components/features/bruno/BrunoApiPageContent.tsx` |
| `src/routes/chat-socket/index.tsx` | `src/app/chat-socket/page.tsx`, `src/components/features/chat-socket/ChatSocketPageContent.tsx` |

## 유지한 것

- 기존 UI 컴포넌트와 admin 페이지 화면 구성
- `components/features`, `components/layout`, `lib`, `types` 중심의 기존 코드 배치
- TanStack Query 기반 서버 상태 관리
- localStorage 기반 admin session 저장소
- login, scores, bruno, chat-socket 사용자 플로우

## 제거한 것

- `RouterProvider`
- `src/routeTree.gen.ts`
- `src/router.tsx`
- `src/routes/**`
- `createFileRoute`, `createRootRoute`
- TanStack Start Vite plugin, Nitro entry
- `@tanstack/react-router`, `@tanstack/react-start`, TanStack Router Devtools 의존성

## 주의할 점

- Vinext는 Next.js 호환 API를 Vite 기반으로 재구현한 도구이므로, Next.js와 완전히 동일한 내부 동작을 전제하지 않는다.
- Vinext build 결과에서 route classification이 `Unknown`으로 표시된다. 현재 Vinext의 정적 분석 한계 안내이며 빌드는 성공한다.
- 현재 로컬 Node가 `v23.10.0`이라 repo 요구사항 `22.x` 경고가 출력된다.
- `@tailwindcss/vite`는 현재 Vite 8 peer range를 공식 포함하지 않아 peer warning이 출력된다. 빌드와 라우트 QA는 통과했다.
- `/`, `/auth/login`, `/scores` 등 API 클라이언트를 import하는 라우트는 `VITE_API_SERVER_URL`이 없으면 기존 로직에 의해 500이 난다. QA는 `.env.example` 기준 예시 값을 주입해 진행했다.

## 검증 결과

- dev: `VITE_API_SERVER_URL=https://api.example.com VITE_S3_BASE_URL=https://s3.example.com pnpm --filter @solid-connect/admin dev` 성공
- build: `pnpm --filter @solid-connect/admin build` 성공
- lint: `pnpm --filter @solid-connect/admin lint:check` 성공
- typecheck: `pnpm --filter @solid-connect/admin typecheck` 성공
- compatibility: `pnpm --filter @solid-connect/admin exec vinext check` 100% compatible
- 주요 페이지 수동 QA: `/`, `/login`, `/auth/login`, `/scores`, `/bruno`, `/chat-socket` 모두 HTTP 200 확인
28 changes: 11 additions & 17 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 4000",
"build": "vite build",
"preview": "vite preview",
"dev": "vinext dev -p 4000",
"build": "vinext build",
"start": "vinext start -p 4000",
"test": "vitest run",
"format": "biome format --write .",
"format:check": "biome format .",
Expand All @@ -22,40 +22,34 @@
"@radix-ui/react-tabs": "^1.1.13",
"@stomp/stompjs": "^7.1.1",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.84.1",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-router-ssr-query": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"axios": "^1.6.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.561.0",
"nitro": "npm:nitro-nightly@latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"sockjs-client": "^1.6.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.6",
"vite-tsconfig-paths": "^6.0.2"
"tailwindcss": "^4.0.6"
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@tanstack/devtools-vite": "^0.3.11",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/sockjs-client": "^1.5.4",
"@vitejs/plugin-react": "^5.0.4",
"@vitejs/plugin-react": "^6.0.2",
"@vitejs/plugin-rsc": "^0.5.26",
"jsdom": "^27.0.0",
"react-server-dom-webpack": "^19.2.6",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vinext": "^0.0.51",
"vite": "^8.0.14",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
}
Expand Down
46 changes: 46 additions & 0 deletions apps/admin/src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { useEffect, useState } from "react";
import { AdminLoginPage } from "@/components/features/auth/AdminLoginPage";
import { ensureSessionToken } from "@/lib/auth/session";

export default function AuthLoginPage() {
const [canRenderLogin, setCanRenderLogin] = useState(false);

useEffect(() => {
let isMounted = true;

const redirectIfAuthenticated = async () => {
try {
const token = await ensureSessionToken();
if (!isMounted) return;

if (token) {
window.location.replace("/scores");
return;
}
} catch {
// 세션 확인 실패 시에도 로그인 화면은 열어둔다.
}

if (!isMounted) return;
setCanRenderLogin(true);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void redirectIfAuthenticated();

return () => {
isMounted = false;
};
}, []);

if (!canRenderLogin) {
return (
<div className="flex min-h-screen items-center justify-center bg-bg-50 typo-medium-3 text-k-500">
관리자 세션을 확인하는 중...
</div>
);
}

return <AdminLoginPage onLoginSuccess={() => window.location.assign("/scores")} />;
}
10 changes: 10 additions & 0 deletions apps/admin/src/app/bruno/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RequireAdminSession } from "@/components/features/auth/RequireAdminSession";
import { BrunoApiPageContent } from "@/components/features/bruno/BrunoApiPageContent";

export default function BrunoPage() {
return (
<RequireAdminSession>
<BrunoApiPageContent />
</RequireAdminSession>
);
}
10 changes: 10 additions & 0 deletions apps/admin/src/app/chat-socket/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RequireAdminSession } from "@/components/features/auth/RequireAdminSession";
import { ChatSocketPageContent } from "@/components/features/chat-socket/ChatSocketPageContent";

export default function ChatSocketPage() {
return (
<RequireAdminSession>
<ChatSocketPageContent />
</RequireAdminSession>
);
}
18 changes: 18 additions & 0 deletions apps/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ReactNode } from "react";
import "../styles.css";
import { Providers } from "./providers";

export const metadata = {
title: "Solid Connection Admin",
viewport: "width=device-width, initial-scale=1",
};

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ko">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
15 changes: 15 additions & 0 deletions apps/admin/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import { useEffect } from "react";

export default function LoginRedirectPage() {
useEffect(() => {
window.location.replace("/auth/login");
}, []);

return (
<div className="flex min-h-screen items-center justify-center bg-bg-50 typo-medium-3 text-k-500">
로그인 페이지로 이동하는 중...
</div>
);
}
10 changes: 10 additions & 0 deletions apps/admin/src/app/mentor-applications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RequireAdminSession } from "@/components/features/auth/RequireAdminSession";
import { MentorApplicationsPageContent } from "@/components/features/mentor-applications/MentorApplicationsPageContent";

export default function MentorApplicationsPage() {
return (
<RequireAdminSession>
<MentorApplicationsPageContent />
</RequireAdminSession>
);
}
25 changes: 25 additions & 0 deletions apps/admin/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { useEffect } from "react";
import { ensureSessionToken } from "@/lib/auth/session";

export default function HomePage() {
useEffect(() => {
const redirectBySession = async () => {
try {
const token = await ensureSessionToken();
window.location.replace(token ? "/scores" : "/auth/login");
} catch {
window.location.replace("/auth/login");
}
};

void redirectBySession();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}, []);

return (
<div className="flex min-h-screen items-center justify-center bg-bg-50 typo-medium-3 text-k-500">
관리자 페이지로 이동하는 중...
</div>
);
}
18 changes: 18 additions & 0 deletions apps/admin/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import type { ReactNode } from "react";
import { Toaster } from "sonner";
import { QueryProvider } from "@/components/providers/QueryProvider";

interface ProvidersProps {
children: ReactNode;
}

export function Providers({ children }: ProvidersProps) {
return (
<QueryProvider>
{children}
<Toaster />
</QueryProvider>
);
}
10 changes: 10 additions & 0 deletions apps/admin/src/app/scores/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RequireAdminSession } from "@/components/features/auth/RequireAdminSession";
import { ScoresPageContent } from "@/components/features/scores/ScoresPageContent";

export default function ScoresPage() {
return (
<RequireAdminSession>
<ScoresPageContent />
</RequireAdminSession>
);
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
"use client";

import { useMutation } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { type FormEvent, useId, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { adminSignInApi } from "@/lib/api/auth";
import { redirectIfAuthenticated } from "@/lib/auth/session";
import { saveAccessToken } from "@/lib/utils/localStorage";

export const Route = createFileRoute("/auth/login")({
beforeLoad: async () => {
await redirectIfAuthenticated();
},
component: LoginPage,
});
interface AdminLoginPageProps {
onLoginSuccess: () => void;
}

function LoginPage() {
const navigate = useNavigate();
export function AdminLoginPage({ onLoginSuccess }: AdminLoginPageProps) {
const emailInputId = useId();
const passwordInputId = useId();
const [email, setEmail] = useState("");
Expand All @@ -44,7 +40,7 @@ function LoginPage() {
description: "관리자 페이지로 이동합니다.",
});

navigate({ to: "/scores" });
onLoginSuccess();
} catch (err: unknown) {
const error = err as { response?: { data?: { message?: string } } };
toast.error("로그인 실패", {
Expand Down
49 changes: 49 additions & 0 deletions apps/admin/src/components/features/auth/RequireAdminSession.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { type ReactNode, useEffect, useState } from "react";
import { ensureSessionToken } from "@/lib/auth/session";

interface RequireAdminSessionProps {
children: ReactNode;
}

export function RequireAdminSession({ children }: RequireAdminSessionProps) {
const [isAuthorized, setIsAuthorized] = useState(false);

useEffect(() => {
let isMounted = true;

const checkSession = async () => {
try {
const token = await ensureSessionToken();
if (!isMounted) return;

if (!token) {
window.location.replace("/auth/login");
return;
}

setIsAuthorized(true);
} catch {
if (!isMounted) return;
window.location.replace("/auth/login");
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void checkSession();

return () => {
isMounted = false;
};
}, []);

if (!isAuthorized) {
return (
<div className="flex min-h-screen items-center justify-center bg-bg-50 typo-medium-3 text-k-500">
관리자 세션을 확인하는 중...
</div>
);
}

return children;
}
Loading
Loading