diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 000000000..f22135cc1 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "employee-management-59973" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a012efddd --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* +firebase-debug.*.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# firebase file +.firebaserc +firestore.rules +storage.rules diff --git a/README.md b/README.md index a29ee163b..4fe068465 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,53 @@ +# ✨ 직원 관리 서비스 -# :camera: 직원 사진 관리 서비스 - -직원들의 사진을 관리할 수 있는 사진 관리자 서비스를 만들어 보세요. - -과제 수행 및 리뷰 기간은 별도 공지를 참고하세요! -## [과제 수행 및 제출 방법] -1. 현재 저장소를 로컬에 클론(Clone)합니다. -2. 자신의 본명으로 브랜치를 생성합니다.(구분 가능하도록 본명을 꼭 파스칼케이스로 표시하세요, git branch KDT0_이름) -3. 자신의 본명 브랜치에서 과제를 수행합니다. -4. 과제 수행이 완료되면, 자신의 본명 브랜치를 원격 저장소에 푸시(Push)합니다.(main 브랜치에 푸시하지 않도록 꼭 주의하세요, git push origin KDT0_이름) -5. 저장소에서 main 브랜치를 대상으로 Pull Request 생성하면, 과제 제출이 완료됩니다!(E.g, main <== KDT0_이름) -6. Pull Request 링크를 LMS로도 제출해 주셔야 합니다. -7. main 혹은 다른 사람의 브랜치로 절대 병합하지 않도록 주의하세요! -8. Pull Request에서 보이는 설명을 다른 사람들이 이해하기 쉽도록 꼼꼼하게 작성하세요! -9. Pull Request에서 과제 제출 후 절대 병합(Merge)하지 않도록 주의하세요! -10. 과제 수행 및 제출 과정에서 문제가 발생한 경우, 바로 담당 멘토나 강사에서 얘기하세요! - -## [필수 요구사항] -- “AWS S3 / Firebase 같은 서비스”를 이용하여 사진을 관리할 수 있는 페이지를 구현하세요. -- 프로필 페이지를 개발하세요. -- 스크롤이 가능한 형태의 리스팅 페이지를 개발하세요. -- 전체 페이지 데스크탑-모바일 반응형 페이지를 개발하세요. -- 사진을 등록, 수정, 삭제가 가능해야 합니다. -- 유저 플로우를 제작하여 리드미에 추가하세요. -* CSS - * 애니메이션 구현 - * 상대수치 사용(rem, em) -* JavaScript - * DOM event 조작 - -## [선택 요구사항] -- 사진 관리 페이지와 관련된 기타 기능도 고려해 보세요. -- 페이지가 보여지기 전에 로딩 애니메이션이 보이도록 만들어보세요. -- 직원을 등록, 수정, 삭제가 가능하게 해보세요. -- 직원 검색 기능을 추가해 보세요. -- infinity scroll 기능을 추가해 보세요. -- 사진을 편집할 수 있는 기능을 추가해 보세요. -- LocalStorage 사용 - -## [화면 예시] -![Untitled (1)](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/38754963/5dda6755-2501-4af4-bc3e-b63a353c44c2) - -![Untitled (2)](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/38754963/6c1805f1-2b00-453e-a729-2b483612726d) - -## [흐름] -![Untitled](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/38754963/e2934c05-26f6-4ef6-88d4-beed76aa007a) +## 📍 프로젝트 소개 +* 진행 기간 : 2023-08-07~2023-08-18 +* 배포주소 : https://my-employee-management.netlify.app/ +* 기술 스택 : +
+ + + + +
+
+## ✔ 구현 내용 +* 로딩 애니매이션, 직원 리스트 +![직원리스트](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/7cf760c7-2175-4204-969b-a3010c4ccceb) + +* 직원 등록 +![직원등록](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/d59a8c52-ea48-4b39-9efc-705da4d4f092) + + +* 직원 상세 +![직원상세](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/24584ba0-a3ad-4af6-a7a3-1afca893600f) + +* 직원 수정 +![직원수정](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/c190c5dc-6195-42f0-bb98-384a818580ca) + +* 직원 이름과 이메일로 검색 +![직원검색](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/32f30222-e32b-4581-8534-1972a5df3cfd) + +* 직원 삭제 +![직원삭제](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/6bba88b3-4914-4dbb-b7c4-ff73e2970212) + +* 여러 직원 삭제 +![여러직원삭제](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/a194fe4f-70f4-4eff-83e8-dfaa04ed1f37) + +* 반응형 구현 (모바일) +![모바일반응형](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/23e9dba6-edbe-4838-901f-4f5656fb8cd6) + * checked 직원 없이 삭제 버튼 클릭 시 alert + +
+ +## ✨ 유저 플로우 +![유저플로우 drawio](https://github.com/KDT1-FE/Y_FE_JAVASCRIPT_PICTURE/assets/111065848/b2a7480e-9084-437d-a825-81faf6657051) + +
+ +## 📌 회고 +* js로 과제를 구현하면서 dom 조작에 익숙해질 수 있었다. +* 파이어베이스를 처음 사용해보는데 연동하는 것에만 며칠이 걸린 것 같다. +영상을 따라서 연동에 성공했는데 v8로 연동해서 v9부터 추가된 유용한 메소드들를 사용하지 못해 아쉬웠다. diff --git a/firebase.json b/firebase.json new file mode 100644 index 000000000..3facb51c7 --- /dev/null +++ b/firebase.json @@ -0,0 +1,9 @@ +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "storage": { + "rules": "storage.rules" + } +} diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 000000000..415027e5d --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,4 @@ +{ + "indexes": [], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 000000000..cd1a4346f --- /dev/null +++ b/firestore.rules @@ -0,0 +1,9 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..a7a84d954 --- /dev/null +++ b/index.html @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + 직원관리 + + + +
+
+
+
loading
+
+ employee-management.com +
+ +
+ +
+
+ 메인 아이콘 + 직원관리 +
+
+ search + +
+
+ + + +
+
+ + +
+ + + + + + + + + +
+ + + 프로필이름이메일휴대폰번호입사날짜
+
+ + + + + +
+ +

직원 정보

+ close +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + + + + + + diff --git a/src/css/animation.css b/src/css/animation.css new file mode 100644 index 000000000..13a968ce1 --- /dev/null +++ b/src/css/animation.css @@ -0,0 +1,85 @@ +.animation { + z-index: 999; +} +#link { + color: #fff; + display: block; + font: 12px "Helvetica Neue", Helvetica, Arial, sans-serif; + text-align: center; + text-decoration: none; +} +#link:hover { + color: #cccccc; +} + +#link, +#link:hover { + transition: color 0.5s ease-out; +} + +body { + background: #28288c; +} + +@keyframes rotate-loading { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes loading-text-opacity { + 0%, + 20%, + 100% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +.loading-container, +.loading { + width: 100px; + height: 100px; + position: relative; + border-radius: 100%; +} + +.loading-container { + margin: 40vh auto; +} + +.loading { + border: 2px solid transparent; + border-color: transparent #ececec transparent #ececec; + animation: rotate-loading 1.5s linear 0s infinite normal; + transform-origin: 50% 50%; +} + +.loading-container:hover .loading { + border-color: transparent #ffffff transparent #ffffff; +} +.loading-container:hover .loading, +.loading-container .loading { + transition: all 0.5s ease-in-out; +} + +#loading-text { + animation: loading-text-opacity 2s linear 0s infinite normal; + color: #ffffff; + font-family: "Noto Sans KR", sans-serif; + white-space: nowrap; + font-size: 10px; + font-weight: bold; + margin-top: 45px; + opacity: 0; + position: absolute; + text-align: center; + text-transform: uppercase; + top: 0; + width: 100px; +} diff --git a/src/css/header.css b/src/css/header.css new file mode 100644 index 000000000..151b756e3 --- /dev/null +++ b/src/css/header.css @@ -0,0 +1,128 @@ +* { + font-family: "Noto Sans KR", sans-serif; + white-space: nowrap; +} +.header { + position: sticky; + top: 0; + display: flex; + width: 95%; + margin: 0 auto; + padding-top: 20px; + padding-bottom: 20px; + align-items: center; + justify-content: space-between; + background-color: #fff; + border-bottom: 1px solid #4b4848; + z-index: 9; +} + +.header__icon { + display: inline-flex; + align-items: center; +} +.main__icon { + width: 2.8rem; + height: 2.8rem; + object-fit: cover; + cursor: pointer; +} +.header__icon > span { + font-size: 28px; + font-weight: 700; + margin-left: 5px; + text-decoration: none; + color: black; + cursor: pointer; +} +.header__icon:hover > span { + color: #4b4848; +} + +.employee-search__wrapper { + display: flex; + width: 33%; + height: 35px; + border: 2px solid #4b4848; + border-radius: 6px; + align-items: center; +} +.search-icon { + width: 15px; + height: 15px; + margin: 5px 10px auto 5px; +} +.employee-search__input { + width: 100%; + margin-right: 3px; + border: none; + font-size: 18px; + background-color: inherit; + overflow: hidden; +} +.employee-search__input:focus { + font-weight: 500; + outline: none; +} + +.button__wrapper { + display: flex; + align-items: center; +} +.employee-add__button, +.employee-delete__button { + margin-left: 15px; +} +.employee-add__button, +.employee-delete__button { + padding: 0.8rem 1.6rem; + font-size: 16px; + font-weight: 600; + background-color: #28288c; + color: white; + border: none; + border-radius: 8px; + box-shadow: 5px 5px rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +.employee-add__button:hover { + background-color: #6e6ed7; +} +.delete__button:hover, +.employee-delete__button:hover { + background-color: #e17878; +} + +@media screen and (max-width: 391px) { + .header { + padding-bottom: 55px; + } + .main__icon { + width: 2rem; + height: 2rem; + } + .header__icon > span { + font-size: 18px; + } + .employee-search__wrapper { + position: absolute; + top: 70px; + width: 95%; + height: 30px; + margin: 5px 0 5px 10px; + } + .employee-search__wrapper > .search-icon { + width: 2rem; + height: 2rem; + } + .employee-search__input { + font-size: 14px; + } + .employee-add__button, + .employee-delete__button { + font-size: 12px; + margin-left: 0; + margin-right: 15px; + } +} diff --git a/src/css/modal.css b/src/css/modal.css new file mode 100644 index 000000000..9b4cb0921 --- /dev/null +++ b/src/css/modal.css @@ -0,0 +1,183 @@ +.modal__background, +.detail-modal__background { + display: none; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(0, 0, 0, 0.8); + z-index: 98; +} +.employee-detail__modal, +.employee-add__modal { + width: 800px; + height: 650px; + margin: 20px auto; + display: flex; + flex-direction: column; + align-items: center; + background: #fff; + border-radius: 8px; + padding: 10px; + z-index: 99; +} +.employee-add__modal > h1, +.employee-detail__modal > h1 { + font-size: 35px; + margin: 0.4em; +} +.close-icon, +.close__icon { + position: absolute; + top: 10px; + right: 10px; + z-index: 99; + cursor: pointer; +} +.employee-add__modal > form, +.employee-detail__modal > form { + width: 100%; + margin-top: 10px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + font-size: 18px; +} + +.add-file__wrapper { + flex-direction: column; +} +.add-data__file, +.update-data__file { + margin: 20px auto; +} + +.add-data__img, +.detail__img { + width: 230px; + height: 230px; + object-fit: cover; + background-color: #ececec; +} + +.add-file__wrapper, +.employee-add__modal > form > input, +.add-data { + width: 450px; + display: flex; + flex: 1 1 auto; + margin: 5px; + justify-content: space-between; + align-items: center; +} +.employee-add__modal > form > input, +.employee-detail__modal > form > input { + margin: 40px auto 30px auto; +} +.add-data > label { + margin-right: 10px; + font-weight: 600; + font-size: 18px; + font-weight: 500; + color: #4b4848; +} +.add-data > input { + width: 50%; + height: 4vh; +} + +.employee-add__modal button, +.edit__button, +.delete__button { + width: 300px; + height: 45px; + margin-top: 30px; + background-color: #28288c; + border-radius: 8px; + border: none; + color: white; + cursor: pointer; + font-size: 16px; + font-weight: 600; + box-shadow: 6px 6px rgba(0, 0, 0, 0.5); +} +.employee-add__modal button:hover, +.edit__button:hover { + background-color: #6e6ed7; +} + +.employee-add__modal input::placeholder, +.employee-add__modal input, +.employee-detail__modal input::placeholder, +.employee-detail__modal input { + color: black; + font-weight: 600; + font-size: 16px; +} + +.detail-button__wrapper { + display: flex; +} +.edit__button, +.delete__button { + width: 210px; + margin-top: 30px; + font-size: 15px; +} +.delete__button { + margin-left: 30px; +} + +@media screen and (max-width: 391px) { + .employee-detail__modal, + .employee-add__modal { + box-sizing: border-box; + width: 90%; + height: 80%; + position: relative; + top: 50px; + left: 20px; + } + + .employee-detail__modal > h1, + .employee-add__modal > h1 { + font-size: 20px; + } + .employee__form { + width: 90%; + } + .employee-detail__modal img, + .employee-add__modal img { + width: 150px; + height: 150px; + } + .employee-add__modal label, + .employee-add__modal input, + .employee-add__modal input::placeholder, + .employee-detail__modal label, + .employee-detail__modal input, + .employee-detail__modal input::placeholder { + width: 100%; + font-size: 14px; + } + .employee-add__modal input, + .employee-detail__modal input { + height: 20px; + } + .add-data__file, + .update-data__file { + margin-left: 130px; + } + .add-data { + width: 90%; + justify-content: space-between; + } + .detail-button__wrapper > button { + width: 80px; + height: 40px; + margin-top: 20%; + font-size: 13px; + } +} diff --git a/src/css/reset.css b/src/css/reset.css new file mode 100644 index 000000000..d2999490c --- /dev/null +++ b/src/css/reset.css @@ -0,0 +1,48 @@ +html, body, div, span, applet, object, iframe, +p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + border: 0; + vertical-align: baseline; +} +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/src/css/table.css b/src/css/table.css new file mode 100644 index 000000000..a7c2496f1 --- /dev/null +++ b/src/css/table.css @@ -0,0 +1,75 @@ +.employee-table__warpper { + width: 93%; + margin: 30px auto; +} + +.employee-table { + width: 100%; + font-size: 18px; +} + +.employee-table tbody > tr { + height: 35px; + font-weight: 600; + margin-top: 3px; + border-top: 1px solid #4b4848; + border-bottom: 1px solid #4b4848; +} + +.row-data { + width: 100%; + text-align: center; + transform: scale(1); + transition: transform 0.8s; + font-weight: 600; +} + +.row-data > input[type="checkbox"] { + margin-top: 20px; +} + +.employee-table th, +.employee-table td { + vertical-align: middle; +} + +.row-data:hover { + transform: scale(1.02); + transition: transform 0.8s; +} +.employee__img { + margin: 10px; + width: 100px; + height: 100px; + object-fit: cover; + border: 2px solid #4b4848; + border-radius: 50%; + box-shadow: 5px 5px rgba(0, 0, 0, 0.5); + cursor: pointer; + transform: scale(1); + transition: transform 0.8s; +} +.employee__img:hover { + transform: scale(1.1); + transition: transform 0.8s; +} + +@media screen and (max-width: 391px) { + .employee-table__warpper { + width: 95%; + } + .employee-table { + width: 100%; + font-size: 14px; + } + .employee-table th:nth-of-type(4), + .employee-table th:nth-of-type(5), + .employee-table td:nth-of-type(4), + .employee-table td:nth-of-type(5) { + display: none; + } + .employee__img { + width: 80px; + height: 80px; + } +} diff --git a/src/image/free-icon-team-management-6103262.png b/src/image/free-icon-team-management-6103262.png new file mode 100644 index 000000000..7963b29ec Binary files /dev/null and b/src/image/free-icon-team-management-6103262.png differ diff --git a/src/js/animation.js b/src/js/animation.js new file mode 100644 index 000000000..9411d4537 --- /dev/null +++ b/src/js/animation.js @@ -0,0 +1,10 @@ +const animation = document.querySelector(".animation"); +const main = document.querySelector(".main"); + +main.style.display = "none"; + +setTimeout(() => { + animation.remove(); + document.querySelector("body").style.background = "none"; + main.style.display = "block"; +}, 1500); diff --git a/src/js/firebaseData.js b/src/js/firebaseData.js new file mode 100644 index 000000000..0d5af9c30 --- /dev/null +++ b/src/js/firebaseData.js @@ -0,0 +1,328 @@ +import { checkedArr, checked } from "./main.js"; +import { firebaseConfig } from "./firebaseKeys.js"; + +firebase.initializeApp(firebaseConfig); + +const db = firebase.firestore(); +const storage = firebase.storage(); + +const detailModal = document.querySelector(".detail-modal__background"); +const closeIcon = document.querySelector(".close__icon"); + +readEmployee(); +createEmployee(); +deleteEmployee(); +searchEmployee(); + +closeIcon.addEventListener("click", () => { + detailModal.style.display = "none"; +}); + +// 직원 데이터 create +function createEmployee() { + const employeeForm = document.querySelector("#employee__form"); + + employeeForm.addEventListener("submit", (e) => { + e.preventDefault(); // 새로고침 방지 + + const file = document.querySelector(`input[type="file"]`).files[0]; + + const storageRef = storage.ref(); + const imgUrl = storageRef.child("image/" + file.name); + const upload = imgUrl.put(file); + + upload.on( + "state_changed", + null, + (error) => { + console.error(error); + }, + () => { + // 이미지 업로드 성공 시 firebase에 추가 + upload.snapshot.ref.getDownloadURL().then((url) => { + const name = document.querySelector(".add-data__name"); + const email = document.querySelector(".add-data__email"); + const tel = document.querySelector(".add-data__tel"); + const date = document.querySelector(".add-data__date"); + + let employee = { + 이미지: url, + 이름: name.value, + 이메일: email.value, + 전화번호: tel.value, + 입사날짜: date.value, + }; + + // 직원 등록 + db.collection("직원") + .add(employee) + .then((result) => { + alert("직원 등록이 완료되었습니다."); + window.location.href = "./index.html"; + }) + .catch((err) => { + console.log(err); + }); + }); + } + ); + }); +} + +// 직원 데이터 read +function readEmployee() { + db.collection("직원") + .orderBy("이름", "asc") + .get() + .then((result) => { + result.forEach((doc) => { + const { 이미지, 이름, 이메일, 전화번호, 입사날짜 } = doc.data(); + + const employee = { + id: doc.id, + img: 이미지, + name: 이름, + email: 이메일, + tel: 전화번호, + date: 입사날짜, + }; + + createEmployeeElement(employee); + }); + }) + .catch((err) => { + console.log(err); + }); + + resetEmployee(); +} + +// 직원테이블 엘리먼트 생성 +function createEmployeeElement(employee) { + const tr = document.createElement("tr"); + + const inputTd = document.createElement("td"); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.className = "checkbox"; + checkbox.addEventListener("click", () => { + checked(checkbox); + }); + inputTd.append(checkbox); + + const imgTd = document.createElement("td"); + const img = document.createElement("img"); + img.src = employee.img; + img.className = "employee__img"; + imgTd.append(img); + + const name = document.createElement("td"); + name.innerText = employee.name; + const tel = document.createElement("td"); + tel.innerText = employee.tel; + const email = document.createElement("td"); + email.innerText = employee.email; + email.className = "employee--list__email"; + const dateTime = document.createElement("td"); + dateTime.innerText = employee.date; + + tr.className = "row-data"; + tr.append(inputTd, imgTd, name, email, tel, dateTime); + + // 직원 테이블에 추가 + const employeeTable = document.querySelector(".employee-table"); + employeeTable.append(tr); + + img.addEventListener("click", () => { + setModal(employee); + }); +} + +// 검색 전 직원 row 삭제 +function resetEmployee() { + const rows = document.querySelectorAll(".row-data"); + rows.forEach((row) => { + row.remove(); + }); +} + +// 검색 데이터 read +function searchData(search, field) { + db.collection("직원") + .where(field, ">=", search) + .where(field, "<=", search + "\uf8ff") + .get() + .then((result) => { + result.forEach((doc) => { + const { 이미지, 이름, 이메일, 전화번호, 입사날짜 } = doc.data(); + + const employee = { + id: doc.id, + img: 이미지, + name: 이름, + email: 이메일, + tel: 전화번호, + date: 입사날짜, + }; + + resetEmployee(); + createEmployeeElement(employee); + }); + }) + .catch((err) => { + console.log(err); + }); +} + +// 직원 검색 +function searchEmployee() { + const searchInput = document.querySelector(".employee-search__input"); + + searchInput.addEventListener("input", (e) => { + let searchString = e.target.value; + if (searchString === "") { + resetEmployee(); + readEmployee(); + return; + } else if (/[a-zA-Z]/.test(searchString)) { + // searchString이 영어일 때 + searchData(searchString, "이메일"); + } else if (/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(searchString)) { + // searchString이 한글일 때 + searchData(searchString, "이름"); + } + }); +} + +// 직원 상세 모달 +function setModal(employee) { + detailModal.style.display = "block"; + const detailImg = document.querySelector(".detail__img"); + detailImg.src = employee.img; + const detailName = document.querySelector(".detail__name"); + detailName.placeholder = employee.name; + const detailEmail = document.querySelector(".detail__email"); + detailEmail.placeholder = employee.email; + const detailTel = document.querySelector(".detail__tel"); + detailTel.placeholder = employee.tel; + const detailDate = document.querySelector(".detail__date"); + detailDate.value = employee.date; + + updateEmployee(employee); + deleteDetailEmployee(employee); +} + +// 직원 데이터 update +function updateEmployee(employee) { + let imgFile; + const fileDom = document.querySelector(".update-data__file"); + fileDom.addEventListener("change", () => { + const imgBox = document.querySelector(".detail__img"); + + if (fileDom.files.length) { + let reader = new FileReader(); + + reader.onload = function (e) { + imgBox.src = e.target.result; + }; + reader.readAsDataURL(fileDom.files[0]); + } else { + imgBox.src = ""; + } + + const file = fileDom.files[0]; + + let storageRef = storage.ref(); + let imgUrl = storageRef.child("image/" + file.name); + let upload = imgUrl.put(file); + + upload.on( + "state_changed", + null, + (error) => { + console.error(error); + }, + () => { + upload.snapshot.ref.getDownloadURL().then(async (url) => { + imgFile = url; + }); + } + ); + }); + + const updateBtn = document.querySelector(".edit__button"); + + updateBtn.addEventListener("click", () => { + const img = document.querySelector(".detail__img"); + const name = document.querySelector(".detail__name"); + const email = document.querySelector(".detail__email"); + const tel = document.querySelector(".detail__tel"); + const date = document.querySelector(".detail__date"); + + let newEmployee = { + 이미지: imgFile || img.src, + 이름: name.value || name.placeholder, + 이메일: email.value || email.placeholder, + 전화번호: tel.value || tel.placeholder, + 입사날짜: date.value || date.placeholder, + }; + db.collection("직원") + .doc(employee.id) + .update(newEmployee) + .then(() => { + alert("직원 정보가 저장되었습니다."); + window.location.href = "./index.html"; + }); + }); +} + +// 직원 상세 모달에서 delete +function deleteDetailEmployee(employee) { + const deleteBtn = document.querySelector(".delete__button"); + + deleteBtn.addEventListener("click", () => { + if (confirm("직원을 삭제하시겠습니까?")) { + db.collection("직원") + .get() + .then(async () => { + await db.collection("직원").doc(employee.id).delete(); + alert("직원 정보가 삭제되었습니다."); + window.location.href = "./index.html"; + }) + .catch((error) => { + console.log(error); + }); + } + }); +} + +// checked 직원 데이터 delete +function deleteEmployee() { + const employeeDeleteBtn = document.querySelector(".employee-delete__button"); + + employeeDeleteBtn.addEventListener("click", () => { + if (checkedArr.length == 0) { + alert("삭제할 직원을 선택해주세요."); + return; + } + + if (confirm("직원을 삭제하시겠습니까?")) { + const arrLength = checkedArr.length; + + for (let i = 0; i < arrLength; i++) { + db.collection("직원") + .where("이메일", "==", `${checkedArr[i]}`) + .get() + .then((result) => { + result.forEach(async (doc) => { + await db.collection("직원").doc(`${doc.id}`).delete(); + window.location.href = "./index.html"; + }); + }) + .catch((error) => { + console.log(error); + }); + } + } + }); +} diff --git a/src/js/firebaseKeys.js b/src/js/firebaseKeys.js new file mode 100644 index 000000000..45525e2bd --- /dev/null +++ b/src/js/firebaseKeys.js @@ -0,0 +1,9 @@ +export const firebaseConfig = { + apiKey: "AIzaSyA0BYrAIfJUyc09tEbBSZkRmDW6pRerOG8", + authDomain: "employee-management-59973.firebaseapp.com", + projectId: "employee-management-59973", + storageBucket: "employee-management-59973.appspot.com", + messagingSenderId: "541264054170", + appId: "1:541264054170:web:9ca90b515958475b6f9c23", + measurementId: "G-FT6KJR7WDD", +}; diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 000000000..176f03eef --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,106 @@ +const modal = document.querySelector(".modal__background"); +const employeeAddBtn = document.querySelector(".employee-add__button"); +const closeIcon = document.querySelector(".close-icon"); + +export let checkedArr = new Array(); + +checkedAll(); +readURL(); + +// 직원 등록 모달창 display +employeeAddBtn.addEventListener("click", () => { + modal.style.display = "block"; +}); +closeIcon.addEventListener("click", () => { + closeIcon.parentNode.parentNode.style.display = "none"; +}); + +function readURL() { + const fileDom = document.querySelector(".add-data__file"); + fileDom.addEventListener("change", () => { + const imgBox = document.querySelector(".add-data__img"); + + if (fileDom.files.length) { + let reader = new FileReader(); + + reader.onload = function (e) { + console.log(e); + imgBox.src = e.target.result; + }; + reader.readAsDataURL(fileDom.files[0]); + } else { + imgBox.src = ""; + } + }); +} + +// checkedArr 배열 중복 제거 +function delDupArr() { + let uniqueArr = []; + checkedArr.forEach((element) => { + if (!uniqueArr.includes(element)) { + uniqueArr.push(element); + } + }); +} + +// 전체 체크 or 해제 +function checkedAll() { + const checkboxAll = document.querySelector(".checkbox__all"); + + checkboxAll.addEventListener("click", () => { + const isChecked = checkboxAll.checked; + const checkboxes = document.querySelectorAll(".checkbox"); + + if (isChecked) { + checkedArr.length = 0; // 배열 초기화 + + for (const checkbox of checkboxes) { + // 전체 체크박스 선택 + let td = checkbox.parentNode; + let tr = td.parentNode; + checkbox.checked = true; + + // 선택된 모든 직원의 이메일 배열에 삽입 + let email = tr.children[3].innerHTML; + checkedArr.push(email); + delDupArr(); + + tr.style.backgroundColor = "#aaa"; + tr.style.color = "white"; + } + } else { + for (const checkbox of checkboxes) { + // 전체 체크박스 해제 + let td = checkbox.parentNode; + let tr = td.parentNode; + checkbox.checked = false; + + checkedArr = []; // 배열 초기화 + + tr.style.backgroundColor = "#fff"; + tr.style.color = "black"; + } + } + console.log(checkedArr); + }); +} + +export function checked(checkbox) { + checkbox.addEventListener("change", () => { + let td = checkbox.parentNode; + let tr = td.parentNode; + let email = tr.children[3].innerHTML; + + // checked에 따라 직원 이메일 배열에 추가 or 삭제 + if (checkbox.checked) { + checkedArr.push(email); + } else { + checkedArr = checkedArr.filter((element) => element !== email); + } + + // checked에 따라 table row 색상 변화 + tr.style.backgroundColor = checkbox.checked ? "#aaa" : "#fff"; + tr.style.color = checkbox.checked ? "#fff" : "#000"; + }); +} diff --git a/storage.rules b/storage.rules new file mode 100644 index 000000000..c807920e6 --- /dev/null +++ b/storage.rules @@ -0,0 +1,12 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if true; + } + } +}