diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..e7d4891a
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,41 @@
+{
+ // 전역변수 환경 설정
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "node": true
+ },
+
+ // npm을 통해 설치한 외부 ESLint 설정 등록 (eslint-config-{name}으로 설치)
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+
+ // ESLint에 지원할 JavaScript 옵션 설정
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "ecmaVersion": 12,
+ "sourceType": "module"
+ },
+
+ // parser 등록
+ "parser": "@typescript-eslint/parser",
+
+ // 사용자 규칙 추가할 플러그인 (eslint-plugin-{name}으로 설치)
+ "plugins": [
+ "@typescript-eslint",
+ "prettier"
+ ],
+
+ // 플러그인을 통해 설치한 것 외에 규칙 설정
+ "rules": {
+ "prettier/prettier": [
+ "error", {
+ "endOfLine": "auto"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..9a9a446c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+package-lock.json
+.env
+# testing
+/coverage
+
+# production
+# /build
+
+# misc
+.DS_Store
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..bacfe924
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "singleQuote": true,
+ "semi": true,
+ "tabWidth": 4,
+ "trailingComma": "all",
+ "printWidth": 120,
+ "arrowParens": "always"
+}
\ No newline at end of file
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..268a37e2
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+## Summary
+
+## Decribe details
+
+## Jira link
+
+## PR guidline check list
+
+Plz check if your code fulfills the following requirements.
+
+- [ ] The Commit message follows our conventions.
+- [ ] The File follows folder structure.
+- [ ] The PR title follows the convention rule.
+
+## any comment...
diff --git a/README.md b/README.md
index d6c98571..1aa9e5cb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,17 @@
+![main](https://github.com/2weeks-team/2weeks-team/assets/39702832/00e5c4cc-0466-4982-a941-38a01abb1a8a)
+
+## 📌 프로젝트 소개
+**FASTUDY**는 프로젝트 및 스터디원 모집 및 정보 공유 서비스입니다
+
+## 📌 배포 사이트
+https://2weeks-team-mzbe-c7xmzksqc-2weeks.vercel.app/
+
+
+야놀자 테크 캠프 토이 프로젝트 설명
+
# **📅 직원들을 위한 위키 사이트**
-직원 들을 위한 위키 사이트를 만들어보세요!
+직원들을 위한 위키 사이트를 만들어보세요!
위키 사이트에는 위키 뿐 아니라 여러 기능이 추가되어야 합니다!
### **[과제 수행 및 제출 방법]**
@@ -18,186 +29,211 @@
### **[필수 구현사항]**
-- 문서편집, revision 기능을 제공하여 업무일지를 작성할 수 있는 직원들을 위한 위키사이트 구현(마크다운 형식)
-- firebase database (Firestore) 이용
-- 모달을 활용한 근무 시간을 표시하는 시계 및 타이머 창 구현
-- 캐러셀을 활용한 회사 공지 페이지
-- **갤러리 페이지 / 업무일지 페이지 등 메뉴를 필터링 또는 카테고리화 하는 선택바 구현**
-- netlify 등을 이용한 정적 페이지 배포
-- TypeScript 사용 필수
-- 과제에 대한 설명을 포함한 `README.md` 파일 작성
- - 팀원별로 구현한 부분 소개
+[x] 문서편집, revision 기능을 제공하여 업무일지를 작성할 수 있는 직원들을 위한 위키사이트 구현(마크다운 형식)
+[x] firebase database (Firestore) 이용
+[x] 모달을 활용한 근무 시간을 표시하는 시계 및 타이머 창 구현
+[x] 캐러셀을 활용한 회사 공지 페이지
+[x] **갤러리 페이지 / 업무일지 페이지 등 메뉴를 필터링 또는 카테고리화 하는 선택바 구현**
+[x] netlify 등을 이용한 정적 페이지 배포
+[x] TypeScript 사용 필수
+[x] 과제에 대한 설명을 포함한 `README.md` 파일 작성
+[x] 팀원별로 구현한 부분 소개
### **[선택 구현사항]**
-- React 사용은 선택
-- 기타 동작이 완료되기 전에 로딩 애니메이션 구현
-- 페이지네이션
-- 관련된 기타 기능도 고려
-- eslint 설정, 커밋컨벤션, 문서화 등 팀프로젝트시 필요한 추가 작업들
-
----
-
-## 가이드
-
-아래 예시는 모두 하나의 의견입니다!
-
-따라하는게 아니라 자신만의 결과물을 만들어보세요.
-
-### 공지사항
-[영상 1]
-
-
-### **모달 타이머**
-[영상 2]
-
-
-https://github.com/KDT1-FE/Y_FE_Toy1/assets/38754963/20c18d28-5a01-4163-876c-be74a24f62db
-
-
-
-### **마크다운 위키사이트**
-[영상 3]
-
-
-https://github.com/KDT1-FE/Y_FE_Toy1/assets/38754963/08e3efca-8137-44d8-a0af-c62a668b810b
-
-
-
-### **갤러리**
-[영상 4]
-
----
-
-# **[Firestore]**
-
-Firestore에 대한 가이드입니다.
-
-자세한 내용은 [공식 홈페이지](https://firebase.google.com/?hl=ko) 를 찾아보길 적극 권장합니다!
-
-### **App init**
-
-```jsx
-import { getFirestore } from "firebase/firestore";
-
-export const db = getFirestore(fireBaseApp);
-```
-
-### Firestore 데이터 추가하기
-
-Firestore의 데이터를 추가하는 방법은 크게 두가지이다.
-
-1. Firebase console에서 손수 데이터 추가해주기
-2. 코드로 데이터 추가하기
-
-### 1. Firebase console에서 손수 데이터 추가해주기
-
-1. [Firebase console](https://console.firebase.google.com/u/0/?hl=ko)에 접속한다.
-2. 자신의 프로젝트를 선택한다.
-3. 왼쪽 메뉴에서 `Firestore Database`를 선택한다.
-4. `+ 버튼`을 눌러 컬렉션 > 문서를 마음대로 추가해준다.
-5. 필드를 추가하여 문서에 데이터를 넣어준다.
-
-### 2. 코드로 데이터 추가하기
-
-Firestore는 `setDoc`, `addDoc` 두 가지 함수로 데이터를 추가할 수 있다.
-
-이제 원하는 데이터를 추가해보자.
-
-**1. `addDoc`**
-
-`addDoc`은 아래와 같이 사용하여 원하는 데이터를 추가할 수 있다.
+[x] React 사용은 선택
+[x] 기타 동작이 완료되기 전에 로딩 애니메이션 구현
+[x] 페이지네이션
+[x] 관련된 기타 기능도 고려
+[x] eslint 설정, 커밋컨벤션, 문서화 등 팀프로젝트시 필요한 추가 작업들
+
+
+
+## 📌 팀 소개
+
+
+
+## 📌 Contributor
+> @[JeongMin83](https://github.com/JeongMin83) (진정민) : 메인페이지, 로그인, 모집
+@[seungjun222](https://github.com/seungjun222) (어승준) : 사이드바, 모집
+@[Yamyam-code](https://github.com/Yamyam-code) (백상원) : 마이페이지
+@[HOOOO98](https://github.com/HOOOO98) (박성후) : 갤러리
+@[syb0127](https://github.com/syb0127) (서예빈) : 위키
+
+## 📌 기술 스택
+
+### Environment
+
+
+
+### FrontEnd
+
+
+
+### DB
+
+
+
+
+
+### Deploy
+
+
+
+
+
+### Communication
+
+
+
+## 📌 주요 화면 및 기능
+### 로그인
+![login](https://github.com/2weeks-team/2weeks-team/assets/39702832/96939258-7566-4c2c-9dd8-43a98e2f1fc2)
+
+### 메인페이지
+![main](https://github.com/2weeks-team/2weeks-team/assets/39702832/00e5c4cc-0466-4982-a941-38a01abb1a8a)
+
+### 위키
+![wiki_demo (4)](https://github.com/2weeks-team/2weeks-team/assets/39702832/bb54b442-7eed-468f-80e1-5caaeb55149e)
+
+### 모집
+![recuritment_demo](https://github.com/2weeks-team/2weeks-team/assets/39702832/04d18ad5-1c37-4f00-baf1-e02f0611d09d)
+
+### 갤러리
+![gallery](https://github.com/2weeks-team/2weeks-team/assets/39702832/296d7cbe-fe18-4b3a-8de8-dd11d65b1b69)
+
+### 마이페이지
+![myPage](https://github.com/2weeks-team/2weeks-team/assets/39702832/fe4ced66-b2e9-4ab7-ad09-18a585fcb29a)
+
+## 📌 DB 스키마
+
+
+## 📌 유저 플로우
+
+
+## 📌 파일 구조
```
-import { addDoc, collection } from "firebase/firestore";
-
-const writtenDoc = await addDoc(collection(db, "wiki"), {
- title: "LGH",
- description: "허먼밀러...사고싶다...",
-});
-
-console.log("Document written with ID: ", writtenDoc.id);
-// 새로 생성된 Document의 ID를 반환한다.
-```
-
-원하는 데이터를 추가하기 위해선 먼저 원하는 collection을 선택해야 한다. 위 예제의 `addDoc` 안에서 사용한 `collection` 함수는 db상에 있는 collection을 선택하거나 없을 경우 새로운 collection을 생성하여 반환한다.
-
-Firebase의 Doc는 기본적으로 ID를 가져야 하는데, addDoc을 사용하면 ID를 자동으로 만든다. 또한, 이미 존재하는 Doc에 `addDoc`을 사용하면 에러가 발생한다.
-
-**2. `setDoc`**
-
-`setDoc`은 아래와 같이 사용하여 원하는 데이터를 추가할 수 있다.
-
-```
-import { setDoc, doc } from "firebase/firestore";
-
-await setDoc(doc(db, "wiki", "new-id"), {
- title: "LGH",
- description: "허먼밀러...사고싶다...",
-});
+2weeks-team/
+├── src/
+│ ├── common/
+│ │ ├── Footer/
+| | | .
+| | | .
+│ │ └── Header/
+│ │
+│ ├── components/
+│ │ ├── ChannelModal/
+│ │ ├── SidebarGallery/
+| | | .
+| | | .
+| | | .
+│ │ └── Slider/
+│ │
+│ ├── fonts/ # 추후 assets로
+│ ├── pages/
+│ │ ├── Gallery/
+│ │ ├── Home/
+| | | .
+| | | .
+| | | .
+│ │ └── Wiki/
+│ │
+│ ├── utils/
+│ │ ├── firebase.ts
+│ │ └── recoil.ts
+│ │
+│ ├── App.tsx
+│ ├── index.tsx
+│ ├── GlobalStyle.tsx
+│ ├── fonts.d.ts
+│ └── custom.d.ts
+│
+├── public/
+│ └── index.html
+│
+├── node_modules/
+├── package.json
+├── tsconfig.json
+├── .eslintrc
+├── README.md
+└── ...
```
-`addDoc`과의 차이점은
-
-1. **id** 를 지정해줘야함
-2. `collection` 대신 `doc`을 사용함
-3. 이미 존재하는 Doc에 사용가능
-
-3가지 이다.
-
-`setDoc`은 `addDoc`과 달리 collection이 아니라 doc를 선택해야 한다. 이는 `setDoc`이 데이터의 추가 뿐 아니라 데이터 덮어쓰기 기능도 가지고 있기 때문이다. 러프하게 생각해보면 `setDoc`은 데이터를 추가할 때
-
-1. doc을 선택하거나 새로운 doc을 생성
-2. doc의 내용을 덮어씀
-
-의 방식으로 동작하는 것이다.
-
-Doc을 선택하는 방법은 `doc` 함수를 사용하는 것이다. `[doc()](https://firebase.google.com/docs/reference/js/firestore_.md?hl=ko#doc)` 함수는 `DocumentReference` instance를 반환한다. 절대 경로를 사용하여 원하는 Document를 선택할 수 있다. 위에서 만들어둔 `wiki > completed` 문서는 아래처럼 불러올 수 있다.
-
-```
-import { doc } from "firebase/firestore";
-
-const docRef = doc(db, "wiki", "completed");
-```
-
-`doc` 함수의 3번째 인자가 바로 **id** 이다. id는 이미 존재하는 Doc의 id를 사용할 수도 있고, 새로운 id를 사용할 수도 있다. 존재하는 id를 사용하는 경우에는 해당 Doc의 데이터를 덮어쓰게 된다. 그렇지 않은 경우엔 새로운 Doc를 생성한다.
-
-어쨌거나 데이터를 새로 추가할 수 있는 것이다.
-
-### Firestore 데이터 수정하기
-
-Firestore의 데이터를 수정하는 방법은 크게 두가지이다.
-
-1. Firebase console에서 손수 데이터 수정해주기
-1. 코드로 데이터 수정하기
-
-1번은 데이터 생성과 비슷하게 진행하면 된다.
-
-**2. 코드로 데이터 수정하기**
-
-Firestore는 `setDoc`, `update` 두 가지 함수로 데이터를 추가할 수 있다.
-
-`setDoc`을 사용하는 방법은 위에 적혀있다.
-
-### `update`
-
-`setDoc`은 데이터를 덮어쓴다. 따라서 기존의 문서를 유지한 채 일부분의 데이터만 변경하고 싶어도 이전의 데이터를 모두 새로 입력해야 한다.
-
-그러나 `update`는 기존의 데이터를 유지한 채 일부분의 데이터만 변경할 수 있다.
-
-```jsx
-import { updateDoc, doc } from "firebase/firestore";
-
-await updateDoc(doc(db, "wiki", "new-id"), {
- description: "허먼밀러...200만원...",
-});
-```
-
-위와 같이 코드를 작성하면 `new-id`라는 id를 가진 문서의 description만 변경된다.
-
----
-
-### *참고 링크
-
-- **[Firebase](https://firebase.google.com/docs?hl=ko)**
-- [**프로토타입 프로젝트**](https://stfe.vercel.app/)
+### 📌 개발 기간 : `2주` `23.09.11 ~ 23.09.22`
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..3bbad772
--- /dev/null
+++ b/package.json
@@ -0,0 +1,88 @@
+{
+ "name": "my-app",
+ "homepage": "https://2weeks-team-mzbe.vercel.app/",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@emotion/styled": "^11.11.0",
+ "@emotion/styled.macro": "^0.10.6",
+ "@loadable/component": "^5.15.3",
+ "@mui/icons-material": "^5.14.9",
+ "@mui/joy": "^5.0.0-beta.7",
+ "@mui/material": "^5.14.9",
+ "@testing-library/jest-dom": "^5.17.0",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "@uiw/react-md-editor": "^3.23.5",
+ "assert": "^2.1.0",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^6.0.3",
+ "crypto-browserify": "^3.12.0",
+ "firebase": "^10.3.1",
+ "https-browserify": "^1.0.0",
+ "lodash.debounce": "^4.0.8",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "^1.0.1",
+ "prettier": "^3.0.3",
+ "react": "^18.2.0",
+ "react-beautiful-dnd": "^13.1.1",
+ "react-dom": "^18.2.0",
+ "react-infinite-scroll-component": "^6.1.0",
+ "react-material-ui-carousel": "^3.4.2",
+ "react-modal": "^3.16.1",
+ "react-router-dom": "^6.15.0",
+ "react-scripts": "5.0.1",
+ "react-scrollbars-custom": "^4.1.1",
+ "react-simply-carousel": "^9.0.4",
+ "recoil": "^0.7.7",
+ "recoil-persist": "^5.1.0",
+ "rehype-sanitize": "^6.0.0",
+ "stream-browserify": "^3.0.0",
+ "stream-http": "^3.2.0",
+ "styled-components": "^6.0.7",
+ "sweetalert": "^2.1.2",
+ "typescript": "^4.9.5",
+ "url": "^0.11.3",
+ "util": "^0.12.5",
+ "vm-browserify": "^1.1.2",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.5",
+ "@types/lodash.debounce": "^4.0.7",
+ "@types/node": "^20.6.3",
+ "@types/react": "^18.2.22",
+ "@types/react-beautiful-dnd": "^13.1.4",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-modal": "^3.16.0",
+ "@types/react-router-dom": "^5.3.3",
+ "@typescript-eslint/eslint-plugin": "^5.62.0",
+ "@typescript-eslint/parser": "^5.62.0",
+ "eslint": "^8.49.0",
+ "eslint-plugin-prettier": "^5.0.0"
+ }
+}
diff --git a/public/MainImg/galleryMain.png b/public/MainImg/galleryMain.png
new file mode 100644
index 00000000..ed89e5f2
Binary files /dev/null and b/public/MainImg/galleryMain.png differ
diff --git a/public/MainImg/recruitment.png b/public/MainImg/recruitment.png
new file mode 100644
index 00000000..784317aa
Binary files /dev/null and b/public/MainImg/recruitment.png differ
diff --git a/public/MainImg/recruitmentPost.png b/public/MainImg/recruitmentPost.png
new file mode 100644
index 00000000..f19e6669
Binary files /dev/null and b/public/MainImg/recruitmentPost.png differ
diff --git a/public/MainImg/wikiMain.png b/public/MainImg/wikiMain.png
new file mode 100644
index 00000000..bdc6a008
Binary files /dev/null and b/public/MainImg/wikiMain.png differ
diff --git a/public/WikiImg/icons8-done-50.png b/public/WikiImg/icons8-done-50.png
new file mode 100644
index 00000000..95cc1458
Binary files /dev/null and b/public/WikiImg/icons8-done-50.png differ
diff --git a/public/WikiImg/icons8-edit-50.png b/public/WikiImg/icons8-edit-50.png
new file mode 100644
index 00000000..2dc535e2
Binary files /dev/null and b/public/WikiImg/icons8-edit-50.png differ
diff --git a/public/_redirects b/public/_redirects
new file mode 100644
index 00000000..78f7f206
--- /dev/null
+++ b/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 00000000..3246a0a5
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ FASTUDY - 프로젝트/스터디 인원 모집 사이트
+
+
+ You need to enable JavaScript to run this app.
+
+
+
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 00000000..080d6c77
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 00000000..e9e57dc4
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 00000000..edebe6c8
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,94 @@
+import React, { useEffect } from 'react';
+import GlobalStyle from './GlobalStyle';
+import Header from './common/Header';
+import { Route, BrowserRouter, Routes } from 'react-router-dom';
+import Home from './pages/Home';
+import Wiki from './pages/Wiki';
+import Gallery from './pages/Gallery';
+import SignIn from './pages/SignIn';
+import LogIn from './pages/LogIn';
+import Recruitment from './pages/Recruitment';
+import RecruitmentDetail from './pages/RecruitmentDetail';
+import RecruitmentPost from './pages/RecruitmentPost';
+import RecruitmentEdit from './pages/RecruitmentEdit';
+import { ThemeProvider } from 'styled-components';
+import { useRecoilState } from 'recoil';
+import { Current, ThemeChange, ThemeRing, UserId } from './utils/recoil';
+import { readUser } from './utils/firebase';
+import { themeBorder } from './components/modal/MyPage/MyPageTheme';
+
+const App: React.FC = () => {
+ const [theme, setTheme] = useRecoilState(ThemeChange);
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [themeRing, setThemeRing] = useRecoilState(ThemeRing);
+
+ const defaultTheme = {
+ navBar: '#350d36',
+ sideMenu: '#3F0E40',
+ pointItem: '#4D2A51',
+ text: '#fff',
+ activeColor1: '#1164A3',
+ activeColor2: '#2BAC76',
+ recruitmentBack: '#ECE7EC',
+ };
+ const user = async () => {
+ {
+ const userData = await readUser('user', userId);
+
+ if (userData) {
+ const userTheme = userData['Theme'];
+ const userThemeRing = userData['ThemeBorder'];
+ setTheme(userTheme);
+ setThemeRing(userThemeRing);
+ localStorage.setItem('theme', JSON.stringify(userTheme));
+ localStorage.setItem('themeRing', JSON.stringify(userThemeRing));
+ }
+ }
+ };
+
+ if (!localStorage.getItem('theme')) {
+ localStorage.setItem('theme', JSON.stringify(defaultTheme));
+ localStorage.setItem('themeRing', JSON.stringify(themeBorder[0]));
+ }
+ useEffect(() => {
+ if (localStorage.getItem('theme') && localStorage.getItem('themeRing')) {
+ const localtheme = localStorage.getItem('theme');
+ const localthemeRing = localStorage.getItem('themeRing');
+ if (localtheme && localthemeRing) {
+ setTheme(JSON.parse(localtheme));
+ setThemeRing(JSON.parse(localthemeRing));
+ }
+ } else if (userId) {
+ user();
+ }
+ }, []);
+ useEffect(() => {
+ if (userId) {
+ user();
+ }
+ }, [userId]);
+
+ return (
+
+
+
+
+
+
+ }>
+ }>
+ }>
+ }>
+ }>
+ }>
+ }>
+ }>
+ }>
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/src/GlobalStyle.tsx b/src/GlobalStyle.tsx
new file mode 100644
index 00000000..d63ae99a
--- /dev/null
+++ b/src/GlobalStyle.tsx
@@ -0,0 +1,80 @@
+import { createGlobalStyle } from 'styled-components';
+import GmarketSansTTFBold from './fonts/GmarketSansTTFBold.ttf';
+import GmarketSansTTFMedium from './fonts/GmarketSansTTFMedium.ttf';
+import GmarketSansTTFLight from './fonts/GmarketSansTTFLight.ttf';
+import YanoljaTTF from './fonts/YanoljaTTF.ttf';
+
+const GlobalStyle = createGlobalStyle`
+ *, *::before, *::after {
+ box-sizing: border-box;
+ }
+
+ @font-face {
+ font-family: 'GmarketSansTTFBold';
+ src: local('GmarketSansTTFBold'), local('GmarketSansTTFBold');
+ font-style: normal;
+ src: url(${GmarketSansTTFBold}) format('truetype');
+ }
+ @font-face {
+ font-family: 'GmarketSansTTFMedium';
+ src: local('GmarketSansTTFMedium'), local('GmarketSansTTFMedium');
+ font-style: normal;
+ src: url(${GmarketSansTTFMedium}) format('truetype');
+ }
+ @font-face {
+ font-family: 'GmarketSansTTFLight';
+ src: local('GmarketSansTTFLight'), local('GmarketSansTTFLight');
+ font-style: normal;
+ src: url(${GmarketSansTTFLight}) format('truetype');
+ }
+ @font-face {
+ font-family: 'YanoljaTTF';
+ src: local('YanoljaTTF'), local('YanoljaTTF');
+ font-style: normal;
+ src: url(${YanoljaTTF}) format('truetype');
+ }
+ @import url('https://fonts.googleapis.com/css2?family=Fuggles&family=Gowun+Dodum&display=swap');
+
+ :root{
+ --navigation-background : #350d36;
+ --cell-background: #3F0E40;
+ --active-item: #1164A3;
+ --active-item-text: #FFFFFF;
+ --point-item: #4D2A51;
+ --text: #FFF;
+ --active-current-status: #2BAC76;
+ --mention-badge: #ECE7EC;
+ --navigation-background: #350D36;
+ --navigation-text: #FFF;
+ }
+
+ body {
+ margin: 0;
+ font-family: GmarketSansTTFMedium,-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+
+ background-color: ${(props) => props.theme.recruitmentBack};
+ font-size:16px;
+ line-height: 1.5;
+ width: 100vw;
+ }
+
+ a{
+ text-decoration:none;
+ }
+
+ h2, p {
+ margin: 0;
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ }
+
+ p {
+ font-size: 1rem;
+ }
+`;
+
+export default GlobalStyle;
diff --git a/src/common/Footer/fastuslogo-2-2.svg b/src/common/Footer/fastuslogo-2-2.svg
new file mode 100644
index 00000000..dabedfeb
--- /dev/null
+++ b/src/common/Footer/fastuslogo-2-2.svg
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/common/Footer/index.tsx b/src/common/Footer/index.tsx
new file mode 100644
index 00000000..1d88a4d7
--- /dev/null
+++ b/src/common/Footer/index.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { FooterComponent } from './style';
+
+const Footer: React.FC = () => {
+ return (
+
+
+ Github
+
+
+ );
+};
+
+export default Footer;
diff --git a/src/common/Footer/style.tsx b/src/common/Footer/style.tsx
new file mode 100644
index 00000000..c7e8cfd0
--- /dev/null
+++ b/src/common/Footer/style.tsx
@@ -0,0 +1,14 @@
+import styled from '@emotion/styled';
+
+export const FooterComponent = styled.div`
+ border-top: 1px solid #efefef;
+ text-align: center;
+ padding-top: 10px;
+
+ position: absolute;
+ bottom: 0;
+ z-index: 1;
+
+ height: 40px;
+ width: 100%;
+`;
diff --git a/src/common/Gallery/bin.png b/src/common/Gallery/bin.png
new file mode 100644
index 00000000..d8431824
Binary files /dev/null and b/src/common/Gallery/bin.png differ
diff --git a/src/common/Gallery/icons8-upload-64.png b/src/common/Gallery/icons8-upload-64.png
new file mode 100644
index 00000000..415235fc
Binary files /dev/null and b/src/common/Gallery/icons8-upload-64.png differ
diff --git a/src/common/Header/index.tsx b/src/common/Header/index.tsx
new file mode 100644
index 00000000..5609e571
--- /dev/null
+++ b/src/common/Header/index.tsx
@@ -0,0 +1,177 @@
+import React, { useEffect } from 'react';
+import {
+ HeaderComponent,
+ HomeHeaderComponent,
+ TitleAnchor,
+ AnchorContainer,
+ ListAnchor,
+ RightAnchorContainer,
+ LogoutButton,
+ LoginButton,
+} from './style';
+import { useRecoilState } from 'recoil';
+import { UserId, TimeLog, TimerOn, ThemeChange } from '../../utils/recoil';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { createTimelog, readUser } from '../../utils/firebase';
+import { CreateDay, CreateTime } from '../../components/modal/Hooks/WhatTime';
+import MyPageBtn from '../../components/modal/MyPage/MyPageBtn';
+import swal from 'sweetalert';
+import { RedCircle } from '../../components/modal/MyPage/style';
+
+const Header: React.FC = () => {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+
+ const logOutHandler = () => {
+ setUserId('');
+ navigate('/', { state: pathname });
+ };
+
+ const [timerOn, setTimerOn] = useRecoilState(TimerOn);
+ const [timeLog, setTimeLog] = useRecoilState(TimeLog);
+ const [theme, setTheme] = useRecoilState(ThemeChange);
+
+ // 페이지 전환 시 세션 스토리지에서 타임로그 현재 상태 받아오기
+
+ useEffect(() => {
+ if (sessionStorage.getItem('timerOn')) {
+ const sessionTimerOn = sessionStorage.getItem('timerOn');
+ setTimerOn(sessionTimerOn === '1' ? true : false);
+ const sessionTimelog = sessionStorage.getItem('timelog');
+ setTimeLog(sessionTimelog ? sessionTimelog : '');
+ console.log(userId);
+ }
+ }, [pathname]);
+ // 퇴실버튼을 누르거나 로그아웃 시 firestore에 데이터 전송
+ useEffect(() => {
+ if (!timerOn) {
+ if (timeLog.length > 0) {
+ createTimelog('user', userId, timeLog);
+ setTimeLog('');
+ sessionStorage.setItem('timerOn', '0');
+ sessionStorage.setItem('timelog', '');
+ return;
+ }
+ } else if (timerOn) {
+ sessionStorage.setItem('timerOn', '1');
+ sessionStorage.setItem('timelog', timeLog);
+ }
+ }, [timeLog]);
+ const logOutTimeCheck = () => {
+ if (timerOn && timeLog !== '') {
+ setTimeLog(timeLog + ' ' + ' ' + '-' + ' ' + ' ' + '퇴실' + ' ' + ' ' + CreateTime());
+ setTimerOn(false);
+ }
+ // 타임로그 기록 전 아이디 정보가 사라지는 것을 막기위해 setTimeout처리
+ setTimeout(() => {
+ logOutHandler();
+ }, 100);
+ };
+
+ const homeValued = location.pathname == '/';
+ console.log(homeValued);
+
+ return (
+ <>
+ {homeValued ? (
+
+
+ FASTUDY
+
+
+
+
+ 위키
+
+
+ 모집
+
+
+ 갤러리
+
+ {userId.length > 0 ? (
+ {
+ swal({
+ title: '로그아웃 하시겠습니까?',
+ icon: 'warning',
+ buttons: ['취소', '로그아웃'],
+ }).then((yes) => {
+ if (yes) {
+ if (timerOn) {
+ logOutTimeCheck();
+ } else {
+ logOutHandler();
+ }
+ }
+ });
+ }}
+ style={{ color: '#fff' }}
+ >
+ 로그아웃
+
+ ) : (
+
+ 로그인
+
+ )}
+ {userId.length > 0 && }
+ {timerOn && (
+
+ )}
+
+
+
+ ) : (
+
+ FASTUDY
+
+
+ 위키
+ 모집
+ 갤러리
+ {userId.length > 0 ? (
+ {
+ swal({
+ title: '로그아웃 하시겠습니까?',
+ icon: 'warning',
+ buttons: ['취소', '로그아웃'],
+ }).then((yes) => {
+ if (yes) {
+ if (timerOn) {
+ logOutTimeCheck();
+ } else {
+ logOutHandler();
+ }
+ }
+ });
+ }}
+ >
+ 로그아웃
+
+ ) : (
+
+ 로그인
+
+ )}
+ {userId.length > 0 && }
+ {timerOn && (
+
+ )}
+
+
+
+ )}
+ >
+ );
+};
+
+export default Header;
diff --git a/src/common/Header/style.tsx b/src/common/Header/style.tsx
new file mode 100644
index 00000000..ac625e39
--- /dev/null
+++ b/src/common/Header/style.tsx
@@ -0,0 +1,125 @@
+import styled from 'styled-components';
+export const HeaderComponent = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: ${(props) => props.theme.navBar};
+
+ position: fixed;
+ z-index: 2;
+
+ top: 0;
+ left: 0;
+ height: 72px;
+ width: 100%;
+`;
+
+export const HomeHeaderComponent = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: rgba(0, 0, 0, 0);
+
+ position: fixed;
+ z-index: 2;
+
+ top: 0;
+ left: 0;
+ height: 72px;
+ width: 100%;
+`;
+
+export const TitleAnchor = styled.a`
+ font-size: 32px;
+ font-weight: bold;
+ color: ${(props) => props.theme.text};
+ margin-left: 30px;
+`;
+
+export const AnchorContainer = styled.div`
+ float: right;
+`;
+
+export const RightAnchorContainer = styled.div`
+ width: 500px;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ align-items: center;
+ gap: 20px;
+`;
+
+export const ListAnchor = styled.a`
+ font-weight: 700;
+ font-size: 24px;
+ font-family:
+ GmarketSansTTFMedium,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ 'Roboto',
+ 'Oxygen',
+ 'Ubuntu',
+ 'Cantarell',
+ 'Fira Sans',
+ 'Droid Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ color: ${(props) => props.theme.text};
+`;
+
+export const LogoutButton = styled.a`
+ display: flex;
+ align-items: center;
+ height: 72px;
+ border-radius: 10%;
+ font-weight: 700;
+ font-size: 24px;
+ color: ${(props) => props.theme.text};
+ background-color: rgba(0, 0, 0, 0);
+ font-family:
+ GmarketSansTTFMedium,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ 'Roboto',
+ 'Oxygen',
+ 'Ubuntu',
+ 'Cantarell',
+ 'Fira Sans',
+ 'Droid Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ border: 0;
+ &:hover {
+ cursor: pointer;
+ }
+`;
+
+export const LoginButton = styled.a`
+ display: flex;
+ align-items: center;
+ height: 72px;
+ border-radius: 10%;
+ font-weight: 700;
+ font-size: 24px;
+ color: ${(props) => props.theme.text};
+ background-color: rgba(0, 0, 0, 0);
+ font-family:
+ GmarketSansTTFMedium,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ 'Roboto',
+ 'Oxygen',
+ 'Ubuntu',
+ 'Cantarell',
+ 'Fira Sans',
+ 'Droid Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ border: 0;
+ &:hover {
+ cursor: pointer;
+ }
+`;
diff --git a/src/common/fastcampusIcon-removebg-preview.png b/src/common/fastcampusIcon-removebg-preview.png
new file mode 100644
index 00000000..e25ff3de
Binary files /dev/null and b/src/common/fastcampusIcon-removebg-preview.png differ
diff --git a/src/common/profileImgloading.gif b/src/common/profileImgloading.gif
new file mode 100644
index 00000000..62d766a2
Binary files /dev/null and b/src/common/profileImgloading.gif differ
diff --git a/src/components/ChannelModal/index.tsx b/src/components/ChannelModal/index.tsx
new file mode 100644
index 00000000..7ed5bbc3
--- /dev/null
+++ b/src/components/ChannelModal/index.tsx
@@ -0,0 +1,117 @@
+import React, { useEffect, useState } from 'react';
+import Modal from 'react-modal';
+import { createChannelDoc, updateChannelDoc, addFieldToDoc, updateFieldKeyInDoc } from '../../utils/firebase';
+import { CloseButton, ModalTitle, CreateButton, FallbackButton, TextInput } from './style';
+
+interface ChannelModalProps {
+ isOpen: boolean;
+ closeModal: () => void;
+ collectionName: string;
+ modalType: string;
+ subChannelId: string;
+ channelId: string;
+}
+
+const ChannelModal: React.FC = ({
+ isOpen,
+ closeModal,
+ collectionName,
+ modalType,
+ subChannelId,
+ channelId,
+}) => {
+ const customStyles = {
+ content: {
+ top: '50%',
+ left: '50%',
+ right: 'auto',
+ bottom: 'auto',
+ marginRight: '-50%',
+ transform: 'translate(-50%, -50%)',
+ borderRadius: '3%',
+ width: '635px',
+ height: '370px',
+ },
+ overlay: {
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
+ },
+ };
+ const [name, setName] = useState('');
+
+ const handleNameChange = (event: React.ChangeEvent) => {
+ setName(event.target.value);
+ };
+
+ const handleSubmit = async () => {
+ try {
+ if (modalType === 'Create') {
+ await createChannelDoc(collectionName, name);
+ } else if (modalType === 'Update') {
+ await updateChannelDoc(collectionName, channelId, name);
+ } else if (modalType === 'CreateSub') {
+ const fieldValue = { content: '', time: '' };
+ await addFieldToDoc(collectionName, channelId, name, fieldValue);
+ } else if (modalType === 'UpdateSub') {
+ await updateFieldKeyInDoc(collectionName, channelId, subChannelId, name);
+ }
+ closeModal();
+ } catch (error) {
+ console.error('오류 발생:', error);
+ }
+ };
+
+ const getTitleText = () => {
+ switch (modalType) {
+ case 'Create':
+ return 'Create a channel';
+ case 'Update':
+ return 'Update a channel';
+ case 'CreateSub':
+ return 'Create a subchannel';
+ case 'UpdateSub':
+ return 'Update a subchannel';
+ default:
+ return '';
+ }
+ };
+ const title = getTitleText();
+
+ useEffect(() => {
+ if (modalType === 'Update' || modalType === 'UpdateSub') {
+ setName(modalType === 'Update' ? channelId : subChannelId);
+ } else {
+ setName('');
+ }
+ }, [modalType, channelId, subChannelId]);
+
+ return (
+
+
+ {title}
+ X
+
+ 이름
+
+ {80 - name.length}
+ 채널에서는 특정 주제에 대한 대화가 이루어집니다. 찾고 이해하기 쉬운 이름을 사용하세요.
+ {
+ setName('');
+ closeModal();
+ }}
+ >
+ 취소
+
+ 저장
+
+ );
+};
+
+export default ChannelModal;
diff --git a/src/components/ChannelModal/style.tsx b/src/components/ChannelModal/style.tsx
new file mode 100644
index 00000000..d0f98bd5
--- /dev/null
+++ b/src/components/ChannelModal/style.tsx
@@ -0,0 +1,63 @@
+import styled from 'styled-components';
+
+export const CloseButton = styled.button`
+ font-size: 1.5rem;
+ padding: 0 10px;
+ margin-left: auto;
+ border: none;
+ border-radius: 5px;
+ background-color: #ffffff;
+ &:hover {
+ background-color: #f0f0f0;
+ cursor: pointer;
+ }
+`;
+
+export const ModalTitle = styled.div`
+ font-weight: bold;
+ font-size: 1.7rem;
+`;
+
+export const FallbackButton = styled.button`
+ background-color: #ffffff;
+ color: black;
+ border: 0.5px solid black;
+ height: 13.5%;
+ width: 15%;
+ font-size: 1.2rem;
+ font-weight: bold;
+ border: 1px solid gray;
+ border-radius: 5px;
+ margin-top: 10%;
+ margin-left: 66%;
+ &:hover {
+ cursor: pointer;
+ }
+`;
+
+export const CreateButton = styled.button`
+ background-color: #2bac76;
+ color: #ffffff;
+ border: none;
+ height: 13.5%;
+ width: 15%;
+ font-size: 1.2rem;
+ font-weight: bold;
+ border: 1px solid gray;
+ border-radius: 5px;
+ margin-top: 10%;
+ margin-left: 1rem;
+ &:hover {
+ cursor: pointer;
+ }
+`;
+
+export const TextInput = styled.input`
+ height: 3rem;
+ width: 100%;
+ font-size: 1.1rem;
+ border-left: 1px solid black;
+ &:hover {
+ box-shadow: 0px 0px 7px 2px #1164a3;
+ }
+`;
diff --git a/src/components/SidebarGallery/index.tsx b/src/components/SidebarGallery/index.tsx
new file mode 100644
index 00000000..edb4fb40
--- /dev/null
+++ b/src/components/SidebarGallery/index.tsx
@@ -0,0 +1,120 @@
+import React, { useEffect, useState } from 'react';
+import { useRecoilState } from 'recoil';
+import { ThemeChange, channelState, subChannelState } from '../../utils/recoil';
+import {
+ AllChannelsWrapper,
+ ChannelWrapper,
+ ChannelFlexDiv,
+ SubChannelFlexDiv,
+ ChannelHr,
+ ChannelDiv,
+ SubChannelDiv,
+} from './style';
+
+import { handleGetDocs, themeType } from '../../utils/firebase';
+import { QuerySnapshot } from 'firebase/firestore';
+
+interface DocumentData {
+ [key: string]: any;
+}
+
+interface SidebarGalleryProps {
+ onKeyClick: (value: any) => void; // 클릭된 값의 핸들러 함수를 props로 받습니다.
+}
+
+const SidebarGallery: React.FC = ({ onKeyClick }) => {
+ const [docsWithFields, setDocsWithFields] = useState<{ docId: string; docKeys: string[]; docData: DocumentData }[]>(
+ [],
+ );
+ const [channel, setChannel] = useRecoilState(channelState);
+ const [subChannel, setSubChannel] = useRecoilState(subChannelState);
+ const defaultChannel = '레퍼런스 공유';
+ const defaultSubChannel = '취업';
+ const [back, setBack] = useState('');
+ const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange);
+
+ useEffect(() => {
+ const selected = (theme: themeType) => {
+ setBack(theme.activeColor1);
+ };
+ if (localStorage.getItem('theme')) {
+ const localtheme = localStorage.getItem('theme');
+ if (localtheme) {
+ const color = JSON.parse(localtheme);
+ selected(color);
+ }
+ }
+ }, [currentTheme]);
+
+ useEffect(() => {
+ if (!channel || !subChannel) {
+ setChannel(defaultChannel);
+ setSubChannel(defaultSubChannel);
+ }
+ }, [channel, subChannel, setChannel, setSubChannel]);
+
+ const isSubChannelActive = (channelName: string, subChannelName: string) => {
+ return channel === channelName && subChannel === subChannelName;
+ };
+
+ useEffect(() => {
+ const updatedQuerySnapshot = handleGetDocs('gallery', (querySnapshot: QuerySnapshot) => {
+ const data: { docId: string; docKeys: string[]; docData: DocumentData }[] = [];
+
+ querySnapshot.forEach((doc: any) => {
+ const docData = doc.data();
+ const docId = doc.id;
+ const docKeys = Object.keys(docData);
+ docKeys.sort(); // 서브채널을 알파벳순으로 정렬
+ data.push({ docId, docKeys, docData });
+ });
+ // 채널 목록을 알파벳순으로 정렬
+ data.sort((a, b) => a.docId.localeCompare(b.docId));
+ setDocsWithFields(data);
+ });
+
+ return () => {
+ updatedQuerySnapshot();
+ };
+ }, []);
+
+ const handleKeyClick = (value: any) => {
+ onKeyClick(value); // 클릭된 값을 상위 컴포넌트로 전달합니다.
+ };
+
+ return (
+
+ {docsWithFields.map((item, index) => (
+
+
+ # {item.docId}
+
+
+
+ {item.docKeys.map((item2, index2) => (
+ {
+ handleKeyClick(item.docData[item2]);
+ setChannel(item.docId);
+ setSubChannel(item2);
+ }}
+ style={{
+ color: isSubChannelActive(item.docId, item2) ? '#fff' : '',
+ backgroundColor: isSubChannelActive(item.docId, item2) ? back : '',
+ borderRadius: isSubChannelActive(item.docId, item2) ? '5px' : '',
+ marginRight: isSubChannelActive(item.docId, item2) ? '10px' : '',
+ fontWeight: isSubChannelActive(item.docId, item2) ? 'bold' : '',
+ }}
+ >
+ {item2}
+
+ ))}
+
+ {index === 0 && }
+
+ ))}
+
+ );
+};
+
+export default SidebarGallery;
diff --git a/src/components/SidebarGallery/style.tsx b/src/components/SidebarGallery/style.tsx
new file mode 100644
index 00000000..8828fe39
--- /dev/null
+++ b/src/components/SidebarGallery/style.tsx
@@ -0,0 +1,48 @@
+import styled from 'styled-components';
+
+export const AllChannelsWrapper = styled.div`
+ height: calc(100vh - 72px);
+ min-width: 16.25%;
+ max-width: 16.25%;
+ padding-top: 1%;
+ background-color: ${(props) => props.theme.sideMenu};
+ color: ${(props) => props.theme.text};
+`;
+
+export const ChannelWrapper = styled.div`
+ padding-left: 10%;
+`;
+
+export const ChannelFlexDiv = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ font-size: 1.4rem;
+`;
+
+export const SubChannelFlexDiv = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ font-size: 1.15rem;
+
+ &:hover {
+ background-color: ${(props) => props.theme.pointItem};
+ cursor: pointer;
+ }
+`;
+
+export const ChannelHr = styled.hr`
+ border: solid 1px ${(props) => props.theme.pointItrm};
+`;
+
+export const ChannelDiv = styled.div`
+ font-size: 1.4rem;
+ font-weight: bold;
+`;
+
+export const SubChannelDiv = styled.div`
+ font-size: 1.15rem;
+ font-weight: bold;
+ padding: 5px;
+`;
diff --git a/src/components/SidebarRecruitment/index.tsx b/src/components/SidebarRecruitment/index.tsx
new file mode 100644
index 00000000..3e26a821
--- /dev/null
+++ b/src/components/SidebarRecruitment/index.tsx
@@ -0,0 +1,86 @@
+import React, { useEffect, useState } from 'react';
+import { useRecoilState } from 'recoil';
+import { ThemeChange, channelState, subChannelState } from '../../utils/recoil';
+import { AllChannelsWrapper, ChannelWrapper, ChannelDiv, SubChannelDiv, ChannelHr } from './style';
+import { themeType } from '../../utils/firebase';
+
+const SidebarRecruitment: React.FC = () => {
+ const [channel, setChannel] = useRecoilState(channelState);
+ const [subChannel, setSubChannel] = useRecoilState(subChannelState);
+ const defaultChannel = 'study';
+ const defaultSubChannel = '전체';
+ const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange);
+ const [back, setBack] = useState('');
+
+ useEffect(() => {
+ const selected = (theme: themeType) => {
+ setBack(theme.activeColor1);
+ };
+ if (localStorage.getItem('theme')) {
+ const localtheme = localStorage.getItem('theme');
+ if (localtheme) {
+ const color = JSON.parse(localtheme);
+ selected(color);
+ }
+ }
+ }, [currentTheme]);
+
+ useEffect(() => {
+ if (!channel || !subChannel) {
+ setChannel(defaultChannel);
+ setSubChannel(defaultSubChannel);
+ }
+ }, [channel, subChannel, setChannel, setSubChannel]);
+
+ const isSubChannelActive = (channelName: string, subChannelName: string) => {
+ return channel === channelName && subChannel === subChannelName;
+ };
+
+ const renderSubChannelDiv = (channelName: string, subChannelName: string, label: string) => {
+ const isActive = isSubChannelActive(channelName, subChannelName);
+ return (
+ {
+ setChannel(channelName);
+ setSubChannel(subChannelName);
+ }}
+ >
+ {label}
+
+ );
+ };
+
+ return (
+
+
+ # 스터디
+
+ {renderSubChannelDiv('study', '전체', '전체')}
+ {renderSubChannelDiv('study', '코딩테스트', '코딩테스트')}
+ {renderSubChannelDiv('study', 'CS', 'CS')}
+ {renderSubChannelDiv('study', '면접', '면접')}
+ {renderSubChannelDiv('study', '알고리즘', '알고리즘')}
+
+
+
+
+
+ # 프로젝트
+
+ {renderSubChannelDiv('project', '전체', '전체')}
+ {renderSubChannelDiv('project', '토이 프로젝트', '토이 프로젝트')}
+ {renderSubChannelDiv('project', '연계 프로젝트', '연계 프로젝트')}
+
+
+
+ );
+};
+
+export default SidebarRecruitment;
diff --git a/src/components/SidebarRecruitment/style.tsx b/src/components/SidebarRecruitment/style.tsx
new file mode 100644
index 00000000..16783c3c
--- /dev/null
+++ b/src/components/SidebarRecruitment/style.tsx
@@ -0,0 +1,39 @@
+import styled from 'styled-components';
+
+export const AllChannelsWrapper = styled.div`
+ height: calc(100vh - 72px);
+
+ min-width: 16.25%;
+ max-width: 16.25%;
+ padding-top: 1%;
+ background-color: ${(props) => props.theme.sideMenu};
+ color: ${(props) => props.theme.text};
+`;
+
+export const ChannelWrapper = styled.div`
+ padding-left: 10%;
+`;
+
+export const ChannelDiv = styled.div`
+ font-size: 1.4rem;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ position: relative;
+`;
+
+export const SubChannelDiv = styled.div`
+ font-size: 1.15rem;
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding: 5px;
+ &:hover {
+ background-color: ${(props) => props.theme.pointItem};
+ cursor: pointer;
+ }
+`;
+
+export const ChannelHr = styled.hr`
+ border: solid 1px ${(props) => props.theme.pointItem};
+`;
diff --git a/src/components/SidebarWiki/index.tsx b/src/components/SidebarWiki/index.tsx
new file mode 100644
index 00000000..b61cedad
--- /dev/null
+++ b/src/components/SidebarWiki/index.tsx
@@ -0,0 +1,352 @@
+// SidebarWiki.tsx 파일 내에서
+import React, { useEffect, useState, useRef } from 'react';
+import { useRecoilState } from 'recoil';
+import { ThemeChange, channelState, subChannelState } from '../../utils/recoil';
+import {
+ AllChannelsWrapper,
+ ChannelWrapper,
+ DropDownOptions,
+ SubDropDownOptions,
+ ChannelFlexDiv,
+ SubChannelFlexDiv,
+ MoreHorizIconWrapper,
+ ChannelHr,
+ CreateChannelDiv,
+ OptionButton,
+ ChannelDiv,
+ SubChannelDiv,
+} from './style';
+
+import { handleGetDocs, deleteChannelDoc, deleteFieldFromDoc, DocumentData, themeType } from '../../utils/firebase';
+import { QuerySnapshot } from 'firebase/firestore';
+import ChannelModal from '../ChannelModal';
+import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
+import swal from 'sweetalert';
+
+interface SidebarWikiProps {
+ onKeyClick: (value: any) => void; // 클릭된 값의 핸들러 함수를 props로 받습니다.
+}
+
+const SidebarWiki: React.FC = ({ onKeyClick }) => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [modalType, setModalType] = useState('');
+ const [selectedSubChannelId, setSelectedSubChannelId] = useState('');
+ const [selectedChannelId, setSelectedChannelId] = useState('');
+
+ const [docsWithFields, setDocsWithFields] = useState<{ docId: string; docKeys: string[]; docData: DocumentData }[]>(
+ [],
+ );
+ const [channel, setChannel] = useRecoilState(channelState);
+ const [subChannel, setSubChannel] = useRecoilState(subChannelState);
+ const defaultChannels = ['기본 정보'];
+ const defaultSubChannels = ['과정 참여 규칙', '링크 모음'];
+
+ const [back, setBack] = useState('');
+ const [themeText, setThemeText] = useState('');
+ const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange);
+
+ useEffect(() => {
+ const selected = (theme: themeType) => {
+ setBack(theme.activeColor1);
+ setThemeText(theme.text);
+ };
+ if (localStorage.getItem('theme')) {
+ const localtheme = localStorage.getItem('theme');
+ if (localtheme) {
+ const color = JSON.parse(localtheme);
+ selected(color);
+ }
+ }
+ }, [currentTheme]);
+
+ useEffect(() => {
+ if (!channel || !subChannel) {
+ setChannel(defaultChannels[0]);
+ setSubChannel(defaultSubChannels[0]);
+ }
+ }, [channel, subChannel, setChannel, setSubChannel]);
+
+ const isSubChannelActive = (channelName: string, subChannelName: string) => {
+ return channel === channelName && subChannel === subChannelName;
+ };
+
+ // 드롭다운 상태를 각 아이템마다 저장할 배열 추가
+ const [dropdownStates, setDropdownStates] = useState([]);
+ const [dropdownSubStates, setDropdownSubStates] = useState([]);
+
+ const dropdownRef = useRef(null);
+ const dropdownSubRef = useRef(null);
+
+ const toggleDropDown = (index: number) => {
+ const updatedStates = [...dropdownStates];
+ updatedStates[index] = !updatedStates[index];
+ setDropdownStates(updatedStates);
+ };
+
+ const toggleDropDownSub = (docIndex: number, itemIndex: number) => {
+ const updatedSubStates = [...dropdownSubStates];
+ updatedSubStates[docIndex][itemIndex] = !updatedSubStates[docIndex][itemIndex];
+ setDropdownSubStates(updatedSubStates);
+ };
+
+ useEffect(() => {
+ const updatedQuerySnapshot = handleGetDocs('wiki', (querySnapshot: QuerySnapshot) => {
+ const data: { docId: string; docKeys: string[]; docData: DocumentData }[] = [];
+
+ querySnapshot.forEach((doc: any) => {
+ const docData = doc.data();
+ const docId = doc.id;
+ const docKeys = Object.keys(docData);
+ docKeys.sort(); // 서브채널을 알파벳순으로 정렬
+ data.push({ docId, docKeys, docData });
+ });
+ // 채널 목록을 알파벳순으로 정렬
+ data.sort((a, b) => a.docId.localeCompare(b.docId));
+ setDocsWithFields(data);
+ // 초기 드롭다운 상태를 false로 초기화
+ setDropdownStates(new Array(data.length).fill(false));
+ const subDropdownStates = new Array(data.length).fill([]).map(() => new Array(data.length).fill(false));
+ setDropdownSubStates(subDropdownStates);
+ });
+
+ return () => {
+ updatedQuerySnapshot();
+ };
+ }, []);
+
+ useEffect(() => {
+ // document에 클릭 이벤트 리스너를 추가하여 DropDownOptions 요소 외부를 클릭했을 때 초기 드롭다운 상태를 false로 변경
+ const handleClickOutside = (event: any, currentIndex: number) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ // 클릭한 요소가 DropDownOptions 영역 외부에 있는 경우
+ const updatedStates = [...dropdownStates];
+ updatedStates[currentIndex] = false;
+ setDropdownStates(updatedStates);
+ }
+ if (dropdownSubRef.current && !dropdownSubRef.current.contains(event.target)) {
+ // 클릭한 요소가 SubDropDownOptions 영역 외부에 있는 경우
+ const updatedSubStates = [...dropdownSubStates];
+ updatedSubStates[currentIndex].fill(false);
+ setDropdownSubStates(updatedSubStates);
+ }
+ };
+
+ document.addEventListener('mousedown', (event) => {
+ docsWithFields.forEach((_, index) => {
+ handleClickOutside(event, index);
+ });
+ });
+
+ return () => {
+ document.removeEventListener('mousedown', (event) => {
+ docsWithFields.forEach((_, index) => {
+ handleClickOutside(event, index);
+ });
+ });
+ };
+ }, [dropdownStates, dropdownSubStates, docsWithFields]);
+
+ const handleKeyClick = (value: any, channel: string, subChannel: string) => {
+ onKeyClick({ value, channel, subChannel }); // 클릭된 값을 상위 컴포넌트로 전달
+ };
+ const openModal = () => {
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ const collectionName = 'wiki';
+
+ return (
+ <>
+
+ {docsWithFields.map((item, index) => {
+ if (item.docId === defaultChannels[0]) {
+ return (
+
+
+ # {item.docId}
+
+
+
+ {item.docKeys.map((item2, index2) => (
+ {
+ handleKeyClick(item.docData[item2], item.docId, item2);
+ setChannel(item.docId);
+ setSubChannel(item2);
+ }}
+ style={{
+ color: isSubChannelActive(item.docId, item2) ? '#' : themeText,
+ backgroundColor: isSubChannelActive(item.docId, item2) ? back : '',
+ borderRadius: isSubChannelActive(item.docId, item2) ? '5px' : '',
+ fontWeight: isSubChannelActive(item.docId, item2) ? 'bold' : '',
+ }}
+ >
+ {item2}
+
+ ))}
+
+
+
+ );
+ }
+ return null;
+ })}
+
+ {docsWithFields.map((item, index) => {
+ if (item.docId !== defaultChannels[0]) {
+ return (
+
+
+ # {item.docId}
+
+ {
+ toggleDropDown(index);
+ console.log(dropdownStates);
+ }}
+ >
+
+ {dropdownStates[index] && (
+
+ {
+ setModalType('CreateSub');
+ setSelectedChannelId(item.docId);
+ openModal();
+ }}
+ >
+ 추가
+
+ {
+ setModalType('Update');
+ // setSelectedSubChannelId(item.docData);
+ setSelectedChannelId(item.docId);
+ openModal();
+ }}
+ >
+ 수정
+
+ {
+ swal({
+ title: '정말로 삭제하시겠습니까?',
+ text: '한번 삭제하면 되돌릴 수 없습니다!',
+ icon: 'warning',
+ buttons: ['취소', '삭제'],
+ dangerMode: true,
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('성공적으로 삭제되었습니다!', {
+ icon: 'success',
+ });
+ deleteChannelDoc('wiki', item.docId);
+ } else {
+ swal('삭제가 취소되었습니다!');
+ }
+ });
+ }}
+ >
+ 삭제
+
+
+ )}
+
+
+
+ {item.docKeys.map((item2, index2) => (
+ {
+ handleKeyClick(item.docData[item2], item.docId, item2);
+ setChannel(item.docId);
+ setSubChannel(item2);
+ }}
+ style={{
+ color: isSubChannelActive(item.docId, item2) ? themeText : '',
+ backgroundColor: isSubChannelActive(item.docId, item2) ? back : '',
+ borderRadius: isSubChannelActive(item.docId, item2) ? '5px' : '',
+ fontWeight: isSubChannelActive(item.docId, item2) ? 'bold' : '',
+ }}
+ >
+ {item2}
+
+ {
+ e.stopPropagation();
+ toggleDropDownSub(index, index2);
+ }}
+ >
+
+ {dropdownSubStates[index][index2] && (
+
+ {
+ setModalType('UpdateSub');
+ setSelectedSubChannelId(item2);
+ setSelectedChannelId(item.docId);
+ openModal();
+ }}
+ >
+ 수정
+
+ {
+ swal({
+ title: '정말로 삭제하시겠습니까?',
+ text: '한번 삭제하면 되돌릴 수 없습니다!',
+ icon: 'warning',
+ buttons: ['취소', '삭제'],
+ dangerMode: true,
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('성공적으로 삭제되었습니다!', {
+ icon: 'success',
+ });
+ deleteFieldFromDoc('wiki', item.docId, item2);
+ } else {
+ swal('삭제가 취소되었습니다!');
+ }
+ });
+ }}
+ >
+ 삭제
+
+
+ )}
+
+ ))}
+
+
+
+ );
+ }
+ return null;
+ })}
+ {
+ setModalType('Create');
+ openModal();
+ }}
+ >
+ + 채널 추가
+
+
+ {isModalOpen && (
+
+ )}
+ >
+ );
+};
+
+export default SidebarWiki;
diff --git a/src/components/SidebarWiki/style.tsx b/src/components/SidebarWiki/style.tsx
new file mode 100644
index 00000000..036a14d2
--- /dev/null
+++ b/src/components/SidebarWiki/style.tsx
@@ -0,0 +1,134 @@
+import styled from 'styled-components';
+
+export const AllChannelsWrapper = styled.div`
+ min-width: 16.25%;
+ max-width: 16.25%;
+ padding-top: 1%;
+ height: calc(100vh - 72px);
+
+ background-color: ${(props) => props.theme.sideMenu};
+ color: ${(props) => props.theme.text};
+ overflow: auto;
+
+ &::-webkit-scrollbar {
+ width: 12px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: #ffffffb3;
+ border-radius: 6px;
+ border: 3px solid transparent;
+ }
+`;
+
+export const ChannelWrapper = styled.div`
+ padding-left: 10%;
+`;
+
+export const DropDownOptions = styled.div`
+ background: #ffffff;
+ border-radius: 8px;
+ position: absolute;
+ top: 90%;
+ left: 55%;
+ min-width: 5rem;
+ min-width: 40%;
+
+ display: block;
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
+ padding: 5px 0;
+ z-index: 1;
+`;
+
+export const SubDropDownOptions = styled.div`
+ background: #ffffff;
+ border-radius: 8px;
+ position: absolute;
+ top: 90%;
+ left: 55%;
+ min-width: 40%;
+ display: block;
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
+ padding: 5px 0;
+ z-index: 1;
+`;
+
+export const MoreHorizIconWrapper = styled.div`
+ background-color: ${(props) => props.theme.pointItem};
+ padding: 1px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+ margin-right: 5%;
+ cursor: pointer;
+`;
+
+export const ChannelFlexDiv = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ font-size: 1.4rem;
+`;
+
+export const SubChannelFlexDiv = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ font-size: 1.15rem;
+ &:hover {
+ background-color: ${(props) => props.theme.pointItem};
+ cursor: pointer;
+ & ${MoreHorizIconWrapper} {
+ background-color: ${(props) => props.theme.sideMenu};
+ color: ${(props) => props.theme.text};
+ }
+ }
+`;
+
+export const ChannelHr = styled.hr`
+ border: solid 1px ${(props) => props.theme.pointItem};
+`;
+
+export const CreateChannelDiv = styled.div`
+ font-size: 1.4rem;
+ font-weight: bold;
+ padding: 5px 0px 5px 10%;
+
+ &:hover {
+ background-color: ${(props) => props.theme.pointItem};
+ cursor: pointer;
+ }
+`;
+
+export const OptionButton = styled.button`
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ font-size: 1.2rem;
+ border: none;
+ background-color: #ffffff;
+
+ &:hover {
+ background-color: ${(props) => props.theme.activeColor1};
+ cursor: pointer;
+ color: #ffffff;
+ }
+`;
+
+export const ChannelDiv = styled.div`
+ font-size: 1.4rem;
+ font-weight: bold;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+`;
+
+export const SubChannelDiv = styled.div`
+ font-size: 1.15rem;
+ font-weight: bold;
+ padding: 5px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+`;
diff --git a/src/components/Slider/index.tsx b/src/components/Slider/index.tsx
new file mode 100644
index 00000000..92c4597f
--- /dev/null
+++ b/src/components/Slider/index.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import banner01 from '../../common/mainImg/main-banner01.png';
+import banner02 from '../../common/mainImg/main-banner02.png';
+import banner03 from '../../common/mainImg/main-banner03.png';
+
+import Carousel from 'react-material-ui-carousel';
+import { Paper } from '@mui/material';
+import { SliderImg, SliderItem } from './style';
+// REactSimplyCarousel uninstall
+
+interface slideItem {
+ item: {
+ img: string;
+ };
+}
+
+function Item(props: slideItem) {
+ return (
+
+
+
+
+
+ );
+}
+
+const Slider: React.FC = () => {
+ const items = [
+ {
+ img: banner01,
+ },
+ {
+ img: banner02,
+ },
+ {
+ img: banner03,
+ },
+ ];
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ );
+};
+
+export default Slider;
diff --git a/src/components/Slider/style.tsx b/src/components/Slider/style.tsx
new file mode 100644
index 00000000..064eb7be
--- /dev/null
+++ b/src/components/Slider/style.tsx
@@ -0,0 +1,17 @@
+import styled from '@emotion/styled';
+
+export const SliderItem = styled.div`
+ width: 1440px;
+ height: 400px;
+
+ margin: 0 auto;
+
+ background-color: #efefef;
+`;
+
+export const SliderImg = styled.img`
+ display: block;
+
+ width: 1440px;
+ height: 400px;
+`;
diff --git a/src/components/modal/Hooks/WhatTime.ts b/src/components/modal/Hooks/WhatTime.ts
new file mode 100644
index 00000000..cd5ea8e2
--- /dev/null
+++ b/src/components/modal/Hooks/WhatTime.ts
@@ -0,0 +1,21 @@
+const CreateDay = () => {
+ // 현재 날짜를 생성
+ const date = new Date();
+ const year = date.getFullYear();
+ const month = date.getMonth();
+ const day = date.getDate();
+ return year + '.' + month + '.' + day;
+};
+
+const CreateTime = () => {
+ // 현재 시간을 생성
+ let Hours: number | string = new Date().getHours();
+ let Min: number | string = new Date().getMinutes();
+ let Sec: number | string = new Date().getSeconds();
+ Hours = String(Hours).padStart(2, '0');
+ Min = String(Min).padStart(2, '0');
+ Sec = String(Sec).padStart(2, '0');
+ return Hours + ' ' + ':' + ' ' + Min + ' ' + ':' + ' ' + Sec;
+};
+
+export { CreateDay, CreateTime };
diff --git a/src/components/modal/MyPage/MyPageBtn.tsx b/src/components/modal/MyPage/MyPageBtn.tsx
new file mode 100644
index 00000000..5002cec6
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageBtn.tsx
@@ -0,0 +1,66 @@
+import React, { useEffect, useState } from 'react';
+import { ModalBtn, ModalBtnImg } from './style';
+import MyPageModal from './MyPageModal';
+import { useRecoilState } from 'recoil';
+import { SlideOn, ThemeChange, UserEmail, UserId, UserImg, UserInfo, UserName } from '../../../utils/recoil';
+import { readUser } from '../../../utils/firebase';
+import Loading from '../../../common/profileImgloading.gif';
+
+export default function MyPageBtn() {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [showMyPage, setShowMyPage] = useState(false);
+ const [slideOn, setSlideOn] = useRecoilState(SlideOn);
+ const [userName, setUserName] = useRecoilState(UserName);
+ const [userEmail, setUserEmail] = useRecoilState(UserEmail);
+ const [userInfo, setUserInfo] = useRecoilState(UserInfo);
+ const [userImg, setUserImg] = useRecoilState(UserImg);
+
+ const onErrorImg = (e: any) => {
+ e.target.src = Loading;
+ };
+
+ useEffect(() => {
+ async function getUserData() {
+ try {
+ const user = await readUser('user', userId);
+ if (user) {
+ setUserName(user['name']);
+ setUserEmail(user['email']);
+ setUserImg(user['imageURL']);
+ setUserInfo(user['info']);
+ }
+ } catch {
+ console.log('error');
+ }
+ }
+ getUserData();
+ }, [userId]);
+
+ const handleMyPage = () => {
+ if (showMyPage) {
+ setSlideOn(false);
+ setTimeout(() => {
+ setShowMyPage(false);
+ }, 900);
+ } else {
+ setShowMyPage(true);
+ setTimeout(() => {
+ setSlideOn(true);
+ }, 10);
+ }
+ };
+ return (
+ {
+ if (userId.length > 0) {
+ handleMyPage();
+ } else {
+ alert('로그인 후 사용이 가능합니다.');
+ }
+ }}
+ >
+
+ {showMyPage && userId.length > 0 && }
+
+ );
+}
diff --git a/src/components/modal/MyPage/MyPageCommute.tsx b/src/components/modal/MyPage/MyPageCommute.tsx
new file mode 100644
index 00000000..5eee3d89
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageCommute.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import ShowCurrentTime from '../Timer/ShowCurrentTime';
+import Btns from '../Timer/Btns';
+import { CommuteModalBox } from './commuteStyle';
+
+interface OwnProps {
+ setTimeRenewal: React.Dispatch>;
+}
+
+const MyPageCommute: React.FC = ({ setTimeRenewal }) => {
+ return (
+
+
+
+
+ );
+};
+
+export default MyPageCommute;
diff --git a/src/components/modal/MyPage/MyPageModal.tsx b/src/components/modal/MyPage/MyPageModal.tsx
new file mode 100644
index 00000000..12a2cee7
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageModal.tsx
@@ -0,0 +1,76 @@
+import React, { useEffect, useState } from 'react';
+import { MarginLeft, MyPage, MyPageHeader, MyPageFooter, MyPageExitBtn, MyPageContents } from './style';
+import CloseIcon from '@mui/icons-material/Close';
+import MyPageUser from './MyPageUser';
+import MyPageTimelog from './MyPageTimelog';
+import { useRecoilState } from 'recoil';
+import { ReadTimelog, SlideOn, TimeLog, UserId } from '../../../utils/recoil';
+import MyPageCommute from './MyPageCommute';
+import FastcampusDday from '../Timer/FastcampusDday';
+import MyPageReadLog from './MyPageReadLog';
+import { readUser } from '../../../utils/firebase';
+import MyPageTheme from './MyPageTheme';
+
+interface OwnProps {
+ handleMyPage(): void;
+}
+
+const MyPageModal: React.FC = ({ handleMyPage }) => {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [slideOn, setSlideOn] = useRecoilState(SlideOn);
+ const [showTimerModal, setShowTimerModal] = useState(false);
+ const [showReadModal, setShowReadModal] = useState(false);
+ const [showModal, setShowModal] = useState(true);
+ const [timeRenewal, setTimeRenewal] = useState();
+ const [readTimelog, setReadTimelog] = useRecoilState(ReadTimelog);
+ const [timelog, setTimelog] = useRecoilState(TimeLog);
+
+ useEffect(() => {
+ async function getTimelog() {
+ try {
+ const userData = await readUser('user', userId);
+ if (userData) {
+ const timelog = userData['timelog'].reverse();
+ setReadTimelog(timelog);
+ }
+ } catch {
+ console.log('error');
+ }
+ }
+ getTimelog();
+ }, [timelog]);
+
+ const handleModal = (number: number) => {
+ if (number) {
+ setShowModal(true);
+ } else {
+ setShowModal(false);
+ }
+ };
+
+ return (
+ {
+ e.stopPropagation();
+ }}
+ value={slideOn}
+ >
+
+ Profile
+
+
+
+
+
+
+
+ {showModal && }
+ {!showModal && }
+
+
+
+
+ );
+};
+
+export default MyPageModal;
diff --git a/src/components/modal/MyPage/MyPageReadLog.tsx b/src/components/modal/MyPage/MyPageReadLog.tsx
new file mode 100644
index 00000000..35c0976d
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageReadLog.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { TimelogBox, TimelogBoxScroll, TimelogEl, TimelogText } from './style';
+import { ReadTimelog } from '../../../utils/recoil';
+import { useRecoilState } from 'recoil';
+
+export default function MyPageReadLog() {
+ const [readTimelog, setReadTimelog] = useRecoilState(ReadTimelog);
+
+ return (
+
+
+ {readTimelog.length > 0 ? (
+ readTimelog.map((e: string, i) => {
+ e = e.replace('|', '\n');
+ return (
+
+ {e}
+
+ );
+ })
+ ) : (
+
+ 현재 저장된 기록이 없습니다.
+
+ )}
+
+
+ );
+}
diff --git a/src/components/modal/MyPage/MyPageTheme.tsx b/src/components/modal/MyPage/MyPageTheme.tsx
new file mode 100644
index 00000000..c67beb2a
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageTheme.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import { MyPageThemeBox, ThemeColorEl, ThemeColors } from './style';
+import { useRecoilState } from 'recoil';
+import { ThemeChange, ThemeRing, UserId } from '../../../utils/recoil';
+import { updateUserTheme } from '../../../utils/firebase';
+
+const themeBorder = [
+ {
+ first: '3px solid #fff',
+ second: 'none',
+ third: 'none',
+ fourth: 'none',
+ },
+ {
+ first: 'none',
+ second: '3px solid #fff',
+ third: 'none',
+ fourth: 'none',
+ },
+ {
+ first: 'none',
+ second: 'none',
+ third: '3px solid #fff',
+ fourth: 'none',
+ },
+ {
+ first: 'none',
+ second: 'none',
+ third: 'none',
+ fourth: '3px solid #fff',
+ },
+];
+
+export default function MyPageTheme() {
+ const [theme, setTheme] = useRecoilState(ThemeChange);
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [themeRing, setThemeRing] = useRecoilState(ThemeRing);
+ const themes = {
+ eggPlant: {
+ navBar: '#350d36',
+ sideMenu: '#3F0E40',
+ pointItem: '#4D2A51',
+ text: '#fff',
+ activeColor1: '#1164A3',
+ activeColor2: '#2BAC76',
+ recruitmentBack: '#ECE7EC',
+ },
+ banana: {
+ navBar: '#FFC806',
+ sideMenu: '#FFEB84',
+ pointItem: '#FFF8D4',
+ text: '#591035',
+ activeColor1: '#c24d51',
+ activeColor2: '#4C6DC2',
+ recruitmentBack: '#fef1ad',
+ },
+ sweetDessert: {
+ navBar: '#FFC2C0',
+ sideMenu: '#FFEEED',
+ pointItem: '#fff',
+ text: '#4A154B',
+ activeColor1: '#f89d48',
+ activeColor2: '#37BD8D',
+ recruitmentBack: '#fff6f5',
+ },
+ indigo: {
+ navBar: '#001A5E',
+ sideMenu: '#7d9bfd',
+ pointItem: '#5d80ff',
+ text: '#fff',
+ activeColor1: '#50ce73',
+ activeColor2: '#2153FF',
+ recruitmentBack: '#e9eeff',
+ },
+ };
+
+ return (
+
+ Theme
+
+ {
+ setThemeRing(themeBorder[0]);
+ setTheme(themes.eggPlant);
+ updateUserTheme('user', userId, themes.eggPlant, themeBorder[0]);
+ localStorage.setItem('theme', JSON.stringify(themes.eggPlant));
+ localStorage.setItem('themeRing', JSON.stringify(themeBorder[0]));
+ }}
+ thisTheme={themes.eggPlant}
+ selectTheme={themeRing.first}
+ >
+ {
+ setThemeRing(themeBorder[1]);
+ setTheme(themes.banana);
+ updateUserTheme('user', userId, themes.banana, themeBorder[1]);
+ localStorage.setItem('theme', JSON.stringify(themes.banana));
+ localStorage.setItem('themeRing', JSON.stringify(themeBorder[1]));
+ }}
+ thisTheme={themes.banana}
+ selectTheme={themeRing.second}
+ >
+ {
+ setThemeRing(themeBorder[2]);
+ setTheme(themes.sweetDessert);
+ updateUserTheme('user', userId, themes.sweetDessert, themeBorder[2]);
+ localStorage.setItem('theme', JSON.stringify(themes.sweetDessert));
+ localStorage.setItem('themeRing', JSON.stringify(themeBorder[2]));
+ }}
+ thisTheme={themes.sweetDessert}
+ selectTheme={themeRing.third}
+ >
+ {
+ setThemeRing(themeBorder[3]);
+ setTheme(themes.indigo);
+ updateUserTheme('user', userId, themes.indigo, themeBorder[3]);
+ localStorage.setItem('theme', JSON.stringify(themes.indigo));
+ localStorage.setItem('themeRing', JSON.stringify(themeBorder[3]));
+ }}
+ thisTheme={themes.indigo}
+ selectTheme={themeRing.fourth}
+ >
+
+
+ );
+}
+
+export { themeBorder };
diff --git a/src/components/modal/MyPage/MyPageTimelog.tsx b/src/components/modal/MyPage/MyPageTimelog.tsx
new file mode 100644
index 00000000..c26d2f66
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageTimelog.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { FlexBox, FlexBoxColumn, MarginLeft, MyPageContents, RedCircle } from './style';
+import { useRecoilState } from 'recoil';
+import { TimerOn } from '../../../utils/recoil';
+import TimerIcon from '@mui/icons-material/Timer';
+import BookIcon from '@mui/icons-material/Book';
+import { ChangeTimelog, ChangeTimer } from './commuteStyle';
+
+interface OwnProps {
+ handleTimerModal(number: number): void;
+ handleReadModal(number: number): void;
+ showModal: boolean;
+}
+
+const MyPageTimelog: React.FC = ({ handleTimerModal, handleReadModal, showModal }) => {
+ const [timerOn, setTimerOn] = useRecoilState(TimerOn);
+
+ return (
+
+
+
+ Commute
+
+
+
+
+
+ {
+ handleTimerModal(1);
+ }}
+ style={{ cursor: 'pointer' }}
+ >
+
+
+ {
+ handleReadModal(0);
+ }}
+ style={{ cursor: 'pointer' }}
+ >
+
+
+
+ );
+};
+
+export default MyPageTimelog;
diff --git a/src/components/modal/MyPage/MyPageUser.tsx b/src/components/modal/MyPage/MyPageUser.tsx
new file mode 100644
index 00000000..08153c0a
--- /dev/null
+++ b/src/components/modal/MyPage/MyPageUser.tsx
@@ -0,0 +1,36 @@
+import { MyPageProfile, ProfileContent, ProfileImg } from './style';
+import { useRecoilState } from 'recoil';
+import { UserId, UserName, UserEmail, UserInfo, UserImg } from '../../../utils/recoil';
+import UserEditBtn from './UserEditBtn';
+import Textarea from '@mui/joy/Textarea';
+import { updateUserInfo } from '../../../utils/firebase';
+
+export default function MyPageUser() {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [userName, setUserName] = useRecoilState(UserName);
+ const [userEmail, setUserEmail] = useRecoilState(UserEmail);
+ const [userInfo, setUserInfo] = useRecoilState(UserInfo);
+ const [userImg, setUserImg] = useRecoilState(UserImg);
+
+ return (
+
+
+
+ {userName}
+
+
+ {userEmail}
+
+ );
+}
diff --git a/src/components/modal/MyPage/UserEditBtn.tsx b/src/components/modal/MyPage/UserEditBtn.tsx
new file mode 100644
index 00000000..9eb7f54b
--- /dev/null
+++ b/src/components/modal/MyPage/UserEditBtn.tsx
@@ -0,0 +1,21 @@
+import React, { useState } from 'react';
+import { ProfileEdit } from './style';
+import UserEditMdoal from './UserEditModal';
+
+const UserEditBtn: React.FC = () => {
+ const [userEditOn, setUserEditOn] = useState(false);
+ function handleEdit() {
+ if (userEditOn) {
+ setUserEditOn(false);
+ } else {
+ setUserEditOn(true);
+ }
+ }
+ return (
+
+
편집
+ {userEditOn &&
}
+
+ );
+};
+export default UserEditBtn;
diff --git a/src/components/modal/MyPage/UserEditModal.tsx b/src/components/modal/MyPage/UserEditModal.tsx
new file mode 100644
index 00000000..6e0ab25f
--- /dev/null
+++ b/src/components/modal/MyPage/UserEditModal.tsx
@@ -0,0 +1,113 @@
+import React, { useEffect, useState } from 'react';
+import { CloseBtn, Modal, ModalHeader, ModalWall } from '../Timer/style';
+import { EditBox, EditInput, EditInputBox, FlexAroundBox, InputImg, InputLabel, SubmitBtn, UploadBtn } from './style';
+import { updateUserEmail, updateUserImg, updateUserInfo, updateUserName, uploadStorage } from '../../../utils/firebase';
+import { useRecoilState } from 'recoil';
+import { UserEmail, UserId, UserImg, UserInfo, UserName } from '../../../utils/recoil';
+import swal from 'sweetalert';
+
+interface ownProps {
+ handleEdit(): void;
+}
+
+const UserEditModal: React.FC = ({ handleEdit }) => {
+ const [userName, setUserName] = useRecoilState(UserName);
+ const [userEmail, setUserEmail] = useRecoilState(UserEmail);
+ const [userInfo, setUserInfo] = useRecoilState(UserInfo);
+ const [userImg, setUserImg] = useRecoilState(UserImg);
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [userImgPre, setUserImgPre] = useState('');
+
+ useEffect(() => {
+ setUserImgPre(userImg);
+ }, []);
+ async function updateImg(image: React.ChangeEvent) {
+ if (image.target.files) {
+ const img = await image.target.files[0];
+ const imgURL = await uploadStorage(userId, img);
+ setUserImgPre(imgURL);
+ await window.URL.revokeObjectURL(imgURL);
+ }
+ }
+ async function updateProfile() {
+ try {
+ await updateUserName('user', userId, userName);
+ await updateUserEmail('user', userId, userEmail);
+ await updateUserInfo('user', userId, userInfo);
+ await updateUserImg('user', userId, userImgPre);
+ setUserImg(userImgPre);
+ } catch (error) {
+ console.log(error);
+ } finally {
+ swal({
+ title: '수정되었습니다!',
+ icon: 'success',
+ });
+ handleEdit();
+ }
+ }
+ return (
+
+ {
+ e.stopPropagation();
+ }}
+ >
+
+ 프로필 수정
+ X
+
+
+
+ 프로필사진
+
+ {
+ updateImg(e);
+ }}
+ />
+
+
+
+ 이름
+ ) => {
+ setUserName(e.target.value);
+ }}
+ >
+
+
+ 이메일
+ ) => {
+ setUserEmail(e.target.value);
+ }}
+ >
+
+
+
+
+
+ 수정하기
+
+
+
+
+
+ );
+};
+
+export default UserEditModal;
diff --git a/src/components/modal/MyPage/commuteStyle.tsx b/src/components/modal/MyPage/commuteStyle.tsx
new file mode 100644
index 00000000..b5842d2a
--- /dev/null
+++ b/src/components/modal/MyPage/commuteStyle.tsx
@@ -0,0 +1,126 @@
+import styled from 'styled-components';
+
+export const CommuteModalBox = styled.div`
+ width: 100%;
+ height: 40%;
+ min-height: 240px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`;
+
+export const CloseBtn = styled.button`
+ width: 35px;
+ height: 35px;
+ position: absolute;
+ font-size: 20px;
+ top: 10px;
+ right: 10px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ font-weight: 700;
+ border-radius: 25px;
+ transition: 0.5s all;
+ &:hover {
+ background-color: #c9c9c9;
+ }
+`;
+
+export const ModalHeader = styled.div`
+ background-color: ${(value) => value.theme.activeColor2};
+ width: 100%;
+ height: 80px;
+ font-size: 28px;
+ font-weight: 600;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: #fff;
+`;
+
+export const ShowTimerOn = styled.div<{ value: boolean }>`
+ visibility: ${(value) => (value.value ? 'visible' : 'hidden')};
+ position: absolute;
+ top: 2%;
+ right: 2%;
+ width: 60px;
+ height: 40px;
+ color: #fff;
+ background-color: #ca1212;
+ border-radius: 15px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 18px;
+`;
+
+export const TimeNowbox = styled.div`
+ width: 100%;
+ padding: 10% 0;
+ color: #000;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ & > p {
+ font-size: 20px;
+ margin-left: 2px;
+ }
+`;
+export const TimeNow = styled.span`
+ font-size: 50px;
+ font-weight: 700;
+`;
+
+export const TimerBtnBox = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ padding-bottom: 10%;
+`;
+
+export const TimerBtnClassic = styled.button`
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ width: 90px;
+ height: 52px;
+ border: none;
+ border-radius: 20px;
+ font-size: 22px;
+ font-weight: 700;
+ box-shadow: 0 3px 3px 1px #ced0d3;
+ cursor: pointer;
+`;
+
+export const TimerOnBtn = styled(TimerBtnClassic)<{ value: boolean }>`
+ margin-top: 2%;
+ color: ${(value) => (value.value ? '#000' : '#fff')};
+ background-color: ${(value) => (value.value ? '#ece7ec' : value.theme.activeColor2)};
+ box-shadow: ${(value) => (value.value ? '0 3px 3px 1px #ced0d3 inset' : '0 3px 3px 1px #ced0d3')};
+`;
+export const TimerOffBtn = styled(TimerBtnClassic)<{ value: boolean }>`
+ margin-top: 2%;
+
+ color: ${(value) => (value.value ? '#fff' : '#000')};
+ background-color: ${(value) => (value.value ? value.theme.activeColor2 : '#ece7ec')};
+ box-shadow: ${(value) => (value.value ? '0 3px 3px 1px #ced0d3' : '0 3px 3px 1px #ced0d3 inset')};
+`;
+export const ChangeTimer = styled.div<{ value: boolean }>`
+ border-radius: 5px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 3px;
+ box-shadow: ${(value) => (value.value ? '1px 2px 2px 1px #a0a0a0 inset' : 'none')};
+`;
+export const ChangeTimelog = styled.div<{ value: boolean }>`
+ border-radius: 5px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 3px;
+ box-shadow: ${(value) => (value.value ? 'none' : '1px 2px 2px 1px #a0a0a0 inset')};
+`;
diff --git a/src/components/modal/MyPage/style.tsx b/src/components/modal/MyPage/style.tsx
new file mode 100644
index 00000000..5761e74f
--- /dev/null
+++ b/src/components/modal/MyPage/style.tsx
@@ -0,0 +1,291 @@
+import styled, { keyframes } from 'styled-components';
+import { BtnClassic } from '../Timer/style';
+import { CommuteModalBox } from './commuteStyle';
+import CloudUploadOutlinedIcon from '@mui/icons-material/CloudUploadOutlined';
+import { themeType } from '../../../utils/firebase';
+
+export const ModalBtnImg = styled.img`
+ width: 50px;
+ height: 50px;
+ border-radius: 10px;
+ background-color: rgba(0, 0, 0, 0);
+ background-size: contain;
+ outline: none;
+ border: none;
+ cursor: pointer;
+`;
+
+export const ModalBtn = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const MyPage = styled.div<{ value: boolean }>`
+ right: ${(props) => (props.value ? '0' : '-500px')};
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 72px);
+ width: 340px;
+ background-color: #fafafa;
+ top: 72px;
+ border-left: 1px solid #ece7ec;
+ box-sizing: border-box;
+ z-index: 11;
+ transition: 1s;
+ overflow: auto;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+export const MyPageExitBtn = styled.button`
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border: none;
+ background-color: #fafafa;
+ border-radius: 10px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ &:hover {
+ background-color: #e2e2e2;
+ transition: 0.3s;
+ }
+`;
+
+export const MyPageCase = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ height: 5%;
+ min-height: 45px;
+ padding: 0 5%;
+`;
+export const MyPageHeader = styled(MyPageCase)`
+ border-bottom: 1px solid #ece7ec;
+`;
+
+export const MyPageProfile = styled.div`
+ height: 50%;
+ max-height: 540px;
+ min-height: 400px;
+ padding: 5% 5%;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 10px;
+`;
+export const ProfileImg = styled.img`
+ width: 220px;
+ min-height: 220px;
+ background-color: rgba(15, 15, 15, 0.1);
+ border-radius: 20px;
+`;
+export const ProfileContent = styled(MyPageCase)`
+ font-size: 20px;
+ font-weight: 700;
+ padding: 0;
+`;
+export const ProfileEdit = styled.span`
+ font-size: 18px;
+ color: var(--active-item);
+ cursor: pointer;
+ transition: 0.3s;
+ &:hover {
+ transform: scale(1.1);
+ }
+`;
+
+export const MyPageContents = styled(MyPageCase)`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 5px;
+ border-top: 1px solid #ece7ec;
+ border-bottom: 1px solid #ece7ec;
+`;
+
+export const MyPageThemeBox = styled(MyPageCase)`
+ border-top: 1px solid #ece7ec;
+ font-weight: 700;
+ font-size: 20px;
+`;
+
+export const ThemeColors = styled.div`
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ gap: 5px;
+`;
+export const ThemeColorEl = styled.button<{ thisTheme: themeType; selectTheme: string }>`
+ background-color: ${(props) => (props ? props.thisTheme.navBar : '#000')};
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 20px;
+ outline: ${(props) => (props ? props.selectTheme : '1px solid #000')};
+ cursor: pointer;
+ transition: 1s;
+ &:hover {
+ transition: 0.2s;
+ transform: scale(1.1);
+ }
+`;
+
+export const MyPageFooter = styled(MyPageContents)`
+ justify-content: center;
+ position: sticky;
+ bottom: 0;
+ background-color: #fafafa;
+`;
+
+export const MarginLeft = styled.span`
+ font-size: 20px;
+ font-weight: 700;
+ cursor: default;
+`;
+
+const Blink = keyframes`
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+`;
+export const RedCircle = styled.span<{ value: boolean }>`
+ display: ${(value) => (value.value ? 'block' : 'none')};
+ width: 10px;
+ height: 10px;
+ border-radius: 10px;
+ background-color: #ca1212;
+ animation: ${Blink} 1.5s 0s infinite;
+ animation-timing-function: linear;
+`;
+export const TimelogBox = styled(CommuteModalBox)`
+ height: 40%;
+ min-height: 210px;
+`;
+export const TimelogBoxScroll = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 40px;
+ width: 100%;
+ height: 100%;
+ padding: 14% 0;
+ background-color: #fafafa;
+ border-left: 1px solid #ece7ec;
+ overflow: auto;
+ transition: 1s;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ &:hover {
+ background-color: rgba(15, 15, 15, 0.1);
+ }
+`;
+
+export const TimelogEl = styled.div`
+ display: flex;
+ justify-content: center;
+ font-size: 20px;
+ width: 90%;
+ border: 1px solid #ece7ec;
+ border-radius: 20px;
+ background-color: #fafafa;
+ box-shadow: 1px 1px 6px 2px rgba(0, 0, 0, 0.3);
+ &:hover {
+ transform: scale(1.05);
+ transition: 0.2s;
+ }
+`;
+export const TimelogText = styled.p`
+ height: 90px;
+ white-space: pre;
+ display: flex;
+ font-size: 16px;
+ font-weight: 600;
+ color: #555555;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const EditBox = styled.div`
+ width: 100%;
+ padding: 5% 10%;
+ display: flex;
+ justify-content: space-between;
+`;
+export const EditInputBox = styled.div`
+ display: flex;
+ width: 50%;
+ gap: 20px;
+ flex-direction: column;
+ justify-content: space-around;
+`;
+export const InputLabel = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+export const EditInput = styled.input`
+ height: 35px;
+ border-radius: 5px;
+ font-size: 18px;
+ border: 1px solid #7e7e7e;
+ &:focus {
+ outline: none;
+ border: none;
+ box-shadow: 0 1px 6px ${(props) => props.theme.activeColor1};
+ }
+`;
+export const InputImg = styled.img`
+ width: 13rem;
+ height: 13rem;
+ border-radius: 20px;
+ background-color: gray;
+`;
+export const SubmitBtn = styled(BtnClassic)`
+ width: 150px;
+ height: 60px;
+ border-radius: 10px;
+ color: #fff;
+ background-color: ${(props) => props.theme.activeColor2};
+`;
+export const FlexBox = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+`;
+export const FlexAroundBox = styled.div`
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ gap: 5px;
+`;
+export const FlexBoxColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+`;
+export const UploadBtn = styled(CloudUploadOutlinedIcon)`
+ display: block;
+ width: 70px;
+ height: 60px;
+ padding: 5px;
+ border-radius: 10px;
+ background-color: ${(props) => props.theme.activeColor1};
+ color: #fafafa;
+ box-shadow: 0 3px 3px 1px #ced0d3;
+ cursor: pointer;
+`;
diff --git a/src/components/modal/Timer/Btns.tsx b/src/components/modal/Timer/Btns.tsx
new file mode 100644
index 00000000..8a35de38
--- /dev/null
+++ b/src/components/modal/Timer/Btns.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { useRecoilState } from 'recoil';
+import { TimeLog, TimerOn, UserId } from '../../../utils/recoil';
+import { CreateTime, CreateDay } from '../Hooks/WhatTime';
+import { TimerBtnBox, TimerOffBtn, TimerOnBtn } from '../MyPage/commuteStyle';
+import swal from 'sweetalert';
+
+const Btns: React.FC = () => {
+ // 입실버튼을 눌렀을 때 입실 시간을 기록, 퇴실버튼을 눌렀을 때 퇴실시간 기록
+ const [userId, setUserId] = useRecoilState(UserId);
+
+ const [timerOn, setTimerOn] = useRecoilState(TimerOn);
+ const [timeLog, setTimeLog] = useRecoilState(TimeLog);
+
+ const timerSwitch = (set = 0) => {
+ if (!timerOn && set) {
+ setTimeLog(`[${CreateDay()}]` + ' ' + '|' + '입실' + ' ' + ' ' + CreateTime());
+ setTimerOn(true);
+ } else if (set === 0) {
+ setTimerOn(false);
+ setTimeLog(timeLog + ' ' + ' ' + '-' + ' ' + ' ' + '퇴실' + ' ' + ' ' + CreateTime());
+ }
+ };
+
+ return (
+
+ {
+ swal({
+ title: '기록을 시작하시겠습니까?',
+ text: '*퇴실 하지않고 페이지 종료 시 입실기록이 삭제됩니다.',
+ icon: 'info',
+ buttons: ['취소', '시작'],
+ }).then((willDelete) => {
+ if (willDelete) {
+ if (userId.length > 0 && !timerOn) {
+ timerSwitch(1);
+ }
+ }
+ });
+
+ e.stopPropagation();
+ }}
+ >
+ 입실
+
+ {
+ if (timerOn) {
+ swal({
+ title: '현재 시간까지 기록됩니다!',
+ icon: 'info',
+ buttons: ['취소', '기록'],
+ }).then((willDelete) => {
+ if (willDelete) {
+ timerSwitch();
+ }
+ });
+ }
+ e.stopPropagation();
+ }}
+ >
+ 퇴실
+
+
+ );
+};
+
+export default Btns;
diff --git a/src/components/modal/Timer/FastcampusDday.tsx b/src/components/modal/Timer/FastcampusDday.tsx
new file mode 100644
index 00000000..cff89853
--- /dev/null
+++ b/src/components/modal/Timer/FastcampusDday.tsx
@@ -0,0 +1,33 @@
+import React, { useEffect, useState } from 'react';
+import { Dday } from './style';
+import Logo from '../../../common/fastcampusIcon-removebg-preview.png';
+
+interface OwnProps {
+ timeRenewal: string | void;
+}
+
+const FastcampusDday: React.FC = ({ timeRenewal }) => {
+ const [dDay, setDday] = useState(0);
+ const dDaySet = () => {
+ const cDay: Date = new Date('2024-01-30');
+ const today: Date = new Date();
+ today.setHours(0, 0, 0, 0);
+ setDday(Math.floor((cDay.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)));
+ };
+
+ //모달창에 처음으로 들어갈 때 디데이를 보여주기 위한 useEffect
+ useEffect(() => {
+ dDaySet();
+ }, []);
+ //디데이를 실시간으로 갱신시켜주는 useEffect
+ useEffect(() => {
+ dDaySet();
+ }, [{ timeRenewal }]);
+ return (
+
+
+ D - {dDay}
+
+ );
+};
+export default FastcampusDday;
diff --git a/src/components/modal/Timer/ShowCurrentTime.tsx b/src/components/modal/Timer/ShowCurrentTime.tsx
new file mode 100644
index 00000000..078c2ba6
--- /dev/null
+++ b/src/components/modal/Timer/ShowCurrentTime.tsx
@@ -0,0 +1,38 @@
+import React, { useEffect, useState } from 'react';
+import { CreateTime } from '../Hooks/WhatTime';
+import { TimeNow, TimeNowbox } from '../MyPage/commuteStyle';
+
+interface OwnProps {
+ setTimeRenewal: React.Dispatch>;
+}
+
+const ShowCurrentTime: React.FC = ({ setTimeRenewal }) => {
+ const [currentTime, setCurrentTime] = useState('');
+ const timeSet = () => {
+ const time = CreateTime();
+ setCurrentTime(time);
+ return time;
+ };
+ //모달창에 처음으로 들어갈 때 시간을 보여주기 위한 useEffect
+ useEffect(() => {
+ timeSet();
+ }, []);
+ //시간을 실시간으로 갱신시켜주는 useEffect
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTimeRenewal(timeSet());
+ }, 1000);
+ return () => clearInterval(interval);
+ }, [currentTime]);
+
+ return (
+
+
+
현재 시간
+
{currentTime}
+
+
+ );
+};
+
+export default ShowCurrentTime;
diff --git a/src/components/modal/Timer/style.tsx b/src/components/modal/Timer/style.tsx
new file mode 100644
index 00000000..cdbf7341
--- /dev/null
+++ b/src/components/modal/Timer/style.tsx
@@ -0,0 +1,84 @@
+import styled from 'styled-components';
+
+export const Modal = styled.div`
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 620px;
+ height: 400px;
+ background-color: #fafafa;
+ border-radius: 20px;
+ box-shadow: 0 5px 5px 2px #7e7e7e;
+ overflow: hidden;
+`;
+
+export const ModalWall = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 100;
+ background-color: rgba(0, 0, 0, 0.6);
+`;
+
+export const CloseBtn = styled.button`
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ font-size: 18px;
+ top: 10px;
+ right: 10px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ font-weight: 700;
+ border-radius: 25px;
+ cursor: pointer;
+ transition: 0.5s all;
+ &:hover {
+ background-color: #c9c9c9;
+ }
+`;
+
+export const ModalHeader = styled.div`
+ background-color: ${(props) => props.theme.navBar};
+ width: 100%;
+ height: 80px;
+ font-size: 28px;
+ font-weight: 600;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: #fff;
+`;
+
+export const Dday = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #000;
+ font-weight: 600;
+`;
+
+export const BtnBox = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+`;
+
+export const BtnClassic = styled.button`
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ width: 120px;
+ height: 62px;
+ border: none;
+ border-radius: 20px;
+ font-size: 24px;
+ font-weight: 700;
+ box-shadow: 0 3px 3px 1px #ced0d3;
+ cursor: pointer;
+`;
diff --git a/src/custom.d.ts b/src/custom.d.ts
new file mode 100644
index 00000000..03f7e4ab
--- /dev/null
+++ b/src/custom.d.ts
@@ -0,0 +1,4 @@
+declare module '*.jpg';
+declare module '*.png';
+declare module '*.jpeg';
+declare module '*.gif';
diff --git a/src/fonts.d.ts b/src/fonts.d.ts
new file mode 100644
index 00000000..1764f41e
--- /dev/null
+++ b/src/fonts.d.ts
@@ -0,0 +1 @@
+declare module '*.ttf';
diff --git a/src/fonts/GmarketSansTTFBold.ttf b/src/fonts/GmarketSansTTFBold.ttf
new file mode 100644
index 00000000..0d20d4a6
Binary files /dev/null and b/src/fonts/GmarketSansTTFBold.ttf differ
diff --git a/src/fonts/GmarketSansTTFLight.ttf b/src/fonts/GmarketSansTTFLight.ttf
new file mode 100644
index 00000000..f315bd35
Binary files /dev/null and b/src/fonts/GmarketSansTTFLight.ttf differ
diff --git a/src/fonts/GmarketSansTTFMedium.ttf b/src/fonts/GmarketSansTTFMedium.ttf
new file mode 100644
index 00000000..2ac3b7ff
Binary files /dev/null and b/src/fonts/GmarketSansTTFMedium.ttf differ
diff --git a/src/fonts/YanoljaTTF.ttf b/src/fonts/YanoljaTTF.ttf
new file mode 100644
index 00000000..34f86028
Binary files /dev/null and b/src/fonts/YanoljaTTF.ttf differ
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 00000000..5e8646b0
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import { RecoilRoot } from 'recoil';
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+root.render(
+ //
+
+
+ ,
+ //
+);
diff --git a/src/pages/Gallery/UploadModal/Modal.tsx b/src/pages/Gallery/UploadModal/Modal.tsx
new file mode 100644
index 00000000..c98b2df3
--- /dev/null
+++ b/src/pages/Gallery/UploadModal/Modal.tsx
@@ -0,0 +1,161 @@
+import React, { useState, ChangeEvent, FormEvent } from 'react';
+import {
+ BtnAlign,
+ CancelBtn,
+ Description,
+ Formalign,
+ InputAndPreview,
+ InputContainer,
+ LinkInput,
+ LinkInputContainer,
+ ModalContainer,
+ ModalFirstLine,
+ ModalLabel,
+ ModalTextarea,
+ PlaceHolder,
+ PreviewBox,
+ PreviewImg,
+ SubmitBtn,
+} from '../style';
+import { ref, getDownloadURL, uploadBytes, StorageReference } from 'firebase/storage';
+import { doc, updateDoc, arrayUnion, DocumentReference } from 'firebase/firestore';
+import { storage, firestore } from '../../../utils/firebase';
+
+interface ModalProps {
+ onClose: () => void;
+}
+
+const Modal: React.FC = ({ onClose }) => {
+ const [link, setLink] = useState(''); // 링크 입력 상태
+ const [imageFile, setImageFile] = useState(null); // 이미지 파일 상태
+ const [thumbnailURL, setThumbnailURL] = useState(null);
+ const [textValue, setTextValue] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+
+ const handleTextChange = (e: ChangeEvent) => {
+ setTextValue(e.target.value);
+ };
+
+ // 링크 입력 핸들러
+ const handleLinkChange = (e: ChangeEvent) => {
+ setLink(e.target.value);
+ };
+
+ // 이미지 파일 선택 핸들러
+ const handleImageChange = (e: ChangeEvent) => {
+ if (e.target.files) {
+ const selectedFile = e.target.files[0];
+ setImageFile(selectedFile);
+ }
+ };
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ setIsUploading(true);
+
+ // 유니크한 이미지 이름
+ const uniqueName = Date.now();
+ // storage 경로에 현재 시간을 추가하여 고유한 경로 참조
+ const storageRef: StorageReference = ref(storage, 'thumbnailR/' + uniqueName);
+
+ // storage에 이미지 파일이 존재하는 경우에만 업로드
+ if (imageFile) {
+ await uploadBytes(storageRef, imageFile);
+ }
+
+ // storage에 업로드 된 이미지 URL 가져오기
+ const url: string = await getDownloadURL(storageRef);
+ // thumbnaulURL 에 이미지 url 할당
+ setThumbnailURL(url);
+
+ // firestore 경로에 고유한 경로 참조
+ const storeRef: DocumentReference = doc(firestore, 'gallery', '레퍼런스 공유');
+ // firestore 추가할 데이터
+ const newArticle = {
+ thumbnailURL: url,
+ recruitURL: link,
+ index: uniqueName,
+ description: textValue,
+ };
+ // firestore 데이터를 취업/articleR 필드에 추가
+ await updateDoc(storeRef, {
+ '취업.articleR': arrayUnion(newArticle),
+ });
+ setIsUploading(false);
+
+ //모달 닫기
+ onClose();
+ };
+
+ return (
+
+
+ 취업 정보 공유
+
+ ×
+
+
+
+
+
+
+ 회사 로고
+
+
+
+
+ 링크
+
+
+
+
+
본 채용 설명
+
+
+
{textValue.length}/100
+
+
+
+ {imageFile ? (
+
+
+
+ {textValue.split('\n').map((line, index) => (
+
+ {line}
+
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+ 취소
+
+ {isUploading ? '업로드 중' : '제출'}
+
+
+
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/src/pages/Gallery/UploadModal/ModalT.tsx b/src/pages/Gallery/UploadModal/ModalT.tsx
new file mode 100644
index 00000000..23b3ed36
--- /dev/null
+++ b/src/pages/Gallery/UploadModal/ModalT.tsx
@@ -0,0 +1,160 @@
+import React, { useState, ChangeEvent, FormEvent } from 'react';
+import {
+ BtnAlign,
+ CancelBtn,
+ Description,
+ Formalign,
+ InputAndPreview,
+ InputContainer,
+ LinkInput,
+ LinkInputContainer,
+ ModalContainer,
+ ModalFirstLine,
+ ModalLabel,
+ ModalTextarea,
+ PlaceHolder,
+ PreviewBox,
+ PreviewImg,
+ SubmitBtn,
+} from '../style';
+import { ref, getDownloadURL, uploadBytes } from 'firebase/storage';
+import { doc, updateDoc, arrayUnion, DocumentReference } from 'firebase/firestore';
+import { storage, firestore } from '../../../utils/firebase';
+
+interface ModalProps {
+ onClose: () => void;
+}
+
+const Modal: React.FC = ({ onClose }) => {
+ const [link, setLink] = useState(''); // 링크 입력 상태
+ const [imageFile, setImageFile] = useState(null); // 이미지 파일 상태
+ const [thumbnailURL, setThumbnailURL] = useState(null);
+ const [textValue, setTextValue] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+
+ const handleTextChange = (e: ChangeEvent) => {
+ setTextValue(e.target.value);
+ };
+
+ // 링크 입력 핸들러
+ const handleLinkChange = (e: ChangeEvent) => {
+ setLink(e.target.value);
+ };
+
+ // 이미지 파일 선택 핸들러
+ const handleImageChange = (e: ChangeEvent) => {
+ if (e.target.files) {
+ const selectedFile = e.target.files[0];
+ setImageFile(selectedFile);
+ }
+ };
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ setIsUploading(true);
+
+ // 유니크한 이미지 이름
+ const uniqueName = Date.now();
+ // storage 경로에 현재 시간을 추가하여 고유한 경로 참조
+ const storageRef = ref(storage, 'thumbnailT/' + uniqueName);
+
+ // storage에 이미지 파일이 존재하는 경우에만 업로드
+ if (imageFile) {
+ await uploadBytes(storageRef, imageFile);
+ }
+
+ // storage에 업로드 된 이미지 URL 가져오기
+ const url = await getDownloadURL(storageRef);
+ // thumbnaulURL 에 이미지 url 할당
+ setThumbnailURL(url);
+
+ // firestore 경로에 고유한 경로 참조
+ const storeRef: DocumentReference = doc(firestore, 'gallery', '레퍼런스 공유');
+ // firestore 추가할 데이터
+ const newArticle = {
+ thumbnailURL: url,
+ recruitURL: link,
+ index: uniqueName,
+ description: textValue,
+ };
+ // firestore 데이터를 취업/articleR 필드에 추가
+ await updateDoc(storeRef, {
+ '테크.articleT': arrayUnion(newArticle),
+ });
+ setIsUploading(false);
+ //모달 닫기
+ onClose();
+ };
+
+ return (
+
+
+ 테크 링크 공유
+
+ ×
+
+
+
+
+
+
+ 썸네일
+
+
+
+
+ 링크
+
+
+
+
+
레퍼런스 설명
+
+
+
{textValue.length}/100
+
+
+
+ {imageFile ? (
+
+
+
+ {textValue.split('\n').map((line, index) => (
+
+ {line}
+
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+ 취소
+
+ {isUploading ? '업로드 중' : '제출'}
+
+
+
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/src/pages/Gallery/index.tsx b/src/pages/Gallery/index.tsx
new file mode 100644
index 00000000..5b344781
--- /dev/null
+++ b/src/pages/Gallery/index.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from 'react';
+import { GalleryContainer } from './style';
+import SidebarGallery from '../../components/SidebarGallery';
+import Profile from './profile';
+import Recruit from './recruit';
+import Tech from './tech';
+
+// clickedValue의 타입 정의
+type ClickedValue = {
+ userInfo?: string;
+ articleR?: string;
+ articleT?: string;
+};
+
+const Gallery: React.FC = () => {
+ const [clickedValue, setClickedValue] = useState(null);
+
+ const handleKeyClick = (value: ClickedValue) => {
+ setClickedValue(value);
+ console.log(value);
+ };
+
+ return (
+
+
+ {clickedValue && clickedValue.userInfo ? (
+
+ ) : clickedValue && clickedValue.articleR ? (
+
+ ) : clickedValue && clickedValue.articleT ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default Gallery;
diff --git a/src/pages/Gallery/profile.tsx b/src/pages/Gallery/profile.tsx
new file mode 100644
index 00000000..8bb1a0e5
--- /dev/null
+++ b/src/pages/Gallery/profile.tsx
@@ -0,0 +1,75 @@
+import React, { useEffect, useState } from 'react';
+import { collection, getDocs, doc, onSnapshot, DocumentData } from 'firebase/firestore';
+import { firestore } from '../../utils/firebase'; // Firebase firestore 객체 가져오기
+import { ProfileContainer, ProfileIMG, ProfileName, ProfileWrapper, StyleProfile } from './style';
+
+// Firebase Firestore에서 반환되는 사용자 데이터의 타입 정의
+interface UserData {
+ id: string;
+ name: string;
+ imgURL: string;
+}
+
+const Profile: React.FC = () => {
+ const [users, setUsers] = useState([]);
+
+ useEffect(() => {
+ const fetchUserData = async () => {
+ const querySnapshot = await getDocs(collection(firestore, 'user'));
+ const documentIds: string[] = [];
+
+ querySnapshot.forEach((doc) => {
+ documentIds.push(doc.id);
+ });
+
+ // 각 문서에 대한 실시간 변경 사항을 추적
+ const unsubscribes = documentIds.map((documentId) => {
+ const userDoc = doc(firestore, 'user', documentId);
+ return onSnapshot(userDoc, (userSnapshot: DocumentData) => {
+ if (userSnapshot.exists()) {
+ const userData = userSnapshot.data();
+ const user: UserData = {
+ id: documentId,
+ name: userData.name,
+ imgURL: userData.imageURL,
+ };
+ // 기존 사용자 데이터를 업데이트하거나 추가
+ setUsers((prevUsers) => {
+ const updatedUsers = [...prevUsers];
+ const userIndex = updatedUsers.findIndex((u) => u.id === documentId);
+ if (userIndex !== -1) {
+ updatedUsers[userIndex] = user;
+ } else {
+ updatedUsers.push(user);
+ }
+ return updatedUsers;
+ });
+ }
+ });
+ });
+
+ // 컴포넌트가 언마운트될 때 모든 구독을 정리
+ return () => {
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
+ };
+ };
+
+ fetchUserData();
+ }, []);
+
+ return (
+
+ {/* 레퍼런스 공유 {'>'} 취업 */}
+
+ {users.map((user: UserData) => (
+
+
+ {user.name}
+
+ ))}
+
+
+ );
+};
+
+export default Profile;
diff --git a/src/pages/Gallery/recruit.tsx b/src/pages/Gallery/recruit.tsx
new file mode 100644
index 00000000..2a57fca2
--- /dev/null
+++ b/src/pages/Gallery/recruit.tsx
@@ -0,0 +1,207 @@
+import React, { useState, useEffect } from 'react';
+import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+import Modal from './UploadModal/Modal';
+import { onSnapshot, updateDoc, DocumentData } from 'firebase/firestore';
+import { ref, deleteObject, StorageReference } from 'firebase/storage';
+import { storeRef, storage } from '../../utils/firebase';
+import {
+ ArticleContainer,
+ ChildArticle,
+ ContentContainer,
+ Description,
+ ModalBackground,
+ TrashCan,
+ UploadBtn,
+} from './style';
+import swal from 'sweetalert';
+
+// Firebase Firestore에서 반환되는 데이터의 타입
+interface FirebaseArticleData {
+ index: number;
+ recruitURL: string;
+ thumbnailURL: string;
+ description: string;
+}
+
+const Recruit: React.FC = () => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [articleRs, setArticleRs] = useState([]);
+ const [isDraggingItem, setIsDraggingItem] = useState(false);
+
+ const openModal = () => {
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ const handleDragStart = () => {
+ setIsDraggingItem(true);
+ };
+
+ const handleDragEnd = async (result: DropResult) => {
+ setIsDraggingItem(false);
+
+ if (result.destination) {
+ if (result.destination.droppableId === 'trashCan') {
+ const itemToDelete = articleRs[result.source.index];
+
+ const shouldDelete = await swal({
+ title: '정말로 사진을 삭제하시겠습니까?',
+ text: '삭제 버튼을 누르시면 사진 파일이 사라집니다!',
+ icon: 'info',
+ buttons: ['취소', '삭제'],
+ });
+
+ if (shouldDelete) {
+ // Firestore에서 해당 요소 삭제
+ const updatedArticleRs = [...articleRs];
+ updatedArticleRs.splice(result.source.index, 1);
+ await updateDoc(storeRef, {
+ '취업.articleR': updatedArticleRs,
+ });
+
+ // Storage에서 이미지 파일 삭제
+ const imageRef: StorageReference = ref(storage, `thumbnailR/${itemToDelete.index}`);
+ await deleteObject(imageRef);
+
+ // 상태 업데이트
+ setArticleRs(updatedArticleRs);
+ } else {
+ swal('사진 삭제를 취소합니다!');
+ }
+ } else {
+ // 기존 드래그 앤 드롭 로직 (항목의 순서 변경)
+ const newArticleRs = [...articleRs];
+ const [reorderedItem] = newArticleRs.splice(result.source.index, 1);
+ newArticleRs.splice(result.destination.index, 0, reorderedItem);
+
+ await updateDoc(storeRef, {
+ '취업.articleR': newArticleRs,
+ });
+ }
+ }
+ };
+
+ // 실시간 데이터 연동
+ useEffect(() => {
+ const unsubscribe = onSnapshot(storeRef, (docSnapshot: DocumentData) => {
+ if (docSnapshot.exists()) {
+ const data = docSnapshot.data();
+ const articleRData: FirebaseArticleData[] = data?.취업?.articleR || [];
+ setArticleRs(articleRData);
+ } else {
+ console.log('Document does not exist.');
+ }
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, []);
+
+ return (
+
+ {isModalOpen && (
+
+
+
+
+ )}
+
+
+
+
+ {(provided) => (
+
+ {provided.placeholder}
+
+ )}
+
+
+ {(provided) => (
+
+ )}
+
+
+
+
+ );
+};
+
+export default Recruit;
diff --git a/src/pages/Gallery/style.tsx b/src/pages/Gallery/style.tsx
new file mode 100644
index 00000000..cca2e610
--- /dev/null
+++ b/src/pages/Gallery/style.tsx
@@ -0,0 +1,332 @@
+import styled, { keyframes } from 'styled-components';
+import bin from '../../common/Gallery/bin.png';
+import upload from '../../common/Gallery/icons8-upload-64.png';
+
+//갤러리 전체
+export const GalleryContainer = styled.div`
+ display: flex;
+ height: 100vh;
+ padding-top: 72px;
+ width: 100vw;
+ overflow: hidden;
+`;
+
+// 컨텐츠 영역
+export const ProfileContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100vh;
+ padding: 3%;
+ background-color: ${(props) => props.theme.recruitmentBack};
+`;
+
+export const RecruitConstainer = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const ContentContainer = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ background-color: ${(props) => props.theme.recruitmentBack};
+`;
+
+export const ArticleContainer = styled.div`
+ overflow-y: auto;
+ height: calc(100vh - 72px);
+ box-sizing: border-box;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+`;
+
+export const ContentFirstLine = styled.div`
+ position: fixed;
+ z-index: 2;
+ left: 22%;
+ top: 100px;
+ font-size: 30px;
+`;
+
+export const UploadBtn = styled.button`
+ position: fixed;
+ right: 20px;
+ bottom: 40px;
+ font-size: 60px;
+ width: 150px;
+ height: 150px;
+ border-radius: 10px;
+ font-size: 26px;
+ padding: 10px;
+ box-sizing: border-box;
+ border: none;
+ background-color: ${(props) => props.theme.activeColor1};
+ color: white;
+ cursor: pointer;
+ background-color: transparent;
+ background-image: url(${upload});
+ background-position: cover;
+ background-size: cover;
+ background-repeat: no-repeat;
+ &:hover {
+ filter: brightness(90%);
+ }
+`;
+
+export const UploadBtnWrapper = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 1%;
+ padding-right: 8%;
+ background-color: transparent;
+ z-index: 1;
+`;
+// 아티클 영역
+const jumpShaking = keyframes`
+ 0% { transform: translateX(0) }
+ 25% { transform: translateY(-9px) }
+ 35% { transform: translateY(-9px) rotate(17deg) }
+ 55% { transform: translateY(-9px) rotate(-17deg) }
+ 65% { transform: translateY(-9px) rotate(17deg) }
+ 75% { transform: translateY(-9px) rotate(-17deg) }
+ 100% { transform: translateY(0) rotate(0) }
+`;
+
+export const TrashCan = styled.div`
+ width: 100px;
+ height: 100px;
+ position: fixed;
+ right: 20px;
+ bottom: 40px;
+ font-size: 60px;
+ background-image: url(${bin});
+ background-size: cover;
+ animation: ${jumpShaking} 2s ease-in-out infinite;
+ z-index: -1;
+`;
+
+export const ImgContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ align-items: flex-start;
+ padding: 10px;
+`;
+
+export const StyleProfile = styled.div`
+ display: flex;
+ justify-content: flex-start;
+ align-content: flex-start;
+ flex-wrap: wrap;
+ height: calc(100vh - 72px);
+ gap: 98px;
+ width: 1100px;
+ overflow-y: auto;
+`;
+
+export const ProfileWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+ height: 250px;
+ text-align: center;
+ border-radius: 10px;
+ background-color: #fff;
+ box-shadow: 2px 2px 1px 2px rgba(0, 0, 0, 0.3);
+`;
+export const ProfileIMG = styled.img`
+ width: 200px;
+ height: 200px;
+ border-radius: 10px 10px 0 0;
+`;
+export const ProfileName = styled.div`
+ text-align: center;
+ font-size: 30px;
+ font-weight: 500;
+`;
+// 모달
+export const ModalContainer = styled.div`
+ width: 650px;
+ height: 420px;
+ z-index: 999;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: #fff;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ padding: 15px 25px;
+`;
+
+export const ModalBackground = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 100;
+ background-color: rgba(0, 0, 0, 0.6);
+`;
+
+export const ModalFirstLine = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: baseline;
+`;
+
+export const ModalLabel = styled.label`
+ font-size: 20;
+ font-weight: bold;
+ margin-bottom: 5px;
+`;
+
+export const ModalTextarea = styled.textarea`
+ width: 100%;
+ font-size: 16px;
+ resize: none;
+ border: 2px solid rgb(118, 118, 118);
+ border-radius: 5px;
+ &:focus {
+ outline: none;
+ border: 1px solid ${(props) => props.theme.recruitmentBack};
+ box-shadow: 0px 1px 6px ${(props) => props.theme.navBar};
+ }
+`;
+
+export const Formalign = styled.form`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const InputAndPreview = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 10px;
+`;
+
+export const InputContainer = styled.div`
+ display: flex;
+ margin: 10px 10px 10px 0;
+ flex-direction: column;
+ z-index: 2;
+`;
+
+export const LinkInput = styled.input`
+ width: 100%;
+ height: 90%;
+ box-sizing: border-box;
+ border-radius: 5px;
+ font-size: 16px;
+ &:focus {
+ outline: none;
+ border: 1px solid ${(props) => props.theme.recruitmentBack};
+ box-shadow: 0px 1px 6px ${(props) => props.theme.navBar};
+ }
+`;
+
+export const LinkInputContainer = styled.div`
+ margin-top: 12px;
+ margin-bottom: 35px;
+`;
+
+export const Description = styled.div`
+ position: absolute;
+ display: flex;
+ align-items: flex-end;
+ top: 0;
+ left: 0;
+ width: 300px;
+ height: 200px;
+ border-radius: 10px;
+ background-color: rgba(0, 0, 0, 0.3);
+ color: #fff;
+ padding: 8px;
+ font-size: 20px;
+ opacity: 0; /* 설명을 숨깁니다. */
+ transition: opacity 0.3s ease-in-out;
+ overflow: hidden;
+`;
+
+export const ChildArticle = styled.li`
+ display: flex;
+ align-items: flex-start;
+ padding-bottom: 5%;
+ &:hover ${Description} {
+ opacity: 1;
+ }
+`;
+
+export const PreviewBox = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 10px 0px 10px 10;
+
+ &:hover ${Description} {
+ opacity: 1;
+ }
+`;
+export const PreviewImg = styled.img`
+ width: 300px;
+ height: 200px;
+ border-radius: 10px;
+ box-shadow: 2px 2px 2px 2px rgba(0, 0, 0, 0.3);
+ position: relative;
+`;
+
+export const PlaceHolder = styled.div`
+ width: 300px;
+ height: 200px;
+ box-shadow: 2px 2px 2px 2px rgba(0, 0, 0, 0.3);
+ border-radius: 10px;
+ background-color: ${(props) => props.theme.recruitmentBack};
+`;
+
+export const SubmitBtn = styled.button`
+ width: 100px;
+ height: 50px;
+ border-radius: 5px;
+ padding: 10px;
+ box-sizing: border-box;
+ vertical-align: middle;
+ margin: 25px;
+ border: none;
+ background-color: ${(props) => props.theme.activeColor1};
+ color: white;
+ cursor: pointer;
+ font-size: 20px;
+ &:hover {
+ filter: brightness(110%);
+ }
+`;
+export const CancelBtn = styled.button`
+ width: 100px;
+ height: 50px;
+ border-radius: 5px;
+ padding: 10px;
+ box-sizing: border-box;
+ vertical-align: middle;
+ border: 0.5px solid black;
+ background-color: #fff;
+ color: #000;
+ font-size: 20px;
+ margin: 25px;
+ cursor: pointer;
+ &:hover {
+ filter: brightness(90%);
+ }
+`;
+
+export const BtnAlign = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-top: 20px;
+`;
diff --git a/src/pages/Gallery/tech.tsx b/src/pages/Gallery/tech.tsx
new file mode 100644
index 00000000..a1fb4bdf
--- /dev/null
+++ b/src/pages/Gallery/tech.tsx
@@ -0,0 +1,208 @@
+import React, { useState, useEffect } from 'react';
+import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+import ModalT from './UploadModal/ModalT';
+import { onSnapshot, updateDoc, DocumentData } from 'firebase/firestore';
+import { ref, deleteObject, StorageReference } from 'firebase/storage';
+import { storeRef, storage } from '../../utils/firebase';
+import {
+ ArticleContainer,
+ ChildArticle,
+ ContentContainer,
+ Description,
+ ModalBackground,
+ TrashCan,
+ UploadBtn,
+} from './style';
+import swal from 'sweetalert';
+
+// Firebase Firestore에서 반환되는 데이터의 타입
+interface FirebaseArticleData {
+ index: number;
+ recruitURL: string;
+ thumbnailURL: string;
+ description: string;
+}
+
+const Tech: React.FC = () => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [articleTs, setArticleTs] = useState([]);
+ const [isDraggingItem, setIsDraggingItem] = useState(false);
+
+ const openModal = () => {
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ const handleDragStart = () => {
+ setIsDraggingItem(true);
+ };
+
+ const handleDragEnd = async (result: DropResult) => {
+ setIsDraggingItem(false);
+
+ if (result.destination) {
+ if (result.destination.droppableId === 'trashCan') {
+ const itemToDelete = articleTs[result.source.index];
+
+ const shouldDelete = await swal({
+ title: '정말로 사진을 삭제하시겠습니까?',
+ text: '삭제 버튼을 누르시면 사진 파일이 사라집니다!',
+ icon: 'info',
+ buttons: ['취소', '삭제'],
+ });
+
+ if (shouldDelete) {
+ // Firestore에서 해당 요소 삭제
+ const updatedArticleTs = [...articleTs];
+ updatedArticleTs.splice(result.source.index, 1);
+ await updateDoc(storeRef, {
+ '테크.articleT': updatedArticleTs,
+ });
+
+ // Storage에서 이미지 파일 삭제
+ const imageRef: StorageReference = ref(storage, `thumbnailT/${itemToDelete.index}`);
+ await deleteObject(imageRef);
+
+ // 상태 업데이트
+ setArticleTs(updatedArticleTs);
+ } else {
+ swal('사진 삭제를 취소합니다!');
+ }
+ } else {
+ // 기존 드래그 앤 드롭 로직 (항목의 순서 변경)
+ const newArticleTs = [...articleTs];
+ const [reorderedItem] = newArticleTs.splice(result.source.index, 1);
+ newArticleTs.splice(result.destination.index, 0, reorderedItem);
+
+ await updateDoc(storeRef, {
+ '테크.articleT': newArticleTs,
+ });
+ }
+ }
+ };
+
+ useEffect(() => {
+ const unsubscribe = onSnapshot(storeRef, (docSnapshot: DocumentData) => {
+ if (docSnapshot.exists()) {
+ const data = docSnapshot.data();
+ const articleTData: FirebaseArticleData[] = data?.테크?.articleT || [];
+ setArticleTs(articleTData);
+ } else {
+ console.log('Document does not exist.');
+ }
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, []);
+
+ return (
+
+ {isModalOpen && (
+
+
+
+
+ )}
+
+
+
+
+ {(provided) => (
+
+ {provided.placeholder}
+
+ )}
+
+
+ {(provided) => (
+
+ )}
+
+
+
+
+ );
+};
+
+export default Tech;
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
new file mode 100644
index 00000000..d0ff9195
--- /dev/null
+++ b/src/pages/Home/index.tsx
@@ -0,0 +1,434 @@
+import React, { useEffect, useRef, useState } from 'react';
+import {
+ Outer,
+ Inner,
+ InnerBox,
+ InnerHeadline,
+ InnerSubTitle,
+ InnerBtnWrapper,
+ SubInnerBox,
+ SubInnerHeadline,
+ SubInnerSubTitle,
+ SubInnerBtnWrapper,
+ SubInnerImg,
+} from './style';
+import Button from '@mui/material/Button';
+import Carousel from 'react-material-ui-carousel';
+import { Paper } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+const Home: React.FC = () => {
+ const outerDivRef = useRef(null);
+ const [scrollIndex, setScrollIndex] = useState(1);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const handleScroll = (e: WheelEvent) => {
+ e.preventDefault();
+
+ const { deltaY } = e;
+ const { scrollTop } = outerDivRef.current!;
+ const pageHeight = window.innerHeight;
+
+ if (deltaY == 0) {
+ return;
+ } else {
+ if (deltaY > 0) {
+ // 스크롤 다운
+ if (scrollTop >= 0 && scrollTop < pageHeight) {
+ console.log('현재 1페이지, down');
+
+ outerDivRef.current!.scrollTo({
+ top: pageHeight,
+ left: 0,
+ behavior: 'smooth',
+ });
+ setScrollIndex(2);
+ } else if (scrollTop >= pageHeight && scrollTop < pageHeight * 2) {
+ console.log('현재 2페이지, down');
+
+ outerDivRef.current!.scrollTo({
+ top: pageHeight * 2,
+ left: 0,
+ behavior: 'smooth',
+ });
+ setScrollIndex(3);
+ } else {
+ console.log('현재 3페이지, down');
+
+ outerDivRef.current!.scrollTo({
+ top: pageHeight * 2,
+ left: 0,
+ behavior: 'smooth',
+ });
+ setScrollIndex(3);
+ }
+ } else {
+ // 스크롤 업
+ if (scrollTop >= 0 && scrollTop < pageHeight) {
+ console.log('현재 1페이지, up');
+
+ outerDivRef.current!.scrollTo({
+ top: 0,
+ left: 0,
+ behavior: 'smooth',
+ });
+ setScrollIndex(1);
+ } else if (scrollTop >= pageHeight && scrollTop < pageHeight * 1.5) {
+ console.log('현재 2페이지, up');
+ outerDivRef.current!.scrollTo({
+ top: 0,
+ left: 0,
+ behavior: 'smooth',
+ });
+ setScrollIndex(1);
+ } else {
+ console.log('현재 3페이지, up');
+ outerDivRef.current!.scrollTo({
+ top: pageHeight,
+ left: 0,
+ behavior: 'smooth',
+ });
+ setScrollIndex(2);
+ }
+ }
+ }
+ }; // 디바운스 지연 시간을 조절합니다.
+
+ const outerDivRefCurrent = outerDivRef.current;
+ if (outerDivRefCurrent) {
+ outerDivRefCurrent.addEventListener('wheel', handleScroll);
+ }
+
+ return () => {
+ if (outerDivRefCurrent) {
+ outerDivRefCurrent.removeEventListener('wheel', handleScroll);
+ }
+ };
+ }, []);
+ const pageHeight = window.innerHeight;
+
+ const handleIntro = () => {
+ outerDivRef.current!.scrollTo({
+ top: pageHeight,
+ left: 0,
+ behavior: 'smooth',
+ });
+ };
+ return (
+
+
+
+
+ FASTUDY
+ 프로젝트 / 스터디 인원 모집 사이트
+
+ {
+ window.location.href = 'https://github.com/2weeks-team';
+ }}
+ >
+ 깃허브
+
+ {
+ handleIntro();
+ }}
+ >
+ 소개
+
+
+
+
+
+
+
+
+ 위키/갤러리
+ 누구나 자유롭게 수정/삭제 가능한 위키와 갤러리
+
+ {
+ navigate('/wiki');
+ }}
+ >
+ 위키
+
+ {
+ navigate('/gallery');
+ }}
+ >
+ 갤러리
+
+
+
+
+
+
+
+
+ 위키
+ 누구나 자유롭게 수정/삭제 가능한 위키
+
+ {
+ navigate('/wiki');
+ }}
+ >
+ 위키
+
+
+
+
+
+
+
+
+
+ 갤러리
+ 누구나 자유롭게 수정/삭제 가능한 갤러리
+
+ {
+ navigate('/gallery');
+ }}
+ >
+ 갤러리
+
+
+
+
+
+
+
+
+
+
+
+ 모집 게시판
+
+ 스터디/프로젝트 인원을 모집해보세요
+
+
+ {
+ navigate('/recruitment');
+ }}
+ >
+ 프로젝트
+
+ {
+ navigate('/recruitment');
+ }}
+ >
+ 스터디
+
+
+
+
+
+
+
+
+ 모집 게시판
+ 카테고리별로 분류된 게시글을 만나실 수 있습니다.
+
+ {
+ navigate('/recruitment');
+ }}
+ >
+ 모집하기
+
+
+
+
+
+
+
+
+
+ 모집글 작성
+ 작성하기에 들어가 프로젝트/스터디 인원을 모집해보세요
+
+ {
+ navigate('/recruitment/post');
+ }}
+ >
+ 작성하기
+
+
+
+
+
+
+
+
+ );
+};
+
+const Dot = ({ num, scrollIndex }: { num: number; scrollIndex: number }) => {
+ return (
+
+ );
+};
+
+const Dots = ({ scrollIndex }: { scrollIndex: number }) => {
+ return (
+
+ );
+};
+
+export default Home;
diff --git a/src/pages/Home/style.tsx b/src/pages/Home/style.tsx
new file mode 100644
index 00000000..08ebf24f
--- /dev/null
+++ b/src/pages/Home/style.tsx
@@ -0,0 +1,97 @@
+import styled from 'styled-components';
+
+export const HomeBody = styled.div`
+ margin: 0;
+ overflow-y: hidden;
+`;
+
+export const Outer = styled.div`
+ height: 100vh;
+ background-color: #efefef;
+
+ overflow: auto;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+export const Inner = styled.div`
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 100px;
+
+ position: relative;
+`;
+
+export const InnerBox = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+`;
+
+export const InnerHeadline = styled.h1`
+ display: block;
+ font-size: 7rem;
+
+ text-align: center;
+ margin: 0;
+`;
+
+export const InnerSubTitle = styled.p`
+ font-size: 2rem;
+ color: rgba(255, 255, 255, 0.7);
+ text-align: center;
+`;
+
+export const InnerBtnWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 30px;
+`;
+
+export const SubInnerBox = styled.div`
+ width: 100%;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const SubInnerHeadline = styled.h1`
+ display: block;
+ font-size: 5rem;
+
+ text-align: center;
+ margin: 0;
+`;
+
+export const SubInnerSubTitle = styled.p`
+ font-size: 1.5rem;
+ color: rgba(255, 255, 255, 0.7);
+ text-align: center;
+`;
+
+export const SubInnerBtnWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 30px;
+`;
+
+export const SubInnerImg = styled.div`
+ width: 650px;
+ height: 450px;
+`;
diff --git a/src/pages/LogIn/index.tsx b/src/pages/LogIn/index.tsx
new file mode 100644
index 00000000..01953c4c
--- /dev/null
+++ b/src/pages/LogIn/index.tsx
@@ -0,0 +1,188 @@
+import * as React from 'react';
+import Avatar from '@mui/material/Avatar';
+import Button from '@mui/material/Button';
+import CssBaseline from '@mui/material/CssBaseline';
+import TextField from '@mui/material/TextField';
+import Box from '@mui/material/Box';
+import Link from '@mui/material/Link';
+import Typography from '@mui/material/Typography';
+import Container from '@mui/material/Container';
+import { createTheme, ThemeProvider } from '@mui/material/styles';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { auth, themeType } from '../../utils/firebase';
+import { signInWithEmailAndPassword } from 'firebase/auth';
+import { useRecoilState } from 'recoil';
+import { ThemeChange, UserId } from '../../utils/recoil';
+import swal from 'sweetalert';
+
+const defaultTheme = createTheme();
+
+export default function LogIn() {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+ const [back, setBack] = React.useState('');
+ const [buttonColor, setButtonColor] = React.useState('');
+ const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange);
+
+ React.useEffect(() => {
+ const selected = (theme: themeType) => {
+ setBack(theme.recruitmentBack);
+ setButtonColor(theme.navBar);
+ };
+ if (localStorage.getItem('theme')) {
+ const localtheme = localStorage.getItem('theme');
+ if (localtheme) {
+ const color = JSON.parse(localtheme);
+ selected(color);
+ }
+ }
+ }, [currentTheme]);
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ const data = new FormData(event.currentTarget);
+
+ const emailEntry = data.get('email');
+ const email = emailEntry !== null ? emailEntry.toString() : '';
+
+ const passwordEntry = data.get('password');
+ const password = passwordEntry !== null ? passwordEntry.toString() : '';
+
+ signInWithEmailAndPassword(auth, email, password)
+ .then((userCredential) => {
+ const user = userCredential.user;
+
+ setUserId(user.uid);
+
+ navigate('/', { state: pathname });
+ })
+ .catch((error) => {
+ switch (error.code) {
+ case 'auth/user-not-found' || 'auth/wrong-password':
+ return swal({
+ title: '로그인 에러',
+ text: '이메일 혹은 비밀번호가 일치하지 않습니다.',
+ icon: 'warning',
+ });
+ case 'auth/email-already-in-use':
+ return swal({
+ title: '로그인 에러',
+ text: '이미 사용중인 이메일입니다.',
+ icon: 'warning',
+ });
+ case 'auth/weak-password':
+ return swal({
+ title: '로그인 에러',
+ text: '비밀번호는 6자리 이상으로 작성해주세요',
+ icon: 'warning',
+ });
+ case 'auth/network-request-failed':
+ return swal({
+ title: '로그인 에러',
+ text: '네트워크 연결에 실패 하였습니다.',
+ icon: 'warning',
+ });
+ case 'auth/invalid-email':
+ return swal({
+ title: '로그인 에러',
+ text: '잘못된 이메일 형식입니다.',
+ icon: 'warning',
+ });
+ case 'auth/internal-error':
+ return swal({
+ title: '로그인 에러',
+ text: '잘못된 요청입니다.',
+ icon: 'warning',
+ });
+ default:
+ return swal({
+ title: '로그인 에러',
+ text: '로그인에 실패 하였습니다. 다시 확인해주세요',
+ icon: 'warning',
+ });
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ 로그인
+
+
+
+
+
+ 로그인
+
+
+
+ 회원가입
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Recruitment/index.tsx b/src/pages/Recruitment/index.tsx
new file mode 100644
index 00000000..b7acb40d
--- /dev/null
+++ b/src/pages/Recruitment/index.tsx
@@ -0,0 +1,206 @@
+import React, { useState, useEffect } from 'react';
+import { useRecoilValue } from 'recoil';
+import { channelState, subChannelState } from '../../utils/recoil';
+import { Link } from 'react-router-dom';
+import {
+ RecruitmentContainer,
+ PostsContainer,
+ PostNav,
+ PostButton,
+ SearchInput,
+ PostsWrapper,
+ PostWrapper,
+ Category,
+ RecruitValued,
+ Title,
+ Time,
+ People,
+ PostPageBtnWrapper,
+ PostPageBtn,
+} from './style';
+import SidebarRecruitment from '../../components/SidebarRecruitment';
+import { showRecruitmentFields } from '../../utils/firebase';
+
+const Recruitment: React.FC = () => {
+ const channel = useRecoilValue(channelState);
+ const subChannel = useRecoilValue(subChannelState);
+
+ const [recruitmentData, setRecruitmentData] = useState([]);
+ const [filteredData, setFilteredData] = useState([]);
+
+ const [searchTitle, setSearchTitle] = useState('');
+ const [searching, setSearching] = useState(false);
+
+ const [lastIndex, setLastIndex] = useState(0);
+
+ const [postStartIndex, setPostStartIndex] = useState(0);
+
+ const handleEnterKey = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ setSearching(true);
+ setPostStartIndex(0);
+ } else {
+ setSearching(false);
+ setLastIndex(recruitmentData.length - 1);
+ }
+ };
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ let updatedChannel = channel;
+ if (subChannel === '') {
+ updatedChannel = 'study';
+ }
+ const { docSnapshots } = await showRecruitmentFields(
+ 'recruitmentContainer',
+ 'recruitment',
+ updatedChannel,
+ );
+ const subChannelFields =
+ subChannel === '전체' || subChannel === ''
+ ? docSnapshots.map((item) => ({ id: item.id, data: item.data }))
+ : docSnapshots
+ .filter((item) => item.data.category === subChannel)
+ .map((item) => ({ id: item.id, data: item.data }));
+
+ setRecruitmentData(subChannelFields);
+ setLastIndex(subChannelFields.length - 1);
+ setSearchTitle('');
+ setPostStartIndex(0);
+ } catch (error) {
+ console.error('데이터 가져오기 실패:', error);
+ }
+ };
+
+ fetchData();
+ }, [channel, subChannel]);
+
+ useEffect(() => {
+ if (searching) {
+ const filtered = recruitmentData.filter(
+ (item) => item?.data?.title?.toLowerCase().includes(searchTitle.toLowerCase()),
+ );
+ setFilteredData(filtered);
+ setLastIndex(filtered.length - 1);
+ setPostStartIndex(0);
+ } else {
+ setFilteredData([]);
+ }
+ }, [searching, searchTitle]);
+
+ console.log(recruitmentData, '1');
+
+ const postItem = [];
+ const postFilterItem = [];
+ const postNumber = Math.floor((window.innerHeight - 100) / 130 - 1);
+ console.log(postNumber);
+
+ for (let i = postStartIndex; i < postStartIndex + postNumber; i++) {
+ if (recruitmentData[i]) {
+ postItem.push(
+ ,
+ );
+ }
+ }
+
+ for (let i = postStartIndex; i < postStartIndex + postNumber; i++) {
+ if (filteredData[i]) {
+ postFilterItem.push(
+ ,
+ );
+ }
+ }
+
+ const handlePage = (e: any) => {
+ setPostStartIndex((Number(e.target.innerHTML) - 1) * postNumber);
+ };
+
+ const pageNumbers = [];
+ const pageFilterNumbers = [];
+
+ for (let i = 0; i < recruitmentData.length / postNumber; i++) {
+ pageNumbers.push({i + 1} );
+ }
+
+ for (let i = 0; i < filteredData.length / postNumber; i++) {
+ pageFilterNumbers.push({i + 1} );
+ }
+
+ return (
+
+
+
+
+
+
+ 게시글 작성
+
+
+ setSearchTitle(event.target.value)}
+ onKeyDown={handleEnterKey}
+ >
+
+ {searching ? postFilterItem : postItem}
+ {searching ? pageFilterNumbers : pageNumbers}
+
+
+ );
+};
+
+const Post: React.FC<{ data: any; id: string; index: number; lastIndex: number; channel: string }> = ({
+ data,
+ id,
+ index,
+ lastIndex,
+ channel,
+}) => (
+
+ {/* ex) /recruitment/study/SD521S3SF3EB3H5 */}
+ = 1 ? '1px solid #BEBEBE' : 'none',
+ borderTopLeftRadius: index === 0 ? '15px' : '0',
+ borderTopRightRadius: index === 0 ? '15px' : '0',
+ borderBottomLeftRadius: index === lastIndex ? '15px' : '0',
+ borderBottomRightRadius: index === lastIndex ? '15px' : '0',
+ }}
+ >
+
+
+ {data.recruitValued ? '모집중' : '모집완료'}
+
+
{data.people}명
+
{data.category}
+
+ {data.title}
+
+
+ {data.editValued
+ ? new Date(data.editTime?.toMillis()).toLocaleString() + ' (수정됨)'
+ : new Date(data.time?.toMillis()).toLocaleString()}
+
+
+
+
+);
+
+export default Recruitment;
diff --git a/src/pages/Recruitment/style.tsx b/src/pages/Recruitment/style.tsx
new file mode 100644
index 00000000..6ccfc044
--- /dev/null
+++ b/src/pages/Recruitment/style.tsx
@@ -0,0 +1,131 @@
+import styled from 'styled-components';
+
+export const RecruitmentContainer = styled.div`
+ display: flex;
+ padding-top: 72px;
+ height: 100vh;
+ width: 100vw;
+`;
+
+export const PostsContainer = styled.div`
+ background-color: ${(props) => props.theme.recruitmentBack};
+ width: 100%;
+ height: 100%;
+
+ overflow: auto;
+ padding: 20px;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+export const PostNav = styled.div`
+ width: 50vw;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+`;
+
+export const PostButton = styled.button`
+ background-color: ${(props) => props.theme.activeColor2};
+ color: #fff;
+ padding: 5px 10px;
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.5);
+ border-radius: 5px;
+ font-size: 1.3rem;
+ font-weight: bold;
+ &:hover {
+ cursor: pointer;
+ }
+`;
+
+export const SearchInput = styled.input`
+ min-width: 250px;
+ padding: 7.5px 0px 5px 12.5px;
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1);
+ border-radius: 5px;
+ font-size: 1.1rem;
+ font-weight: bold;
+`;
+
+export const PostsWrapper = styled.div`
+ background-color: #fff;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
+ border-radius: 15px;
+`;
+
+export const PostWrapper = styled.div`
+ width: 50vw;
+ /* min-height: 60vh; */
+ padding: 20px 15px;
+
+ &:hover {
+ background-color: #bfefff;
+ cursor: pointer;
+ }
+`;
+
+export const Category = styled.div`
+ background-color: ${(props) => props.theme.activeColor1};
+ color: #fff;
+ padding: 2px 5px;
+ border-radius: 5px;
+`;
+
+export const People = styled.div`
+ background-color: ${(props) => props.theme.activeColor1};
+ color: #fff;
+ padding: 2px 5px;
+ margin-right: 10px;
+ border-radius: 5px;
+`;
+
+export const RecruitValued = styled.div<{ isRecruitCompleted: boolean }>`
+ background-color: ${(props) => (props.isRecruitCompleted ? 'gray' : props.theme.activeColor2)};
+ color: #fff;
+ font-weight: bold;
+ padding: 2px 5px;
+ margin-right: 10px;
+ border-radius: 5px;
+`;
+
+export const Title = styled.div`
+ color: black;
+ font-weight: bold;
+ font-size: 1.5rem;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+`;
+
+export const Time = styled.div`
+ color: black;
+`;
+
+export const PostPageBtnWrapper = styled.div`
+ width: 100%;
+ height: 50px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ margin-top: 20px;
+ position: relative;
+`;
+
+export const PostPageBtn = styled.a`
+ color: black;
+
+ margin-left: 2px;
+ padding: 4px 8px;
+
+ border: none;
+
+ text-decoration: underline;
+ &:hover {
+ color: blue;
+ }
+`;
diff --git a/src/pages/RecruitmentDetail/CommentItem.tsx b/src/pages/RecruitmentDetail/CommentItem.tsx
new file mode 100644
index 00000000..488dac55
--- /dev/null
+++ b/src/pages/RecruitmentDetail/CommentItem.tsx
@@ -0,0 +1,139 @@
+import React, { useState, useEffect } from 'react';
+import {
+ CommentContent,
+ CommentItemWrapper,
+ Btn,
+ BtnWrapper,
+ CommentName,
+ CommentTime,
+ CommentForm,
+ ContentUserImage,
+ CommentHeader,
+ CommentUserImage,
+} from './style';
+import { getRecruitmentDetail, getUserData, deleteComment } from '../../utils/firebase';
+import { useRecoilState } from 'recoil';
+import { UserId, Render } from '../../utils/recoil';
+import { useParams, useNavigate } from 'react-router-dom';
+import { checkedListCommand } from '@uiw/react-md-editor';
+import swal from 'sweetalert';
+
+import Carousel from 'react-material-ui-carousel';
+import { Paper } from '@mui/material';
+
+interface CommentProps {
+ comment: {
+ name: string;
+ uid: string;
+ time: string;
+ content: string;
+ imageURL: string;
+ email: string;
+ }; // comment 프로퍼티의 타입은 any로 설정하거나 실제 타입으로 지정
+ i: number;
+}
+const CommentItem: React.FC = (props) => {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [render, setRender] = useRecoilState(Render);
+ const [userData, setUserData] = useState({});
+
+ const [data, setData] = useState({});
+ const { channel, path } = useParams<{ channel: string; path: string }>();
+
+ useEffect(() => {
+ if (channel && path) {
+ // Null 체크
+ getRecruitmentDetail(channel, path)
+ .then((result) => {
+ setData(result);
+ })
+ .catch((error) => {
+ console.error('Error fetching data:', error);
+ });
+ getUserData(userId)
+ .then((result) => {
+ setUserData(result);
+ })
+ .catch((error) => {
+ // 에러 핸들링
+ console.error('Error fetching data:', error);
+ });
+ }
+ }, [channel, path]);
+
+ const handleDeleteCommentSubmit = async (e: any) => {
+ e.preventDefault();
+ if (channel && path) {
+ swal({
+ title: '정말로 삭제하시겠습니까?',
+ text: '한번 삭제하면 되돌리실 수 없습니다!',
+ icon: 'warning',
+ buttons: ['취소', '삭제'],
+ dangerMode: true,
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('댓글이 성공적으로 삭제되었습니다!', {
+ icon: 'success',
+ });
+ if (
+ e.target &&
+ e.target.uid &&
+ e.target.content &&
+ e.target.time &&
+ e.target.uid.value &&
+ e.target.content.value &&
+ e.target.time.value
+ ) {
+ const value = {
+ uid: e.target.uid.value,
+ name: props.comment.name,
+ imageURL: props.comment.imageURL,
+ content: props.comment.content,
+ time: e.target.time.value,
+ email: props.comment.email,
+ };
+ deleteComment(channel, path, value);
+ setRender(!render);
+ } else {
+ console.error('uid 또는 content가 정의되지 않았습니다.');
+ }
+ } else {
+ swal('삭제가 취소되었습니다!');
+ }
+ });
+ // Null 체크
+ } else {
+ console.error('channel 또는 path가 정의되지 않았습니다.');
+ }
+ };
+
+ return (
+
+
+
+
+
+ {props.comment.uid == data.uid ? 글쓴이 : props.comment.name}
+ {' (' + props.comment.email.slice(0, 4) + '**)'}
+
+
+
+
+ {userId == props.comment.uid || userId == data.uid ? (
+
+
+ 삭제
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
+
+export default CommentItem;
diff --git a/src/pages/RecruitmentDetail/index.tsx b/src/pages/RecruitmentDetail/index.tsx
new file mode 100644
index 00000000..221b54dc
--- /dev/null
+++ b/src/pages/RecruitmentDetail/index.tsx
@@ -0,0 +1,289 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Btn,
+ BtnWrapper,
+ CommentInputName,
+ CommentWrapper,
+ ContentContainer,
+ ContentHeader,
+ ContentHeaderName,
+ ContentHeaderValuedFalse,
+ ContentHeaderValuedTrue,
+ ContentSub,
+ ContentTitleWrapper,
+ ContentWrapper,
+ Content,
+ RecruitmentDetailContainer,
+ RecruitmentEndBtn,
+ ContentUserImage,
+ CommentCreateWrapper,
+ CommentInputWrapper,
+ CommentBtn,
+} from './style';
+import CommentItem from './CommentItem';
+import { getUserData, createComment, deleteRecruitment, updateRecruitment } from '../../utils/firebase';
+import { useRecoilState } from 'recoil';
+import { UserId, Render, RecruitmentData } from '../../utils/recoil';
+import { useNavigate } from 'react-router-dom';
+import { serverTimestamp, doc, onSnapshot } from 'firebase/firestore';
+import { firestore } from '../../utils/firebase';
+import swal from 'sweetalert';
+
+const RecruitmentDetail: React.FC = () => {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [recruitmentData, setRecruitmentData] = useRecoilState(RecruitmentData);
+ const [commentValue, setCommentValue] = useState('');
+ const [comments, setComments] = useState([]);
+ const [render, setRender] = useRecoilState(Render);
+ const [userData, setUserData] = useState({});
+ const [commentLength, setCommentLength] = useState(0);
+
+ const [data, setData] = useState({});
+
+ const navigate = useNavigate();
+
+ const channel = location.pathname.split('/')[2];
+ const path = location.pathname.split('/')[3];
+
+ useEffect(() => {
+ const fetchUserData = async () => {
+ const docRef = doc(firestore, 'recruitmentContainer', 'recruitment', channel, path);
+ const unsub = await onSnapshot(
+ doc(firestore, 'recruitmentContainer', 'recruitment', channel, path),
+ (doc) => {
+ setData(doc.data());
+ setComments(doc.data()?.comment);
+ setCommentLength(doc.data()?.comment.length);
+ // 수정
+ },
+ );
+ return () => {
+ unsub();
+ };
+ };
+
+ fetchUserData();
+ }, []);
+
+ useEffect(() => {
+ if (!render) {
+ setRender(!render);
+ }
+ getUserData(userId)
+ .then((result) => {
+ setUserData(result);
+ })
+ .catch((error) => {
+ // 에러 핸들링
+ console.error('Error fetching data:', error);
+ });
+ }, [render]);
+
+ const handleCreateCommentSubmit = async (e: any) => {
+ e.preventDefault();
+
+ if (!userId) {
+ swal({
+ title: '댓글을 작성하실 수 없습니다.',
+ text: '로그인 후 사용해주세요 !',
+ icon: 'warning',
+ // buttons: true,
+ // dangerMode: true,
+ });
+ return;
+ }
+ // e.target 및 속성의 존재 여부 확인
+ if (e.target && e.target.uid && e.target.content && e.target.uid.value && e.target.content.value) {
+ const fullDate = new Date();
+ const date =
+ fullDate.getFullYear() +
+ '/' +
+ (fullDate.getMonth() + 1) +
+ '/' +
+ fullDate.getDate() +
+ ' ' +
+ fullDate.getHours() +
+ ':' +
+ fullDate.getMinutes();
+ const value = {
+ uid: e.target.uid.value,
+ content: e.target.content.value,
+ time: date,
+ name: userData.name,
+ imageURL: userData.imageURL,
+ email: userData.email,
+ };
+ await createComment(channel, path, value);
+
+ swal('댓글이 성공적으로 등록되었습니다!', {
+ icon: 'success',
+ });
+ setCommentValue('');
+ // location.reload();
+ } else {
+ console.error('uid 또는 content가 정의되지 않았습니다.');
+ }
+ };
+
+ const handleEdit = () => {
+ swal({
+ title: '정말로 글을 수정하시겠습니까? ',
+ text: '수정 버튼을 누르시면 수정 페이지로 이동합니다.!',
+ icon: 'info',
+ buttons: ['취소', '수정'],
+ }).then((willDelete) => {
+ if (willDelete) {
+ setRecruitmentData({ ...data, channel: channel });
+ navigate('/recruitment/edit/' + channel + '/' + path, data);
+ } else {
+ swal('글 수정을 취소합니다!');
+ }
+ });
+ };
+
+ const handleDeleteRecruitment = () => {
+ swal({
+ title: '정말로 글을 삭제하시겠습니까? ',
+ text: '삭제된 글을 되돌리실 수 없습니다!',
+ icon: 'warning',
+ buttons: ['취소', '삭제'],
+ dangerMode: true,
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('글을 성공적으로 삭제하였습니다.', {
+ icon: 'success',
+ });
+ deleteRecruitment(channel, path);
+ navigate('/recruitment');
+ } else {
+ swal('글 삭제를 취소합니다!');
+ }
+ });
+ };
+
+ const handleRecruitmentValued = () => {
+ swal({
+ title: '모집을 완료하시겠습니까? ',
+ text: '모집중으로 되돌리실 수 없습니다!',
+ icon: 'info',
+ buttons: ['취소', '모집 완료'],
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('모집이 성공적으로 마감되었습니다.', {
+ icon: 'success',
+ });
+ const value = data;
+ value.recruitValued = !value.recruitValued;
+
+ console.log(value);
+
+ updateRecruitment(channel, path, value);
+ } else {
+ swal('모집 마감이 취소되었습니다.!');
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+ {data.name}
+ {data.recruitValued ? (
+ 모집중
+ ) : (
+ 모집완료
+ )}
+
+
+ {data.title}
+
+ {data.editValued
+ ? new Date(data.editTime?.toMillis()).toLocaleString() + ' (수정됨)'
+ : new Date(data.time?.toMillis()).toLocaleString()}
+
+ {userId == data.uid ? (
+
+ {data.recruitValued ? 수정 : ''}
+
+ 삭제
+
+ ) : (
+ ''
+ )}
+
+
+ 분야 : {data.category}
+ 인원 : {data.people} 명
+ 댓글 : {commentLength} 개
+
+
+ {data.content?.split('\n').map((line: string) => {
+ //this.props.data.content: 내용
+ return (
+
+ {line}
+
+
+ );
+ })}
+
+
+ {userId == data.uid ? (
+ data.recruitValued ? (
+ 모집 마감
+ ) : (
+ ''
+ )
+ ) : (
+ ''
+ )}
+
+ {render ? (
+
+ {comments ? comments.map((v: any, i: number) => ) : ''}
+
+ 댓글 쓰기
+
+
+
+ 작성
+
+
+
+
+ ) : (
+ <>로딩중..>
+ )}
+
+
+ );
+};
+
+export default RecruitmentDetail;
diff --git a/src/pages/RecruitmentDetail/style.tsx b/src/pages/RecruitmentDetail/style.tsx
new file mode 100644
index 00000000..35f245c0
--- /dev/null
+++ b/src/pages/RecruitmentDetail/style.tsx
@@ -0,0 +1,291 @@
+import styled from 'styled-components';
+
+export const RecruitmentDetailContainer = styled.div`
+ display: flex;
+ padding-top: 72px;
+ height: 100vh;
+ width: 100vw;
+
+ overflow: auto;
+ background-color: ${(props) => props.theme.recruitmentBack};
+
+ position: relative;
+`;
+
+export const ContentContainer = styled.div`
+ width: 100%;
+ max-width: 1080px;
+ height: 100%;
+
+ margin: 0 auto;
+`;
+
+export const ContentWrapper = styled.div`
+ background-color: white;
+ margin: 20px auto;
+
+ width: 95%;
+ min-height: 500px;
+ height: auto;
+
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.5);
+ border-radius: 10px;
+
+ position: relative;
+`;
+
+export const ContentHeader = styled.div`
+ width: 100%;
+ height: 50px;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ border-bottom: 1px solid #efefef;
+ position: relative;
+`;
+
+export const ContentHeaderName = styled.div`
+ margin-left: 50px;
+
+ font-size: 1.2rem;
+ font-weight: 500;
+`;
+
+export const Content = styled.div`
+ padding: 20px;
+ width: 100%;
+ word-break: break-all;
+`;
+
+export const ContentUserImage = styled.img`
+ width: 30px;
+ height: 30px;
+ border-radius: 20px;
+
+ position: absolute;
+ top: 8px;
+ left: 10px;
+`;
+
+export const ContentHeaderValuedTrue = styled.div`
+ margin-right: 25px;
+ padding: 4px 10px;
+
+ font-size: 1rem;
+
+ color: white;
+ background-color: ${(props) => props.theme.activeColor2};
+
+ border-radius: 15px;
+`;
+
+export const ContentHeaderValuedFalse = styled.div`
+ margin-right: 20px;
+ padding: 4px 10px;
+
+ font-size: 1rem;
+
+ color: black;
+ background-color: #efefef;
+
+ border-radius: 15px;
+`;
+
+export const ContentTitleWrapper = styled.div`
+ width: 100%;
+ height: 70px;
+
+ padding-left: 20px;
+
+ border-bottom: 1px solid #efefef;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ position: relative;
+`;
+
+export const ContentSub = styled.div`
+ width: 100%;
+ height: 100px;
+
+ padding-left: 20px;
+
+ border-bottom: 1px solid #efefef;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`;
+
+export const CommentWrapper = styled.div`
+ width: 95%;
+ min-height: 140px;
+ height: auto;
+
+ background-color: white;
+
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.5);
+ border-radius: 10px;
+
+ margin: 20px auto;
+ padding: 20px 0;
+`;
+
+export const CommentItemWrapper = styled.div`
+ width: 100%;
+ height: 100px;
+
+ padding: 20px;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ border-bottom: 1px solid #efefef;
+
+ position: relative;
+`;
+
+export const CommentHeader = styled.div`
+ position: relative;
+`;
+
+export const CommentUserImage = styled.img`
+ width: 25px;
+ height: 25px;
+ border-radius: 20px;
+
+ position: absolute;
+ top: 0px;
+ left: 0px;
+`;
+
+export const CommentName = styled.p`
+ font-size: 1rem;
+ font-weight: 500;
+ margin-left: 35px;
+ margin-top: 2px;
+`;
+
+export const CommentInputName = styled.p`
+ font-size: 1rem;
+ font-weight: 500;
+ margin-bottom: 4px;
+`;
+
+export const CommentForm = styled.form`
+ margin-top: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`;
+
+export const CommentContent = styled.input`
+ font-size: 1.2rem;
+ margin-top: 5px;
+
+ background-color: white;
+ border: none;
+`;
+
+export const CommentTime = styled.input`
+ font-size: 0.8rem;
+ margin-left: 2px;
+
+ background-color: white;
+ border: none;
+
+ color: black;
+`;
+
+export const BtnWrapper = styled.div`
+ position: absolute;
+
+ top: 20px;
+ right: 20px;
+
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+`;
+
+export const Btn = styled.button`
+ margin-right: 10px;
+ padding: 5px 16px;
+
+ background-color: #fff;
+ border: 1px solid #efefef;
+ box-shadow: 1px 1px rgba(0, 0, 0, 0.4);
+
+ font-size: 0.8rem;
+
+ border-radius: 5px;
+
+ &:hover {
+ background-color: #f0f0f0;
+ cursor: pointer;
+ }
+`;
+
+export const RecruitmentEndBtn = styled.button`
+ margin-left: 10px;
+ padding: 8px 24px;
+
+ background-color: #fff;
+ border: 1px solid #efefef;
+ box-shadow: 1px 1px rgba(0, 0, 0, 0.4);
+
+ font-size: 0.8rem;
+ font-weight: bold;
+
+ border-radius: 5px;
+
+ position: absolute;
+ bottom: 20px;
+ left: 50%;
+ transform: translate(-50%, 0);
+
+ &:hover {
+ background-color: #f0f0f0;
+ cursor: pointer;
+ }
+`;
+
+export const CommentCreateWrapper = styled.div`
+ width: 98%;
+
+ margin: 0 auto;
+ margin-top: 20px;
+ padding: 20px;
+
+ border-radius: 10px;
+ border: 1px solid gray;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`;
+
+export const CommentInputWrapper = styled.div`
+ display: flex;
+`;
+
+export const CommentBtn = styled.button`
+ margin-left: 20px;
+ width: 100px;
+ height: 60px;
+
+ background-color: #fff;
+ border: 1px solid #efefef;
+ border-radius: 5px;
+ box-shadow: 1px 1px rgba(0, 0, 0, 0.4);
+
+ &:hover {
+ background-color: #f0f0f0;
+ cursor: pointer;
+ }
+`;
diff --git a/src/pages/RecruitmentEdit/index.tsx b/src/pages/RecruitmentEdit/index.tsx
new file mode 100644
index 00000000..18f2e7b5
--- /dev/null
+++ b/src/pages/RecruitmentEdit/index.tsx
@@ -0,0 +1,206 @@
+import React, { useEffect, useState } from 'react';
+import { useRecoilState } from 'recoil';
+import { UserId } from '../../utils/recoil';
+import { PostContainer, RecruitmentPostContainer, PostForm, PostBox, PostBtn, PostH, PostTextarea } from './style';
+import { updateRecruitment } from '../../utils/firebase';
+import { useNavigate } from 'react-router-dom';
+import { serverTimestamp } from 'firebase/firestore';
+import Select from '@mui/material/Select';
+import MenuItem from '@mui/material/MenuItem';
+import InputLabel from '@mui/material/InputLabel';
+import TextField from '@mui/material/TextField';
+import { RecruitmentData } from '../../utils/recoil';
+import swal from 'sweetalert';
+
+const RecruitmentEdit: React.FC = () => {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [recruitmentData, setRecruitmentData] = useRecoilState(RecruitmentData);
+
+ const [categoryToggle, setCategoryToggle] = useState(true);
+ const [peopleValue, setPeopleValue] = useState(recruitmentData.people);
+ const min = 1;
+ const max = 50;
+
+ const navigate = useNavigate();
+
+ const channel = location.pathname.split('/')[3];
+ const path = location.pathname.split('/')[4];
+
+ useEffect(() => {
+ if (recruitmentData.uid != userId) {
+ navigate('/recruitment');
+ }
+
+ if (channel == 'project') {
+ setCategoryToggle(false);
+ }
+ }, [channel, path]);
+
+ console.log(recruitmentData);
+
+ const handleUpdateRecruitment = (e: any) => {
+ e.preventDefault();
+
+ swal({
+ title: '수정을 완료하시겠습니까? ',
+ text: '수정된 정보로 글이 수정됩니다.',
+ icon: 'info',
+ buttons: ['취소', '수정'],
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('수정이 성공적으로 마감되었습니다.', {
+ icon: 'success',
+ });
+ if (
+ e.target &&
+ e.target.content &&
+ e.target.content.value &&
+ e.target.category &&
+ e.target.category.value &&
+ e.target.title &&
+ e.target.title.value &&
+ e.target.people &&
+ e.target.people.value &&
+ e.target.recruitmentType &&
+ e.target.recruitmentType.value
+ ) {
+ // const date = new Date().getTime();
+ // console.log(date);
+ const updated_at_timestamp = serverTimestamp();
+
+ const value = {
+ uid: userId,
+ category: e.target.category.value,
+ title: e.target.title.value,
+ content: e.target.content.value,
+ people: Number(e.target.people.value),
+ recruitValued: true,
+ comment: recruitmentData.comment,
+ time: recruitmentData.time,
+ name: recruitmentData.name,
+ imageURL: recruitmentData.imageURL,
+ editValued: true,
+ editTime: updated_at_timestamp,
+ };
+
+ updateRecruitment(channel, path, value);
+ navigate('/recruitment/' + channel + '/' + path);
+ }
+ } else {
+ swal('수정이 취소되었습니다.!');
+ }
+ });
+ };
+
+ const handleCategory = (e: any) => {
+ e.preventDefault();
+
+ if (e.target.value == 'study') {
+ setCategoryToggle(true);
+ } else {
+ setCategoryToggle(false);
+ }
+ };
+
+ return (
+
+
+
+
+ 분야
+
+ 대분류
+
+ 프로젝트
+
+ 스터디
+
+
+
+ {categoryToggle ? (
+
+ 분류
+
+ 코딩테스트
+
+ CS
+ 면접
+ 알고리즘
+
+ ) : (
+
+ 분류
+ 토이 프로젝트
+ 연계 프로젝트
+
+ )}
+
+
+ 모집 인원
+ {
+ let peopleValue = parseInt(e.target.value, 10);
+
+ if (peopleValue > max) peopleValue = max;
+ if (peopleValue < min) peopleValue = min;
+
+ setPeopleValue(peopleValue);
+ }}
+ helperText="최대 50명"
+ style={{ width: '150px' }}
+ />
+
+
+ 제목
+
+
+
+
+
+
+ 수정완료
+
+
+
+ );
+};
+
+export default RecruitmentEdit;
diff --git a/src/pages/RecruitmentEdit/style.tsx b/src/pages/RecruitmentEdit/style.tsx
new file mode 100644
index 00000000..7c364b7d
--- /dev/null
+++ b/src/pages/RecruitmentEdit/style.tsx
@@ -0,0 +1,74 @@
+import styled from 'styled-components';
+export const RecruitmentPostContainer = styled.div`
+ padding-top: 72px;
+ height: 100vh;
+ width: 100vw;
+
+ overflow: auto;
+ background-color: var(--mention-badge);
+`;
+
+export const PostContainer = styled.div`
+ width: 100%;
+ max-width: 1080px;
+ height: auto;
+
+ margin: 20px auto;
+ background-color: white;
+
+ padding: 20px;
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.5);
+ border-radius: 10px;
+`;
+
+export const PostForm = styled.form`
+ padding-top: 0;
+
+ display: flex;
+ flex-direction: column;
+`;
+
+export const PostBox = styled.div`
+ border-bottom: 1px solid #eee;
+
+ padding: 10px;
+ margin-bottom: 10px;
+`;
+
+export const PostH = styled.p`
+ font-size: 1.2rem;
+ font-weight: 700;
+
+ margin: 0;
+ margin-bottom: 5px;
+`;
+
+export const PostTextarea = styled.textarea`
+ width: 100%;
+ height: 300px;
+ padding: 10px;
+ box-sizing: border-box;
+ border: solid 2px #1e90ff;
+ border-radius: 5px;
+ font-size: 16px;
+ resize: both;
+`;
+
+export const PostBtn = styled.button`
+ width: 100%;
+ height: 50px;
+
+ border-radius: 10px;
+ border: none;
+
+ font-size: 1.2rem;
+ font-weight: 700;
+
+ color: white;
+
+ background-color: var(--active-item);
+
+ &:hover {
+ cursor: pointer;
+ }
+`;
diff --git a/src/pages/RecruitmentPost/index.tsx b/src/pages/RecruitmentPost/index.tsx
new file mode 100644
index 00000000..08321fb1
--- /dev/null
+++ b/src/pages/RecruitmentPost/index.tsx
@@ -0,0 +1,225 @@
+import React, { useEffect, useState } from 'react';
+import { useRecoilState } from 'recoil';
+import { UserId } from '../../utils/recoil';
+import { PostContainer, RecruitmentPostContainer, PostForm, PostBox, PostBtn, PostH, PostTextarea } from './style';
+import { createRecruitment } from '../../utils/firebase';
+import { useNavigate } from 'react-router-dom';
+import { serverTimestamp } from 'firebase/firestore';
+import { getUserData } from '../../utils/firebase';
+import Select from '@mui/material/Select';
+import MenuItem from '@mui/material/MenuItem';
+import InputLabel from '@mui/material/InputLabel';
+import TextField from '@mui/material/TextField';
+import swal from 'sweetalert';
+
+const RecruitmentPost: React.FC = () => {
+ const [userId, setUserId] = useRecoilState(UserId);
+ const [categoryToggle, setCategoryToggle] = useState(true);
+ const [userData, setUserData] = useState({});
+ const [peopleValue, setPeopleValue] = useState(1);
+ const [categoryViewToggle, setCategoryViewToggle] = useState(false);
+
+ const min = 1;
+ const max = 50;
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!userId) {
+ swal({
+ title: '글을 작성하실 수 없습니다.',
+ text: '로그인 후 사용해주세요 !',
+ icon: 'warning',
+ // buttons: true,
+ // dangerMode: true,
+ });
+ navigate('/recruitment');
+ }
+ getUserData(userId)
+ .then((result) => {
+ setUserData(result);
+ })
+ .catch((error) => {
+ // 에러 핸들링
+ console.error('Error fetching data:', error);
+ });
+ }, []);
+
+ const handleCreateRecruitment = (e: any) => {
+ e.preventDefault();
+ if (
+ e.target &&
+ e.target.content &&
+ e.target.content.value &&
+ e.target.category &&
+ e.target.category.value &&
+ e.target.title &&
+ e.target.title.value &&
+ e.target.people &&
+ e.target.people.value &&
+ e.target.recruitmentType &&
+ e.target.recruitmentType.value
+ ) {
+ swal({
+ title: '글을 작성하시겠습니까?',
+ text: '작성 후 수정 삭제를 자유롭게 하실 수 있습니다.',
+ icon: 'info',
+ buttons: ['취소', '작성'],
+ }).then((willDelete) => {
+ if (willDelete) {
+ swal('글을 성공적으로 작성하였습니다.', {
+ icon: 'success',
+ });
+
+ // const date = new Date().getTime();
+ // console.log(date);
+ const updated_at_timestamp = serverTimestamp();
+
+ const value = {
+ uid: userId,
+ name: userData.name,
+ imageURL: userData.imageURL,
+ category: e.target.category.value,
+ title: e.target.title.value,
+ content: e.target.content.value,
+ people: Number(e.target.people.value),
+ recruitValued: true,
+ comment: [],
+ time: updated_at_timestamp,
+ editValued: false,
+ editTime: updated_at_timestamp,
+ };
+ console.log(value);
+
+ console.log(e.target.recruitmentType.value);
+ createRecruitment(e.target.recruitmentType.value, value);
+
+ navigate('/recruitment');
+ } else {
+ swal('글 작성이 취소되었습니다.');
+ }
+ });
+ } else {
+ swal({ title: '빈 공간 없이 입력해주세요', text: '부탁드립니다..', icon: 'warning' });
+ }
+ };
+
+ const handleCategory = (e: any) => {
+ e.preventDefault();
+ if (!categoryViewToggle) {
+ setCategoryViewToggle(true);
+ }
+ if (e.target.value == 'study') {
+ setCategoryToggle(true);
+ } else {
+ setCategoryToggle(false);
+ }
+ };
+
+ return (
+
+
+
+
+ 분야
+ {categoryViewToggle ? (
+
+ 대분류
+
+ 프로젝트
+ 스터디
+
+ ) : (
+
+ 대분류
+
+ 프로젝트
+ 스터디
+
+ )}
+ {categoryViewToggle ? (
+ categoryToggle ? (
+
+ 분류
+ 코딩테스트
+ CS
+ 면접
+ 알고리즘
+
+ ) : (
+
+ 분류
+ 토이 프로젝트
+ 연계 프로젝트
+
+ )
+ ) : (
+ ''
+ )}
+
+
+ 모집 인원
+ {
+ let peopleValue = parseInt(e.target.value, 10);
+
+ if (peopleValue > max) peopleValue = max;
+ if (peopleValue < min) peopleValue = min;
+
+ setPeopleValue(peopleValue);
+ }}
+ helperText="최대 50명"
+ style={{ width: '150px', marginTop: '10px' }}
+ />
+
+
+ 제목
+
+
+
+
+
+
+ 작성완료
+
+
+
+ );
+};
+
+export default RecruitmentPost;
diff --git a/src/pages/RecruitmentPost/style.tsx b/src/pages/RecruitmentPost/style.tsx
new file mode 100644
index 00000000..ebe36048
--- /dev/null
+++ b/src/pages/RecruitmentPost/style.tsx
@@ -0,0 +1,80 @@
+import styled from 'styled-components';
+
+export const RecruitmentPostContainer = styled.div`
+ padding-top: 72px;
+ height: 100vh;
+ width: 100vw;
+
+ overflow: auto;
+ background-color: ${(props) => props.theme.recruitmentBack};
+`;
+
+export const PostContainer = styled.div`
+ width: 100%;
+ max-width: 1080px;
+ height: auto;
+
+ margin: 20px auto;
+ background-color: white;
+
+ padding: 20px;
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.5);
+ border-radius: 10px;
+`;
+
+export const PostForm = styled.form`
+ padding-top: 0;
+
+ display: flex;
+ flex-direction: column;
+`;
+
+export const PostBox = styled.div`
+ border-bottom: 1px solid #eee;
+
+ padding: 10px;
+ margin-bottom: 10px;
+`;
+
+export const PostH = styled.p`
+ font-size: 1.2rem;
+ font-weight: 700;
+
+ margin: 0;
+ margin-bottom: 5px;
+`;
+
+export const PostTextarea = styled.textarea`
+ width: 100%;
+ height: 300px;
+ padding: 10px;
+ box-sizing: border-box;
+ border: 2px solid black;
+ border-radius: 5px;
+ font-size: 16px;
+ resize: both;
+ box-sizing: border-box;
+ &:focus {
+ outline: 1px solid ${(props) => props.theme.activeColor1};
+ border: 2px solid ${(props) => props.theme.activeColor1};
+ }
+`;
+
+export const PostBtn = styled.button`
+ width: 100%;
+ height: 50px;
+
+ border-radius: 10px;
+ border: none;
+
+ font-size: 1.2rem;
+ font-weight: 700;
+
+ color: white;
+
+ background-color: ${(props) => props.theme.activeColor1};
+
+ &:hover {
+ cursor: pointer;
+ }
+`;
diff --git a/src/pages/SignIn/index.tsx b/src/pages/SignIn/index.tsx
new file mode 100644
index 00000000..50532305
--- /dev/null
+++ b/src/pages/SignIn/index.tsx
@@ -0,0 +1,230 @@
+import * as React from 'react';
+import Avatar from '@mui/material/Avatar';
+import Button from '@mui/material/Button';
+import CssBaseline from '@mui/material/CssBaseline';
+import TextField from '@mui/material/TextField';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Container from '@mui/material/Container';
+import { createTheme, ThemeProvider } from '@mui/material/styles';
+import { useNavigate, useLocation } from 'react-router-dom';
+
+import { auth, addUser } from '../../utils/firebase';
+import { createUserWithEmailAndPassword } from 'firebase/auth';
+import swal from 'sweetalert';
+
+const defaultTheme = createTheme();
+
+export default function SignIn() {
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+
+ // 회원가입 기능
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ const data = new FormData(event.currentTarget);
+
+ const emailEntry = data.get('email');
+ const email = emailEntry !== null ? emailEntry.toString() : '';
+
+ const passwordEntry = data.get('password');
+ const password = passwordEntry !== null ? passwordEntry.toString() : '';
+
+ const confirmPasswordEntry = data.get('confirmPassword');
+ const confirmPassword = confirmPasswordEntry !== null ? confirmPasswordEntry.toString() : '';
+
+ const nameEntry = data.get('name');
+ const name = nameEntry !== null ? nameEntry.toString() : '';
+
+ if (password != confirmPassword) {
+ swal({
+ title: '비밀번호가 일치하지 않습니다.',
+ text: '비밀번호를 다시 확인해주세요',
+ icon: 'warning',
+ });
+ return;
+ }
+
+ // firebase Auth 등록
+ createUserWithEmailAndPassword(auth, email, password)
+ .then((userCredential) => {
+ const user = userCredential.user;
+
+ const value = {
+ name: name,
+ email: email,
+ info: `안녕하세요 ${name}입니다.`,
+ login: false,
+ imageURL:
+ 'https://firebasestorage.googleapis.com/v0/b/wiki-for-fastcampus.appspot.com/o/images%2Fprofile.jpeg?alt=media&token=29da086e-74a8-445c-99b3-7332569544f7',
+ timelog: [],
+ Theme: {
+ navBar: '#350d36',
+ sideMenu: '#3F0E40',
+ pointItem: '#4D2A51',
+ text: '#fff',
+ activeColor1: '#1164A3',
+ activeColor2: '#2BAC76',
+ recruitmentBack: '#ECE7EC',
+ },
+ ThemeBorder: {
+ first: '3px solid #fff',
+ second: 'none',
+ third: 'none',
+ fourth: 'none',
+ },
+ };
+ addUser(user.uid, value);
+
+ swal({
+ title: '회원가입 성공',
+ text: '가입해주셔서 감사합니다 !',
+ icon: 'success',
+ });
+ // 회원가입 성공시 redirect
+ navigate('/login', { state: pathname });
+ })
+ .catch((error) => {
+ switch (error.code) {
+ case 'auth/user-not-found' || 'auth/wrong-password':
+ return swal({
+ title: '회원가입 에러',
+ text: '이메일 혹은 비밀번호가 일치하지 않습니다.',
+ icon: 'warning',
+ });
+ case 'auth/email-already-in-use':
+ return swal({
+ title: '회원가입 에러',
+ text: '이미 사용중인 이메일입니다.',
+ icon: 'warning',
+ });
+ case 'auth/weak-password':
+ return swal({
+ title: '회원가입 에러',
+ text: '비밀번호는 6자리 이상으로 작성해주세요',
+ icon: 'warning',
+ });
+ case 'auth/network-request-failed':
+ return swal({
+ title: '회원가입 에러',
+ text: '네트워크 연결에 실패 하였습니다.',
+ icon: 'warning',
+ });
+ case 'auth/invalid-email':
+ return swal({
+ title: '회원가입 에러',
+ text: '잘못된 이메일 형식입니다.',
+ icon: 'warning',
+ });
+ case 'auth/internal-error':
+ return swal({
+ title: '회원가입 에러',
+ text: '잘못된 요청입니다.',
+ icon: 'warning',
+ });
+ default:
+ return swal({
+ title: '회원가입 에러',
+ text: '회원가입에 실패 하였습니다. 다시 확인해주세요',
+ icon: 'warning',
+ });
+ }
+ // ..
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ 회원가입
+
+
+
+
+
+
+
+
+
+ 회원가입
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Wiki/index.tsx b/src/pages/Wiki/index.tsx
new file mode 100644
index 00000000..8d2f6779
--- /dev/null
+++ b/src/pages/Wiki/index.tsx
@@ -0,0 +1,210 @@
+import React, { useEffect, useState } from 'react';
+import { WikiContainer, EditCompletedButton, WikiContent, ChannelNames, MDEditBtn, ReadChannel } from './style';
+import SidebarWiki from '../../components/SidebarWiki';
+import MDEditor, { bold } from '@uiw/react-md-editor';
+import rehypeSanitize from 'rehype-sanitize';
+import { themeType, updateChannelContent } from '../../utils/firebase';
+import { handleGetDocs, DocumentData } from '../../utils/firebase';
+import { QuerySnapshot } from 'firebase/firestore';
+import { Timestamp } from 'firebase/firestore';
+import { useRecoilState } from 'recoil';
+import { ThemeChange } from '../../utils/recoil';
+const Wiki: React.FC = () => {
+ const defaultChannel = '기본 정보';
+ const defaultSubChannel = '과정 참여 규칙';
+ const [back, setBack] = useState('');
+ const [currentTheme, setCurrentTheme] = useRecoilState(ThemeChange);
+
+ useEffect(() => {
+ const selected = (theme: themeType) => {
+ setBack(theme.recruitmentBack);
+ };
+ if (localStorage.getItem('theme')) {
+ const localtheme = localStorage.getItem('theme');
+ if (localtheme) {
+ const color = JSON.parse(localtheme);
+ selected(color);
+ }
+ }
+ }, [currentTheme]);
+ const [clickedValue, setClickedValue] = useState({
+ channel: defaultChannel,
+ subChannel: defaultSubChannel,
+ value: {
+ content: '',
+ },
+ });
+ const [docsWithFields, setDocsWithFields] = useState<{ docId: string; docKeys: string[]; docData: DocumentData }[]>(
+ [],
+ );
+ const [isClicked, setIsClicked] = useState(false);
+
+ const [md, setMd] = useState('');
+ const [time, setTime] = useState('');
+ const [isToggled, setIsToggled] = useState(true);
+ const defaultChannels = ['기본 정보'];
+ const defaultSubChannels = ['과정 참여 규칙', '링크 모음'];
+ const toggleButton = () => {
+ setIsToggled(!isToggled);
+ };
+
+ const handleKeyClick = (value: any) => {
+ setClickedValue(value);
+ setIsClicked(true);
+ if (!isToggled) {
+ setIsToggled(true);
+ }
+ };
+
+ // Define an onChange handler for the MDEditor
+ const handleEditorChange = (value: string | undefined) => {
+ if (value !== undefined) {
+ setMd(value);
+
+ if (isClicked) {
+ setClickedValue((prevState: any) => {
+ const newTime = Timestamp.fromDate(new Date());
+ return {
+ ...prevState,
+ value: {
+ ...prevState.value,
+ content: value,
+ time: newTime,
+ },
+ };
+ });
+ }
+ }
+ };
+
+ // Function to handle the update when the button is clicked
+ const handleUpdateButtonClick = async () => {
+ // First, update the state by calling setMd
+ setMd((currentMd) => {
+ // Now, you can call the updateChannelContent function with the updated md content
+ updateChannelContent('wiki', clickedValue.channel, clickedValue.subChannel, currentMd)
+ .then(() => {
+ console.log('Content updated successfully'); // Handle success
+ })
+ .catch((error) => {
+ console.error('Content update failed', error); // Handle error
+ });
+ return currentMd; // Return the current value to update the state
+ });
+ };
+
+ // Function to handle both toggle and update button click
+ const handleToggleAndUpdateClick = async () => {
+ await handleUpdateButtonClick();
+ if (clickedValue.value.time) {
+ setTime(new Date().toLocaleString());
+ } else {
+ setTime(''); // Set a default value or an empty string if time is undefined
+ }
+ if (!isToggled) {
+ setIsToggled(true);
+ }
+ };
+
+ useEffect(() => {
+ handleGetDocs('wiki', (querySnapshot: QuerySnapshot) => {
+ const data: { docId: string; docKeys: string[]; docData: DocumentData }[] = [];
+
+ querySnapshot.forEach((doc: any) => {
+ const docData = doc.data();
+ const docId = doc.id;
+ const docKeys = Object.keys(docData);
+ data.push({ docId, docKeys, docData });
+ });
+ setDocsWithFields(data);
+ });
+ }, []);
+
+ useEffect(() => {
+ if (isClicked) {
+ setMd(clickedValue.value.content);
+ if (clickedValue.value.time) {
+ setTime(clickedValue.value.time.toDate().toLocaleString());
+ } else {
+ setTime(''); // 시간이 정의되지 않은 경우 기본값 또는 빈 문자열 설정
+ }
+ } else {
+ docsWithFields.forEach((item) => {
+ item.docKeys.forEach((item2) => {
+ if (item.docId === defaultChannels[0] && item2 === defaultSubChannels[0]) {
+ setMd(item.docData[item2].content);
+ if (item.docData[item2].time) {
+ setTime(item.docData[item2].time.toDate().toLocaleString());
+ } else {
+ setTime(''); // 시간이 정의되지 않은 경우 기본값 또는 빈 문자열 설정
+ }
+ }
+ });
+ });
+ }
+ }, [docsWithFields, clickedValue, isClicked]);
+
+ return (
+
+
+ {isToggled ? (
+
+
+
+
+ {clickedValue.subChannel}
+
+ {
+ toggleButton();
+ }}
+ >
+
+
+
+ {time}
+
+
+
+ ) : (
+
+
+
+
+ {clickedValue.subChannel}
+
+ {
+ handleToggleAndUpdateClick();
+ }}
+ >
+
+
+
+
+
+
+ )}
+
+ );
+};
+export default Wiki;
diff --git a/src/pages/Wiki/style.tsx b/src/pages/Wiki/style.tsx
new file mode 100644
index 00000000..ff382f97
--- /dev/null
+++ b/src/pages/Wiki/style.tsx
@@ -0,0 +1,68 @@
+import styled from 'styled-components';
+
+export const WikiContainer = styled.div`
+ display: flex;
+ padding-top: 72px;
+ height: auto;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+`;
+export const WikiContent = styled.div`
+ display: flex;
+ justify-content: left;
+
+ background-color: ${(props) => props.theme.recruitmentBack};
+ width: 100%;
+ padding: 20px 40px 20px 40px;
+ overflow: auto;
+ &::-webkit-scrollbar {
+ width: 12px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--point-item);
+ border-radius: 6px;
+ border: 3px solid transparent;
+ }
+`;
+
+export const EditCompletedButton = styled.div`
+ display: absolute;
+ border: none;
+ margin-right: 70px;
+ margin-left: 10px;
+ font-weight: 500;
+ font-size: 24px;
+ color: ${(props) => props.theme.text};
+`;
+export const BeforeEdit = styled.div`
+ display: absolute;
+ border: none;
+ position: left;
+ display: flex;
+ flex-direction: column;
+ margin-top: 20px;
+ font-weight: 500;
+ font-size: 24px;
+ color: ${(props) => props.theme.text};
+`;
+
+export const ChannelNames = styled.div`
+ width: 100%;
+`;
+
+export const MDEditBtn = styled.button`
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ width: 50px;
+ height: 50px;
+`;
+
+export const ReadChannel = styled.div`
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 3px solid black;
+ margin-bottom: 20px;
+`;
diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts
new file mode 100644
index 00000000..8d12ecf9
--- /dev/null
+++ b/src/utils/firebase.ts
@@ -0,0 +1,459 @@
+import { initializeApp } from 'firebase/app';
+
+import {
+ getFirestore,
+ Firestore,
+ doc,
+ getDocs,
+ addDoc,
+ collection,
+ deleteDoc,
+ getDoc,
+ setDoc,
+ onSnapshot,
+ QuerySnapshot,
+ arrayUnion,
+ arrayRemove,
+ updateDoc,
+ query,
+ orderBy,
+ serverTimestamp,
+ FieldValue,
+} from 'firebase/firestore';
+
+import { getDownloadURL, getStorage, ref, uploadBytes } from 'firebase/storage';
+import { getAuth } from 'firebase/auth';
+import { Snapshot } from 'recoil';
+
+const firebaseConfig = {
+ apiKey: process.env.REACT_APP_API_KEY,
+ authDomain: process.env.REACT_APP_AUTH_DOMAIN,
+ projectId: process.env.REACT_APP_PROJECT_ID,
+ storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
+ messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
+ appId: process.env.REACT_APP_APP_ID,
+ measurementId: process.env.REACT_APP_MEASUREMENT_ID,
+};
+
+const app = initializeApp(firebaseConfig);
+export const storage = getStorage(app);
+export const firestore: Firestore = getFirestore(app);
+export const auth = getAuth(app);
+export const storeRef = doc(firestore, 'gallery', '레퍼런스 공유');
+import swal from 'sweetalert';
+
+export type DocumentData = { [key: string]: any };
+
+export const handleGetDocs = (
+ collectionName: string,
+ callback: (querySnapshot: QuerySnapshot) => void,
+) => {
+ const collectionRef = collection(firestore, collectionName);
+
+ // onSnapshot 함수를 사용하여 실시간 업데이트를 수신
+ const updatedQuerySnapshot = onSnapshot(collectionRef, (querySnapshot) => {
+ console.log('문서 가져오기 성공 (실시간 업데이트)!');
+ callback(querySnapshot); // 실시간 업데이트를 콜백 함수로 전달
+ });
+
+ return updatedQuerySnapshot;
+};
+
+export const createChannelDoc = async (collectionName: string, documentName: string) => {
+ const dataToAdd = {}; // 서브채널 없이 채널만 생성하기 위해 빈 객체 삽입
+ const documentRef = doc(firestore, collectionName, documentName);
+ try {
+ await setDoc(documentRef, dataToAdd);
+ console.log('채널 생성 성공!');
+ } catch (error) {
+ console.error('채널 생성 실패!', error);
+ throw error;
+ }
+};
+// user의 타임로그 배열 형태로 저장
+export const createTimelog = async (collectionName: string, userName: string, currentTime: string) => {
+ const value = currentTime;
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const pushTimeLog = await updateDoc(userRef, { timelog: arrayUnion(value) });
+ // alert(`${value}\n입/퇴실기록이 정상 기록되었습니다!`);
+ swal({
+ title: '정상적으로 기록되었습니다!',
+ text: `${value}`,
+ icon: 'success',
+ });
+ console.log(value);
+ console.log('입/퇴실기록이 정상 기록되었습니다!');
+ } catch (error) {
+ console.error('타임로그 생성 실패!', error);
+ throw error;
+ }
+};
+// user의 정보를 가져옴
+export const readUser = async (collectionName: string, userName: string) => {
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const Docs = await getDoc(userRef);
+ if (Docs) {
+ const data = Docs.data();
+ if (data) {
+ return data;
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+};
+export const updateUserName = async (collectionName: string, userName: string, updateData: string) => {
+ const value = updateData;
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const pushTimeLog = await updateDoc(userRef, { name: value });
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+};
+export const updateUserEmail = async (collectionName: string, userName: string, updateData: string) => {
+ const value = updateData;
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const pushTimeLog = await updateDoc(userRef, { email: value });
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+};
+export const updateUserInfo = async (collectionName: string, userName: string, updateData: string) => {
+ const value = updateData;
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const pushTimeLog = await updateDoc(userRef, { info: value });
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+};
+export const updateUserImg = async (collectionName: string, userName: string, updateData: string) => {
+ const value = updateData;
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const pushTimeLog = await updateDoc(userRef, { imageURL: value });
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+};
+export interface themeType {
+ navBar: string;
+ sideMenu: string;
+ text: string;
+ activeColor1: string;
+ activeColor2: string;
+ recruitmentBack: string;
+}
+export interface themeBorder {
+ first: string;
+ second: string;
+ third: string;
+ fourth: string;
+}
+export const updateUserTheme = async (
+ collectionName: string,
+ userName: string,
+ updateData: themeType,
+ selecData: themeBorder,
+) => {
+ const theme = updateData;
+ const themeBorder = selecData;
+ const userRef = doc(firestore, collectionName, userName);
+ try {
+ const pushTheme = await updateDoc(userRef, { Theme: theme });
+ const pushThemeBorder = await updateDoc(userRef, { ThemeBorder: themeBorder });
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+};
+
+export const addFieldToDoc = async (
+ collectionName: string,
+ documentName: string,
+ fieldName: string,
+ fieldValue: any,
+) => {
+ const documentRef = doc(firestore, collectionName, documentName);
+ try {
+ const documentSnapshot = await getDoc(documentRef); // document를 가져와서 기존 데이터를 읽어옴
+ if (documentSnapshot.exists()) {
+ const data = documentSnapshot.data();
+ data[fieldName] = fieldValue; // 새로운 필드 추가
+ await setDoc(documentRef, data); // 업데이트된 데이터를 다시 document에 저장
+ console.log(`서브채널 생성 성공!`);
+ } else {
+ console.error('채널이 존재하지 않습니다.');
+ }
+ } catch (error) {
+ console.error('서브채널 생성 실패!', error);
+ throw error;
+ }
+};
+
+export const deleteChannelDoc = async (collectionName: string, documentName: string) => {
+ const documentRef = doc(firestore, collectionName, documentName);
+ try {
+ await deleteDoc(documentRef);
+ console.log('채널 삭제 성공!');
+ } catch (error) {
+ console.error('채널 삭제 실패!', error);
+ throw error;
+ }
+};
+
+export const deleteFieldFromDoc = async (collectionName: string, documentName: string, fieldName: string) => {
+ const documentRef = doc(firestore, collectionName, documentName);
+ try {
+ const documentSnapshot = await getDoc(documentRef);
+ if (documentSnapshot.exists()) {
+ const data = documentSnapshot.data();
+ delete data[fieldName];
+ await setDoc(documentRef, data);
+ console.log(`서브채널 삭제 성공!`);
+ } else {
+ console.error('채널이 존재하지 않습니다.');
+ }
+ } catch (error) {
+ console.error('서브채널 삭제 실패!', error);
+ throw error;
+ }
+};
+
+export const updateChannelDoc = async (collectionName: string, oldDocName: string, newDocName: string) => {
+ const oldDocRef = doc(firestore, collectionName, oldDocName);
+ const newDocRef = doc(firestore, collectionName, newDocName);
+
+ try {
+ // 수정하려는 document의 field 값을 읽어온 후, 새로운 document에 복사
+ const oldDocumentSnapshot = await getDoc(oldDocRef);
+ const data = oldDocumentSnapshot.data();
+ await deleteDoc(oldDocRef);
+ await setDoc(newDocRef, data);
+ console.log('채널 수정 성공!');
+ } catch (error) {
+ console.error('채널 수정 실패!', error);
+ throw error;
+ }
+};
+
+export const updateFieldKeyInDoc = async (
+ collectionName: string,
+ documentName: string,
+ oldKey: string,
+ newKey: string,
+) => {
+ const documentRef = doc(firestore, collectionName, documentName);
+ try {
+ const documentSnapshot = await getDoc(documentRef);
+ if (documentSnapshot.exists()) {
+ const data = documentSnapshot.data();
+ // Do not access Object.prototype method 'hasOwnProperty' from target object 에러로 인해
+ // (data.hasOwnProperty(oldKey))를 다음과 같이 변경. (Object.prototype.hasOwnProperty.call(data, oldKey))
+ if (Object.prototype.hasOwnProperty.call(data, oldKey)) {
+ const updatedData = { ...data }; // 새로운 객체 생성
+ updatedData[newKey] = updatedData[oldKey]; // 기존 키를 새로운 키로 복사하고, 기존 키는 삭제
+ delete updatedData[oldKey];
+ await setDoc(documentRef, updatedData); // 업데이트된 데이터를 다시 문서에 저장
+ } else {
+ console.error('수정하려는 서브채널이 존재하지 않습니다.');
+ }
+ } else {
+ console.error('채널이 존재하지 않습니다.');
+ }
+ } catch (error) {
+ console.error('서브채널 수정 실패!', error);
+ throw error;
+ }
+};
+
+export const addUser = async (uid: string, value: any) => {
+ try {
+ const addUserFirestore = await setDoc(doc(firestore, 'user', uid), value);
+ } catch (error) {
+ console.error('계정 등록에 실패했습니다.', error);
+ }
+};
+
+export const uploadStorage = async (userId: string, file: File) => {
+ const storageRef = ref(storage, 'user');
+ const userRef = ref(storageRef, userId);
+ await uploadBytes(userRef, file);
+ const imgURL = await getDownloadURL(userRef);
+ return imgURL;
+};
+
+export const getRecruitmentDetail = async (channel: string, path: string) => {
+ const docRef = doc(firestore, 'recruitmentContainer', 'recruitment', channel, path);
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) {
+ console.log('Document data:', docSnap.data());
+ const userDocRef = doc(firestore, 'user', docSnap.data().uid);
+ const userDocSnap = await getDoc(userDocRef);
+ const data = { ...docSnap.data(), ...userDocSnap.data() };
+ for (let i = 0; i < data.comment.length; i++) {
+ const commentUserDocRef = doc(firestore, 'user', data.comment[i].uid);
+ const commentUserDocSnap = await getDoc(commentUserDocRef);
+ data.comment[i] = { ...data.comment[i], ...commentUserDocSnap.data() };
+ }
+ return data;
+ } else {
+ // docSnap.data() will be undefined in this case
+ console.log('No such document!');
+ }
+};
+
+export const getUserName = async (uid: string) => {
+ const docRef = doc(firestore, 'user', uid);
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) {
+ return docSnap.data().name;
+ } else {
+ console.log('No such document!');
+ }
+};
+
+export const getUserData = async (uid: string) => {
+ const docRef = doc(firestore, 'user', uid);
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) {
+ return docSnap.data();
+ } else {
+ console.log('No such document!');
+ }
+};
+
+interface Value {
+ uid: string;
+ content: string;
+ time: string;
+}
+
+export const createComment = async (channel: string, path: string, value: Value) => {
+ const docRef = doc(firestore, 'recruitmentContainer', 'recruitment', channel, path);
+
+ try {
+ const pushComment = await updateDoc(docRef, { comment: arrayUnion(value) });
+ console.log('댓글 작성에 성공했습니다');
+ } catch (error) {
+ console.error('댓글 작성에 실패했습니다.', error);
+ throw error;
+ }
+};
+
+export const deleteComment = async (channel: string, path: string, value: Value) => {
+ const docRef = doc(firestore, 'recruitmentContainer', 'recruitment', channel, path);
+
+ try {
+ const deleteComment = await updateDoc(docRef, { comment: arrayRemove(value) });
+ console.log('댓글 삭제에 성공했습니다');
+ } catch (error) {
+ console.error('댓글 삭제에 실패했습니다.', error);
+ }
+};
+
+export const showRecruitmentFields = async (
+ collectionName: string,
+ documentName: string,
+ subcollectionName: string,
+): Promise<{ docSnapshots: any[] }> => {
+ const documentRef = doc(firestore, collectionName, documentName);
+
+ try {
+ const documentSnapshot = await getDoc(documentRef);
+
+ if (documentSnapshot.exists()) {
+ const subcollectionRef = collection(documentRef, subcollectionName);
+ const querySnapshot = await getDocs(query(subcollectionRef, orderBy('time', 'desc')));
+ const docSnapshots = querySnapshot.docs.map((docSnapshot) => ({
+ id: docSnapshot.id,
+ data: docSnapshot.data(),
+ }));
+
+ return { docSnapshots };
+ } else {
+ console.error('상위 문서가 존재하지 않습니다.');
+ return { docSnapshots: [] }; // 빈 배열을 반환하여 오류 시 데이터가 없음을 표시
+ }
+ } catch (error) {
+ console.error('데이터 가져오기 실패!', error);
+ throw error;
+ }
+};
+
+export const createRecruitment = async (channel: string, value: any) => {
+ try {
+ const addRecruitment = await addDoc(
+ collection(firestore, 'recruitmentContainer', 'recruitment', channel),
+ value,
+ );
+ console.log('게시글 생성에 성공했습니다');
+ } catch (error) {
+ console.error('게시글 작성에 실패했습니다.', error);
+ throw error;
+ }
+};
+
+export const updateRecruitment = async (channel: string, path: string, value: any) => {
+ try {
+ const updateRecruitment = await setDoc(
+ doc(firestore, 'recruitmentContainer', 'recruitment', channel, path),
+ value,
+ );
+ console.log('게시글 수정에 성공했습니다');
+ } catch (error) {
+ console.error('게시글 수정에 실패했습니다.', error);
+ throw error;
+ }
+};
+
+export const deleteRecruitment = async (channel: string, path: string) => {
+ try {
+ const updateRecruitment = await deleteDoc(doc(firestore, 'recruitmentContainer', 'recruitment', channel, path));
+ console.log('게시글 삭제에 성공했습니다');
+ } catch (error) {
+ console.error('게시글 삭제에 실패했습니다.', error);
+ throw error;
+ }
+};
+
+export const updateChannelContent = async (
+ collectionName: string,
+ Channel: string,
+ subChannel: string,
+ docContent: string,
+) => {
+ const docRef = doc(firestore, collectionName, Channel);
+ try {
+ // Update the specific field (subChannel) with the new content (docContent)
+ const docSnapshot = await getDoc(docRef);
+ if (docSnapshot.exists()) {
+ const updated_at_timestamp = serverTimestamp();
+ const currentData = docSnapshot.data();
+ // Update the nested field
+ currentData[subChannel].content = docContent;
+ currentData[subChannel].time = updated_at_timestamp;
+ // Update the document with the modified data
+ await updateDoc(docRef, currentData);
+ console.log(currentData[subChannel].time);
+ console.log('Nested field updated successfully.');
+ } else {
+ console.error('Document does not exist.');
+ }
+ } catch (error) {
+ console.error('채널 내용 수정 실패!', error);
+ throw error;
+ }
+};
diff --git a/src/utils/recoil.ts b/src/utils/recoil.ts
new file mode 100644
index 00000000..98788af9
--- /dev/null
+++ b/src/utils/recoil.ts
@@ -0,0 +1,88 @@
+import { Timestamp } from 'firebase/firestore';
+import { atom } from 'recoil';
+import { recoilPersist } from 'recoil-persist';
+
+const { persistAtom } = recoilPersist({
+ key: 'userId',
+ storage: sessionStorage,
+});
+
+export const UserId = atom({
+ key: 'userId',
+ default: '',
+ effects_UNSTABLE: [persistAtom],
+});
+
+export const channelState = atom({
+ key: 'channelState',
+ default: '',
+});
+
+export const subChannelState = atom({
+ key: 'subChannelState',
+ default: '',
+});
+
+export const RecruitmentData = atom({
+ key: 'recruitmentData',
+ default: {},
+});
+// 타이머 ON/OFF
+export const TimerOn = atom({
+ key: 'TimerOn',
+ default: false,
+});
+
+// 입퇴실 기록
+export const TimeLog = atom({
+ key: 'TimeLog',
+ default: '',
+});
+
+// 불러온 입퇴실 기록
+export const ReadTimelog = atom({
+ key: 'ReadTimeLog',
+ default: [],
+});
+
+export const SlideOn = atom({
+ key: 'SlideOn',
+ default: false,
+});
+export const UserName = atom({
+ key: 'UserName',
+ default: '',
+});
+export const UserEmail = atom({
+ key: 'UserEmail',
+ default: '',
+});
+export const UserInfo = atom({
+ key: 'UserInfo',
+ default: '',
+});
+export const UserImg = atom({
+ key: 'UserImg',
+ default: '',
+});
+export const Render = atom({
+ key: 'Render',
+ default: true,
+});
+export const ThemeChange = atom({
+ key: 'ThemeChange',
+ default: {},
+});
+export const Current = atom({
+ key: 'Current',
+ default: '',
+});
+export const ThemeRing = atom({
+ key: 'ThemeRing',
+ default: {
+ first: '1px solid black',
+ second: '1px solid black',
+ third: '1px solid black',
+ fourth: '1px solid black',
+ },
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..23cd6f47
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "lib": ["dom", "dom.iterable", "esnext", "es6"],
+ "typeRoots": ["./@types", "./node_modules/@types"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"]
+}