Skip to content
Open
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
14 changes: 14 additions & 0 deletions src/components/auth/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';

interface PrivateRouteProps {
children: ReactNode;
}

export const PrivateRoute = ({ children }: PrivateRouteProps) => {
// TODO: 현재 백엔드에서 로그인 응답을 sessionId로 제공하고 있어
// 임시로 session_id로 인증 체크 중. 추후 access_token으로 통일 예정
const token = localStorage.getItem('session_id');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

보안 상 localStorage에 세션 ID와 같은 민감한 데이터를 저장하는 것은 XSS(Cross-Site Scripting) 공격에 취약점을 노출할 수 있습니다. localStorage는 JavaScript를 통해 쉽게 접근할 수 있어, 악성 스크립트가 주입될 경우 토큰이 탈취될 위험이 있습니다.

권장 사항:
httpOnly 속성을 적용한 쿠키를 사용하여 토큰을 관리하는 것을 강력히 권장합니다. httpOnly 쿠키는 JavaScript로 접근할 수 없기 때문에 XSS 공격으로부터 토큰을 효과적으로 보호할 수 있습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

인증 토큰의 키로 사용된 'session_id'가 매직 문자열(magic string)로 하드코딩되어 있습니다. 이는 오타 발생 가능성을 높이고, 향후 키 이름이 변경될 경우 여러 곳을 수정해야 하는 등 유지보수를 어렵게 만듭니다.

별도의 상수 파일(예: src/constants/auth.ts)에 키를 상수로 정의하고 이를 가져와 사용하는 것이 좋습니다. 이렇게 하면 코드의 가독성과 안정성이 크게 향상됩니다.

개선 예시:

// src/constants/auth.ts
export const AUTH_TOKEN_KEY = 'session_id';

// src/components/auth/PrivateRoute.tsx
import { AUTH_TOKEN_KEY } from '@/constants/auth';
// ...
const token = localStorage.getItem(AUTH_TOKEN_KEY);


return token ? <>{children}</> : <Navigate to="/login" replace />;
};
Comment on lines +8 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

프로젝트 스타일 가이드에 따르면 모든 코드에 자동화된 테스트가 필요합니다.1 PrivateRoute 컴포넌트는 애플리케이션의 핵심 인증 로직을 담당하므로, 안정적인 동작을 보장하기 위한 테스트 코드를 작성하는 것이 매우 중요합니다.

아래와 같은 시나리오에 대한 테스트 케이스를 추가하는 것을 권장합니다.

  • 인증된 사용자: 토큰이 존재할 경우, children이 정상적으로 렌더링되는지 확인합니다.
  • 인증되지 않은 사용자: 토큰이 없을 경우, /login 경로로 올바르게 리다이렉트되는지 확인합니다.

Style Guide References

Footnotes

  1. 스타일 가이드에서는 모든 코드에 대한 자동화 테스트 작성을 의무화하고 있습니다. (link)

23 changes: 14 additions & 9 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const queryClient = new QueryClient({
});
import { OnboardingPage } from "./pages/onboarding/OnboardingPage.tsx";
import { PostDetailPage } from "./pages/community/PostDetailPage.tsx";
import { PrivateRoute } from "./components/auth/PrivateRoute.tsx";

async function enableMocking() {
if (import.meta.env.DEV) {
Expand All @@ -36,7 +37,11 @@ async function enableMocking() {
const router = createBrowserRouter([
{
path: "/",
element: <App />,
element: (
<PrivateRoute>
<App />
</PrivateRoute>
),
children: [
{
index: true,
Expand All @@ -62,20 +67,20 @@ const router = createBrowserRouter([
path: "community",
element: <PostDetailPage />,
},
{
path: "login",
element: <LoginPage />,
},
{
path: "onboarding",
element: <OnboardingPage />,
},
],
},
{
path: "/login",
element: <LoginPage />,
},
{
path: "/callback",
element: <AuthCallbackPage />,
},
{
path: "/onboarding",
element: <OnboardingPage />,
},
Comment on lines +72 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

로그인 및 온보딩 페이지를 App 컴포넌트의 하위 라우트에서 최상위 라우트로 분리한 구조 변경은 페이지 접근 제어를 명확하게 만들어주는 좋은 시도입니다.

다만 이로 인해 App.tsx 파일 내 layoutGroups 객체에 정의된 /login/onboarding 경로에 대한 레이아웃 분기 로직이 더 이상 사용되지 않는 코드가 되었습니다. 코드의 혼란을 방지하고 유지보수성을 높이기 위해, App.tsx에서 해당 경로 관련 코드를 제거하는 후속 조치가 필요해 보입니다.

]);

enableMocking().then(() => {
Expand Down
49 changes: 26 additions & 23 deletions src/pages/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import GlobalLayout from "@/components/layout/GlobalLayout";
import { Button } from "@/components/ui/button";

export function LoginPage() {
Expand All @@ -20,22 +21,23 @@ export function LoginPage() {
};

return (
<div className="flex flex-col justify-between min-h-full p-8">
<section className="mt-20">
<p className="text-brand-primary text-lg mb-8">
우리 동네 개발자 커뮤니티
</p>
<h1 className="text-brand-primary text-5xl font-bold">CO-MEET</h1>
</section>
<GlobalLayout showHeader={false} showBottomNavigation={false}>
<div className="flex flex-col justify-between min-h-full p-8">
<section className="mt-20">
<p className="text-brand-primary text-lg mb-8">
우리 동네 개발자 커뮤니티
</p>
<h1 className="text-brand-primary text-5xl font-bold">CO-MEET</h1>
</section>

<section className="mx-auto">
<img src="/logo.svg" alt="CO-MEET 로고" className="w-52 h-auto" />
</section>
<section className="mx-auto">
<img src="/logo.svg" alt="CO-MEET 로고" className="w-52 h-auto" />
</section>

<section className="mb-4">
<Button
onClick={handleGitHubLogin}
className="
<section className="mb-4">
<Button
onClick={handleGitHubLogin}
className="
w-full
h-12
bg-brand-primary
Expand All @@ -46,14 +48,15 @@ export function LoginPage() {
gap-5
cursor-pointer
">
<img
src="/github-black.svg"
alt="Github 로고"
className="w-8 h-auto"
/>
<span>Github 로그인</span>
</Button>
</section>
</div>
<img
src="/github-black.svg"
alt="Github 로고"
className="w-8 h-auto"
/>
<span>Github 로그인</span>
</Button>
</section>
</div>
</GlobalLayout>
);
}
59 changes: 31 additions & 28 deletions src/pages/onboarding/OnboardingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { OnboardingStep2 } from "@/pages/onboarding/__components/OnboardingStep2
import { OnboardingStep3 } from "@/pages/onboarding/__components/OnboardingStep3";
import { OnboardingStep4 } from "@/pages/onboarding/__components/OnboardingStep4";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import GlobalLayout from "@/components/layout/GlobalLayout";

export interface OnboardingData {
nickname?: string;
Expand Down Expand Up @@ -55,36 +56,38 @@ export function OnboardingPage() {
};

return (
<div className="w-full pt-8">
{step < TOTAL_STEPS && (
<div className="w-full bg-gray-700 rounded-full h-2 mb-4">
<div
className="bg-lime-400 h-2 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
)}
<Card className="bg-transparent border-none text-white">
<GlobalLayout showHeader={false} showBottomNavigation={false}>
<div className="w-full pt-8">
{step < TOTAL_STEPS && (
<CardHeader>
<CardTitle className="text-2xl font-bold">
{getStepTitle()}
</CardTitle>
</CardHeader>
<div className="w-full bg-gray-700 rounded-full h-2 mb-4">
<div
className="bg-lime-400 h-2 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
)}
<CardContent>
{step === 1 && (
<OnboardingStep1 onNext={handleNext} data={onboardingData} />
<Card className="bg-transparent border-none text-white">
{step < TOTAL_STEPS && (
<CardHeader>
<CardTitle className="text-2xl font-bold">
{getStepTitle()}
</CardTitle>
</CardHeader>
)}
{step === 2 && (
<OnboardingStep2 onNext={handleNext} data={onboardingData} />
)}
{step === 3 && (
<OnboardingStep3 onNext={handleNext} data={onboardingData} />
)}
{step === 4 && <OnboardingStep4 />}
</CardContent>
</Card>
</div>
<CardContent>
{step === 1 && (
<OnboardingStep1 onNext={handleNext} data={onboardingData} />
)}
{step === 2 && (
<OnboardingStep2 onNext={handleNext} data={onboardingData} />
)}
{step === 3 && (
<OnboardingStep3 onNext={handleNext} data={onboardingData} />
)}
{step === 4 && <OnboardingStep4 />}
</CardContent>
</Card>
</div>
</GlobalLayout>
);
}