diff --git a/src/assets/refresh_icon.svg b/src/assets/refresh_icon.svg new file mode 100644 index 0000000..9fb8f4c --- /dev/null +++ b/src/assets/refresh_icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/LogTable.jsx b/src/components/LogTable.jsx new file mode 100644 index 0000000..ed88486 --- /dev/null +++ b/src/components/LogTable.jsx @@ -0,0 +1,43 @@ +export default function LogTable({ logs }) { + return ( + <> +
+ + + + + + + + + + + + + {logs.map((l, i) => { + return ( + + + + + + + + + ); + })} + +
분류세분류위험도발생 시각센서ID측정값
{l.targetType}{l.abnormalType}{l.dangerLevel}{l.timestamp}{l.targetId}{l.value}
+
+ + ); +} diff --git a/src/components/WorkerTable.jsx b/src/components/WorkerTable.jsx new file mode 100644 index 0000000..2892196 --- /dev/null +++ b/src/components/WorkerTable.jsx @@ -0,0 +1,143 @@ +import { useState } from "react"; + +export default function WorkerTable({ + worker_list, + isDetail = false, // "현재 위치" 포함 여부 (Y=false, N=true) + selectWorker, + openModal, + isManager = false, +}) { + const [searchType, setSearchType] = useState("byName"); + const [search, setSearch] = useState(""); + const [selectedStatus, setSelectedStatus] = useState("전체"); + + const filteredWorkers = worker_list.filter((worker) => { + if (searchType === "byName") { + return worker.name.includes(search.trim()); + } else if (searchType === "byStatus") { + if (selectedStatus === "전체") { + return worker_list; + } + return worker.status === selectedStatus; + } + return true; + }); + + const directCall = (email, phone) => { + const confirmed = window.confirm(`작업자를 호출하시겠습니까?`); + if (confirmed) { + /* To-Do: 긴급 호출 기능 구현하면 됨!! */ + console.log("긴급 호출!!!!!", `${email} ${phone}`); + } + }; + return ( + <> +
+ + + {!isManager && ( + + + + )} + + + + {!isDetail && } + + + + + + + {filteredWorkers.map((worker, i) => { + let tmp = ""; + if (worker.status == "위험") { + tmp = "critical"; + } + return ( + + + + {!isDetail && } + + + + + ); + })} + +
+
+ {/* 이름검색 라디오버튼 */} +
+ + setSearch(e.target.value)} + disabled={searchType !== "byName"} + /> +
+ {/* 상태별 분류 라디오버튼 */} +
+ + +
+
+
상태이름현재 위치웨어러블 ID연락처호출
{worker.status}{worker.name}{worker.zone}{worker.wearableId} { + selectWorker(worker); + openModal(true); + }} + > + 조회 + directCall(worker.email, worker.phone)} + > + 🚨 +
+
+ + ); +} diff --git a/src/components/modal/WorkerInfoModal.jsx b/src/components/modal/WorkerInfoModal.jsx new file mode 100644 index 0000000..586495b --- /dev/null +++ b/src/components/modal/WorkerInfoModal.jsx @@ -0,0 +1,52 @@ +import XIcon from "../../assets/x_icon.svg?react"; + +function ContactTable({ email, phone, id }) { + return ( +
+ + + + + + + + + + + + + + + + + +
이메일{email}
휴대폰 번호{phone}
웨어러블 ID{id}
+
+ ); +} + +export default function WorkerInfoModal({ isOpen, onClose, workerInfo }) { + if (isOpen) { + console.log(workerInfo); + return ( +
+
e.stopPropagation()}> +
+ +
+
+

+ {workerInfo.name}의 연락처 정보 +

+ +
+
+
+ ); + } + return <>; +} diff --git a/src/pages/Safety.jsx b/src/pages/Safety.jsx index e40b686..745f398 100644 --- a/src/pages/Safety.jsx +++ b/src/pages/Safety.jsx @@ -1,38 +1,83 @@ +import { useCallback, useEffect, useState } from "react"; +import axiosInstance from "../api/axiosInstance"; +import WorkerTable from "../components/WorkerTable"; +import WorkerInfoModal from "../components/modal/WorkerInfoModal"; + export default function Safety() { - const mock_workers = { - normal_workers: [], - abnormal_workers: [], - disconnected_workers: [], // 논의필요! + const [workerList, setWorkerList] = useState([]); + + const [isOpen, setIsOpen] = useState(false); + const onClose = () => { + setSelectedWorker(); + setIsOpen(false); }; + const [selectedWorkerInfo, setSelectedWorker] = useState(); + + const mock_workers = [ + { + name: "김00", + // role: "사원", + status: "위험", + zone: "포장 구역 A", + wearableId: "WEARABLE000111000", + email: "test1@example.com", + phone: "010111111111", + }, + { + name: "윤00", + // role: "공장장", + status: "정상", + zone: "휴게실", + wearableId: "인식되지 않음", + email: "test2@example.com", + phone: "010222222222", + }, + { + name: "정00", + // role: "반장", + status: "정상", + zone: "조립 구역 B", + wearableId: "WEARABLE111111111", + email: "test3@example.com", + phone: "01033333333", + }, + ]; + + const fetchWorkers = useCallback(() => { + axiosInstance + .get("/api/workers") + .then(() => { + console.log("작업자 정보 get!"); + }) + .catch((e) => { + console.log("작업자 정보 조회 실패 - mock data를 불러옵니다", e); + setWorkerList(mock_workers); + }); + }); + + useEffect(() => { + fetchWorkers(); + const interval = setInterval(() => { + fetchWorkers(); + }, 60000); // 1분! + return () => clearInterval(interval); + }, []); + + console.log("rerendering"); return ( <> +

작업자 안전관리

- {/* 정상 작업자 */} -
-
- 정상 -
-
- {/* 작업자 목록 - * 아니 뭐더라 */} -
-
- {/* 이상 작업자 */} -
-
- 주의 -
-
- {/* 작업자 목록 - * 아니 뭐더라 */} -
-
- {/* 연결되지 않은 작업자 */} -
-
- 연결되지 않은 사용자 -
-
+
+
); diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index ccb8b8d..8ec8d1c 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -124,6 +124,9 @@ export default function Settings() { }; const handleFacilityUpdate = (newValue) => { + if (newValue.length == 0) { + return; + } axiosInstance .post("/api/equips", { zoneName: selectedZone, @@ -152,6 +155,9 @@ export default function Settings() { }; const handleEditZone = (newZoneName) => { + if (newZoneName.length == 0) { + return; + } axiosInstance .post(`/api/zones/${selectedZone}`, { zoneName: newZoneName, @@ -173,6 +179,9 @@ export default function Settings() { }; const handleEditFac = (newFacName, equipId) => { + if (newFacName.length == 0) { + return; + } axiosInstance .post(`/api/equips/${equipId}`, { equipName: newFacName, diff --git a/src/pages/ZoneDetail_2.jsx b/src/pages/ZoneDetail_2.jsx index 92f1d85..6d88f8c 100644 --- a/src/pages/ZoneDetail_2.jsx +++ b/src/pages/ZoneDetail_2.jsx @@ -1,28 +1,209 @@ import { useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; +import WorkerTable from "../components/WorkerTable"; +import axiosInstance from "../api/axiosInstance"; +import RefreshIcon from "../assets/refresh_icon.svg?react"; +import LogTable from "../components/LogTable"; +import WorkerInfoModal from "../components/modal/WorkerInfoModal"; + +function ManagerSetting({ manager, workerList, modalParam }) { + const [mode, setMode] = useState(""); + + return ( + <> + {/* 매니저 존재 여부에 따라 매니저 정보 표 보여줌 */} + {manager && ( + <> +
+ +
+ + + )} + {!manager && ( + <> +
+

매니저가 할당되지 않았습니다

+ +
+ + )} + {/* 모드에 따라 추가하는 부분 보여주기 */} + {mode && ( + <> +
+
+

담당자 선택

+ + + {!manager && mode == "add" && ( + + )} + {manager && mode == "edit" && ( + + )} +
+
+ + )} + + ); +} export default function ZoneDetail_2() { const { zoneId } = useParams(); - const [isLogOpen, setLogOpen] = useState(false); const bottomRef = useRef(null); - // Kibana 대시보드 ID (미리 저장해둔 고정된 dashboard) - const dashboardId = "d9cad7d0-2d48-11f0-b003-9ddfbb58f11c"; + const [refreshLog, setRefreshLog] = useState(0); + const [logs, setLogs] = useState([]); - const sensorTypes = ["temp", "humid"]; // 원하는 센서 타입들 추가 + const [refreshWorkers, setRefreshWorkers] = useState(0); + const [workerList, setWorkerList] = useState([]); - /* mock data */ - /* zoneId로 detail 정보를 요청하면, 아래 정보를 줬으면 좋겠다...*/ + // Mock data 시작 + const mock_workers = [ + { + name: "S00", + status: "위험", + wearableId: "WEARABLE000111000", + email: "test@example.com", + phone: "01022222222", + }, + { + name: "Y00", + status: "정상", + wearableId: "인식되지 않음", + email: "test@example.com", + phone: "01033333333", + }, + { + name: "J00", + status: "정상", + wearableId: "WEARABLE111111111", + email: "test@example.com", + phone: "01011112222", + }, + ]; - // 최종 프로젝트 - // 시뮬레이션 시각화 - // 핸드폰 앱으로 만들어서 센서 데이터 전송가능하게끔. + const mock_loglist = [ + { + zoneId: "zone123", + targetType: "환경", + sensorType: "TEMPERATURE", + dangerLevel: 2, + value: 35.5, + timestamp: "2024-03-20T14:30:00", + abnormalType: "온도 위험", + targetId: "sensor456", + }, + { + zoneId: "zone123", + targetType: "환경", + sensorType: "TEMPERATURE", + dangerLevel: 1, + value: 35.5, + timestamp: "2024-03-20T14:30:00", + abnormalType: "온도 위험", + targetId: "sensor456", + }, + { + zoneId: "zone123", + targetType: "환경", + sensorType: "TEMPERATURE", + dangerLevel: 0, + value: 35.5, + timestamp: "2024-03-20T14:30:00", + abnormalType: "온도 안정", + targetId: "sensor456", + }, + ]; - // 데이터 기반 의사 결정 + const mock_manager = { + wearableId: "WKR20250521001", + name: "홍길동", + phone: "010-1234-5678", + email: "honggildong@example.com", + zoneId: "ZONE001", + zone: "포장 구역 A", + status: "정상", + }; - // 줌으로 면접 녹화해보기 + // const mock_manager = null; + // mock data 끝 - // cam이 아닌 클라우드 Am으로 말하기 + // 공간의 작업자 정보 받아오기 + const fetchWorkers = () => { + axiosInstance + .get(`/api/workers/${zoneId}`) + .then(() => { + console.log(`${zoneId}의 작업자 정보 get!`); + }) + .catch((e) => { + console.log(`${zoneId}의 작업자 로드 실패 - mock data를 불러옵니다`, e); + setWorkerList(mock_workers); + }); + }; + + useEffect(() => { + fetchWorkers(); + const interval = setInterval(() => { + fetchWorkers(); + }, 60000); // 1분! + return () => clearInterval(interval); + }, [refreshWorkers]); // 여기서 리프레시 버튼을 추가해도 좋을 것 같네요! + + // 로그 정보 받아오기 + useEffect(() => { + if (refreshLog) { + axiosInstance + .get(`/api/system-logs/zone/${zoneId}`) + .then((res) => {}) + .catch((e) => { + console.log("로그 조회 실패 - mock-data를 불러옵니다", e); + setLogs(mock_loglist); + }); + } + }, [refreshLog]); + + const [manager, setManager] = useState(); + + // 매니저 정보 받아오기 + useEffect(() => { + axiosInstance + .get(``) + .then((res) => { + console.log(res.data); + }) + .catch((e) => { + console.log("매니저 정보 조회에 실패했습니다.", e); + console.log("mock data를 불러옵니다."); + setManager(mock_manager); + }); + }, []); + + // Kibana 대시보드 ID (미리 저장해둔 고정된 dashboard) + const dashboardId = "d9cad7d0-2d48-11f0-b003-9ddfbb58f11c"; + const sensorTypes = ["temp", "humid"]; // 원하는 센서 타입들 추가 const mock_details_sensor = { zoneId: zoneId, @@ -32,7 +213,6 @@ export default function ZoneDetail_2() { { type: "humid", id: "SID-YYY" }, ], }; - console.log(mock_details_sensor.zoneId); const mapSensorType = (sensorType) => { const sensorMap = { temp: "온도 센서", humid: "습도 센서" }; @@ -48,18 +228,33 @@ export default function ZoneDetail_2() { time:(from:now-10m,to:now) )`.replace(/\s+/g, ""); + // 로그 펴질 때 화면 부드럽게 펼쳐지기 useEffect(() => { - if (isLogOpen && bottomRef.current) { + if (logs.length !== 0 && bottomRef.current) { setTimeout(() => { bottomRef.current.scrollIntoView({ behavior: "smooth", block: "end", }); - }, 200); // transition 후 약간의 시간 대기 (300ms) + }, 200); } - }, [isLogOpen]); + }, [logs.length]); + + const [isOpen, setIsOpen] = useState(false); + const onClose = () => { + setSelectedWorker(); + setIsOpen(false); + }; + const [selectedWorkerInfo, setSelectedWorker] = useState(); return ( <> + { + setIsOpen(false); + }} + workerInfo={selectedWorkerInfo} + />

{mock_details_sensor.zoneName}

{/* 환경 리포트 부분 :: ELK */}
@@ -90,9 +285,36 @@ export default function ZoneDetail_2() {
{/* 근무자 현황 :: 스프린트2 */}
-
근무자 현황
+
+ 근무자 현황 + setRefreshWorkers((prev) => prev + 1)} + > + + +
+
+ +
+
+ {/* 담당자 :: 스프린트2 */} +
+
담당자 정보
-

스프린트2에서 진행 예정

+
{/* 설비 현황 :: 스프린트3 */} @@ -106,18 +328,20 @@ export default function ZoneDetail_2() {
시스템 로그 조회 - setLogOpen((prev) => !prev)}> - {isLogOpen ? "▲" : "▼"} + setRefreshLog((prev) => prev + 1)} + > +
-
-

logs

-

logs

-

logs

-

logs

-

logs

+
+
- {/*
스크롤 위치 조정용
*/}
diff --git a/src/styles/style.css b/src/styles/style.css index 829ddf2..efd2008 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -7,6 +7,9 @@ --box-color: #f6f6f6; --blue: #1d4a7a; + --warning: #fde1ad; + --warning2: #f2c97e; + --warning3: #fcbc45; } #root { position: relative; @@ -48,8 +51,9 @@ a:visited { .contents { display: flex; flex-direction: column; - margin: 4rem 7.5% 0 7.5%; + margin: 4rem auto 0 auto; width: 100%; + max-width: 70vw; } /* 사이드바 설정 */ @@ -178,6 +182,12 @@ p.sidebar-open { cursor: pointer; } +.refresh { + cursor: pointer; + margin-right: 0.25rem; + transform: translateY(1px); +} + .moving-box:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); transform: translateY(-3px); @@ -367,6 +377,7 @@ p.sidebar-open { box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); transform: translateY(1px); } + .button-flex { display: flex; justify-content: flex-end; @@ -429,6 +440,7 @@ p.sidebar-open { } .modal-box { background-color: var(--c4); + /* background-color: white; */ border-radius: 2rem; width: 40vw; height: auto; @@ -566,7 +578,7 @@ p.sidebar-open { } .monitor-box.warn { - background-color: #fde1ad; + background-color: var(--warning); } .icon-container { @@ -828,3 +840,173 @@ strong.normal { font-weight: bold; color: var(--blue); } + +.table-container { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.search-container { + text-align: start; + margin-bottom: 0.5rem; +} + +.worker-table, +.logs-table { + width: calc(100% - 4rem); + border-collapse: collapse; + text-align: center; + overflow: hidden; + margin: auto; +} + +.table-container th { + background-color: transparent; + border: none; +} + +.table-header th { + padding: 0.5rem; + border: 1px solid #ccc; + background-color: var(--c4); + width: 10%; +} + +.worker-table td, +.logs-table td { + border: 1px solid #ccc; + background-color: white; +} + +.worker-table th:nth-child(3) { + width: 15%; +} + +.id-row { + width: auto !important; +} + +.worker-table .critical td, +.logs-table .critical td { + background-color: var(--warning); +} + +.search-container input { + border: none; + border-bottom: 1px solid black; + background-color: transparent; + margin: 0.5rem; + outline: none; + margin-left: 1rem; +} + +.safety-body { + height: auto; + width: 100%; + background-color: var(--box-color); + border-radius: 2rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + transform: translateY(1px); + margin-bottom: 3rem; +} +.safety-body > div { + margin: 1rem 0; + height: auto; + width: 100%; + gap: 0.5rem; + display: flex; + justify-content: flex-start; + flex-wrap: wrap; +} + +.radio { + margin-left: 0 !important; +} + +.search-field { + margin-left: 0.5rem; + font-size: 0.95rem; + transition: border-color 0.2s ease; + width: 10rem; +} + +select.search-field { + border-radius: 0.5rem; +} + +.search-field:disabled { + color: var(--box-color); + border-color: var(--box-color); + cursor: not-allowed; +} + +/* 연락처용 표 */ +.contact-table { + width: 80%; + border-collapse: collapse; + table-layout: fixed; +} + +.contact-table th, +.contact-table td { + text-align: center; + word-wrap: break-word; + height: 3rem; +} + +.contact-table th { + background-color: var(--box-color); + width: 40%; + border-bottom: 1px solid var(--box-color); +} + +.contact-table td { + background-color: white; + color: #555; + border-bottom: 1px solid var(--box-color); +} + +.manager-button button { + width: 5rem; + height: 1.75rem; + font-size: 1rem; + margin: 0.5rem 2rem 0.5rem 0; + align-self: center; +} + +.edit-manager { + margin: 2rem; +} + +.select-flex { + text-align: center; + display: flex; + flex-direction: row; + gap: 1rem; + justify-content: center; + align-items: center; +} + +.select-flex select { + width: 15rem; + height: 2rem; +} + +.edit-manager button { + background-color: var(--warning2); + color: white; + font-size: 1rem; +} + +.edit-manager button:hover { + background-color: #fcbc45; +} + +.edit-manager button:active { + background-color: #f7a816; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + transform: translateY(1px); +}