Compare commits
6 Commits
1a0cc9376f
...
ea7861b63a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7861b63a | ||
|
|
069f58075b | ||
|
|
edef4273c0 | ||
|
|
e3091494b1 | ||
|
|
3859099074 | ||
|
|
54c84dbc87 |
@@ -1 +1 @@
|
|||||||
VITE_API_URL=http://localhost:8080
|
VITE_API_URL=http://localhost:8088
|
||||||
|
|||||||
26
src/App.tsx
26
src/App.tsx
@@ -7,19 +7,41 @@ import { PageRouting } from './const/PageRouting';
|
|||||||
import LoginPage from './ui/page/account/login/LoginPage';
|
import LoginPage from './ui/page/account/login/LoginPage';
|
||||||
import ResetPasswordPage from './ui/page/account/resetPassword/ResetPasswordPage';
|
import ResetPasswordPage from './ui/page/account/resetPassword/ResetPasswordPage';
|
||||||
import { HomePage } from './ui/page/home/HomePage';
|
import { HomePage } from './ui/page/home/HomePage';
|
||||||
|
import type { AuthData } from './data/AuthData';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { ScheduleMainPage } from './ui/page/schedule/ScheduleMainPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { authData } = useAuthStore();
|
const { authData, login } = useAuthStore();
|
||||||
|
useEffect(() => {
|
||||||
|
const autoLogin = localStorage.getItem('autoLogin') === 'true';
|
||||||
|
if (autoLogin) {
|
||||||
|
const stored = localStorage.getItem('auth-storage');
|
||||||
|
if (stored) {
|
||||||
|
const storedAuthData = JSON.parse(stored).state as AuthData;
|
||||||
|
login(storedAuthData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
|
{
|
||||||
|
!authData
|
||||||
|
? <>
|
||||||
<Route element={<LoginPage />} path={PageRouting["LOGIN"].path} />
|
<Route element={<LoginPage />} path={PageRouting["LOGIN"].path} />
|
||||||
<Route element={<SignUpPage />} path={PageRouting["SIGN_UP"].path} />
|
<Route element={<SignUpPage />} path={PageRouting["SIGN_UP"].path} />
|
||||||
<Route element={<ResetPasswordPage />} path={PageRouting["RESET_PASSWORD"].path} />
|
<Route element={<ResetPasswordPage />} path={PageRouting["RESET_PASSWORD"].path} />
|
||||||
{!authData ? <Route element={<Navigate to={PageRouting["LOGIN"].path} />} path="*" /> : null}
|
<Route element={<Navigate to={PageRouting["LOGIN"].path} />} path="*" />
|
||||||
|
</>
|
||||||
|
: <>
|
||||||
|
<Route element={<Navigate to={PageRouting["HOME"].path} />} path="*" />
|
||||||
<Route element={<HomePage />} path={PageRouting["HOME"].path} />
|
<Route element={<HomePage />} path={PageRouting["HOME"].path} />
|
||||||
|
<Route element={<ScheduleMainPage />} path={PageRouting["SCHEDULES"].path} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ function SidebarProvider({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
// Helper to toggle the sidebar.
|
||||||
const toggleSidebar = React.useCallback(() => {
|
const toggleSidebar = () => setOpen((open) => !open);
|
||||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -157,13 +155,15 @@ function Sidebar({
|
|||||||
collapsible = "offcanvas",
|
collapsible = "offcanvas",
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
forceSheet = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
side?: "left" | "right"
|
side?: "left" | "right"
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
variant?: "sidebar" | "floating" | "inset"
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
collapsible?: "offcanvas" | "icon" | "none",
|
||||||
|
forceSheet?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
const { isMobile, state, open, setOpen } = useSidebar()
|
||||||
|
|
||||||
if (collapsible === "none") {
|
if (collapsible === "none") {
|
||||||
return (
|
return (
|
||||||
@@ -180,14 +180,14 @@ function Sidebar({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile || forceSheet) {
|
||||||
return (
|
return (
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
<Sheet open={open} onOpenChange={setOpen} {...props}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar"
|
data-slot="sidebar"
|
||||||
data-mobile="true"
|
data-mobile="true"
|
||||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
className="bg-sidebar text-sidebar-foreground rounded-br-2xl rounded-tr-2xl w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
import { Validator } from '@/util/Validator';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const LoginSchema = z.object({
|
export const LoginSchema = z.object({
|
||||||
id: z
|
id: z
|
||||||
.string()
|
.string()
|
||||||
|
.refine((val) => {
|
||||||
|
if (val.includes('@')) {
|
||||||
|
return Validator.isEmail(val);;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
message: "이메일 형식이 올바르지 않습니다."
|
||||||
|
})
|
||||||
, password: z
|
, password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.regex(/^[a-z](?=.*[0-9])(?=.*[!@#$]).*$/, "비밀번호는 영소문자로 시작하여 숫자, 특수문자(!@#$)를 한 개 이상 포함하여야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "비밀번호는 영소문자로 시작하여 숫자, 특수문자(!@#$)를 한 개 이상 포함하여야 합니다.")
|
||||||
});
|
});
|
||||||
@@ -6,12 +6,12 @@ export const ResetPasswordSchema = z.object({
|
|||||||
, code: z
|
, code: z
|
||||||
.string()
|
.string()
|
||||||
.length(8)
|
.length(8)
|
||||||
.regex(/^[a-z](?=.*[0-9])(?=.*[!@#$%^]).*$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, password: z
|
, password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.regex(/^[a-z](?=.*[0-9])(?=.*[!@#$%^]).*$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, passwordConfirm: z
|
, passwordConfirm: z
|
||||||
.string()
|
.string()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ export const SignUpSchema = z.object({
|
|||||||
accountId: z
|
accountId: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "아이디는 5 자리 이상이어야 합니다.")
|
.min(5, "아이디는 5 자리 이상이어야 합니다.")
|
||||||
|
.refine((val) => {
|
||||||
|
return /^[a-zA-z-_.]*$/.test(val);
|
||||||
|
}, {
|
||||||
|
message: "영문, 숫자, '- _ .' 를 제외한 문자를 사용할 수 없습니다."
|
||||||
|
})
|
||||||
, email: z
|
, email: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "이메일을 입력해주십시오.")
|
.min(5, "이메일을 입력해주십시오.")
|
||||||
@@ -11,7 +16,7 @@ export const SignUpSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.regex(/^[a-z](?=.*[0-9])(?=.*[!@#$%^]).*$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, name: z
|
, name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "이름을 입력해주시십시오.")
|
.min(1, "이름을 입력해주시십시오.")
|
||||||
|
|||||||
@@ -120,7 +120,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html, body, #root {
|
html, body, #root {
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-width: 1280px;
|
||||||
|
min-height: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome, Safari, Edge */
|
/* Chrome, Safari, Edge */
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ export default function Layout() {
|
|||||||
<SideBar />
|
<SideBar />
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
{ authData ? <Header /> : null}
|
{ authData ? <Header /> : null}
|
||||||
{/* <Header /> */}
|
<div className="w-full h-full p-2.5">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import type {
|
|||||||
InternalAxiosRequestConfig,
|
InternalAxiosRequestConfig,
|
||||||
} from "axios";
|
} from "axios";
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import type { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
|
import { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
|
||||||
|
import type { AuthData } from '@/data/AuthData';
|
||||||
|
|
||||||
export class BaseNetwork {
|
export class BaseNetwork {
|
||||||
protected instance: AxiosInstance;
|
protected instance: AxiosInstance;
|
||||||
@@ -98,30 +99,16 @@ export class BaseNetwork {
|
|||||||
this.isRefreshing = true;
|
this.isRefreshing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.get<RefreshAccessTokenResponse>(
|
await this.refreshToken();
|
||||||
'/account/refresh-access-token',
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${refreshToken}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const newAccessToken = response.data.accessToken;
|
const newAccessToken = useAuthStore.getState().authData!.accessToken;
|
||||||
const newRefreshToken = response.data.refreshToken;
|
|
||||||
|
|
||||||
useAuthStore.getState().login({
|
|
||||||
...authData,
|
|
||||||
accessToken: newAccessToken,
|
|
||||||
refreshToken: newRefreshToken
|
|
||||||
});
|
|
||||||
|
|
||||||
this.refreshQueue.forEach((cb) => cb(newAccessToken));
|
this.refreshQueue.forEach((cb) => cb(newAccessToken));
|
||||||
this.refreshQueue = [];
|
this.refreshQueue = [];
|
||||||
originalRequest.headers = {
|
originalRequest.headers = {
|
||||||
...originalRequest.headers,
|
...originalRequest.headers,
|
||||||
Authorization: `Bearer ${newAccessToken}`,
|
Authorization: `Bearer ${newAccessToken}`,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
return this.instance(originalRequest);
|
return this.instance(originalRequest);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -143,4 +130,39 @@ export class BaseNetwork {
|
|||||||
protected async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { authPass?: boolean }) {
|
protected async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { authPass?: boolean }) {
|
||||||
return await this.instance.post<T>(url, data, config);
|
return await this.instance.post<T>(url, data, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async refreshToken() {
|
||||||
|
const storedAuth = localStorage.getItem('auth-storage');
|
||||||
|
|
||||||
|
if (!storedAuth) {
|
||||||
|
localStorage.setItem('autoLogin', 'false');
|
||||||
|
throw new Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData: AuthData = JSON.parse(storedAuth).state;
|
||||||
|
|
||||||
|
if (!authData || !authData.refreshToken) {
|
||||||
|
localStorage.setItem('autoLogin', 'false');
|
||||||
|
throw new Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.get<RefreshAccessTokenResponse>(
|
||||||
|
'/account/refresh-access-token',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authData.refreshToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.data.success) throw new Error;
|
||||||
|
|
||||||
|
const newAccessToken = result.data.accessToken;
|
||||||
|
const newRefreshToken = result.data.refreshToken;
|
||||||
|
|
||||||
|
useAuthStore.getState().login({
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AuthData } from '@/data/AuthData';
|
import type { AuthData } from '@/data/AuthData';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
interface AuthStoreProps {
|
interface AuthStoreProps {
|
||||||
authData: AuthData | undefined;
|
authData: AuthData | undefined;
|
||||||
@@ -7,17 +8,23 @@ interface AuthStoreProps {
|
|||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthStoreProps>((set) => ({
|
const storage = sessionStorage;
|
||||||
|
export const useAuthStore = create<AuthStoreProps>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
authData: undefined,
|
authData: undefined,
|
||||||
login: (data: AuthData) => {
|
login: (data: AuthData) => {
|
||||||
set({ authData: data });
|
set({ authData: data });
|
||||||
Object.entries(data)
|
|
||||||
.forEach((entry) => {
|
|
||||||
localStorage.setItem(entry[0], entry[1]);
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
|
localStorage.setItem('autoLogin', 'false');
|
||||||
|
localStorage.removeItem('auth-storage');
|
||||||
set({ authData: undefined });
|
set({ authData: undefined });
|
||||||
localStorage.clear();
|
|
||||||
}
|
}
|
||||||
}));
|
}),
|
||||||
|
{
|
||||||
|
name: 'auth-storage',
|
||||||
|
storage: createJSONStorage(() => storage)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -2,13 +2,53 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
import { LogOutIcon } from 'lucide-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { PageRouting } from '@/const/PageRouting';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { logout } = useAuthStore();
|
||||||
|
|
||||||
|
const handleClickLogoutButton = () => {
|
||||||
|
logout();
|
||||||
|
navigate(PageRouting["LOGIN"].path);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex shrink-0 items-center gap-2 border-b px-4 w-full h-12">
|
<header className="w-full flex shrink-0 flex-row justify-between items-center border-b px-4 h-12">
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
<Label>{import.meta.env.BASE_URL}</Label>
|
<Label>{import.meta.env.BASE_URL}</Label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className={`
|
||||||
|
group flex items-center justify-start
|
||||||
|
pr-2 pl-2 border border-red-500 bg-white
|
||||||
|
transition-all duration-150
|
||||||
|
w-10 hover:w-25 hover:bg-red-500
|
||||||
|
overflow-hidden rounded-md
|
||||||
|
`}
|
||||||
|
type="button"
|
||||||
|
onClick={handleClickLogoutButton}
|
||||||
|
>
|
||||||
|
<LogOutIcon
|
||||||
|
className="text-red-500 transition-colors duration-150 group-hover:text-white"
|
||||||
|
/>
|
||||||
|
<span className="
|
||||||
|
text-red-500 group-hover:text-white
|
||||||
|
opacity-0 scale-1
|
||||||
|
transition-all duration-150
|
||||||
|
group-hover:opacity-100 group-hover:scale-100
|
||||||
|
">
|
||||||
|
로그아웃
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
|
|
||||||
export default function SideBar() {
|
export default function SideBar() {
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar forceSheet={true}>
|
||||||
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useCallback, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { PageRouting } from '@/const/PageRouting';
|
import { PageRouting } from '@/const/PageRouting';
|
||||||
@@ -15,10 +15,15 @@ import { LoginRequest } from '@/data/request/account/LoginRequest';
|
|||||||
import { AccountNetwork } from '@/network/AccountNetwork';
|
import { AccountNetwork } from '@/network/AccountNetwork';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [autoLogin, setAutoLogin] = useState<boolean>(false);
|
||||||
const { login } = useAuthStore();
|
const { login } = useAuthStore();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const accountNetwork = new AccountNetwork();
|
const accountNetwork = new AccountNetwork();
|
||||||
const loginForm = useForm<z.infer<typeof LoginSchema>>({
|
const loginForm = useForm<z.infer<typeof LoginSchema>>({
|
||||||
@@ -30,6 +35,10 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') };
|
const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('autoLogin', `${autoLogin}`)
|
||||||
|
}, [autoLogin]);
|
||||||
|
|
||||||
const moveToSignUpPage = useCallback(() => {
|
const moveToSignUpPage = useCallback(() => {
|
||||||
navigate(PageRouting["SIGN_UP"].path);
|
navigate(PageRouting["SIGN_UP"].path);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -53,39 +62,27 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const loginPromise = accountNetwork.login(data);
|
const loginPromise = accountNetwork.login(data);
|
||||||
|
|
||||||
toast.promise<{ message?: string }>(
|
toast.promise(
|
||||||
() => new Promise(async (resolve, reject) => {
|
loginPromise,
|
||||||
try {
|
|
||||||
loginPromise.then((res) => {
|
|
||||||
if (res.data.success) {
|
|
||||||
resolve({message: ''});
|
|
||||||
} else {
|
|
||||||
reject(res.data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
reject ("서버 에러 발생");
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
loading: "로그인 중입니다.",
|
loading: "로그인 중입니다.",
|
||||||
success: "로그인이 완료되었습니다.",
|
success: (res) => {
|
||||||
error: (err) => `${err}`
|
setIsLoading(false);
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
loginPromise
|
|
||||||
.then((res) => {
|
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
const data = {
|
const data = {
|
||||||
accessToken: res.data.accessToken!,
|
accessToken: res.data.accessToken!,
|
||||||
refreshToken: res.data.refreshToken!
|
refreshToken: res.data.refreshToken!
|
||||||
}
|
};
|
||||||
login({ ...data });
|
login({...data});
|
||||||
moveToHomePage();
|
moveToHomePage();
|
||||||
|
return "로그인 성공";
|
||||||
|
} else {
|
||||||
|
throw new Error(res.data.message);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.finally(() => setIsLoading(false));
|
error: (err: Error) => err.message || "에러 발생"
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextSeparator = ({ text }: { text: string }) => {
|
const TextSeparator = ({ text }: { text: string }) => {
|
||||||
@@ -98,9 +95,16 @@ export default function LoginPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEnterKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (!(e.key === 'Enter')) return;
|
||||||
|
const result = await loginForm.trigger();
|
||||||
|
if (!result) return;
|
||||||
|
await reqLogin();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col justify-center items-center">
|
<div className="w-full h-full flex flex-col justify-center items-center">
|
||||||
<Card className="w-md pl-2 pr-2">
|
<Card className={isMobile ? "w-full pl-2 pr-2" : "w-md pl-2 pr-2"}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
로그인
|
로그인
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -117,6 +121,8 @@ export default function LoginPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
id="form-login-id"
|
id="form-login-id"
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
|
tabIndex={1}
|
||||||
|
onKeyDown={handleEnterKeyDown}
|
||||||
/>
|
/>
|
||||||
<FieldError errors={[fieldState.error]} />
|
<FieldError errors={[fieldState.error]} />
|
||||||
</Field>
|
</Field>
|
||||||
@@ -134,6 +140,7 @@ export default function LoginPage() {
|
|||||||
className="p-0 bg-transparent hover:bg-transparent h-fit w-fit text-xs text-gray-400 hover:text-gray-500 cursor-pointer"
|
className="p-0 bg-transparent hover:bg-transparent h-fit w-fit text-xs text-gray-400 hover:text-gray-500 cursor-pointer"
|
||||||
onClick={moveToResetPasswordPage}
|
onClick={moveToResetPasswordPage}
|
||||||
type="button"
|
type="button"
|
||||||
|
tabIndex={3}
|
||||||
>
|
>
|
||||||
비밀번호를 잊으셨습니까?
|
비밀번호를 잊으셨습니까?
|
||||||
</Button>
|
</Button>
|
||||||
@@ -143,13 +150,28 @@ export default function LoginPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
id="form-login-password"
|
id="form-login-password"
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
|
tabIndex={2}
|
||||||
|
onKeyDown={handleEnterKeyDown}
|
||||||
/>
|
/>
|
||||||
<FieldError errors={[fieldState.error]} />
|
<FieldError errors={[fieldState.error]} />
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
</Controller>
|
</Controller>
|
||||||
|
<div className="flex flex-row gap-2 mt-2">
|
||||||
|
<Checkbox
|
||||||
|
className={[
|
||||||
|
"data-[state=checked]:bg-indigo-500 data-[state=checked]:text-white"
|
||||||
|
, "data-[state=checked]:outline-none data-[state=checked]:border-0"
|
||||||
|
].join(' ')}
|
||||||
|
id="auto-login"
|
||||||
|
checked={autoLogin}
|
||||||
|
onCheckedChange={(value) => setAutoLogin(value === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="auto-login">자동 로그인</Label>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter
|
<CardFooter
|
||||||
className="w-full flex flex-col items-center gap-5"
|
className="w-full flex flex-col items-center gap-5"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { PageRouting } from '@/const/PageRouting';
|
import { PageRouting } from '@/const/PageRouting';
|
||||||
@@ -122,7 +122,7 @@ export default function ResetPasswordPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleThirdStepButton = async () => {
|
const handleClickThirdStepButton = async () => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
const passwordValid = await resetPasswordForm.trigger('password');
|
const passwordValid = await resetPasswordForm.trigger('password');
|
||||||
if (!passwordValid) return;
|
if (!passwordValid) return;
|
||||||
@@ -159,6 +159,19 @@ export default function ResetPasswordPage() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEnterKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentStep === 1) {
|
||||||
|
await handleClickFirstStepButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentStep === 3) {
|
||||||
|
await handleClickThirdStepButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stepper
|
<Stepper
|
||||||
value={currentStep}
|
value={currentStep}
|
||||||
@@ -211,13 +224,7 @@ export default function ResetPasswordPage() {
|
|||||||
type="email"
|
type="email"
|
||||||
id="reset-password-email"
|
id="reset-password-email"
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={handleEnterKeyDown}
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (email.length === 0) return;
|
|
||||||
handleClickFirstStepButton();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<FieldError className="font-[12px]" errors={[fieldState.error]} />
|
<FieldError className="font-[12px]" errors={[fieldState.error]} />
|
||||||
</>
|
</>
|
||||||
@@ -275,6 +282,7 @@ export default function ResetPasswordPage() {
|
|||||||
type={ showPassword ? "text" : "password" }
|
type={ showPassword ? "text" : "password" }
|
||||||
id="reset-password-password"
|
id="reset-password-password"
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
|
onKeyDown={handleEnterKeyDown}
|
||||||
className="pr-10"
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -306,6 +314,7 @@ export default function ResetPasswordPage() {
|
|||||||
id="reset-password-password-confirm"
|
id="reset-password-password-confirm"
|
||||||
className="pr-10"
|
className="pr-10"
|
||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
|
onKeyDown={handleEnterKeyDown}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -380,7 +389,7 @@ export default function ResetPasswordPage() {
|
|||||||
(password.trim().length < 1)
|
(password.trim().length < 1)
|
||||||
&& (passwordConfirm.trim().length < 1)
|
&& (passwordConfirm.trim().length < 1)
|
||||||
}
|
}
|
||||||
onClick={handleThirdStepButton}
|
onClick={handleClickThirdStepButton}
|
||||||
>
|
>
|
||||||
비밀번호 변경
|
비밀번호 변경
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { AccountNetwork } from '@/network/AccountNetwork';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { PageRouting } from '@/const/PageRouting';
|
import { PageRouting } from '@/const/PageRouting';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
export default function SignUpPage() {
|
export default function SignUpPage() {
|
||||||
const [emailVerificationModalOpen, setEmailVerificationModalOpen] = useState<boolean>(false);
|
const [emailVerificationModalOpen, setEmailVerificationModalOpen] = useState<boolean>(false);
|
||||||
@@ -28,6 +30,7 @@ export default function SignUpPage() {
|
|||||||
|
|
||||||
const accountNetwork = new AccountNetwork();
|
const accountNetwork = new AccountNetwork();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const signUpForm = useForm<z.infer<typeof SignUpSchema>>({
|
const signUpForm = useForm<z.infer<typeof SignUpSchema>>({
|
||||||
resolver: zodResolver(SignUpSchema),
|
resolver: zodResolver(SignUpSchema),
|
||||||
@@ -119,9 +122,11 @@ export default function SignUpPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col justify-center items-center">
|
<div className={"w-full h-full flex flex-col justify-center items-center"}>
|
||||||
<Card className="w-md pl-2 pr-2">
|
|
||||||
|
<Card className={isMobile ? "w-full pl-2 pr-2" : "w-md pl-2 pr-2"}>
|
||||||
<CardHeader>회원가입</CardHeader>
|
<CardHeader>회원가입</CardHeader>
|
||||||
|
<ScrollArea className="h-72 [&>div>div:last-child]:hidden">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form id="form-signup">
|
<form id="form-signup">
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
@@ -244,6 +249,7 @@ export default function SignUpPage() {
|
|||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
</ScrollArea>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<EmailVerificationModal
|
<EmailVerificationModal
|
||||||
trigger={
|
trigger={
|
||||||
@@ -257,7 +263,9 @@ export default function SignUpPage() {
|
|||||||
onVerifySuccess={signup} // ✅ 인증 성공 시 signup 호출
|
onVerifySuccess={signup} // ✅ 인증 성공 시 signup 호출
|
||||||
/>
|
/>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-full h-full flex flex-column">
|
||||||
HomePage
|
HomePage
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
26
src/ui/page/schedule/ScheduleMainPage.tsx
Normal file
26
src/ui/page/schedule/ScheduleMainPage.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { DayButton } from "react-day-picker";
|
||||||
|
|
||||||
|
export function ScheduleMainPage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col justify-start items-center">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
className="rounded-lg w-full h-full max-h-10/12 border"
|
||||||
|
components={{
|
||||||
|
Weeks: (props) => (
|
||||||
|
<tbody {...props} className={props.className}></tbody>
|
||||||
|
),
|
||||||
|
Week: (props) => (
|
||||||
|
<tr {...props} className={props.className + " h-1/10"}></tr>
|
||||||
|
),
|
||||||
|
Day: (props) => (
|
||||||
|
<td {...props} className={props.className + " h-1"}></td>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</Calendar>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export class Validator {
|
|||||||
|
|
||||||
static validatePasswordFormat = (password: string): boolean => {
|
static validatePasswordFormat = (password: string): boolean => {
|
||||||
if (password.length < 8) return false;
|
if (password.length < 8) return false;
|
||||||
|
if (password.includes(' ')) return false;
|
||||||
|
|
||||||
const alphabets = 'abcdefghijklmnopqrstuvwxyz';
|
const alphabets = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
const numbers = '0123456789';
|
const numbers = '0123456789';
|
||||||
|
|||||||
Reference in New Issue
Block a user