project init
This commit is contained in:
parent
9f77ca2dfc
commit
4f6510a031
|
@ -1,3 +1,43 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"tsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint", "prettier", "react-hooks"],
|
||||
"rules": {
|
||||
"no-use-before-define": "off",
|
||||
"no-unused-vars": "warn",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"valid-typeof": "off",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
_auth=YWRtaW46MjUxMzI3NDEyMjg3
|
||||
@sdt:registry=http://192.168.1.232:8081/repository/npm-hosted/
|
||||
//192.168.1.232:8081/repository/npm-group/:_authToken=NpmToken.e286dac7-851d-3a20-8429-80e0b28fde87
|
||||
//192.168.1.232:8081/repository/npm-hosted/:username=admin
|
||||
//192.168.1.232:8081/repository/npm-hosted/:_password=MjUxMzI3NDEyMjg3
|
|
@ -1,4 +1,8 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
const nextConfig = {
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "tuya-admin-front-2",
|
||||
"name": "tuya-admin-front",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -9,17 +9,44 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "20.5.3",
|
||||
"@types/react": "18.2.21",
|
||||
"@sdt/sdt-ui-kit": "^0.1.17",
|
||||
"@tanstack/react-query": "^4.33.0",
|
||||
"@tanstack/react-query-devtools": "^4.33.0",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/node": "20.5.1",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "18.2.20",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"autoprefixer": "10.4.15",
|
||||
"axios": "^1.4.0",
|
||||
"eslint": "8.47.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "13.4.19",
|
||||
"postcss": "8.4.28",
|
||||
"react": "18.2.0",
|
||||
"qs": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-cookie": "^6.1.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-modal": "^3.16.1",
|
||||
"styled-components": "^6.0.7",
|
||||
"tailwindcss": "3.3.3",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.1.6",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"prettier": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
ACCESS_TOKEN_NAME,
|
||||
getCookie,
|
||||
REFREASH_TOKEN_NAME,
|
||||
removeCookie,
|
||||
} from '../utils/cookies';
|
||||
import { setToken } from '../utils/setToken';
|
||||
import axios, { AxiosRequestConfig, Method } from 'axios';
|
||||
import { silentSignInApi } from './oauth';
|
||||
|
||||
export interface CallApiType {
|
||||
url: string;
|
||||
method: Method;
|
||||
data?: any;
|
||||
contentType?: HttpContentType;
|
||||
config?: AxiosRequestConfig;
|
||||
instanceType?: InstanceType;
|
||||
noToken?: boolean;
|
||||
}
|
||||
|
||||
type InstanceType = 'Default' | 'Relay' | 'Refresh';
|
||||
|
||||
export type HttpContentType =
|
||||
| 'application/json'
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'multipart/form-data'
|
||||
| 'form-data';
|
||||
|
||||
const createInstance = (config?: AxiosRequestConfig) => {
|
||||
const defaultTimeoutMillSec = 60 * 1000; // 60초
|
||||
return axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_BASE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: defaultTimeoutMillSec,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultInstance = createInstance();
|
||||
|
||||
export const fetchApi = ({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
contentType = 'application/json',
|
||||
config,
|
||||
instanceType = 'Default',
|
||||
noToken,
|
||||
}: CallApiType) => {
|
||||
/**
|
||||
* Api request interceptor
|
||||
*/
|
||||
defaultInstance.interceptors.request.use(
|
||||
(req: any) => {
|
||||
// console.log('######## req interceptors', req);
|
||||
const SDT_AT = getCookie(ACCESS_TOKEN_NAME);
|
||||
|
||||
if (noToken) {
|
||||
delete req.headers?.Authorization;
|
||||
} else if (SDT_AT && req.headers) {
|
||||
req.headers = {
|
||||
...req.headers,
|
||||
Authorization: `Bearer ${SDT_AT}`,
|
||||
};
|
||||
}
|
||||
|
||||
return req;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Api response interceptor
|
||||
*/
|
||||
defaultInstance.interceptors.response.use(
|
||||
(req) => {
|
||||
return req;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
const {
|
||||
response: { status, ...response },
|
||||
config,
|
||||
} = error;
|
||||
|
||||
if (status === 401 && config.url !== 'oauth/token') {
|
||||
silentSignInApi().then((apiRes) => {
|
||||
const { status } = apiRes;
|
||||
const originalRequest = config;
|
||||
if (status === 200) {
|
||||
const { accessToken } = apiRes.data;
|
||||
setToken(apiRes.data, true);
|
||||
|
||||
defaultInstance.defaults.headers.common[
|
||||
'Authorization'
|
||||
] = `Bearer ${accessToken}`;
|
||||
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
return fetchApi({ ...originalRequest });
|
||||
} else {
|
||||
removeCookie(REFREASH_TOKEN_NAME);
|
||||
removeCookie(ACCESS_TOKEN_NAME);
|
||||
return (window.location.href = `${process.env.NEXT_PUBLIC_CLOUD_CONSOLE_PATH}/login?redirect=${window.location.href}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
return defaultInstance({
|
||||
url: url,
|
||||
method: method,
|
||||
data: data || {},
|
||||
params: method === 'GET' || method === 'get' ? data : {},
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
},
|
||||
...config,
|
||||
}).catch((error) => {
|
||||
console.error(
|
||||
`api error status: ${error.response?.code || ''}, message: ${
|
||||
error.response?.message || ''
|
||||
}`,
|
||||
);
|
||||
return (
|
||||
error.response || {
|
||||
data: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
interface DefaultApiResponse<T> {
|
||||
status: number;
|
||||
message?: string;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
}
|
||||
export const afterAxios = (res: DefaultApiResponse<any>) => {
|
||||
if (res.status < 400) {
|
||||
return res.data;
|
||||
} else {
|
||||
const err = new Error();
|
||||
(err as any).response = res;
|
||||
throw err;
|
||||
}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './oauthApi';
|
|
@ -0,0 +1,94 @@
|
|||
import { getCookie, REFREASH_TOKEN_NAME } from '../../utils/cookies';
|
||||
import qs from 'qs';
|
||||
import { fetchApi, afterAxios } from '../config';
|
||||
|
||||
export interface SignInRequestType {
|
||||
email: string;
|
||||
password: string;
|
||||
refreshToken?: string;
|
||||
customerCode?: string;
|
||||
}
|
||||
|
||||
export interface SignInResponseType {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export const signInApi = async (req: SignInRequestType) => {
|
||||
// const params = new URLSearchParams();
|
||||
const params = {
|
||||
...req,
|
||||
grantType: 'password',
|
||||
};
|
||||
|
||||
return await fetchApi({
|
||||
url: `/oauth/token`,
|
||||
method: 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
data: qs.stringify(params),
|
||||
noToken: true,
|
||||
});
|
||||
};
|
||||
|
||||
export interface RefreshTokenRequestType {
|
||||
refreshToken: string;
|
||||
grantType: 'refresh_token';
|
||||
}
|
||||
|
||||
export const silentSignInApi = () => {
|
||||
const refreshToken = getCookie(REFREASH_TOKEN_NAME);
|
||||
|
||||
if (refreshToken) {
|
||||
const params = {
|
||||
grantType: 'refresh_token',
|
||||
refreshToken,
|
||||
};
|
||||
|
||||
return fetchApi({
|
||||
url: `/oauth/token`,
|
||||
method: 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
data: params,
|
||||
});
|
||||
} else {
|
||||
throw Error('no refresh Token');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*/
|
||||
export interface SignUpApiRequestType {
|
||||
email: string;
|
||||
password: string;
|
||||
certNumber: string;
|
||||
organizationName: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const signUpApi = (req: SignUpApiRequestType) => {
|
||||
return fetchApi({
|
||||
url: `/sign-up`,
|
||||
method: 'POST',
|
||||
data: { ...req },
|
||||
noToken: true,
|
||||
}).then(afterAxios);
|
||||
};
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*/
|
||||
export interface PostPasswordApiRequestType {
|
||||
password: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export const postPasswordApi = (req: PostPasswordApiRequestType) => {
|
||||
return fetchApi({
|
||||
method: 'POST',
|
||||
url: 'oauth/change-password',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
data: qs.stringify(req),
|
||||
}).then(afterAxios);
|
||||
};
|
|
@ -2,20 +2,6 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
import './globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
"use client";
|
||||
import { Button, TextInput } from "@sdt/sdt-ui-kit";
|
||||
import Link from "next/link";
|
||||
import useLogin from "../hooks/useLogin";
|
||||
import { emailRegex, passwordRegex } from "@/utils/reg";
|
||||
|
||||
interface LoginPropsType {}
|
||||
|
||||
function Login({}: LoginPropsType) {
|
||||
const {
|
||||
hookForm: { handleSubmit, register, formState },
|
||||
} = useLogin();
|
||||
return (
|
||||
<div className="flex justify-center items-center w-full h-screen bg-cover bg-center">
|
||||
<div className="w-[37.5rem] shadow-md border rounded-[16px] px-10 sm:px-14 lg:px-5 py-7 sm:py-9 flex flex-col justify-center">
|
||||
<h1 className="font-title-32 text-center">쿠폰 관리 시스템</h1>
|
||||
<form onSubmit={handleSubmit((data) => console.log(data))}>
|
||||
<div className="mt-5 flex flex-col">
|
||||
<TextInput
|
||||
{...register("email", {
|
||||
required: "이메일을 입력하세요.",
|
||||
pattern: {
|
||||
value: emailRegex,
|
||||
message: "올바른 이메일을 입력하세요.",
|
||||
},
|
||||
})}
|
||||
error={!!formState.errors.email}
|
||||
message={(formState.errors.email?.message as string) ?? ""}
|
||||
maxLength={40}
|
||||
type="text"
|
||||
className={`rounded-t-[6px] text-[20px] w-full`}
|
||||
placeholder="이메일 주소"
|
||||
/>
|
||||
</div>
|
||||
ㄹㅁㄴ어래ㅑㅓㅐ
|
||||
<div className="flex flex-col">
|
||||
<TextInput
|
||||
{...register("password", {
|
||||
required: "비밀번호를 다시 입력하세요.",
|
||||
pattern: {
|
||||
value: passwordRegex,
|
||||
message:
|
||||
"8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.",
|
||||
},
|
||||
})}
|
||||
error={!!formState.errors.password?.message}
|
||||
message={(formState.errors.password?.message as string) ?? ""}
|
||||
maxLength={16}
|
||||
type="password"
|
||||
className={`rounded-b-[6px] text-[20px] w-full border-t-none`}
|
||||
placeholder="비밀번호"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<Button
|
||||
type="submit"
|
||||
className={`mt-8 text-[20px] font-semibold w-full bg-main`}
|
||||
backgroundColor="bg-teal-500"
|
||||
>
|
||||
로그인
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="w-full mt-2 flex justify-center">
|
||||
<span className="mr-3 text-[#9e9e9e]">비밀번호를 잊으셨나요?</span>
|
||||
<Link href={"/reset-password"} className="text-teal-500">
|
||||
비밀번호 재설정
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Login';
|
|
@ -0,0 +1,8 @@
|
|||
import { useForm } from 'react-hook-form';
|
||||
|
||||
function useLogin() {
|
||||
const hookForm = useForm();
|
||||
return { hookForm };
|
||||
}
|
||||
|
||||
export default useLogin;
|
|
@ -0,0 +1,10 @@
|
|||
"use client";
|
||||
import StyledComponentsRegistry from "@/components/__common/StyledComponentProvider/StyledComponentProvider";
|
||||
|
||||
export default function LoginLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <StyledComponentsRegistry>{children}</StyledComponentsRegistry>;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import Login from './Login/Login';
|
||||
|
||||
function LoginPage() {
|
||||
return (
|
||||
<Login />
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
|
@ -0,0 +1,69 @@
|
|||
import Button from '@sdt/sdt-ui-kit/components/Button';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
export interface AlertPropsType {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
body?: string;
|
||||
onClose?: Function;
|
||||
}
|
||||
const DEFAULT_MODAL_STYLE: ReactModal.Styles = {
|
||||
overlay: {
|
||||
outline: 'none',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0 ,0, 0, 0.7)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 50,
|
||||
},
|
||||
content: {
|
||||
borderRadius: '50%',
|
||||
width: '100%',
|
||||
outline: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
function Alert({ open, onClose, body, title }: AlertPropsType) {
|
||||
const titleText = title?.split('\n').map((line, idx) => {
|
||||
return <p key={idx}>{line}</p>;
|
||||
});
|
||||
const contentText = body?.split('\n').map((line, idx) => {
|
||||
return <p key={idx}>{line}</p>;
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={open}
|
||||
style={DEFAULT_MODAL_STYLE}
|
||||
ariaHideApp={false}
|
||||
className={'z-40'}
|
||||
>
|
||||
<div className="w-[520px] mx-auto box-border rounded-lg bg-white">
|
||||
<div className="px-8 py-6 box-border">
|
||||
<h1 className="text-[22px] text-[#161616] font-medium mb-3">
|
||||
{titleText}
|
||||
</h1>
|
||||
{body && <div className="text-525252 text-base">{contentText}</div>}
|
||||
</div>
|
||||
<div className="flex justify-end items-center w-full gap-3 px-6 pb-6">
|
||||
<Button
|
||||
className="w-[94px] h-[42px]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClose && onClose();
|
||||
}}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default Alert;
|
|
@ -0,0 +1,37 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAlertStore } from '@/stores';
|
||||
|
||||
export function useAlert() {
|
||||
const {
|
||||
alertOpen,
|
||||
alertContent,
|
||||
setAlertOpen,
|
||||
setAlertClose,
|
||||
alertCallback,
|
||||
} = useAlertStore();
|
||||
const router = useRouter();
|
||||
/**
|
||||
* Alert Close Handler
|
||||
*/
|
||||
const onCloseAlert = useCallback(() => {
|
||||
if (alertOpen) {
|
||||
setAlertClose();
|
||||
}
|
||||
if (alertCallback) alertCallback();
|
||||
}, [alertCallback, alertOpen, setAlertClose]);
|
||||
|
||||
/**
|
||||
* 라우터 변경 시 Alert Close
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (alertOpen) {
|
||||
setAlertClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router]);
|
||||
|
||||
return { alertOpen, alertContent, setAlertOpen, setAlertClose, onCloseAlert };
|
||||
}
|
||||
|
||||
export default useAlert;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Alert';
|
|
@ -0,0 +1,28 @@
|
|||
import Head from 'next/head';
|
||||
import useAlert from '../Alert/hooks/useAlert';
|
||||
import Alert from '../Alert';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactElement }) {
|
||||
const { alertOpen, alertContent, onCloseAlert } = useAlert();
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-hidden">
|
||||
<Head>
|
||||
<title>BlokWorks</title>
|
||||
<meta name="description" content="BlokWorks" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="flex pt-14">
|
||||
<main className={`w-full p-12`}>{children}</main>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
open={alertOpen}
|
||||
onClose={onCloseAlert}
|
||||
title={alertContent.title}
|
||||
body={alertContent.body}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export const BLOKWORKS_LNB_MENU = [
|
||||
{
|
||||
title: '프로젝트 생성',
|
||||
path: '/project/add',
|
||||
},
|
||||
{
|
||||
title: '프로젝트 목록',
|
||||
path: '/project',
|
||||
},
|
||||
];
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Layout';
|
|
@ -0,0 +1,28 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { useServerInsertedHTML } from "next/navigation";
|
||||
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
|
||||
|
||||
export default function StyledComponentsRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// Only create stylesheet once with lazy initial state
|
||||
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
|
||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styles = styledComponentsStyleSheet.getStyleElement();
|
||||
styledComponentsStyleSheet.instance.clearTag();
|
||||
return styles;
|
||||
});
|
||||
|
||||
if (typeof window !== "undefined") return <>{children}</>;
|
||||
|
||||
return (
|
||||
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
||||
{children}
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default } from './StyledComponentProvider';
|
|
@ -0,0 +1,2 @@
|
|||
export * from './useAuthStore';
|
||||
export * from './useAlertStore';
|
|
@ -0,0 +1,61 @@
|
|||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
export interface AlertContentType {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface AlertOpenPayloadType extends Partial<AlertContentType> {
|
||||
alertCallback?: Function;
|
||||
}
|
||||
/**
|
||||
* Auth State Type
|
||||
*/
|
||||
export interface AlertStateType {
|
||||
alertOpen: boolean;
|
||||
alertContent: AlertContentType;
|
||||
alertCallback: Function | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert Action Type
|
||||
*/
|
||||
|
||||
export interface AlertActionType {
|
||||
setAlertOpen: (by: AlertOpenPayloadType) => void;
|
||||
setAlertClose: () => void;
|
||||
alertCallback: Function | null;
|
||||
}
|
||||
|
||||
export type AlertStoreType = AlertStateType & AlertActionType;
|
||||
|
||||
export const useAlertStore = create<AlertStoreType>()(
|
||||
devtools(
|
||||
(set): AlertStoreType => ({
|
||||
alertOpen: false,
|
||||
alertContent: {
|
||||
title: '',
|
||||
body: '',
|
||||
},
|
||||
alertCallback: null,
|
||||
|
||||
setAlertOpen: (by: AlertOpenPayloadType) =>
|
||||
set(() => ({
|
||||
alertOpen: true,
|
||||
alertContent: { title: by.title ?? '', body: by.body ?? '' },
|
||||
alertCallback: by.alertCallback ? by.alertCallback : null,
|
||||
})),
|
||||
setAlertClose: () =>
|
||||
set(() => ({
|
||||
alertOpen: false,
|
||||
alertContent: {
|
||||
title: '',
|
||||
body: '',
|
||||
},
|
||||
alertCallback: null,
|
||||
})),
|
||||
}),
|
||||
{ name: 'alert' },
|
||||
),
|
||||
);
|
|
@ -0,0 +1,53 @@
|
|||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
export interface ProfileType {
|
||||
id: string;
|
||||
sub: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth State Type
|
||||
*/
|
||||
export interface AuthStateType {
|
||||
loggedIn: boolean;
|
||||
email: string | null;
|
||||
profile: ProfileType | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth Action Type
|
||||
*/
|
||||
|
||||
export interface AuthActionType {
|
||||
setSignIn: (by: ProfileType) => void;
|
||||
setEmail: (by: string) => void;
|
||||
setSignOut: () => void;
|
||||
}
|
||||
|
||||
export type AuthStoreType = AuthStateType & AuthActionType;
|
||||
|
||||
export const useAuthStore = create<AuthStoreType>()(
|
||||
devtools(
|
||||
(set): AuthStoreType => ({
|
||||
loggedIn: false,
|
||||
email: null,
|
||||
profile: null,
|
||||
|
||||
setSignIn: (by: ProfileType) =>
|
||||
set(() => ({
|
||||
profile: { ...by },
|
||||
loggedIn: true,
|
||||
})),
|
||||
setEmail: (by: string) => set(() => ({ email: by })),
|
||||
setSignOut: () =>
|
||||
set(() => ({
|
||||
loggedIn: false,
|
||||
email: null,
|
||||
})),
|
||||
}),
|
||||
{ name: 'auth' },
|
||||
),
|
||||
);
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Tailwind Status Color
|
||||
*/
|
||||
export const TAILWIND_STATUS_COLORS: any = {
|
||||
primary: {
|
||||
DEFAULT: '#007BFF',
|
||||
hover: '#0069D9',
|
||||
disable: '#58AAFF',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#6C757D',
|
||||
hover: '#5A6268',
|
||||
disable: '#B0B6BA',
|
||||
},
|
||||
success: {
|
||||
DEFAULT: '#28A745',
|
||||
hover: '#218838',
|
||||
disable: '#74C686',
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DC3545',
|
||||
hover: '#C82333',
|
||||
disable: '#E97B86',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: '#FFC107',
|
||||
hover: '#E0A800',
|
||||
disable: '#FFD75E',
|
||||
},
|
||||
info: {
|
||||
DEFAULT: '#17A2B8',
|
||||
hover: '#138496',
|
||||
disable: '#67C3D0',
|
||||
},
|
||||
light: {
|
||||
DEFAULT: '#F8F9FA',
|
||||
hover: '#E2E6EA',
|
||||
disable: '#FAFCFC',
|
||||
},
|
||||
dark: {
|
||||
DEFAULT: '#343A40',
|
||||
hover: '#23272B',
|
||||
disable: '#7A7E83',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Tailwind Main Color
|
||||
*/
|
||||
export const TAILWIND_MAIN_COLOR: any = {
|
||||
main: {
|
||||
DEFAULT: '#14B8A6',
|
||||
50: '#FAFDF0',
|
||||
100: `#CCFBF1`,
|
||||
200: '#99F6E4',
|
||||
300: '#5EEAD4',
|
||||
400: '#2DD4BF',
|
||||
500: '#14B8A6',
|
||||
600: '#0D9488',
|
||||
700: '#0F766E',
|
||||
800: '#115E59',
|
||||
900: '#134E4A',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Tailwind Etc Color
|
||||
*/
|
||||
export const TAILWIND_ETC_COLOR: any = {
|
||||
'main-text': '#000000',
|
||||
'sub-text': '#707070',
|
||||
'divider-01': '#DEDEDE',
|
||||
'bg-01': '#EFEFEF',
|
||||
'bg-02': '#F8F8F8',
|
||||
};
|
||||
|
||||
/**
|
||||
* Tailwind hover color
|
||||
*/
|
||||
export const TAILWIND_HOVER_COLOR: any = {
|
||||
'.opacity-hover': {
|
||||
opacity: '0.85',
|
||||
},
|
||||
'.opacity-focus': {
|
||||
opacity: '0.75',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Brand Color
|
||||
*/
|
||||
export const TAILWIND_BRAND_COLOR: any = {
|
||||
'251327': '#251327',
|
||||
FFF4E0: '#FFF4E0',
|
||||
F26419: '#F26419',
|
||||
FFD222: '#FFD222',
|
||||
'4ECE89': '#4ECE89',
|
||||
'08757B': '#08757B',
|
||||
'6DA8D2': '#6DA8D2',
|
||||
'6667AB': '#6667AB',
|
||||
'3D77E2': '#3D77E2',
|
||||
};
|
||||
|
||||
/**
|
||||
* All Color Set
|
||||
*/
|
||||
export const SDT_TAILWIND_COLORS: any = {
|
||||
...TAILWIND_STATUS_COLORS,
|
||||
...TAILWIND_MAIN_COLOR,
|
||||
...TAILWIND_ETC_COLOR,
|
||||
...TAILWIND_HOVER_COLOR,
|
||||
...TAILWIND_BRAND_COLOR,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
|
@ -0,0 +1,35 @@
|
|||
import Modal from 'react-modal';
|
||||
/**
|
||||
* 기본 Input style
|
||||
*/
|
||||
export const defaultInputStyle =
|
||||
'border border-gray-200 bg-zinc-10 rounded-lg h-14 p-2 mt-2 focus:outline-none focus:border-blue-400';
|
||||
|
||||
/**
|
||||
* React modal default style
|
||||
*/
|
||||
export const DEFAULT_MODAL_STYLE: Modal.Styles = {
|
||||
overlay: {
|
||||
outline: 'none',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0 ,0, 0, 0.7)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 50,
|
||||
},
|
||||
content: {
|
||||
borderRadius: '50%',
|
||||
width: '100%',
|
||||
outline: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Table Item border, text-center
|
||||
*/
|
||||
export const TableItemClassName = 'table-auto border-collapse py-3';
|
|
@ -0,0 +1,134 @@
|
|||
import { css } from 'styled-components';
|
||||
|
||||
export type FontKeyType =
|
||||
| 'font-title-40'
|
||||
| 'font-title-32'
|
||||
| 'font-title-28'
|
||||
| 'font-title-24'
|
||||
| 'font-title-20'
|
||||
| 'font-title-16'
|
||||
| 'font-subtitle-14'
|
||||
| 'font-body-22'
|
||||
| 'font-body-20'
|
||||
| 'font-body-18'
|
||||
| 'font-body-16'
|
||||
| 'font-body-14'
|
||||
| 'font-body-12';
|
||||
|
||||
/**
|
||||
* Tailwind typograpy
|
||||
*/
|
||||
|
||||
export const TAILWIND_TYPOGRAPY = {
|
||||
'.font-title-40': {
|
||||
fontSize: '2.5rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-title-32': {
|
||||
fontSize: '2rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-title-28': {
|
||||
fontSize: '1.75rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-title-24': {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-title-20': {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-title-16': {
|
||||
fontSize: '1rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-subtitle-14': {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '500',
|
||||
},
|
||||
'.font-body-22': {
|
||||
fontSize: '1.375rem',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'.font-body-20': {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'.font-body-18': {
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'.font-body-16': {
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'.font-body-14': {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'.font-body-12': {
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: '400',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Styled-Component typograpy
|
||||
*/
|
||||
|
||||
export const getTypograpy = {
|
||||
'font-title-40': css`
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-title-32': css`
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-title-28': css`
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-title-24': css`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-title-20': css`
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-title-16': css`
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-subtitle-14': css`
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
`,
|
||||
'font-body-22': css`
|
||||
font-size: 1.375rem;
|
||||
font-weight: 400;
|
||||
`,
|
||||
'font-body-20': css`
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
`,
|
||||
'font-body-18': css`
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
`,
|
||||
'font-body-16': css`
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
`,
|
||||
'font-body-14': css`
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
`,
|
||||
'font-body-12': css`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { Cookies } from 'react-cookie';
|
||||
import { CookieSetOptions } from 'universal-cookie';
|
||||
|
||||
const cookies = new Cookies();
|
||||
|
||||
export const REFREASH_TOKEN_NAME = 'SDT_RT';
|
||||
export const ACCESS_TOKEN_NAME = 'SDT_AT';
|
||||
|
||||
export const setCookie = (
|
||||
name: string,
|
||||
value: string,
|
||||
option: CookieSetOptions | undefined,
|
||||
) => {
|
||||
return cookies.set(name, value, { ...option });
|
||||
};
|
||||
|
||||
export const getCookie = (name: string) => {
|
||||
return cookies.get(name);
|
||||
};
|
||||
|
||||
export const removeCookie = (name: string) => {
|
||||
return cookies.remove(name, {
|
||||
path: '/',
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export * from './cookies';
|
||||
export * from './setToken';
|
||||
export * from './reg';
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* 파일 크기에 따른 단위 변환
|
||||
*/
|
||||
export const handleSizeConvert = (size: number) => {
|
||||
if (!size) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let convertedSize = size;
|
||||
let unit = 'KB';
|
||||
|
||||
if (convertedSize >= 1024 * 1024 * 1024) {
|
||||
convertedSize = convertedSize / (1024 * 1024 * 1024);
|
||||
unit = 'GB';
|
||||
} else if (convertedSize >= 1024 * 1024) {
|
||||
convertedSize = convertedSize / (1024 * 1024);
|
||||
unit = 'MB';
|
||||
} else if (convertedSize >= 1024) {
|
||||
convertedSize = convertedSize / 1024;
|
||||
}
|
||||
|
||||
return `${convertedSize.toFixed(2)} ${unit}`;
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 특수문자 제거
|
||||
*/
|
||||
|
||||
export function regExp(str: string) {
|
||||
var reg = /[\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/gi;
|
||||
//특수문자 검증
|
||||
if (reg.test(str)) {
|
||||
//특수문자 제거후 리턴
|
||||
return str.replace(reg, '');
|
||||
} else {
|
||||
//특수문자가 없으므로 본래 문자 리턴
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 정규식
|
||||
*/
|
||||
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
|
||||
/**
|
||||
* 비밀번호
|
||||
*/
|
||||
export const passwordRegex =
|
||||
/^(?=.*\d)(?=.*[!@#$%^&*])(?=.*[a-z])(?=.*[A-Z]).{8,16}$/;
|
||||
|
||||
/**
|
||||
* 영문, 한글 이름
|
||||
*/
|
||||
export const englishKoreanRegex = /^[a-zA-Z\uAC00-\uD7A3 ]+$/;
|
||||
|
||||
/**
|
||||
* 조직 이름
|
||||
*/
|
||||
export const organizationNameRegex = /^[a-zA-Z\uAC00-\uD7A3-_.\s]+$/;
|
||||
|
||||
/**
|
||||
* 장비 이름 정규식
|
||||
* 영문, 한글, 숫자, 특수문자 입력 가능
|
||||
*/
|
||||
export const engKorNumSymbolRegex =
|
||||
/^[a-zA-Z가-힣ㄱ-ㅎ0-9!@#$%^&*()\-_=+[\]{}|;:'",.<>?/~`]+$/;
|
||||
|
||||
/**
|
||||
* 영문, 한글, 숫자, 특수문자, 스페이스 가능
|
||||
*/
|
||||
export const engKorNumSymbolSpaceRegex =
|
||||
/^[a-zA-Z가-힣ㄱ-ㅎ0-9!@#$%^&*()\-_=+[\]{}|;:'",.<>?/~`\s]+$/;
|
|
@ -0,0 +1,33 @@
|
|||
import { SignInResponseType } from '../api/oauth';
|
||||
import { ACCESS_TOKEN_NAME, REFREASH_TOKEN_NAME, setCookie } from './cookies';
|
||||
|
||||
export function setToken(
|
||||
loginResponse: SignInResponseType,
|
||||
isRefresh?: boolean,
|
||||
) {
|
||||
const { accessToken, refreshToken } = loginResponse;
|
||||
|
||||
const expires = new Date();
|
||||
expires.setDate(Date.now() + 1000 * 60 * 60 * 24);
|
||||
|
||||
setCookie(ACCESS_TOKEN_NAME, accessToken, {
|
||||
path: '/',
|
||||
expires,
|
||||
});
|
||||
|
||||
if (!isRefresh) {
|
||||
setCookie(REFREASH_TOKEN_NAME, refreshToken, {
|
||||
path: '/',
|
||||
expires,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token decode
|
||||
*/
|
||||
export const decodeToken = (token: string) => {
|
||||
const base64Payload = token.split('.')[1];
|
||||
const payload = Buffer.from(base64Payload, 'base64');
|
||||
return JSON.parse(payload.toString());
|
||||
};
|
|
@ -1,4 +1,7 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
import { SDT_TAILWIND_COLORS, TAILWIND_HOVER_COLOR } from './src/styles/colors';
|
||||
import { TAILWIND_TYPOGRAPY } from './src/styles/typography';
|
||||
import type { Config } from 'tailwindcss';
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
|
@ -13,8 +16,15 @@ const config: Config = {
|
|||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
colors: {
|
||||
...SDT_TAILWIND_COLORS,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
export default config
|
||||
plugins: [
|
||||
plugin(function ({ addComponents }: { addComponents: any }) {
|
||||
addComponents({ ...TAILWIND_TYPOGRAPY, ...TAILWIND_HOVER_COLOR });
|
||||
}),
|
||||
],
|
||||
};
|
||||
export default config;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
@ -19,9 +23,19 @@
|
|||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue