diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..14ef0437 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": ["next/babel"], + "plugins": [ + [ + "styled-components", + { + "ssr": true, + "displayName": true, + "preprocess": false + } + ] + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..c079d1dc --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "extends": "next/core-web-vitals", + "env": { + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": "latest" + }, + "rules": { + "no-unused-vars": "warn" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c87c9b39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..946b621a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "useTabs": false, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2, + "semi": true +} \ No newline at end of file diff --git a/README.md b/README.md index 733a8622..fbd456dd 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,1033 @@ -# ✔️ 미니 프로젝트_연차/당직 프로그램 만들기 +# WORK FAIRY(근태 요정) -## 필수 요구 사항 +# 👩‍🚀 프론트엔드 개발팀 -- 로그인 / 회원가입 페이지 -- 개인 정보 수정 페이지 -- 사용자간 공유 게시 페이지 캘린더 사용 + + + + + + + + + + + + + -## 선택 요구 사항 +
+ + ChoEun-Sang
+ 조은상
+
+
+ + Pildrum
+ 김필진
+
+
+ + Siwoo Lee
+ 이시우
+
+
+ + Jung SeungWon
+ 정승원
+
+
+ - 관리자 페이지
+ - 프론트엔드 팀장
+ - README 및 프로젝트 관련 서류 담당
+
+ - Auth페이지 / 관리자페이지
+ - 프로젝트 리더
+ - Git Management
+ - 와이어프레임 담당
+
+ - 사원 페이지
+ - 템플릿 디자인 담당
+
+ - 캘린더 컴포넌트
+
-- `useCallback`, `useMemo `등을 통한 컴포넌트 렌더링 최적화 -- 내가 작성한 코드를 팀원 중 누가봐도 쉽게 알아볼 수 있도록 고민하면서 작성해주세요. +
+
-## 과제 수행 및 제출 방법 +# 사용기술 및 개발환경 -1. 현재 저장소를 로컬에 클론(Clone)합니다. -2. 팀별로 브랜치를 생성합니다.(`git branch KDT5_TEAM_ABC`) -3. 팀별 브랜치에서 과제를 수행합니다. -4. 과제 수행이 완료되면, 자신의 본명 브랜치를 원격 저장소에 푸시(Push)합니다.(`main` 브랜치에 푸시하지 않도록 꼭 주의하세요, `git push origin KDT5_TEAM_ABC`) -5. 저장소에서 `main` 브랜치를 대상으로 Pull Request 생성하면, 과제 제출이 완료됩니다!(E.g, `main` <== `KDT5_TEAM_ABC`) +### Development -### 주의사항! +

+ + + + + +

-- `main` 혹은 다른 사람의 브랜치로 절대 병합하지 않도록 주의하세요! -- Pull Request에서 보이는 설명을 다른 사람들이 이해하기 쉽도록 꼼꼼하게 작성하세요! -- Pull Request에서 과제 제출 후 절대 병합(Merge)하지 않도록 주의하세요! -- 과제 수행 및 제출 과정에서 문제가 발생한 경우, 바로 담당 멘토나 강사에서 얘기하세요! +
+
+ +## 화면 구성 + +### 로그인 페이지 + +**사용자별 로그인** +로그인메인 +**회원가입** +사원회원가입 +**사원용 로그인** +사원로그인 +**관리자용 로그인** +관리자로그인 + +
+ +### 사원 페이지 + +**메인페이지** +사원용메인 +**연차/당직 등록** +사원용모달 +**결재 내역 페이지** +사원결재내역 +**결재 상세 내역** +사원내역모달 + +
+ +### 관리자 페이지 + +**요청 관리 페이지** +관리자메인 +**결재 처리 창** +관리자모달 +**일별 사용대장** +관리자일별 +**월별 사용대장** +관리자월별 +**사원 조회 페이지** +관리자조회 + +
+
+ +## API 사용법 + +모든 API 요청(Request) `headers`에 아래 정보가 꼭 포함돼야 합니다! +`username`은 `KDT5_TeamX`와 같이 본명 혹은 팀 이름을 포함해야 합니다! +확인할 수 없는 사용자나 팀의 DB 정보는 임의로 삭제될 수 있습니다! + +```json +{ + "content-type": "application/json" +} +``` + +
+ +## 인증 + +'인증' 관련 API는 모두 일반 사용자 전용입니다. + +### 회원가입 + +사용자가 `username`에 종속되어 회원가입합니다. + +- 사용자 비밀번호는 암호화해 저장합니다.(관리자는 확인할 수 없습니다!) + +```curl +curl http://3.34.110.127/api/register + \ -X 'POST' +``` + +요청 데이터 타입 및 예시: + +```ts +interface RequestBody { + email: string; // 사용자 아이디 (필수!) + password: string; // 사용자 비밀번호, 8자 이상 (필수!) + empName: string; // 사용자 이름, 20자 이하 (필수!) + position?: string; // 직급 (선택 사항!) +} +``` + +```json +{ + "email": "aaa@gmail.com", + "password": "********", + "empName": "team1", + "position": "팀장" +} +``` + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + success: boolean; + code: number; + message: string; +} +``` + +```json +{ + "success": false, + "code": -1, + "message": "이메일이 중복되었습니다." +} +``` + +### 로그인 + +- 발급된 `accessToken`은 30분 후 만료됩니다.(만료 후 다시 로그인 필요) + +```curl +curl http://3.34.110.127/api/login + \ -X 'POST' +``` + +요청 데이터 타입 및 예시: + +```ts +interface RequestBody { + email: string; // 사용자 아이디 (필수!) + password: string; // 사용자 비밀번호 (필수!) +} +``` + +```json +{ + "email": "aaa@gmail.com", + "password": "********" +} +``` + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + Authorization: string; + Authorization-refresh: string; +} +``` + +```json +{ + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjlQS3I...(생략)", + "Authorization-refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjlQS3I...(생략)" +} +``` + +
+ +### 요청관리 - 결재처리 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/update + \ -X 'POST' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +```ts +interface RequestBody { + id: number; + status: string; +} +``` + +```json +{ + "id" : 1 + "status" : "승인" +} +``` + +응답 데이터 타입 및 예시: + +- 없음 + +### 요청관리 리스트 조회 - 결재대기 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list/status/wait?page=${page}&size=${size} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + content: ContentData[]; + pageable: { + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; + }; + totalElements: number; + totalPages: number; + last: boolean; + number: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + size: number; + numberOfElements: number; + first: boolean; + empty: boolean; +} + +interface ContentData { + id: number; + empName: string; + createdAt: string; + orderType: string; + status: string; + startDate: string; + endDate: string; + reason: string; + category: string; + etc: string; +} +``` + +```json +{ + "content": [ + { + "id": 1, + "empName": "박지훈", + "createdAt": "2023-07-15", + "orderType": "연차", + "status": "대기", + "startDate": "2023-07-15", + "endDate": "2023-07-20", + "reason": "이유", + "category": "경조사", + "etc": "기타" + } + ], + "pageable": { + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "offset": 0, + "pageNumber": 0, + "pageSize": 4, + "paged": true, + "unpaged": false + }, + "totalElements": 22, + "totalPages": 6, + "last": false, + "number": 0, + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "size": 4, + "numberOfElements": 4, + "first": true, + "empty": false +} +``` + +### 요청관리 리스트 조회 - 결재완료 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list/status/complete?page=${page}&size=${size} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + content: { + id: number; + empName: string; + createdAt: string; + orderType: string; + status: string; + startDate: string; + endDate: string; + reason: string; + category: string; + etc: string; + }[]; + pageable: { + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; + }; + totalElements: number; + totalPages: number; + last: boolean; + number: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + size: number; + numberOfElements: number; + first: boolean; + empty: boolean; +} +``` + +```json +{ + "content": [ + { + "id": 1, + "empName": "박지훈", + "createdAt": "2023-07-15", + "orderType": "연차", + "status": "승인", + "startDate": "2023-07-15", + "endDate": "2023-07-20", + "reason": "이유", + "category": "경조사", + "etc": "기타" + } + ], + "pageable": { + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "offset": 0, + "pageNumber": 0, + "pageSize": 4, + "paged": true, + "unpaged": false + }, + "totalElements": 22, + "totalPages": 6, + "last": false, + "number": 0, + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "size": 4, + "numberOfElements": 4, + "first": true, + "empty": false +} +``` + +### 월별 사용대장 - 당직 조회 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list/monthly/duty?year=${year} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +type TDutyData = IDutyItem[]; + +interface IDutyItem { + id: number; + empName: string; + empNo: number; + month: { + jan: number; + feb: number; + mar: number; + apr: number; + may: number; + jun: number; + jul: number; + aug: number; + sept: number; + oct: number; + nov: number; + dec: number; + totalCount: number; + }; + total: number; +} +``` + +```json +[ + { + "id": 1, + "empName": "박지훈", + "empNo": 20200001, + "month": { + "jan": 0, + "feb": 0, + "mar": 0, + "apr": 0, + "may": 0, + "jun": 0, + "jul": 6, + "aug": 0, + "sept": 0, + "oct": 0, + "nov": 0, + "dec": 0, + "totalCount": 6 + }, + "total": 6 + } +] +``` + +### 월별 사용대장 - 연차 조회 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list/monthly/annual?year=${year} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +type TAnnualData = IDutyItem[]; + +interface IAnnualItem { + id: number; + empName: string; + empNo: number; + month: { + jan: number; + feb: number; + mar: number; + apr: number; + may: number; + jun: number; + jul: number; + aug: number; + sept: number; + oct: number; + nov: number; + dec: number; + totalCount: number; + }; + total: number; +} +``` + +```json +[ + { + "id": 1, + "empName": "박지훈", + "empNo": 20200001, + "month": { + "jan": 0, + "feb": 0, + "mar": 0, + "apr": 0, + "may": 0, + "jun": 0, + "jul": 6, + "aug": 0, + "sept": 0, + "oct": 0, + "nov": 0, + "dec": 0, + "totalCount": 6 + }, + "total": 6 + } +] +``` + +### 일별 사용대장 - 연차 조회 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list/daily/annual?year=${year}&month=${month} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface IResponseBody { + empName: string; + empNo: number; + orderType: string; + date: string; +} +``` + +```json +[ + { + "empName": "홍길동", + "empNo": 20230001, + "orderType": "당직", + "date": "2023-08--20" + } +] +``` + +### 일별 사용대장 - 당직 조회 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list/daily/duty?year=${year}&month=${month} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface IResponseBody { + empName: string; + empNo: number; + orderType: string; + date: string; +} +``` + +```json +[ + { + "empName": "홍길동", + "empNo": 20230001, + "orderType": "당직", + "date": "2023-08--20" + } +] +``` + +### 사원조회 - 사원명 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/user/search?name=${name} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ISearch { + id: number; + empNo: number; + empName: string; + createdAt: string; +} +``` + +```json +{ + "id": 4, + "empNo": 20210004, + "empName": "홍길동", + "createdAt": "2023-08-03" +} +``` + +### 사원조회 - 사원번호 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/user/search?empno=${empno} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ISearch { + id: number; + empNo: number; + empName: string; + createdAt: string; +} +``` + +```json +{ + "id": 4, + "empNo": 20210004, + "empName": "홍길동", + "createdAt": "2023-08-03" +} +``` + +### 사원조회 - 연차/당직 내역 + +- 관리자 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/list?user=${user}&page=${page}&size=${size} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + content: { + id: number; + empName: string; + createdAt: string; + orderType: string; + status: string; + startDate: string; + endDate: string; + reason: string; + category: string; + etc: string; + }[]; + pageable: { + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; + }; + totalElements: number; + totalPages: number; + last: boolean; + number: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + size: number; + numberOfElements: number; + first: boolean; + empty: boolean; +} +``` + +```json +{ + "content": [ + { + "id": 1, + "empName": "박지훈", + "createdAt": "2023-07-15", + "orderType": "연차", + "status": "승인", + "startDate": "2023-07-15", + "endDate": "2023-07-20", + "reason": "이유", + "category": "경조사", + "etc": "기타" + } + ], + "pageable": { + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "offset": 0, + "pageNumber": 0, + "pageSize": 4, + "paged": true, + "unpaged": false + }, + "totalElements": 22, + "totalPages": 6, + "last": false, + "number": 0, + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "size": 4, + "numberOfElements": 4, + "first": true, + "empty": false +} +``` + +--- + +### 연차/당직 등록 + +- 시원 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/admin/order/add + \ -X 'POST' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +```ts +interface RequestBody { + orderType: string; + startAt: string; + endAt: string; + reason: string | null; + category: string | null; + etc: string | null; +} +``` + +```json +{ + "orderType": "당직 or 연차", + "startAt": "2023-07-31", + "endAt": "2023-07-31", + "reason" : "이유" + "category"? : "경조사" + "etc"?: "특이사항입니다." +} +``` + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + success: boolean; + response: string; + error: null; +} +``` + +```json +{ + "success": true, + "response": "등록 완료", + "error": null +} +``` + +### 전자결제내역 + +- 시원 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/user/myorder?page={page}&size={size} + \ -X 'GET' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + success: boolean; + response: { + content: [ + { + id: number; + empName: string; + createdAt: string; + orderType: string; + status: string; + startDate: string; + endDate: string; + reason: string | null; + }, + ]; + pageable: { + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; + }; + totalPages: number; + totalElements: number; + last: boolean; + number: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + size: number; + numberOfElements: number; + first: boolean; + empty: boolean; + }; + error: string | null; +} +``` + +```json +{ + "success": true, + "response": { + "content": [ + { + "id": 87, + "empName": "마두기", + "createdAt": "2023-08-09", + "orderType": "당직", + "status": "승인", + "startDate": "2023-08-10", + "endDate": "2023-08-10", + "reason": null + } + ], + "pageable": { + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "offset": 0, + "pageNumber": 0, + "pageSize": 10, + "paged": true, + "unpaged": false + }, + "totalPages": 2, + "totalElements": 12, + "last": false, + "number": 0, + "sort": { + "empty": true, + "sorted": false, + "unsorted": true + }, + "size": 10, + "numberOfElements": 10, + "first": true, + "empty": false + }, + "error": null +} +``` + +### 연차/당직 내역 삭제 + +- 시원 전용 API 입니다. + +```curl +curl http://3.34.110.127/api/user/order/delete?id={id} + \ -X 'POST' + \ -H 'Authorization: accessToken' +``` + +요청 데이터 타입 및 예시: + +- 없음 + +응답 데이터 타입 및 예시: + +```ts +interface ResponseValue { + success: boolean; + response: string; + error: string | null; +} +``` + +```json +{ + "success": "true", + "response": "등록 완료", + "error": null +} +``` diff --git a/components/admin/ApprovalModal.tsx b/components/admin/ApprovalModal.tsx new file mode 100644 index 00000000..b064a1e4 --- /dev/null +++ b/components/admin/ApprovalModal.tsx @@ -0,0 +1,107 @@ +import { Modal } from "antd"; +import Button from "@components/common/Button"; +import styled from "styled-components"; +import { postUpdateOrder } from "@lib/api/adminAPI"; +import { IModalProps } from "@lib/interface/Admin"; + +function ApprovalModal({ open, setOpen, details }: IModalProps) { + const handleClick = async (event: MouseEvent, id: number, status: string) => { + event.preventDefault(); + try { + await postUpdateOrder({ id, status }); + setOpen(false); + window.location.reload(); + } catch (error) { + alert("결재 처리 실패하였습니다!"); + } + }; + + return ( + setOpen(false)} footer={[]}> + {details && ( + <> +
+

{details?.orderType} 결재 내역

+ +
+ {details.status == "대기" && ( +
+ + +
+ )} + + )} +
+ ); +} + +export default ApprovalModal; + +const StyledModal = styled(Modal)` + .details { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + h3 { + font-size: 16px; + font-weight: 700; + } + ul { + margin-top: 50px; + + li { + display: flex; + margin-bottom: 20px; + border-bottom: 1px solid #e6e6e6; + span { + width: 100px; + } + } + } + } + .btnBox { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 30px; + } +`; diff --git a/components/admin/MonthlyTable.tsx b/components/admin/MonthlyTable.tsx new file mode 100644 index 00000000..5a317a97 --- /dev/null +++ b/components/admin/MonthlyTable.tsx @@ -0,0 +1,161 @@ +import { Table } from "antd"; +import type { ColumnsType } from "antd/es/table"; +import { IMonthlyPros, IColumnsData } from "@lib/interface/Admin"; + +function MonthlyTable({ dataSource }: IMonthlyPros) { + const columnsData: ColumnsType = [ + { + title: "사원", + children: [ + { + title: "사원번호", + dataIndex: "empNo", + key: "empNo", + width: 100, + align: "center", + sorter: (a, b) => a.empNo - b.empNo, + render: (_, data) =>

{data.empNo}

, + }, + { + title: "사원명", + dataIndex: "empName", + key: "empName", + width: 80, + align: "center", + render: (_, data) =>

{data.empName}

, + }, + ], + }, + { + title: "1월", + dataIndex: "jan", + key: "jan", + width: 70, + align: "center", + render: (_, data) => + data.month.jan === 0 ? "" :

{data.month.jan}

, + }, + { + title: "2월", + dataIndex: "feb", + key: "feb", + width: 70, + align: "center", + render: (_, data) => + data.month.feb === 0 ? "" :

{data.month.feb}

, + }, + { + title: "3월", + dataIndex: "mar", + key: "mar", + width: 70, + align: "center", + render: (_, data) => + data.month.mar === 0 ? "" :

{data.month.mar}

, + }, + { + title: "4월", + dataIndex: "apr", + key: "apr", + width: 70, + align: "center", + render: (_, data) => + data.month.apr === 0 ? "" :

{data.month.apr}

, + }, + { + title: "5월", + dataIndex: "may", + key: "may", + width: 70, + align: "center", + render: (_, data) => + data.month.may === 0 ? "" :

{data.month.may}

, + }, + { + title: "6월", + dataIndex: "jun", + key: "jun", + width: 70, + align: "center", + render: (_, data) => + data.month.jun === 0 ? "" :

{data.month.jun}

, + }, + { + title: "7월", + dataIndex: "jul", + key: "jul", + width: 70, + align: "center", + render: (_, data) => + data.month.jul === 0 ? "" :

{data.month.jul}

, + }, + { + title: "8월", + dataIndex: "aug", + key: "aug", + width: 70, + align: "center", + render: (_, data) => + data.month.aug === 0 ? "" :

{data.month.aug}

, + }, + { + title: "9월", + dataIndex: "sept", + key: "sept", + width: 70, + align: "center", + render: (_, data) => + data.month.sept === 0 ? "" :

{data.month.sept}

, + }, + { + title: "10월", + dataIndex: "oct", + key: "oct", + width: 70, + align: "center", + render: (_, data) => + data.month.oct === 0 ? "" :

{data.month.oct}

, + }, + { + title: "11월", + dataIndex: "nov", + key: "nov", + width: 70, + align: "center", + render: (_, data) => + data.month.nov === 0 ? "" :

{data.month.nov}

, + }, + { + title: "12월", + dataIndex: "dec", + key: "dec", + width: 70, + align: "center", + render: (_, data) => + data.month.dec === 0 ? "" :

{data.month.dec}

, + }, + { + title: "합계", + key: "합계", + fixed: "right", + align: "center", + width: 80, + sorter: (a, b) => a.total - b.total, + render: (_, data) =>

{data.total}

, + }, + ]; + + return ( + <> + + + ); +} + +export default MonthlyTable; diff --git a/components/auth/README.md b/components/auth/README.md new file mode 100644 index 00000000..a7c25a89 --- /dev/null +++ b/components/auth/README.md @@ -0,0 +1,19 @@ +2023.8.7 (월) +- 로그인 API 함수 구현 시작 +- auth 컴포넌트 폴더 세분화 작업 + +2023.8.6 (일) +- AdminAuthForm / AdminAuthTemplate UI 수정 + +2023.8.4 (금) +- AdminAuthForm / AdminAuthTemplate UI 작업 완료 + +2023.8.1 (화) +- login/register 타입에 따라서 input을 다르게 구현 + +2023.7.31 (월) +- AuthTemplate 컴포넌트 완성 + +컴포넌트 +AuthTemplate: 로그인 / 회원가입 레이아웃 컴포넌트 +AuthForm: 로그인 / 회원가입 컴포넌트 \ No newline at end of file diff --git a/components/auth/admin/AdminAuthForm.tsx b/components/auth/admin/AdminAuthForm.tsx new file mode 100644 index 00000000..e02abaaf --- /dev/null +++ b/components/auth/admin/AdminAuthForm.tsx @@ -0,0 +1,114 @@ +import { login } from "@lib/api/authAPI"; +import { useRouter } from "next/router"; +import { FormEvent, useCallback, useState } from "react"; +import styled from "styled-components"; + +// Component +function AdminAuthForm() { + const router = useRouter(); + // Hooks + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const onLoginChange = useCallback((event: FormEvent) => { + const { name, value } = event.target as HTMLInputElement; + if (name === "email") { + setEmail(value); + } else if (name === "password") { + setPassword(value); + } + }, []); + + const onLogin = async (event: FormEvent) => { + event.preventDefault(); + await login({ email, password })?.then((res) => { + localStorage.setItem("Token", res.data.response.accessToken); + router.push({ + pathname: "/admin", + }); + }); + }; + + // Render + return ( + +

관리자 로그인

+
+ + + + 로그인 + + +
+ ); +} + +// Style +const AuthFormBlock = styled.div` + padding: 0 40px; + h3 { + margin: 0; + color: #fff; + font-size: 24px; + font-weight: 700; + margin-bottom: 1rem; + text-align: center; + margin-bottom: 40px; + } +`; + +const StyledInput = styled.input` + font-size: 1rem; + border: none; + border-bottom: 2px solid #aaa; + padding-bottom: 0.5rem; + outline: none; + width: 100%; + background: transparent; + color: #fff; + &::placeholder { + color: #fff; + } + &:focus { + border-bottom: 2px solid #fff; + } + &:focus::-webkit-input-placeholder { + color: transparent; + } + & + & { + margin-top: 2rem; + } +`; + +const ButtonBlock = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +const StyledButton = styled.button` + margin-top: 2rem; + width: 180px; + height: 30px; + border: none; + outline: none; + font-size: 16px; + border-radius: 8px; + cursor: pointer; + background: #707070; + color: #fff; +`; + +export default AdminAuthForm; diff --git a/components/auth/admin/AdminAuthTemplate.tsx b/components/auth/admin/AdminAuthTemplate.tsx new file mode 100644 index 00000000..c0cbc67f --- /dev/null +++ b/components/auth/admin/AdminAuthTemplate.tsx @@ -0,0 +1,34 @@ +import { PropsWithChildren } from "react"; +import styled from "styled-components"; + +function AdminAuthTemplate({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} + +const AuthTemplateBlock = styled.div` + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-image: url("https://github.com/FAST-MINI-TEAM1/client-team1/assets/125563995/ff793dc1-4cfb-4c40-83f6-a5874d3465c9"); + background-size: 100%; +`; + +const NavyBox = styled.div` + box-shadow: 0 0 8px rgba(0, 0, 0, 0.025); + width: 480px; + padding: 2rem; + background: #192859; + border-radius: 24px; +`; + +export default AdminAuthTemplate; diff --git a/components/auth/employee/AuthForm.tsx b/components/auth/employee/AuthForm.tsx new file mode 100644 index 00000000..dba92495 --- /dev/null +++ b/components/auth/employee/AuthForm.tsx @@ -0,0 +1,351 @@ +import Input from "@components/common/Input"; +import Link from "next/link"; +import styled from "styled-components"; +import { MdEmail, MdLock, MdVerifiedUser, MdPerson } from "react-icons/md"; +import { BsFillPersonBadgeFill } from "react-icons/bs"; +import { IAuthFormProps, ITextMap } from "@lib/interface/Auth"; +import { FormEvent, useCallback, useState } from "react"; +import { login, register } from "@lib/api/authAPI"; +import { useRouter } from "next/router"; +import Loading from "@components/common/Loading"; + +// Constant / Variation +const textMap: ITextMap = { + login: "로그인", + register: "회원가입", +}; + +// Component +function AuthForm({ type }: IAuthFormProps) { + // 컴포넌트 타입에 따른 이름 + const text = textMap[type]; + + // Hooks + const router = useRouter(); + const [email, setEmail] = useState(""); + const [emailMessage, setEmailMessage] = useState(""); + const [password, setPassword] = useState(""); + const [passwordMessage, setPasswordMessage] = useState(""); + const [empName, setEmpName] = useState(""); + const [nameMessage, setNameMessage] = useState(""); + const [passwordConfirm, setPasswordConfirm] = useState(""); + const [passwordConfirmMessage, setPasswordConfirmMessage] = useState(""); + const [isPasswordConfirm, setIsPasswordConfirm] = useState(false); + const [position, setPosition] = useState(""); + const [registerMessage, setRegisterMessage] = useState(""); + const [loading, setLoading] = useState(false); + + const onLoginChange = useCallback((event: FormEvent) => { + const { name, value } = event.target as HTMLInputElement; + if (name === "email") { + setEmail(value); + } else if (name === "password") { + setPassword(value); + } + }, []); + + const onRegisterChange = useCallback( + (event: FormEvent) => { + const { name, value } = event.target as HTMLInputElement; + if (name === "email") { + const rEmail = + /([\w-.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/; + if (!rEmail.test(value)) { + setEmailMessage("이메일 형식으로 적어주세요."); + } else { + setEmailMessage(""); + setEmail(value); + } + } else if (name === "password") { + const rPassword = + /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$/; + if (!rPassword.test(value)) { + setPasswordMessage( + "숫자 + 영문 + 특수문자 조합으로 8자리 이상 입력해주세요.", + ); + } else { + setPasswordMessage(""); + setPassword(value); + } + } else if (name === "name") { + if (value === "") { + setNameMessage("이름을 입력하세요."); + } else { + setEmpName(value); + } + } else if (name === "passwordConfirm") { + if (password === value) { + setPasswordConfirm(value); + setPasswordConfirmMessage("비밀번호가 일치합니다."); + setIsPasswordConfirm(true); + } else { + setPasswordConfirmMessage("비밀번호가 일치하지 않습니다."); + setIsPasswordConfirm(false); + } + } else if (name === "rank") { + setPosition(value); + } + }, + [password], + ); + + const onLogin = useCallback( + async (event: FormEvent) => { + try { + event.preventDefault(); + await login({ email, password })?.then((res) => { + localStorage.setItem("Token", res.data.response.accessToken); + localStorage.setItem("empName", res.data.response.empName); + router.push({ + pathname: "/employee", + }); + }); + } catch (e) { + console.error(e, "로그인 오류!"); + } + }, + [email, password, router], + ); + + const onRegister = useCallback( + async (event: FormEvent) => { + event.preventDefault(); + if ( + email === "" || + password === "" || + passwordConfirm === "" || + empName === "" + ) { + setRegisterMessage("필수 입력 사항입니다."); + } else { + setLoading(true); + await register({ email, password, empName, position }).then((res) => { + if (typeof res === "string") { + setRegisterMessage("이미 계정이 있습니다!"); + console.log(res); + } else { + } + }); + router.push("/login"); + } + }, + [email, password, empName, position, passwordConfirm, router], + ); + + // Render + return ( + <> + {loading && } + +

{text}

+ + + +
+ {type === "login" && ( + <> + + + + + + + + + + )} + {type === "register" && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + {text} + + + {registerMessage || + emailMessage || + passwordMessage || + (isPasswordConfirm && passwordConfirmMessage) || + nameMessage} + + +
+ {type === "login" ? ( + 회원가입 + ) : ( + 로그인 + )} +
+
+ + ); +} + +// Style +const AuthFormBlock = styled.div` + padding: 0 20px; + h3 { + margin: 0; + color: #707070; + font-size: 24px; + font-weight: 700; + margin-bottom: 1rem; + text-align: center; + margin-bottom: 40px; + } +`; + +const InputWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + input { + flex: 1; + } + & + & { + margin-top: 2rem; + } +`; + +const IconWrapper = styled.div` + &::before { + content: "*"; + margin: 0 8px; + color: #f00; + font-size: 18px; + } +`; + +const RankIconWrapper = styled.div` + &::before { + content: ""; + margin: 0 12px; + } +`; + +const EmailIcon = styled(MdEmail)` + font-size: 24px; +`; + +const PersonIcon = styled(MdPerson)` + font-size: 24px; +`; + +const PasswordIcon = styled(MdLock)` + font-size: 24px; +`; + +const PasswordConfirmIcon = styled(MdVerifiedUser)` + font-size: 24px; +`; + +const RankIcon = styled(BsFillPersonBadgeFill)` + font-size: 24px; +`; + +const ButtonBlock = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +const StyledButton = styled.button` + margin-top: 2rem; + width: 180px; + height: 30px; + border: none; + outline: none; + font-size: 16px; + border-radius: 8px; + cursor: pointer; +`; + +const Footer = styled.div` + margin-top: 2rem; + text-align: right; + a { + color: #bbb; + text-decoration: underline; + &:hover { + color: #707070; + } + } +`; + +export default AuthForm; diff --git a/components/auth/employee/AuthTemplate.tsx b/components/auth/employee/AuthTemplate.tsx new file mode 100644 index 00000000..0fe3d20c --- /dev/null +++ b/components/auth/employee/AuthTemplate.tsx @@ -0,0 +1,36 @@ +import { PropsWithChildren } from "react"; +import styled from "styled-components"; + +// Component +function AuthTemplate({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} + +// Style +const AuthTemplateBlock = styled.div` + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-image: url("https://github.com/FAST-MINI-TEAM1/client-team1/assets/125563995/ff793dc1-4cfb-4c40-83f6-a5874d3465c9"); + background-size: 100%; +`; + +const WhiteBox = styled.div` + box-shadow: 0 0 8px rgba(0, 0, 0, 0.025); + width: 480px; + padding: 2rem; + background: #fff; + border-radius: 8px; +`; + +export default AuthTemplate; diff --git a/components/common/AdminHeader.tsx b/components/common/AdminHeader.tsx new file mode 100644 index 00000000..7009b3b3 --- /dev/null +++ b/components/common/AdminHeader.tsx @@ -0,0 +1,205 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useCallback } from "react"; +import styled from "styled-components"; +import Image from "next/image"; +import logo from "public/workFairy_logo.png"; + +function AdminHeader() { + const router = useRouter(); + const onClick = useCallback(() => { + localStorage.removeItem("Token"); + router.push("/admin/login"); + }, [router]); + return ( + + + + + + + + 관리자 + 님, 반갑습니다! + + 로그아웃 ⇢ + + + + + ); +} + +const HeaderBlock = styled.header` + width: 100%; + height: 80px; + background: ${(props) => props.theme.headerColor}; + display: flex; + justify-content: center; + align-items: center; + padding: 0 20px; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); +`; + +const HeaderContent = styled.div` + width: 1320px; + height: inherit; + max-width: 1280px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const LogoContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; +`; + +const Logo = styled(Image)` + cursor: pointer; +`; + +const UserWelcome = styled.div` + span { + &:first-child { + color: #00f; + font-weight: 600; + } + } +`; + +const Nav = styled.nav` + ul { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + + li { + position: relative; + span { + cursor: pointer; + color: ${(props) => props.theme.inactiveColor}; + &.active { + font-weight: 600; + color: ${(props) => props.theme.activeColor}; + } + } + a { + color: ${(props) => props.theme.inactiveColor}; + &.active { + font-weight: 700; + color: ${(props) => props.theme.activeColor}; + } + } + } + } +`; + +const SheetSection = styled.li` + .subMenu { + display: none; + font-size: 12px; + } + &:hover { + .subMenu { + display: visible; + &.hoverActive { + opacity: 1; + } + width: 250px; + background: #fff; + position: absolute; + left: -100px; + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + padding: 10px 5px; + border-radius: 10px; + /* opacity: 0; */ + z-index: 99999; + a { + text-align: center; + transition: color 0.2s ease-in-out; + &:hover { + color: #000; + } + } + } + } +`; + +const LogOutBtn = styled.button` + font-size: 12px; + border: 1px solid #adb5bd; + padding: 5px 12px; + border-radius: 30px; + background-color: transparent; + color: #adb5bd; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + color: #f27676; + border: 1px solid #f27676; + } +`; +export default AdminHeader; diff --git a/components/common/Button.tsx b/components/common/Button.tsx new file mode 100644 index 00000000..35b6fe3c --- /dev/null +++ b/components/common/Button.tsx @@ -0,0 +1,201 @@ +import { styled, css } from "styled-components"; + +interface IButtonProps { + [props: string]: any; +} + +function Button({ ...props }: IButtonProps) { + return ; +} + +const StyledButton = styled.button<{ + employee?: boolean; + admin?: boolean; + accept?: boolean; + deny?: boolean; + pending?: boolean; + delete?: boolean; + submit?: boolean; + annual?: boolean; + duty?: boolean; + cancle?: boolean; + application?: boolean; +}>` + border: none; + border-radius: 10px; + padding: 0.5rem 1rem; + color: ${(props) => props.theme.buttonTextColor}; + outline: none; + cursor: pointer; + background: ${(props) => props.theme.bgColor}; + box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.04); + transition: all 0.2s ease 0s; + &:hover { + background: ${(props) => props.theme.hoverColor}; + } + + ${(props) => + props.employee && + css` + background: rgba(255, 255, 255); + border: 1px solid ${(props) => props.theme.buttonColor.empButton}; + color: ${(props) => props.theme.buttonTextColor.empColor}; + font-size: 20px; + font-weight: 500; + border-radius: 30px; + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + &:hover { + color: ${(props) => props.theme.pointColor.green}; + background: rgba(255, 255, 255, 0.75); + border: 1px solid ${(props) => props.theme.buttonColor.empButton}; + } + `} + ${(props) => + props.admin && + css` + background: ${(props) => props.theme.pointColor.red}; + color: ${(props) => props.theme.buttonTextColor.adminColor}; + font-size: 20px; + font-weight: 500; + border-radius: 30px; + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + &:hover { + color: ${(props) => props.theme.buttonColor.managerButton}; + background: rgba(255, 255, 255, 0.75); + // border: 1px solid ${(props) => + props.theme.buttonColor.managerButton}; + } + `} + ${(props) => + props.accept && + css` + background: ${(props) => props.theme.buttonColor.acceptButton}; + border: 1px solid ${(props) => props.theme.buttonColor.acceptButton}; + &:hover { + color: ${(props) => props.theme.buttonColor.acceptButton}; + border: 1px solid ${(props) => props.theme.buttonColor.acceptButton}; + } + `} + ${(props) => + props.deny && + css` + background: ${(props) => props.theme.buttonColor.denyButton}; + border: 1px solid ${(props) => props.theme.buttonColor.denyButton}; + &:hover { + color: ${(props) => props.theme.buttonColor.denyButton}; + border: 1px solid ${(props) => props.theme.buttonColor.denyButton}; + } + `} + ${(props) => + props.pending && + css` + background: ${(props) => props.theme.buttonColor.pendingButton}; + border: 1px solid ${(props) => props.theme.buttonColor.pendingButton}; + &:hover { + color: ${(props) => props.theme.buttonColor.pendingButton}; + border: 1px solid ${(props) => props.theme.buttonColor.pendingButton}; + } + `} + ${(props) => + props.delete && + css` + width: 457px; + height: 49px; + font-size: 18px; + color: ${(props) => props.theme.pointColor.rightGray}; + background: ${(props) => props.theme.pointColor.red}; + box-shadow: 0px 3px 12px 2px rgba(106, 106, 106, 0.15); + transition: 0.4s; + &:hover { + color: ${(props) => props.theme.pointColor.red}; + border: 0.5px solid ${(props) => props.theme.pointColor.red}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + } + `} + ${(props) => + props.submit && + css` + width: 247px; + height: 58px; + font-size: 16px; + background: ${(props) => props.theme.pointColor.green}; + color: ${(props) => props.theme.pointColor.rightGray}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + transition: 0.3s; + &:hover { + color: ${(props) => props.theme.pointColor.green}; + border: 1px solid ${(props) => props.theme.pointColor.green}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + } + `} + ${(props) => + props.annual && + css` + width: 247px; + height: 58px; + font-size: 16px; + background: ${(props) => props.theme.pointColor.yellow}; + color: ${(props) => props.theme.pointColor.rightGray}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + transition: 0.3s; + &:hover { + color: ${(props) => props.theme.pointColor.yellow}; + border: 1px solid ${(props) => props.theme.pointColor.yellow}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + } + `} + ${(props) => + props.duty && + css` + width: 247px; + height: 58px; + font-size: 16px; + border-radius: 13px; + background: ${(props) => props.theme.pointColor.blue}; + color: ${(props) => props.theme.pointColor.rightGray}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + transition: 0.3s; + &:hover { + color: ${(props) => props.theme.pointColor.blue}; + border: 1px solid ${(props) => props.theme.pointColor.blue}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + } + `} + ${(props) => + props.cancle && + css` + width: 125px; + height: 40px; + font-size: 16px; + border-radius: 10px; + background: #fbfbfb; + color: ${(props) => props.theme.pointColor.red}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + transition: 0.3s; + &:hover { + color: ${(props) => props.theme.pointColor.rightGray}; + background: ${(props) => props.theme.pointColor.red}; + box-shadow: 0px 3px 7px 2px rgba(106, 106, 106, 0.25); + } + `} + ${(props) => + props.application && + css` + width: 125px; + height: 40px; + font-size: 16px; + border-radius: 10px; + box-shadow: 0px 2px 4px rgba(106, 106, 106, 0.25); + background: #fbfbfb; + color: ${(props) => props.theme.pointColor.green}; + box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.16); + transition: 0.3s; + &:hover { + color: ${(props) => props.theme.pointColor.rightGray}; + background: ${(props) => props.theme.pointColor.green}; + box-shadow: 0px 3px 3px 0px #6a6a6a; + } + `} +`; + +export default Button; diff --git a/components/common/Calender.tsx b/components/common/Calender.tsx new file mode 100644 index 00000000..18e8dca9 --- /dev/null +++ b/components/common/Calender.tsx @@ -0,0 +1,301 @@ +import React, { useEffect, useState } from "react"; +import { styled } from "styled-components"; +import ReactCalendar from "react-calendar"; +import { IEmployeeMonthly } from "@lib/interface/EmployeeInterface"; +import { userscheduleApi } from "@lib/api/employeeAPI"; +import { OnArgs, TileArgs } from "react-calendar/dist/cjs/shared/types"; + +interface EmployeeTableTabProps { + selectedTap: string; + toggle?: boolean; +} + +function Calendar({ selectedTap }: EmployeeTableTabProps) { + const moment = require("moment"); + const [value] = useState(new Date()); + // 월별 조회 + const [scheduleDatas, setScheduleDatas] = useState([]); + + const [year, setYear] = useState(new Date().getFullYear()); + const [month, setMonth] = useState(new Date().getMonth() + 1); + + // 월별 조회 api 호출 + useEffect(() => { + const fetchData = async () => { + try { + console.log(year, month); + const res = await userscheduleApi({ + year: year, + month: month, + }); + const data: IEmployeeMonthly[] = res?.data.response; + if (selectedTap == "전체") { + const scheduleData = data; + setScheduleDatas(scheduleData); + return; + } + if (selectedTap == "연차") { + const scheduleData = data.filter((item) => { + if (selectedTap == "연차") { + return item.orderType == "연차"; + } + }); + setScheduleDatas(scheduleData); + return; + } + if (selectedTap == "당직") { + const scheduleData = data.filter((item) => { + if (selectedTap == "당직") { + return item.orderType == "당직"; + } + }); + setScheduleDatas(scheduleData); + return; + } + } catch (error) { + console.error(error); + } + }; + fetchData(); + }, [selectedTap, month, year]); + + // month,year 가져오기 + const handleChange = (activeStartDate: Date | null) => { + if (activeStartDate) { + const activeYear = new Date(activeStartDate).getFullYear(); + const activeMonth = new Date(activeStartDate).getMonth() + 1; + setYear(activeYear); + setMonth(activeMonth); + } + }; + + //DateRange 계산하는 로직 + const getDateRange = (startDate: string, endDate: string) => { + const start = new Date(startDate); + const end = new Date(endDate); + const result = []; + while (start <= end) { + result.push(start.toISOString().split("T")[0]); + start.setDate(start.getDate() + 1); + } + return result; + }; + + // 달력에 일정 mark + //대기 상태 + const markStanbyDate = scheduleDatas + .filter((item) => { + if (item.status == "대기") { + return item.status == "대기"; + } + }) + .map((item) => getDateRange(`${item.startDate}`, `${item.endDate}`)); + //승인 상태 + const markAprovedDate = scheduleDatas + .filter((item) => { + if (item.status == "승인") { + return item.status == "승인"; + } + }) + .map((item) => getDateRange(`${item.startDate}`, `${item.endDate}`)); + //반려 상태 + const markRejectedDate = scheduleDatas + .filter((item) => { + if (item.status == "반려") { + return item.status == "반려"; + } + }) + .map((item) => getDateRange(`${item.startDate}`, `${item.endDate}`)); + + // 달력에 mark 될 날짜 합쳐서 새로운 배열 생성 (대기/승인/반려) + const stanByDate: string[] = ([] as string[]).concat(...markStanbyDate); + const aprovedDate: string[] = ([] as string[]).concat(...markAprovedDate); + const rejectedDate: string[] = ([] as string[]).concat(...markRejectedDate); + + return ( + <> + + handleChange(activeStartDate) + } + formatDay={(__locale: string | undefined, date: Date) => + moment(date).format("DD") + } + value={value} + allowPartialRange={true} + className="mx-auto w-full text-sm border-b" + tileContent={({ date }: TileArgs) => { + if (stanByDate.find((x) => x === moment(date).format("YYYY-MM-DD"))) { + return ( + <> +
+
+ +
+
+ + ); + } + if ( + aprovedDate.find((x) => x === moment(date).format("YYYY-MM-DD")) + ) { + return ( + <> +
+
+ {selectedTap == "전체" ? ( + + ) : selectedTap == "연차" ? ( + + ) : ( + + )} +
+
+ + ); + } + if ( + rejectedDate.find((x) => x === moment(date).format("YYYY-MM-DD")) + ) { + return ( + <> +
+
+ +
+
+ + ); + } + }} + /> + + ); +} +const StyeldCalendar = styled(ReactCalendar)` + width: 100%; + height: 100%; + border: none; + font-family: "Noto Sans KR", sans-serif; + + .react-calendar button { + margin: 0; + border: 0; + outline: none; + background-color: aqua; + color: red; + } + + .react-calendar__navigation { + display: flex; + height: 44px; + margin: 1em; + button { + min-width: 44px; + background: none; + border: none; + font-size: 20px; + text-shadow: 0px 3px 4px rgba(81, 81, 81, 0.25); + } + button:enabled:hover { + background-color: none; + color: ${(props) => props.theme.pointColor.green}; + } + } + + .react-calendar__month-view__weekdays { + text-align: center; + font-weight: 600; + font-size: 16px; + color: #707070; + &__weekday { + padding: 1em; + } + abbr { + text-shadow: 0px 3px 4px rgba(81, 81, 81, 0.25); + text-decoration: none; + } + } + + //주말 컬러 변경 + .react-calendar__month-view__days__day { + &--weekend { + color: ${(props) => props.theme.pointColor.red}; + } + &--neighboringMonth { + color: ${(props) => props.theme.pointColor.rightGray}; + } + &--neighboringMonth:hover { + color: ${(props) => props.theme.pointColor.gray}; + } + } + + .react-calendar__tile { + max-width: 100%; + padding: 0.5em; + background: none; + height: 108px; + border: none; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + text-shadow: 0px 3px 4px rgba(81, 81, 81, 0.25); + &:enabled:hover { + color: ${(props) => props.theme.pointColor.green}; + background-color: ${(props) => props.theme.pointColor.rightGray}; + } + &:enabled:focus { + color: ${(props) => props.theme.pointColor.green}; + background-color: ${(props) => props.theme.pointColor.rightGray}; + } + } + + .react-calendar__tile--active { + background-color: ${(props) => props.theme.pointColor.rightGray}; + color: ${(props) => props.theme.pointColor.black}; + } + + .react-calendar__month-view__days__day { + text-align: top; + } +`; + +const AnnualIcon = styled.div` + width: 12px; + height: 12px; + border-radius: 50px; + margin: 34px auto; + background-color: ${(props) => props.theme.pointColor.blue}; +`; +const DutyIcon = styled.div` + width: 12px; + height: 12px; + border-radius: 50px; + margin: 34px auto; + background-color: ${(props) => props.theme.pointColor.yellow}; +`; +const AllIcon = styled.div` + width: 12px; + height: 12px; + border-radius: 50px; + margin: 34px auto; + background-color: ${(props) => props.theme.pointColor.green}; +`; +const RejectIcon = styled.div` + width: 12px; + height: 12px; + border-radius: 50px; + margin: 34px auto; + background-color: ${(props) => props.theme.pointColor.red}; +`; +const StanByIcon = styled.div` + width: 12px; + height: 12px; + border-radius: 50px; + margin: 34px auto; + background-color: ${(props) => props.theme.pointColor.gray}; +`; + +export default Calendar; diff --git a/components/common/DataTable.tsx b/components/common/DataTable.tsx new file mode 100644 index 00000000..25c507cf --- /dev/null +++ b/components/common/DataTable.tsx @@ -0,0 +1,140 @@ +import { Space, Table } from "antd"; +import { useState } from "react"; +import ApprovalModal from "@components/admin/ApprovalModal"; +import EmployeeHistoyModal from "@components/employee/EmployeeHistoyModal"; +import Button from "@components/common/Button"; +import type { ColumnsType } from "antd/es/table"; +import { IDataTableProps, IDataSourceItem } from "@lib/interface/Admin"; + +function DataTable({ tableTitle, dataSource, type }: IDataTableProps) { + const [open, setOpen] = useState(false); + const [employeeOpen, setEmployeeOpen] = useState(false); + const [details, setDetils] = useState(); + const [listUpdate, setListUpdate] = useState(false); + + const adminOnClickHandler = (data: IDataSourceItem) => { + setOpen(true); + setDetils(data); + }; + + const employeeOnClickHandler = (data: IDataSourceItem) => { + setEmployeeOpen(true); + setDetils(data); + setListUpdate(listUpdate); + }; + + //테이블 형식 + const columns: ColumnsType = [ + { + title: "사원명", + dataIndex: "사원명", + key: "사원명", + align: "center", + render: (_, data) => ( + <> +

{data.empName}

+ + ), + }, + { + title: "결재요청날짜", + dataIndex: "결재요청날짜", + key: "결재요청날짜", + align: "center", + render: (_, data) => ( + <> +

{data.createdAt}

+ + ), + }, + { + title: "유형", + dataIndex: "유형", + key: "유형", + align: "center", + render: (_, data) => ( + <> +

{data.orderType}

+ + ), + }, + { + title: "승인여부", + dataIndex: "승인여부", + key: "승인여부", + align: "center", + render: (_, data) => { + if (data.status === "대기") { + return ( + + ); + } else if (data.status === "승인") { + return ( + + ); + } else { + return ( + + ); + } + }, + }, + ]; + + return ( + +

{tableTitle}

+
+ {type === "admin" ? ( + + ) : ( + + )} + + ); +} + +export default DataTable; diff --git a/components/common/Header.tsx b/components/common/Header.tsx new file mode 100644 index 00000000..2c520ccb --- /dev/null +++ b/components/common/Header.tsx @@ -0,0 +1,136 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useCallback } from "react"; +import styled from "styled-components"; +import Image from "next/image"; +import logo from "public/workFairy_logo.png"; + +function Header() { + const router = useRouter(); + const onClick = useCallback(() => { + localStorage.removeItem("Token"); + localStorage.removeItem("empName"); + router.push("/login"); + }, [router]); + const username = + typeof window !== "undefined" && localStorage.getItem("empName"); + + return ( + + + + + + + +

+ {username} 님, 반갑습니다! +

+ 로그아웃 ⇢ +
+
+ +
+
+ ); +} + +const HeaderBlock = styled.header` + width: 100%; + height: 100px; + background: ${(props) => props.theme.headerColor}; + box-shadow: 0px 1px 20px rgba(139, 189, 175, 0.4); + display: flex; + justify-content: center; + align-items: center; + padding: 0 20px; +`; + +const HeaderContent = styled.div` + width: 1320px; + height: inherit; + /* background: coral; */ + max-width: 1280px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const LogoContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; +`; + +const UserWelcome = styled.div` + font-size: 18px; + display: flex; + align-items: center; + gap: 10px; + span { + &:first-child { + color: #1fbf92; + margin-left: 5px; + font-weight: 500; + } + } +`; + +const Nav = styled.nav` + ul { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + li { + a { + color: ${(props) => props.theme.inactiveColor}; + &.active { + font-weight: 700; + color: ${(props) => props.theme.activeColor}; + } + } + } + } +`; + +const LogOutBtn = styled.button` + font-size: 12px; + border: 1px solid #adb5bd; + padding: 5px 12px; + margin: 0 5px 4px 5px; + border-radius: 30px; + background-color: transparent; + color: #adb5bd; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + color: #f27676; + border: 1px solid #f27676; + } +`; + +export default Header; diff --git a/components/common/Input.tsx b/components/common/Input.tsx new file mode 100644 index 00000000..dd083ee1 --- /dev/null +++ b/components/common/Input.tsx @@ -0,0 +1,36 @@ +import styled, { css } from "styled-components"; + +// Interface +interface IInputProps { + [props: string]: any; +} + +function Input({ ...props }: IInputProps) { + return ; +} + +const StyledInput = styled.input<{ auth?: boolean }>` + width: 100%; + font-size: 1rem; + border: none; + padding-bottom: 0.5rem; + border-bottom: 1px solid ${(props) => props.theme.inputColor.authColor}; + outline: none; + ${(props) => + props.auth && + css` + border-bottom: 1px solid #ccc; + width: 330px; + &::placeholder { + color: #ccc; + } + &:focus { + border-bottom: 1px solid #000; + &::placeholder { + color: #707070; + } + } + `} +`; + +export default Input; diff --git a/components/common/Loading.tsx b/components/common/Loading.tsx new file mode 100644 index 00000000..1f74f3f6 --- /dev/null +++ b/components/common/Loading.tsx @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import { BounceLoader } from "react-spinners"; +import { theme } from "@styles/theme"; + +function Loading() { + return ( + + + 잠시만 기다려주세요 + + ); +} + +const LoadingBlock = styled.div` + width: 100%; + height: 100vh; + background: ${(props) => props.theme.borderColor}; // 임시 + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 32px; + position: fixed; + top: 0; + left: 0; + z-index: 99999; + span { + font-size: 20px; + font-weight: 700; + color: #fff; + } +`; + +export default Loading; diff --git a/components/common/README.md b/components/common/README.md new file mode 100644 index 00000000..bdaaa245 --- /dev/null +++ b/components/common/README.md @@ -0,0 +1,12 @@ +# 커스텀 버튼 사용방법 + +1. 컴포넌트를 불러온다. +2. 컴포넌트에 props로 다음 속성값을 넣으면 자동으로 사이즈와
색상이 적용된다. +3. 모든 속성의 타입은 boolean + +# 커스텀 버튼 props + +1. empButton (사원 로그인 버튼) +2. manageButton (관리자 로그인 버튼) +3. acceptButton (신청 버튼) +4. denyButton (반려 버튼) diff --git a/components/common/VaildMessage.tsx b/components/common/VaildMessage.tsx new file mode 100644 index 00000000..2bf1a7ce --- /dev/null +++ b/components/common/VaildMessage.tsx @@ -0,0 +1,5 @@ +function ValidMessage() { + return null; +} + +export default ValidMessage; diff --git a/components/employee/EmployeeDutyModalForm.tsx b/components/employee/EmployeeDutyModalForm.tsx new file mode 100644 index 00000000..d5b98286 --- /dev/null +++ b/components/employee/EmployeeDutyModalForm.tsx @@ -0,0 +1,274 @@ +import { useState, useEffect, ChangeEvent } from "react"; +import { Input, Modal, Select, Space, DatePicker } from "antd"; +import Button from "@components/common/Button"; +import { styled } from "styled-components"; +import Image from "next/image"; +import bottomDot from "public/bottomDot.png"; +import { employeeOrderApi } from "@lib/api/employeeAPI"; +import type { DatePickerProps, RangePickerProps } from "antd/es/date-picker"; + +interface IEmployeeDutyModalprops { + toggle?: boolean; + setListUpdate: React.Dispatch>; +} + +function EmployeeDutyModalForm({ + toggle, + setListUpdate, +}: IEmployeeDutyModalprops) { + const [isModalOpen, setIsModalOpen] = useState(false); + //modal에서 받는 inputVlaue값 + const [startAt, setStartAt] = useState(""); + const [endAt, setEndAt] = useState(""); + const [inputCategory, setInputCategory] = useState(""); + const [inputReason, setInputReason] = useState(""); + const [inputEtc, setInputEtc] = useState(""); + + // select 연차/당직일 + const { RangePicker } = DatePicker; + + const handleDateChange = ( + _: DatePickerProps["value"] | RangePickerProps["value"], + dateStrings: string[], + ) => { + const selectedDates = dateStrings; + const inputstartAt = selectedDates[0]; + const inputendAt = selectedDates[1]; + setStartAt(inputstartAt); + setEndAt(inputendAt); + console.log(startAt, endAt); + }; + + useEffect(() => { + if (endAt) { + setStartAt((startAt) => startAt); + setEndAt((endAt) => endAt); + } + }, [startAt, endAt]); + + // select 연차종류 + const selectCategory = (value: string) => { + setInputCategory(value); + }; + const searchCategory = (value: string) => { + console.log("search:", value); + }; + + const showModal = () => { + setIsModalOpen(true); + }; + + // 당직 등록 API + const dutyOrder = async () => { + try { + const res = await employeeOrderApi({ + orderType: "당직", + startAt: startAt, + endAt: endAt, + reason: null, + category: null, + etc: inputEtc, + }); + const Data = res?.data; + if (!Data.success) { + console.error("서버로 부터 응답, 에러 발생"); + return; + } + } catch (error) { + console.error("서버로 부터 응답 없음", error); + } finally { + setListUpdate((prev: boolean) => !prev); + setIsModalOpen(false); + setStartAt(""); + setEndAt(""); + setInputCategory(""); + setInputReason(""); + setInputEtc(""); + } + }; + + // 연차 등록 API + const annualOrder = async () => { + try { + const res = await employeeOrderApi({ + orderType: "연차", + startAt: startAt, + endAt: endAt, + reason: inputReason, + category: inputCategory, + etc: inputEtc, + }); + const Data = res?.data; + if (!Data.success) { + console.error("서버로 부터 응답, 에러 발생"); + return; + } + } catch (error) { + console.error("서버로 부터 응답 없음", error); + } finally { + setListUpdate((prev: boolean) => !prev); + setIsModalOpen(false); + setStartAt(""); + setEndAt(""); + setInputCategory(""); + setInputReason(""); + setInputEtc(""); + } + }; + + return ( + <> + {toggle ? ( + + ) : ( + + )} + + { + setIsModalOpen(false); + }} + footer={null} + width={520} + > + + {toggle ? "연차일" : "당직일"} + + + {toggle ? ( + + 휴가종류 + { + setSelectedOption(value); + console.log(selectedOption); + }} + /> + + Search + + + {visible && ( + <> + +

기본정보

+
+
    +
  • + 사원명 +

    {basicData.empName}

    +
  • +
  • + 사원번호 +

    {basicData.empNo}

    +
  • +
  • + 입사일 +

    {basicData.createdAt}

    +
  • +
+
+
+ +

연차 / 당직

+
+ + +
+
+ + )} + + + ); +} + +const Search = styled.section` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 50px 0 0; + margin-bottom: 30px; + h3 { + font-size: 20px; + margin-bottom: 10px; + } +`; + +const SearchBar = styled.div` + margin-bottom: 30px; +`; +const SearchForm = styled.form` + position: relative; + display: grid; + grid-template-columns: 100px auto; + align-items: center; + width: 950px; + height: 50px; + padding: 5px 20px; + box-sizing: border-box; + border-radius: 30px; + background: #fff; + box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.16); + + .ant-select:not(.ant-select-customize-input) .ant-select-selector { + border: none; + } +`; + +const StyledInput = styled.input` + height: 100%; + border: none; + outline: none; + padding-bottom: 3px; + font-size: 14px; + text-indent: 30px; +`; + +const StyeldBtn = styled.button` + position: absolute; + right: 15px; + background-color: transparent; + border: none; + padding: 10px; + cursor: pointer; +`; + +const BasicSection = styled.section` + width: 950px; + .container { + padding: 20px 30px 30px; + box-sizing: border-box; + border-radius: 30px; + background: #fff; + box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.16); + + ul { + width: 30%; + li { + margin-top: 20px; + display: flex; + span { + width: 100px; + } + } + } + } +`; + +const TableSection = styled.section` + margin-top: 30px; + .details { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 950px; + padding: 20px 30px 30px; + box-sizing: border-box; + border-radius: 30px; + background: #fff; + box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.16); + gap: 20px; + } +`; + +export default SearchPage; diff --git a/pages/employee/history.tsx b/pages/employee/history.tsx new file mode 100644 index 00000000..26cc59c9 --- /dev/null +++ b/pages/employee/history.tsx @@ -0,0 +1,84 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import DataTabel from "@components/common/DataTable"; +import Header from "@components/common/Header"; +import { useEffect, useState } from "react"; +import { styled } from "styled-components"; +import { employeeListApi } from "@lib/api/employeeAPI"; +import { IDataSourceItem } from "@lib/interface/Admin"; + +function History() { + const [datas, setDatas] = useState([]); + const [pageSize, setPageSize] = useState(10); + + const setlist = async () => { + try { + const res = await employeeListApi(pageSize); + const Data = res?.data; + setDatas(Data.response.content); + if (Data.response.totalElements > 10) { + setPageSize(Data.response.totalElements + 1); + } + if (!Data.success) { + console.error("등록 실패"); + return; + } + } catch (error) { + console.error("서버 응답 없음", error); + } + }; + + useEffect(() => { + setlist(); + }, []); + + return ( + <> +
+ +
+ { + return data.status == "대기"; + })} + /> +
+ +
+ { + return data.status != "대기"; + })} + /> +
+
+ + ); +} + +const Container = styled.section` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 50px 0 0; + + .details { + margin-top: 30px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 700px; + padding: 20px 30px 30px; + border-radius: 10px; + box-sizing: border-box; + background-color: #fff; + box-shadow: #e2e2e2 0px 5px 10px; + } +`; + +export default History; diff --git a/pages/employee/index.tsx b/pages/employee/index.tsx new file mode 100644 index 00000000..0ecff5d9 --- /dev/null +++ b/pages/employee/index.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from "react"; +import Header from "@components/common/Header"; +import EmployeeTableTab from "@components/employee/EmployeeTableTab"; +import { styled } from "styled-components"; +import { useRouter } from "next/router"; + +function EmployeePage() { + const router = useRouter(); + + useEffect(() => { + const token = localStorage.getItem("Token"); + if (token === undefined) { + alert("로그인 하십시오!"); + router.push("/login"); + } else { + return; + } + }, [router]); + + return ( + <> +
+ + + + + ); +} + +const Inner = styled.div` + width: 1200px; + margin: 100px auto; + display: flex; + position: relative; + justify-content: space-between; +`; +export default EmployeePage; diff --git a/pages/index.tsx b/pages/index.tsx new file mode 100644 index 00000000..279c0820 --- /dev/null +++ b/pages/index.tsx @@ -0,0 +1,97 @@ +import Button from "@components/common/Button"; +import Link from "next/link"; +import { styled } from "styled-components"; +import Image from "next/image"; +import Back1 from "public/back1.png"; +import Back2 from "public/back2.png"; + +function Home() { + return ( + <> + + + +
+ +
+ Work fairy +
+ +
+
+ + + 사원 로그인 + + + 관리자 로그인 + + +
+
+ + ); +} + +const HomeBlock = styled.div` + width: 100%; + padding: 0 20px; + display: flex; + justify-content: center; + align-items: center; + background-image: url("https://github.com/FAST-MINI-TEAM1/client-team1/assets/125563995/ff793dc1-4cfb-4c40-83f6-a5874d3465c9"); + background-size: 100%; +`; + +const HomeContent = styled.div` + width: 1320px; + height: 100vh; + min-width: 1320px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const Logo = styled.div` + font-family: "Satisfy", cursive; + font-size: 130px; + color: #4f4a45; + text-shadow: 0px 3px 7px rgba(81, 81, 81, 0.25); + margin-bottom: 100px; + max-width: 1320px; + display: flex; + justify-content: center; + align-items: center; + position: relative; + .deco { + position: absolute; + top: -126px; + left: -156px; + } + .deco2 { + position: absolute; + bottom: -146px; + right: -96px; + } +`; + +const ButtonWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; +`; + +const EmployeeButton = styled(Button)` + width: 240px; + height: 60px; +`; + +const AdminButton = styled(Button)` + width: 240px; + height: 60px; +`; + +export default Home; diff --git a/pages/login/README.md b/pages/login/README.md new file mode 100644 index 00000000..e69de29b diff --git a/pages/login/index.tsx b/pages/login/index.tsx new file mode 100644 index 00000000..88d9a2e5 --- /dev/null +++ b/pages/login/index.tsx @@ -0,0 +1,13 @@ +import AuthForm from "@components/auth/employee/AuthForm"; +import AuthTemplate from "@components/auth/employee/AuthTemplate"; +import { memo } from "react"; + +function LoginPage() { + return ( + + + + ); +} + +export default memo(LoginPage); diff --git a/pages/register/README.md b/pages/register/README.md new file mode 100644 index 00000000..e69de29b diff --git a/pages/register/index.tsx b/pages/register/index.tsx new file mode 100644 index 00000000..d2170180 --- /dev/null +++ b/pages/register/index.tsx @@ -0,0 +1,13 @@ +import AuthForm from "@components/auth/employee/AuthForm"; +import AuthTemplate from "@components/auth/employee/AuthTemplate"; +import { memo } from "react"; + +function RegsiterPage() { + return ( + + + + ); +} + +export default memo(RegsiterPage); diff --git a/public/back.png b/public/back.png new file mode 100644 index 00000000..94f1de58 Binary files /dev/null and b/public/back.png differ diff --git a/public/back1.png b/public/back1.png new file mode 100644 index 00000000..e0759b47 Binary files /dev/null and b/public/back1.png differ diff --git a/public/back2.png b/public/back2.png new file mode 100644 index 00000000..c75e2b4f Binary files /dev/null and b/public/back2.png differ diff --git a/public/bottomDot.png b/public/bottomDot.png new file mode 100644 index 00000000..d0c45965 Binary files /dev/null and b/public/bottomDot.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..eafb01d4 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 00000000..fbf0e25a --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/public/workFairy_logo.png b/public/workFairy_logo.png new file mode 100644 index 00000000..5bb7a6cc Binary files /dev/null and b/public/workFairy_logo.png differ diff --git a/styles/GlobalStyle.ts b/styles/GlobalStyle.ts new file mode 100644 index 00000000..3ba3d457 --- /dev/null +++ b/styles/GlobalStyle.ts @@ -0,0 +1,63 @@ +import { createGlobalStyle } from "styled-components"; + +const GlobalStyle = createGlobalStyle` + @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500&family=Satisfy&display=swap'); + html, body, div, span, applet, object, iframe, + h1, h2, h3, h4, h5, h6, p, blockquote, pre, + a, abbr, acronym, address, big, cite, code, + del, dfn, em, img, ins, kbd, q, s, samp, + small, strike, strong, sub, sup, tt, var, + b, u, i, center, + dl, dt, dd, ol, ul, li, + fieldset, form, label, legend, + table, caption, tbody, tfoot, thead, tr, th, td, + article, aside, canvas, details, embed, + figure, figcaption, footer, header, hgroup, + menu, nav, output, ruby, section, summary, + time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + box-sizing: border-box; + } + /* HTML5 display-role reset for older browsers */ + article, aside, details, figcaption, figure, + footer, header, hgroup, menu, nav, section { + display: block; + } + body { + background: ${(props) => props.theme.bgColor}; + background-image: url("https://github.com/FAST-MINI-TEAM1/client-team1/assets/125563995/ff793dc1-4cfb-4c40-83f6-a5874d3465c9"); + background-size: 100%; + box-sizing: inherit; + font-family: 'Noto Sans KR', sans-serif; + } + ol, ul { + list-style: none; + } + blockquote, q { + quotes: none; + } + blockquote:before, blockquote:after, + q:before, q:after { + content: ''; + content: none; + } + table { + border-collapse: collapse; + border-spacing: 0; + } + a { + text-decoration: none; + color: inherit; + display: block; + } + * { + box-sizing: inherit; + } +`; + +export default GlobalStyle; diff --git a/styles/styled.d.ts b/styles/styled.d.ts new file mode 100644 index 00000000..f5e163de --- /dev/null +++ b/styles/styled.d.ts @@ -0,0 +1,37 @@ +import "styled-components"; + +declare module "styled-components" { + export interface DefaultTheme { + bgColor: string; + headerColor: string; + textColor: string; + containerBoxColor: string; + buttonColor: { + empButton: string; + managerButton: string; + acceptButton: string; + denyButton: string; + pendingButton: string; + }; + inputColor: { + authColor: string; + }; + borderColor: string; + buttonTextColor: { + empColor: string; + adminColor: string; + }; + hoverColor: string; + inactiveColor: string; + activeColor: string; + pointColor: { + blue: string; + green: string; + yellow: string; + red: string; + black: string; + rightGray: string; + gray: string; + }; + } +} diff --git a/styles/theme.ts b/styles/theme.ts new file mode 100644 index 00000000..53fab006 --- /dev/null +++ b/styles/theme.ts @@ -0,0 +1,36 @@ +import { DefaultTheme } from "styled-components"; + +// 추후 색상 변경 +export const theme: DefaultTheme = { + bgColor: "#F9F9F9", + textColor: "#191919", + headerColor: "#fff", + containerBoxColor: "#fff", + buttonColor: { + empButton: "#fff", + managerButton: "#f27777", + acceptButton: "#1EBF91", + denyButton: "#F27676", + pendingButton: "#FFBD13", + }, + inputColor: { + authColor: "#666", + }, + borderColor: "#a55eea", + buttonTextColor: { + empColor: "#5d5d5d", + adminColor: "#fff", + }, + hoverColor: "transparent", + inactiveColor: "#939393", + activeColor: "#000", + pointColor: { + blue: "rgba(53, 95, 240, 1)", + green: "rgba(30, 191, 145, 1)", + yellow: "rgba(255, 189, 19, 1)", + red: "rgba(242, 118, 118, 1)", + black: "rgba(12, 12, 12, 1)", + rightGray: "#fbfbfb", + gray: "#D9D9D9", + }, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..63a202a6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": "./", + "paths": { + "@pages/*": ["pages/*"], + "@styles/*": ["styles/*"], + "@lib/*": ["lib/*"], + "@components/*": ["components/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}