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
Binary file added .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions week6/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_SERVER_API_URL=http://test2.shop:44003

24 changes: 24 additions & 0 deletions week6/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
54 changes: 54 additions & 0 deletions week6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```
28 changes: 28 additions & 0 deletions week6/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
13 changes: 13 additions & 0 deletions week6/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
39 changes: 39 additions & 0 deletions week6/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "login-fe",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5",
"axios": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"react-intersection-observer": "^9.16.0",
"react-router-dom": "^7.5.0",
"tailwindcss": "^4.1.3",
"zod": "^3.24.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}
1 change: 1 addition & 0 deletions week6/public/images/google_logo.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 week6/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added week6/src/App.css
Empty file.
69 changes: 69 additions & 0 deletions week6/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

import "./App.css"
import { createBrowserRouter, RouterProvider } from 'react-router'

import NotFoundPage from './pages/NotFoundPage'
import LoginPage from './pages/LoginPage'
import HomeLayout from './layouts/HomeLayout'
import HomePage from './pages/HomePage'
import SignupPage from './pages/SignupPage'
import MyPage from './pages/MyPage'
import { AuthProvider } from './context/AuthContext'
import ProtectedLayout from './layouts/ProtectedLayout'
import GoogleLoginRedirectPage from './pages/GoogleLoginRedirectPage'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'


//publicRoutes : 인증없이 접근 가능한 라우트
const publicRoutes = [
{
path: "/",
element: <HomeLayout />,
errorElement: <NotFoundPage />,
children: [
{index: true, element: <HomePage />},
{path: 'login', element: <LoginPage />},
{path: 'signup', element: <SignupPage />},
{path: 'v1/auth/google/callback' , element: <GoogleLoginRedirectPage />},
],
}
]
//protectedRoutes : 인증이 필요한 라우트
const protectedRoutes = [
{
path: "/",
element: <ProtectedLayout/>,
errorElement: <NotFoundPage />,
children: [
{
path: 'my',
element: <MyPage />,
}
]
}
]
const router = createBrowserRouter([...publicRoutes, ...protectedRoutes,])

export const queryClient = new QueryClient({

defaultOptions: {
queries:{
retry: 3,
}
}
} // QueryClient의 기본 옵션을 설정합니다.(전역적으로 모든 쿼리에 적용됨)
)

function App() {

return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<RouterProvider router={router} />
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</AuthProvider>
</QueryClientProvider>
)
}
export default App
25 changes: 25 additions & 0 deletions week6/src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import axios from 'axios';
import { RequestSigninDto, RequestSignupDto, ResponseMyInfoDto, ResponseSigninDto, ResponseSignupDto } from '../types/auth';
import { axiosInstance } from './axios';

export const postSignup = async(body: RequestSignupDto): Promise<ResponseSignupDto> => {

const {data} = await axiosInstance.post("/v1/auth/signup", body)
return data;
}

export const postSignin = async(body: RequestSigninDto): Promise<ResponseSigninDto> => {
const {data} = await axiosInstance.post( "/v1/auth/signin", body)
return data;
}

export const getMyInfo = async(): Promise<ResponseMyInfoDto> => {

const {data} = await axiosInstance.get("/v1/users/me")
return data;
}

export const postLogout = async() => {
const {data} = await axiosInstance.post("/v1/auth/signout")
return data;
}
35 changes: 35 additions & 0 deletions week6/src/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { LOCAL_STORAGE_KEY } from '../constants/key';
import { useLocalStorage } from '../hooks/useLocalStorage';
import axios, { AxiosRequestConfig } from 'axios';


interface CustomAxiosRequestConfig extends AxiosRequestConfig { // 요청 재시로 여부 나타내는 플래그
retry?: boolean
}

// refreshToken 요청을 위한 Promise 전역으로 저장해서 중복요청 방지
let refreshPromise: Promise<string> | null = null

export const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_SERVER_API_URL,
// headers: {
// Authorization: `Bearer ${localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN)}`,
// }
withCredentials: true, //include cookie
});


// 요청이 들어오기 전에 실행되는 인터셉터
// 요청이 들어오기 전에 access토큰을 authorization헤더에 추가
axiosInstance.interceptors.request.use((config) => {
const{getItem} = useLocalStorage(LOCAL_STORAGE_KEY.ACCESS_TOKEN)
const accessToken = getItem()

if (accessToken) {
config.headers = config.headers || {}; // headers가 undefined일 수 있으므로 초기화
config.headers.Authorization = `Bearer ${accessToken}`; // accessToken을 Authorization 헤더에 추가
}
return config; // 수정된 요청 객체를 반환
},(error) => Promise.reject(error) // 에러가 발생했을 때는 Promise.reject(error)로 에러를 전달하여 다음 인터셉터로 넘김

)
16 changes: 16 additions & 0 deletions week6/src/apis/lp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import { axiosInstance } from "../apis/axios";
import { PaginationDto } from "../types/common";
import { ResponseLpListDto } from '../types/lp';


export const getLpList = async(
paginationDto: PaginationDto,
): Promise<ResponseLpListDto> => {
const {data} = await axiosInstance.get("/v1/lps",{
params: paginationDto,
})
return data;
}


1 change: 1 addition & 0 deletions week6/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.
20 changes: 20 additions & 0 deletions week6/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Link } from 'react-router-dom'

const Footer = () => {
return (
<footer className="bg-gray-100 dark:bg-gray-900 py-6 mt-12">
<div className="container mx-auto text-center text-gray-600 dark:text-gray-400">
<p>
&copy;{new Date().getFullYear()} <span className="text-[#807bff]">돌려돌려 돌림판</span>. All rights reserved.
</p>
<div className={'flex justify-center space-x-4 mt-4'}>
<Link to={'#'}>Privacy Policy</Link>
<Link to={'#'}>Terms of Service</Link>
<Link to={'#'}>Contact</Link>
</div>
</div>
</footer>
)
}

export default Footer
25 changes: 25 additions & 0 deletions week6/src/components/LpCard/LpCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Lp} from '../../types/lp.ts';
import LpCardSkeleton from './LpCardSkeleton.tsx';

interface LpCardProps{
lp:Lp
}

const LpCard = ({lp}:LpCardProps) => {
return (
<div
className='relative rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300'>
<img
src={lp.thumbnail}
alt={lp.title}
className='object-cover w-full h-48' />
<div className='absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 p-2'>
<h3 className='text-white text-sm font-semibold'>
{lp.title}
</h3>
</div>
</div>
)
}

export default LpCard
13 changes: 13 additions & 0 deletions week6/src/components/LpCard/LpCardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

const LpCardSkeleton = () => {
return (
<div className='relative rounded-lg overflow-hidden shadow-lg animate-pulse'>
<div className={'bg-gray-300 w-full h-48'}/>
<div className='absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 p-2'>
<div className='bg-gray-400 h-4 w-3/4 rounded-sm'/>
</div>
</div>
)
}

export default LpCardSkeleton
Loading