Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,124 changes: 1,124 additions & 0 deletions keyword/chapter05/keyword05.md

Large diffs are not rendered by default.

Empty file.
48 changes: 48 additions & 0 deletions mission/chapter05/mission_1/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import HomeLayout from './layouts/HomeLayout';
import NotFound from './pages/NotFoundPage';
import LoginPage from './pages/LoginPage';
import HomePage from './pages/HomePage';
import SignupPage from './pages/SignupPage';
import MyPage from './pages/MyPage';
import ProtectedRoute from './components/ProtectedRoute';
import { AuthProvider } from './context/AuthContext';

const router = createBrowserRouter([
{
path: '/',
element: <HomeLayout />,
errorElement: <NotFound />,
children: [
{ index: true, element: <HomePage /> },
{
path: 'mypage',
element: (
<ProtectedRoute>
<MyPage />
</ProtectedRoute>
),
},
],
},
{
path: '/login',
element: <LoginPage />,
errorElement: <NotFound />,
},
{
path: '/signup',
element: <SignupPage />,
errorElement: <NotFound />,
},
]);
function App() {
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
}

export default App;
Binary file added mission/chapter05/mission_1/src/assets/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions mission/chapter05/mission_1/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions mission/chapter05/mission_1/src/assets/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions mission/chapter05/mission_1/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
const Navbar = () => {
const navigate = useNavigate();
const { user, logout } = useAuth();

return (
<nav className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
{/* 로고 */}
<div
className="font-bold text-lg cursor-pointer"
onClick={() => navigate('/')}
>
</div>

{/* 버튼 영역 */}
<div className="flex gap-2">
{user ? (
//로그인 된 상태
<div className="flex items-center gap-3">
<span
className="text-gray-600 cursor-pointer hover:text-blue-400 transition-colors"
onClick={() => navigate('/mypage')}
>
마이페이지
</span>
<button
onClick={() => {
logout();
navigate('/');
}}
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-100 transition-colors"
>
로그아웃
</button>
</div>
) : (
// 로그인 안 된 상태
<>
<button
onClick={() => navigate('/login')}
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-100 transition-colors"
>
로그인
</button>
<button
onClick={() => navigate('/signup')}
className="px-4 py-2 text-sm bg-blue-300 text-white rounded-md hover:bg-blue-500 transition-colors"
>
회원가입
</button>
Comment thread
12234538 marked this conversation as resolved.
</>
)}
</div>
</nav>
);
};

export default Navbar;
11 changes: 11 additions & 0 deletions mission/chapter05/mission_1/src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { user } = useAuth();

if (!user) return <Navigate to="/login" replace />;
return <>{children}</>;
};

export default ProtectedRoute;
44 changes: 44 additions & 0 deletions mission/chapter05/mission_1/src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createContext, useContext, useState } from 'react';

interface User {
email: string;
nickname: string;
}

interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<User | null>(() => {
// 로컬스토리지에서 초기값 불러오기
const stored = localStorage.getItem('user');
return stored ? JSON.parse(stored) : null;
});

const login = (userData: User) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};

const logout = () => {
setUser(null);
localStorage.removeItem('user');
};

return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('AuthProvider 밖에서 사용 불가!');
return context;
};
54 changes: 54 additions & 0 deletions mission/chapter05/mission_1/src/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect, useState, type ChangeEvent } from 'react';

interface UseFormProps<T> {
initialValue: T;
//값이 올바른지 검증하는 함수
validate: (values: T) => Record<keyof T, string>;
}

function useForm<T>({ initialValue, validate }: UseFormProps<T>) {
const [values, setValues] = useState(initialValue);

//"email": true -> touch 됨
//"password": false -> touch 안 됨
const [touched, setTouched] = useState<Record<string, boolean>>();

//"email": 이메일은 반드시 @를 포함해야 합니다.
const [errors, setErrors] = useState<Record<string, string>>();

//사용자가 입력값 바꿀 때 실행되는 함수
const handleChange = (name: keyof T, text: string) => {
setValues({
...values, //기존 입력값 유지
[name]: text,
});
};

const handleBlur = (name: keyof T) => {
setTouched({
...touched,
[name]: true,
});
};

//이메일과 비밀번호 인풋, 속성들을 가져오는 것
const getInputProps = (name: keyof T) => {
const value = values[name];
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
handleChange(name, e.target.value);
const onBlur = () => handleBlur(name);

return { value, onChange, onBlur };
};

//values가 변경될 때 에러검증 로직
//변경될 때마다 로직이 변경되어야 하니 useEffect 사용
useEffect(() => {
const newErrors = validate(values);
setErrors(newErrors); //오류 메시지 업데이트
}, [validate, values]);

return {values, errors, touched, getInputProps};
}

export default useForm;
29 changes: 29 additions & 0 deletions mission/chapter05/mission_1/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState } from 'react';

function useLocalStorage<T>(key: string, initialValue: T) {
// 로컬스토리지에서 값을 가져오는 함수
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
// 저장된 값이 있으면 파싱해서 반환, 없으면 초기값 반환
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});

// 값을 저장하는 함수
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};

return [storedValue, setValue] as const;
}

export default useLocalStorage;
1 change: 1 addition & 0 deletions mission/chapter05/mission_1/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'tailwindcss';
Loading