Compare commits
10 Commits
9f77ca2dfc
...
2096a56164
Author | SHA1 | Date |
---|---|---|
hyojin kim | 2096a56164 | |
hyojin | 9e9dc5494a | |
hyojin | 541936ad92 | |
hyojin | 5d240595e9 | |
hyojin | b730fbca93 | |
hyojin | bda3dfdbf4 | |
hyojin | 595cbebe15 | |
hyojin | c3fe093f5f | |
hyojin | 3a639e9a38 | |
skyun92 | 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
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
40
README.md
40
README.md
|
@ -1,34 +1,26 @@
|
||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
# GSEPS Control Center
|
||||||
|
|
||||||
## Getting Started
|
## 프로젝트 소개
|
||||||
|
|
||||||
First, run the development server:
|
- 카메라에서 수집된 사진 이미지를 inference 한 결과 이미지와 결과 데이터를 히스토그램 차트로 보여주는 Control Center 화면
|
||||||
|
|
||||||
```bash
|
### 배포 주소
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
- [GSEPS Control Center](http://13.209.39.139:31985/dashboard)
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
### 기술 스택
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
- react
|
||||||
|
- next.js
|
||||||
|
- typescript
|
||||||
|
- tailwindCSS
|
||||||
|
- nivo chart
|
||||||
|
|
||||||
## Learn More
|
### 화면 구성
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
<img src="./public/readmeImage.png">
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
### 주요 기능
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
- 카메라에서 수집된 사진 원본 이미지, inference 결과 이미지 제공
|
||||||
|
- inference 결과 데이터에 따른 히스토그램 차트 제공
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies based on the preferred package manager (Yarn 2)
|
||||||
|
COPY package.json yarn.lock* ./
|
||||||
|
RUN yarn install --immutable
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
|
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
|
||||||
|
# RUN yarn build
|
||||||
|
|
||||||
|
# If using npm comment out above and use below instead
|
||||||
|
# RUN npm run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# ENV NODE_ENV production
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||||
|
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
|
@ -1,4 +1,20 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {}
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
output: 'standalone',
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: '13.209.39.139',
|
||||||
|
port: '31192',
|
||||||
|
pathname: '/api/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
compiler: {
|
||||||
|
styledComponents: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tuya-admin-front-2",
|
"name": "gesps-front",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
@ -9,17 +9,53 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "20.5.3",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@types/react": "18.2.21",
|
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||||
"@types/react-dom": "18.2.7",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@nivo/bar": "^0.83.0",
|
||||||
|
"@nivo/core": "^0.83.0",
|
||||||
|
"@nivo/line": "^0.83.0",
|
||||||
|
"@sdt/sdt-ui-kit": "^0.1.20",
|
||||||
|
"@tanstack/react-query": "^4.33.0",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"cookies-next": "^3.0.0",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
"eslint": "8.47.0",
|
"eslint": "8.47.0",
|
||||||
"eslint-config-next": "13.4.19",
|
"eslint-config-next": "13.4.19",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"next": "13.4.19",
|
"next": "13.4.19",
|
||||||
"postcss": "8.4.28",
|
"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-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.45.4",
|
||||||
|
"react-modal": "^3.16.1",
|
||||||
|
"styled-components": "^6.0.7",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@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",
|
||||||
|
"@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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 387 KiB |
|
@ -0,0 +1,152 @@
|
||||||
|
import { ACCESS_TOKEN_NAME, REFREASH_TOKEN_NAME } from '@/utils';
|
||||||
|
import { deleteCookie, getCookie } from 'cookies-next';
|
||||||
|
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);
|
||||||
|
// todo fix
|
||||||
|
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 {
|
||||||
|
deleteCookie(REFREASH_TOKEN_NAME);
|
||||||
|
deleteCookie(ACCESS_TOKEN_NAME);
|
||||||
|
return (window.location.href = `/login`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,12 @@
|
||||||
|
import { ResultDataType } from '@/app/dashboard/types';
|
||||||
|
import { afterAxios, fetchApi } from '../config';
|
||||||
|
|
||||||
|
export interface GetResultApiRequestType {}
|
||||||
|
|
||||||
|
export type GetResultApiResponseType = ResultDataType;
|
||||||
|
|
||||||
|
export const getResultApi = (): Promise<GetResultApiResponseType> =>
|
||||||
|
fetchApi({
|
||||||
|
url: 'blokworks/v1/transport/inference/results',
|
||||||
|
method: 'GET',
|
||||||
|
}).then(afterAxios);
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './oauthApi';
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { REFREASH_TOKEN_NAME } from '@/utils';
|
||||||
|
import qs from 'qs';
|
||||||
|
import { fetchApi, afterAxios } from '../config';
|
||||||
|
import { getCookie } from 'cookies-next';
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ResponsiveBar } from '@nivo/bar';
|
||||||
|
|
||||||
|
function BarChart({ data }: any) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-[calc(100vh-300px)]">
|
||||||
|
<ResponsiveBar
|
||||||
|
data={data}
|
||||||
|
keys={['count']}
|
||||||
|
indexBy="range"
|
||||||
|
margin={{ top: 50, right: 20, bottom: 50, left: 60 }}
|
||||||
|
padding={0.3}
|
||||||
|
valueScale={{ type: 'linear' }}
|
||||||
|
indexScale={{ type: 'band', round: true }}
|
||||||
|
colors={{ scheme: 'nivo' }}
|
||||||
|
borderColor={{
|
||||||
|
from: 'color',
|
||||||
|
modifiers: [['darker', 1.6]],
|
||||||
|
}}
|
||||||
|
// maxValue={10}
|
||||||
|
axisTop={null}
|
||||||
|
axisRight={null}
|
||||||
|
axisBottom={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
}}
|
||||||
|
enableLabel={false}
|
||||||
|
labelTextColor={{
|
||||||
|
from: 'color',
|
||||||
|
modifiers: [['darker', 1.6]],
|
||||||
|
}}
|
||||||
|
legends={[]}
|
||||||
|
role="application"
|
||||||
|
ariaLabel="Nivo bar chart demo"
|
||||||
|
barAriaLabel={(e) =>
|
||||||
|
e.id + ': ' + e.formattedValue + ' in country: ' + e.indexValue
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BarChart;
|
|
@ -0,0 +1,77 @@
|
||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import BarChart from './BarChart';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import useDashboard from '../hooks/useDashboard';
|
||||||
|
import { Button } from '@sdt/sdt-ui-kit';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faArrowsRotate } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const { getResult, convertChartData } = useDashboard();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className=" flex items-center h-[80px] px-12 bg-white border-b border-gray-300">
|
||||||
|
<Image src={'/logo.png'} alt="logo" width={100} height={50} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-8 p-12 h-[calc(100vh-150px)]">
|
||||||
|
{getResult.data && (
|
||||||
|
<>
|
||||||
|
<div className="mt-20 col-span-2">
|
||||||
|
<h2 className="text-lg font-semibold text-center">Input Image</h2>
|
||||||
|
<div className="relative w-full h-[calc(50vh-250px)] mt-2 mb-12">
|
||||||
|
<img
|
||||||
|
src={getResult.data?.origin.imageUrl}
|
||||||
|
alt="origin image"
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-center">
|
||||||
|
Inference Result Image
|
||||||
|
</h2>
|
||||||
|
<div className="relative w-full h-[calc(50vh-250px)] mt-2">
|
||||||
|
<img
|
||||||
|
src={getResult.data?.latest.imageUrl}
|
||||||
|
alt="inference image"
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-6">
|
||||||
|
<div className="text-right text-lg font-semibold">
|
||||||
|
촬영일시 :{' '}
|
||||||
|
{format(
|
||||||
|
getResult.data?.latest.timestamp,
|
||||||
|
'yyyy-MM-dd HH:mm:ss',
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => queryClient.invalidateQueries(['result'])}
|
||||||
|
backgroundColor="#fafafa"
|
||||||
|
className="ml-2"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowsRotate}
|
||||||
|
style={{ color: '#7a7a7a' }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BarChart data={convertChartData} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-[#7a7a7a]">
|
||||||
|
Copyright 2023 SDT Inc. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
|
@ -0,0 +1,29 @@
|
||||||
|
export const DUMMY_DATA = {
|
||||||
|
latest: {
|
||||||
|
imageUrl:
|
||||||
|
'http://13.209.39.139:31192/api/v1/buckets/gseps-test-a/objects/download?prefix=MjAyMzA5MDctMTEyNzU2LmpwZw==&version_id=null',
|
||||||
|
timestamp: 1694073245526,
|
||||||
|
particleSizeRatio: [
|
||||||
|
{
|
||||||
|
range: 186,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range: 102.5,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range: 79.5,
|
||||||
|
count: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range: 313.5,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range: 146.5,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { getResultApi } from '@/api/dashboard/resultApi';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
function useDashboard() {
|
||||||
|
const getResult = useQuery(['result'], () => getResultApi(), {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bar chart convert data
|
||||||
|
*/
|
||||||
|
const convertChartData = useMemo(() => {
|
||||||
|
const data = getResult.data?.latest.particleSizeRatio?.map((result, i) => {
|
||||||
|
return {
|
||||||
|
range: Number(result.range).toFixed(2),
|
||||||
|
count: result.count,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [getResult.data]);
|
||||||
|
|
||||||
|
return { getResult, convertChartData };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDashboard;
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function LoginLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
return <Dashboard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { StaticImageData } from 'next/image';
|
||||||
|
|
||||||
|
export interface ResultDataType {
|
||||||
|
origin: {
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
|
latest: {
|
||||||
|
imageUrl: string;
|
||||||
|
timestamp: number;
|
||||||
|
particleSizeRatio: { range: string; count: number }[];
|
||||||
|
};
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -2,20 +2,6 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
body {
|
||||||
color: rgb(var(--foreground-rgb));
|
color: rgb(var(--foreground-rgb));
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
|
|
|
@ -1,22 +1,37 @@
|
||||||
import './globals.css'
|
import QueryClientProvider from '@/utils/QueryClientProvider';
|
||||||
import type { Metadata } from 'next'
|
import './globals.css';
|
||||||
import { Inter } from 'next/font/google'
|
import type { Metadata } from 'next';
|
||||||
|
import StyledComponentsRegistry from '@/utils/StyledComponentProvider/StyledComponentProvider';
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
import Layout from '@/components/Layout';
|
||||||
|
import { ACCESS_TOKEN_NAME } from '@/utils';
|
||||||
|
import { defaultInstance } from '@/api/config';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Create Next App',
|
title: 'GSEPS',
|
||||||
description: 'Generated by create next app',
|
description: 'GSEPS Control Center',
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
const accessToken = cookies().get(ACCESS_TOKEN_NAME);
|
||||||
<html lang="en">
|
|
||||||
<body className={inter.className}>{children}</body>
|
if (accessToken) {
|
||||||
</html>
|
defaultInstance.defaults.headers.common.Authorization = `Bearer ${accessToken.value}`;
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<QueryClientProvider>
|
||||||
|
<StyledComponentsRegistry>
|
||||||
|
<Layout>{children}</Layout>
|
||||||
|
</StyledComponentsRegistry>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
'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">
|
||||||
|
<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'}>비밀번호 재설정</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
function useLogin() {
|
||||||
|
const hookForm = useForm();
|
||||||
|
return { hookForm };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useLogin;
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function LoginLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Login from './components/Login';
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
return <Login />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
118
src/app/page.tsx
118
src/app/page.tsx
|
@ -1,113 +1,15 @@
|
||||||
import Image from 'next/image'
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
const router = useRouter();
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
const targetRoutePath = '/dashboard';
|
||||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
|
||||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
|
||||||
Get started by editing
|
|
||||||
<code className="font-mono font-bold">src/app/page.tsx</code>
|
|
||||||
</p>
|
|
||||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
|
||||||
<a
|
|
||||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
By{' '}
|
|
||||||
<Image
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel Logo"
|
|
||||||
className="dark:invert"
|
|
||||||
width={100}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
useEffect(() => {
|
||||||
<Image
|
router.push(targetRoutePath);
|
||||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
}, [router]);
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js Logo"
|
|
||||||
width={180}
|
|
||||||
height={37}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
return <main></main>;
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Docs{' '}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Find in-depth information about Next.js features and API.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Learn{' '}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Learn about Next.js in an interactive course with quizzes!
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Templates{' '}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Explore the Next.js 13 playground.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
|
||||||
Deploy{' '}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
|
||||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,39 @@
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useAlertStore } from '@/stores';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
export function useAlert() {
|
||||||
|
const {
|
||||||
|
alertOpen,
|
||||||
|
alertContent,
|
||||||
|
setAlertOpen,
|
||||||
|
setAlertClose,
|
||||||
|
alertCallback,
|
||||||
|
} = useAlertStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return { alertOpen, alertContent, setAlertOpen, setAlertClose, onCloseAlert };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAlert;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './Alert';
|
|
@ -0,0 +1,25 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import useAlert from '../Alert/hooks/useAlert';
|
||||||
|
import Alert from '../Alert';
|
||||||
|
import useSilentAuth from '@/hooks/useSilentAuth';
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const { alertOpen, alertContent, onCloseAlert } = useAlert();
|
||||||
|
useSilentAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full overflow-hidden bg-[#fafafa]">
|
||||||
|
<div className="flex">
|
||||||
|
<main className={`w-full h-screen`}>{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,46 @@
|
||||||
|
import { useAuthStore } from '@/stores';
|
||||||
|
import { ACCESS_TOKEN_NAME, REFREASH_TOKEN_NAME } from '@/utils';
|
||||||
|
import { getCookie, deleteCookie } from 'cookies-next';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
function useSilentAuth() {
|
||||||
|
const router = useRouter();
|
||||||
|
// todo fix
|
||||||
|
const SDT_AT = getCookie(ACCESS_TOKEN_NAME) ?? '';
|
||||||
|
const { loggedIn, setSignIn, setSignOut } = useAuthStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (SDT_AT && !loggedIn) {
|
||||||
|
setSignIn();
|
||||||
|
}
|
||||||
|
}, [SDT_AT, loggedIn, setSignIn]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle Logout
|
||||||
|
*/
|
||||||
|
const handleLogout = useCallback(async () => {
|
||||||
|
deleteCookie(ACCESS_TOKEN_NAME);
|
||||||
|
deleteCookie(REFREASH_TOKEN_NAME);
|
||||||
|
setSignOut();
|
||||||
|
}, [setSignOut]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* not Server redirect
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!loggedIn &&
|
||||||
|
!SDT_AT &&
|
||||||
|
!(pathname.includes('login') || pathname.includes('reset-password'))
|
||||||
|
) {
|
||||||
|
// router.push(`/login`);
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
}, [SDT_AT, handleLogout, loggedIn, pathname, router, setSignOut]);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSilentAuth;
|
|
@ -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,38 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth State Type
|
||||||
|
*/
|
||||||
|
export interface AuthStateType {
|
||||||
|
loggedIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Action Type
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AuthActionType {
|
||||||
|
setSignIn: () => void;
|
||||||
|
setSignOut: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthStoreType = AuthStateType & AuthActionType;
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthStoreType>()(
|
||||||
|
devtools(
|
||||||
|
(set): AuthStoreType => ({
|
||||||
|
loggedIn: false,
|
||||||
|
|
||||||
|
setSignIn: () =>
|
||||||
|
set(() => ({
|
||||||
|
loggedIn: true,
|
||||||
|
})),
|
||||||
|
setSignOut: () =>
|
||||||
|
set(() => ({
|
||||||
|
loggedIn: false,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
{ 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,18 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
|
function Providers({ children }: React.PropsWithChildren) {
|
||||||
|
const [client] = React.useState(new QueryClient());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={client}>
|
||||||
|
{children}
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Providers;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './QueryClientProviider';
|
|
@ -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 './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,37 @@
|
||||||
|
import { SignInResponseType } from '../api/oauth';
|
||||||
|
import { setCookie } from 'cookies-next';
|
||||||
|
|
||||||
|
// todo fix
|
||||||
|
export const REFREASH_TOKEN_NAME = 'SDT_RT';
|
||||||
|
export const ACCESS_TOKEN_NAME = 'SDT_AT';
|
||||||
|
|
||||||
|
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 = {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
|
@ -13,8 +16,15 @@ const config: Config = {
|
||||||
'gradient-conic':
|
'gradient-conic':
|
||||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
},
|
},
|
||||||
|
colors: {
|
||||||
|
...SDT_TAILWIND_COLORS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
},
|
||||||
}
|
plugins: [
|
||||||
export default config
|
plugin(function ({ addComponents }: { addComponents: any }) {
|
||||||
|
addComponents({ ...TAILWIND_TYPOGRAPY, ...TAILWIND_HOVER_COLOR });
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
@ -19,9 +23,19 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
}
|
"./src/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"forceConsistentCasingInFileNames": true
|
||||||
"exclude": ["node_modules"]
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue