From 9d583521b0177cfc25d79d111dd7ce35a5373b47 Mon Sep 17 00:00:00 2001 From: YeIn kim <2076070@ewhain.net> Date: Tue, 20 May 2025 17:51:15 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat=20|=20sprint2=20|=20FRB-152=20|=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9E=90=20=EA=B4=80=EB=A6=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=9C=EC=9E=91=20|=20=EA=B9=80?= =?UTF-8?q?=EC=98=88=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/WorkerTable.jsx | 108 +++++++++++++++++++++++ src/components/modal/WorkerInfoModal.jsx | 3 + src/pages/Safety.jsx | 77 +++++++++------- src/styles/style.css | 76 +++++++++++++++- 4 files changed, 231 insertions(+), 33 deletions(-) create mode 100644 src/components/WorkerTable.jsx create mode 100644 src/components/modal/WorkerInfoModal.jsx diff --git a/src/components/WorkerTable.jsx b/src/components/WorkerTable.jsx new file mode 100644 index 0000000..1871808 --- /dev/null +++ b/src/components/WorkerTable.jsx @@ -0,0 +1,108 @@ +import { useState } from "react"; + +export default function WorkerTable({ worker_list }) { + 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; + }); + return ( + <> +
+ + + + + + + {/* 초록색 빨간색 동그라미 */} + + + + + + + + + {filteredWorkers.map((worker, i) => { + let tmp = ""; + if (worker.status == "위험") { + tmp = "critical"; + } + return ( + + + + + + + + + ); + })} + +
+
+ + + + {searchType && searchType === "byName" ? ( +
+ + setSearch(e.target.value)} + /> +
+ ) : ( +
+ +
+ )} +
+
+
상태직급이름현재 위치웨어러블 ID호출
{worker.status}{worker.role}{worker.name}{worker.zone}{worker.wearableId}🚨
+
+ + ); +} diff --git a/src/components/modal/WorkerInfoModal.jsx b/src/components/modal/WorkerInfoModal.jsx new file mode 100644 index 0000000..04bfe5c --- /dev/null +++ b/src/components/modal/WorkerInfoModal.jsx @@ -0,0 +1,3 @@ +export default function WorkerInfoModal() { + return <>; +} diff --git a/src/pages/Safety.jsx b/src/pages/Safety.jsx index e40b686..72c5330 100644 --- a/src/pages/Safety.jsx +++ b/src/pages/Safety.jsx @@ -1,38 +1,53 @@ +import { useEffect } from "react"; +import axiosInstance from "../api/axiosInstance"; +import WorkerInfoModal from "../components/modal/WorkerInfoModal"; +import WorkerTable from "../components/WorkerTable"; + export default function Safety() { - const mock_workers = { - normal_workers: [], - abnormal_workers: [], - disconnected_workers: [], // 논의필요! - }; + const mock_workers = [ + { + name: "김00", + role: "사원", + status: "위험", + zone: "포장 구역 A", + wearableId: "WEARABLE000111000", + email: "test@example.com", + phone: "01011112222", + }, + { + name: "윤00", + role: "공장장", + status: "정상", + zone: "휴게실", + wearableId: "인식되지 않음", + email: "test@example.com", + phone: "01011112222", + }, + { + name: "정00", + role: "반장", + status: "정상", + zone: "조립 구역 B", + wearableId: "WEARABLE111111111", + email: "test@example.com", + phone: "01011112222", + }, + ]; + + useEffect(() => { + axiosInstance + .get("/api/workers") + .then(() => { + console.log("작업자 정보 get!"); + }) + .catch((e) => console.log("작업자 정보 조회 실패", e)); + }, []); return ( <> +

작업자 안전관리

- {/* 정상 작업자 */} -
-
- 정상 -
-
- {/* 작업자 목록 - * 아니 뭐더라 */} -
-
- {/* 이상 작업자 */} -
-
- 주의 -
-
- {/* 작업자 목록 - * 아니 뭐더라 */} -
-
- {/* 연결되지 않은 작업자 */} -
-
- 연결되지 않은 사용자 -
-
+
+
); diff --git a/src/styles/style.css b/src/styles/style.css index 829ddf2..0b75adf 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -7,6 +7,7 @@ --box-color: #f6f6f6; --blue: #1d4a7a; + --warning: #fde1ad; } #root { position: relative; @@ -48,8 +49,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; } /* 사이드바 설정 */ @@ -566,7 +568,7 @@ p.sidebar-open { } .monitor-box.warn { - background-color: #fde1ad; + background-color: var(--warning); } .icon-container { @@ -828,3 +830,73 @@ strong.normal { font-weight: bold; color: var(--blue); } + +.table-container { + width: 100%; + margin: 2rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.search-container { + text-align: end; + margin-bottom: 0.5rem; +} + +.worker-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: 7.5%; +} + +.worker-table td { + border: 1px solid #ccc; + background-color: white; +} + +.worker-table th:nth-child(4) { + width: 15%; +} + +.worker-table th:nth-child(5) { + width: auto; +} + +.worker-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; + max-width: 5rem; +} + +.search-field { + display: inline; + margin-left: 1rem; +} + +.search-field select { + margin-left: 0.2rem; +} From 075a6f9ddc7036d37017b2eb1a6895746a74cc73 Mon Sep 17 00:00:00 2001 From: YeIn kim <2076070@ewhain.net> Date: Wed, 21 May 2025 13:06:13 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat=20|=20sprint2=20|=20FRB-72=20|=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9E=90=20=EA=B4=80=EB=A6=AC=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=99=84=EB=A3=8C=20|=20=EA=B9=80=EC=98=88?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디자인 변경 작업은 '작업자 관리 페이지' 디자인 변경 작업과 함께 진행될 예정입니다. --- src/components/WorkerTable.jsx | 14 ++-- src/pages/Safety.jsx | 10 ++- src/pages/ZoneDetail_2.jsx | 140 +++++++++++++++++++++++++++------ src/styles/style.css | 32 +++++++- 4 files changed, 158 insertions(+), 38 deletions(-) diff --git a/src/components/WorkerTable.jsx b/src/components/WorkerTable.jsx index 1871808..fded8d6 100644 --- a/src/components/WorkerTable.jsx +++ b/src/components/WorkerTable.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -export default function WorkerTable({ worker_list }) { +export default function WorkerTable({ worker_list, isDetail = false }) { const [searchType, setSearchType] = useState("byName"); const [search, setSearch] = useState(""); const [selectedStatus, setSelectedStatus] = useState("전체"); @@ -76,10 +76,10 @@ export default function WorkerTable({ worker_list }) { 상태 {/* 초록색 빨간색 동그라미 */} - 직급 + {/* 직급 */} 이름 - 현재 위치 - 웨어러블 ID + {!isDetail && 현재 위치} + 웨어러블 ID 호출 @@ -92,10 +92,10 @@ export default function WorkerTable({ worker_list }) { return ( {worker.status} - {worker.role} + {/* {worker.role} */} {worker.name} - {worker.zone} - {worker.wearableId} + {!isDetail && {worker.zone}} + {worker.wearableId} 🚨 ); diff --git a/src/pages/Safety.jsx b/src/pages/Safety.jsx index 72c5330..21e7abd 100644 --- a/src/pages/Safety.jsx +++ b/src/pages/Safety.jsx @@ -7,7 +7,7 @@ export default function Safety() { const mock_workers = [ { name: "김00", - role: "사원", + // role: "사원", status: "위험", zone: "포장 구역 A", wearableId: "WEARABLE000111000", @@ -16,7 +16,7 @@ export default function Safety() { }, { name: "윤00", - role: "공장장", + // role: "공장장", status: "정상", zone: "휴게실", wearableId: "인식되지 않음", @@ -25,7 +25,7 @@ export default function Safety() { }, { name: "정00", - role: "반장", + // role: "반장", status: "정상", zone: "조립 구역 B", wearableId: "WEARABLE111111111", @@ -42,11 +42,13 @@ export default function Safety() { }) .catch((e) => console.log("작업자 정보 조회 실패", e)); }, []); + return ( <> +

작업자 안전관리

-
+
diff --git a/src/pages/ZoneDetail_2.jsx b/src/pages/ZoneDetail_2.jsx index 92f1d85..572c910 100644 --- a/src/pages/ZoneDetail_2.jsx +++ b/src/pages/ZoneDetail_2.jsx @@ -1,28 +1,112 @@ 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"; 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 mock_workers = [ + { + name: "S00", + role: "사원", + status: "위험", + // zone: "포장 구역 A", + wearableId: "WEARABLE000111000", + email: "test@example.com", + phone: "01011112222", + }, + { + name: "Y00", + role: "공장장", + status: "정상", + // zone: "휴게실", + wearableId: "인식되지 않음", + email: "test@example.com", + phone: "01011112222", + }, + { + name: "J00", + role: "반장", + status: "정상", + // zone: "조립 구역 B", + wearableId: "WEARABLE111111111", + email: "test@example.com", + phone: "01011112222", + }, + ]; - /* mock data */ - /* zoneId로 detail 정보를 요청하면, 아래 정보를 줬으면 좋겠다...*/ + 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: 2, + value: 35.5, + timestamp: "2024-03-20T14:30:00", + abnormalType: "온도 위험", + targetId: "sensor456", + }, + { + zoneId: "zone123", + targetType: "환경", + sensorType: "TEMPERATURE", + dangerLevel: 2, + value: 35.5, + timestamp: "2024-03-20T14:30:00", + abnormalType: "온도 위험", + targetId: "sensor456", + }, + ]; - // 최종 프로젝트 - // 시뮬레이션 시각화 - // 핸드폰 앱으로 만들어서 센서 데이터 전송가능하게끔. + // 공간의 작업자 정보 받아오기 + useEffect(() => { + axiosInstance + .get(`/api/workers/${zoneId}`) + .then(() => { + console.log(`${zoneId}의 작업자 정보 get!`); + }) + .catch((e) => console.log(`${zoneId}의 작업자 로드 실패`, e)); + }, []); // 여기서 리프레시 버튼을 추가해도 좋을 것 같네요! - // 데이터 기반 의사 결정 + // 로그 상세조회 + useEffect(() => { + axiosInstance + .get(`/api/system-logs/zone/${zoneId}`) + .then((res) => { + // console.log(res.data); + // setLogs(); + }) + .catch((e) => { + console.log("로그 조회 실패 - mock-data를 불러옵니다", e); + setLogs(mock_loglist); + }); + }, [refreshLog]); - // 줌으로 면접 녹화해보기 + // Kibana 대시보드 ID (미리 저장해둔 고정된 dashboard) + const dashboardId = "d9cad7d0-2d48-11f0-b003-9ddfbb58f11c"; + const sensorTypes = ["temp", "humid"]; // 원하는 센서 타입들 추가 - // cam이 아닌 클라우드 Am으로 말하기 + /* mock data */ + /* zoneId로 detail 정보를 요청하면, 아래 정보를 줬으면 좋겠다...*/ const mock_details_sensor = { zoneId: zoneId, @@ -48,16 +132,17 @@ 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]); return ( <>

{mock_details_sensor.zoneName}

@@ -91,6 +176,13 @@ export default function ZoneDetail_2() { {/* 근무자 현황 :: 스프린트2 */}
근무자 현황
+
+ +
+
+ {/* 담당자 :: 스프린트2 */} +
+
담당자 정보

스프린트2에서 진행 예정

@@ -106,18 +198,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 0b75adf..ce7a012 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -180,6 +180,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); @@ -833,7 +839,6 @@ strong.normal { .table-container { width: 100%; - margin: 2rem; display: flex; flex-direction: column; justify-content: center; @@ -870,12 +875,12 @@ strong.normal { background-color: white; } -.worker-table th:nth-child(4) { +.worker-table th:nth-child(3) { width: 15%; } -.worker-table th:nth-child(5) { - width: auto; +.id-row { + width: auto !important; } .worker-table .critical td { @@ -900,3 +905,22 @@ strong.normal { .search-field select { margin-left: 0.2rem; } + +.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; +} From 9ae67f22b667fb452d0173fe392d3ca25cf6467d Mon Sep 17 00:00:00 2001 From: YeIn kim <2076070@ewhain.net> Date: Wed, 21 May 2025 13:42:54 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat=20|=20sprint2=20|=20FRB-73=20|=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=A1=9C=EA=B7=B8=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=99=84=EB=A3=8C=20|=20=EA=B9=80=EC=98=88?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/refresh_icon.svg | 19 ++++++++++++++++ src/components/LogTable.jsx | 44 +++++++++++++++++++++++++++++++++++++ src/pages/ZoneDetail_2.jsx | 29 ++++++++++++------------ src/styles/style.css | 9 +++++--- 4 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 src/assets/refresh_icon.svg create mode 100644 src/components/LogTable.jsx 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..e9a8562 --- /dev/null +++ b/src/components/LogTable.jsx @@ -0,0 +1,44 @@ +export default function LogTable({ logs }) { + console.log(logs); + return ( + <> +
+ + + + + + + + + + + + + {logs.map((l, i) => { + return ( + + + + + + + + + ); + })} + +
분류세분류위험도발생 시각센서ID측정값
{l.targetType}{l.abnormalType}{l.dangerLevel}{l.timestamp}{l.targetId}{l.value}
+
+ + ); +} diff --git a/src/pages/ZoneDetail_2.jsx b/src/pages/ZoneDetail_2.jsx index 572c910..deeee81 100644 --- a/src/pages/ZoneDetail_2.jsx +++ b/src/pages/ZoneDetail_2.jsx @@ -7,7 +7,6 @@ import LogTable from "../components/LogTable"; export default function ZoneDetail_2() { const { zoneId } = useParams(); - const [isLogOpen, setLogOpen] = useState(false); const bottomRef = useRef(null); const [refreshLog, setRefreshLog] = useState(0); @@ -59,7 +58,7 @@ export default function ZoneDetail_2() { zoneId: "zone123", targetType: "환경", sensorType: "TEMPERATURE", - dangerLevel: 2, + dangerLevel: 1, value: 35.5, timestamp: "2024-03-20T14:30:00", abnormalType: "온도 위험", @@ -69,10 +68,10 @@ export default function ZoneDetail_2() { zoneId: "zone123", targetType: "환경", sensorType: "TEMPERATURE", - dangerLevel: 2, + dangerLevel: 0, value: 35.5, timestamp: "2024-03-20T14:30:00", - abnormalType: "온도 위험", + abnormalType: "온도 안정", targetId: "sensor456", }, ]; @@ -89,16 +88,18 @@ export default function ZoneDetail_2() { // 로그 상세조회 useEffect(() => { - axiosInstance - .get(`/api/system-logs/zone/${zoneId}`) - .then((res) => { - // console.log(res.data); - // setLogs(); - }) - .catch((e) => { - console.log("로그 조회 실패 - mock-data를 불러옵니다", e); - setLogs(mock_loglist); - }); + if (refreshLog) { + axiosInstance + .get(`/api/system-logs/zone/${zoneId}`) + .then((res) => { + // console.log(res.data); + // setLogs(); + }) + .catch((e) => { + console.log("로그 조회 실패 - mock-data를 불러옵니다", e); + setLogs(mock_loglist); + }); + } }, [refreshLog]); // Kibana 대시보드 ID (미리 저장해둔 고정된 dashboard) diff --git a/src/styles/style.css b/src/styles/style.css index ce7a012..35a4d95 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -850,7 +850,8 @@ strong.normal { margin-bottom: 0.5rem; } -.worker-table { +.worker-table, +.logs-table { width: calc(100% - 4rem); border-collapse: collapse; text-align: center; @@ -870,7 +871,8 @@ strong.normal { width: 7.5%; } -.worker-table td { +.worker-table td, +.logs-table td { border: 1px solid #ccc; background-color: white; } @@ -883,7 +885,8 @@ strong.normal { width: auto !important; } -.worker-table .critical td { +.worker-table .critical td, +.logs-table .critical td { background-color: var(--warning); } From 3f502c78b4ba5e2bc0262398998f27760686ac5c Mon Sep 17 00:00:00 2001 From: YeIn kim <2076070@ewhain.net> Date: Wed, 21 May 2025 14:19:58 +0900 Subject: [PATCH 4/6] =?UTF-8?q?style=20|=20sprint2=20|=20FRB-154=20|=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9E=90=20=EA=B2=80=EC=83=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?|=20=EA=B9=80=EC=98=88=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/WorkerTable.jsx | 102 +++++++++++++++++---------------- src/pages/Safety.jsx | 16 ++++-- src/styles/style.css | 33 +++++++---- 3 files changed, 87 insertions(+), 64 deletions(-) diff --git a/src/components/WorkerTable.jsx b/src/components/WorkerTable.jsx index fded8d6..5f55453 100644 --- a/src/components/WorkerTable.jsx +++ b/src/components/WorkerTable.jsx @@ -24,59 +24,64 @@ export default function WorkerTable({ worker_list, isDetail = false }) {
-
- 상태 {/* 초록색 빨간색 동그라미 */} - {/* 직급 */} + 상태 이름 {!isDetail && 현재 위치} 웨어러블 ID @@ -92,7 +97,6 @@ export default function WorkerTable({ worker_list, isDetail = false }) { return ( {worker.status} - {/* {worker.role} */} {worker.name} {!isDetail && {worker.zone}} {worker.wearableId} diff --git a/src/pages/Safety.jsx b/src/pages/Safety.jsx index 21e7abd..f970da6 100644 --- a/src/pages/Safety.jsx +++ b/src/pages/Safety.jsx @@ -1,9 +1,10 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import axiosInstance from "../api/axiosInstance"; import WorkerInfoModal from "../components/modal/WorkerInfoModal"; import WorkerTable from "../components/WorkerTable"; export default function Safety() { + const [workerList, setWorkerList] = useState([]); const mock_workers = [ { name: "김00", @@ -34,13 +35,20 @@ export default function Safety() { }, ]; - useEffect(() => { + const fetchWorkers = () => { axiosInstance .get("/api/workers") .then(() => { console.log("작업자 정보 get!"); }) - .catch((e) => console.log("작업자 정보 조회 실패", e)); + .catch((e) => { + console.log("작업자 정보 조회 실패 - mock data를 불러옵니다", e); + setWorkerList(mock_workers); + }); + }; + + useEffect(() => { + fetchWorkers(); }, []); return ( @@ -49,7 +57,7 @@ export default function Safety() {

작업자 안전관리

- +
); diff --git a/src/styles/style.css b/src/styles/style.css index 35a4d95..324a381 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -846,7 +846,7 @@ strong.normal { } .search-container { - text-align: end; + text-align: start; margin-bottom: 0.5rem; } @@ -897,16 +897,6 @@ strong.normal { margin: 0.5rem; outline: none; margin-left: 1rem; - max-width: 5rem; -} - -.search-field { - display: inline; - margin-left: 1rem; -} - -.search-field select { - margin-left: 0.2rem; } .safety-body { @@ -927,3 +917,24 @@ strong.normal { 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; +} From f4e3d1cb20a915353cfd85481319cbd496172adb Mon Sep 17 00:00:00 2001 From: YeIn kim <2076070@ewhain.net> Date: Wed, 21 May 2025 14:46:27 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat=20|=20sprint2=20|=20FRB-72=20|=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9E=90=20=EA=B4=80=EB=A6=AC=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20-=20=EC=A3=BC=EA=B8=B0=EC=A0=81=20=EB=A6=AC?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20|=20=EA=B9=80=EC=98=88=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LogTable.jsx | 1 - src/pages/Safety.jsx | 11 ++++++++--- src/pages/ZoneDetail_2.jsx | 39 +++++++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/components/LogTable.jsx b/src/components/LogTable.jsx index e9a8562..ed88486 100644 --- a/src/components/LogTable.jsx +++ b/src/components/LogTable.jsx @@ -1,5 +1,4 @@ export default function LogTable({ logs }) { - console.log(logs); return ( <>
diff --git a/src/pages/Safety.jsx b/src/pages/Safety.jsx index f970da6..cefceba 100644 --- a/src/pages/Safety.jsx +++ b/src/pages/Safety.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import axiosInstance from "../api/axiosInstance"; import WorkerInfoModal from "../components/modal/WorkerInfoModal"; import WorkerTable from "../components/WorkerTable"; @@ -35,7 +35,7 @@ export default function Safety() { }, ]; - const fetchWorkers = () => { + const fetchWorkers = useCallback(() => { axiosInstance .get("/api/workers") .then(() => { @@ -45,12 +45,17 @@ export default function Safety() { 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/ZoneDetail_2.jsx b/src/pages/ZoneDetail_2.jsx index deeee81..f07ff28 100644 --- a/src/pages/ZoneDetail_2.jsx +++ b/src/pages/ZoneDetail_2.jsx @@ -12,6 +12,9 @@ export default function ZoneDetail_2() { const [refreshLog, setRefreshLog] = useState(0); const [logs, setLogs] = useState([]); + const [refreshWorkers, setRefreshWorkers] = useState(0); + const [workerList, setWorkerList] = useState([]); + // 작업자 정보 const mock_workers = [ { @@ -77,24 +80,32 @@ export default function ZoneDetail_2() { ]; // 공간의 작업자 정보 받아오기 - useEffect(() => { + const fetchWorkers = () => { axiosInstance .get(`/api/workers/${zoneId}`) .then(() => { console.log(`${zoneId}의 작업자 정보 get!`); }) - .catch((e) => console.log(`${zoneId}의 작업자 로드 실패`, e)); - }, []); // 여기서 리프레시 버튼을 추가해도 좋을 것 같네요! + .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) => { - // console.log(res.data); - // setLogs(); - }) + .then((res) => {}) .catch((e) => { console.log("로그 조회 실패 - mock-data를 불러옵니다", e); setLogs(mock_loglist); @@ -117,7 +128,7 @@ export default function ZoneDetail_2() { { type: "humid", id: "SID-YYY" }, ], }; - console.log(mock_details_sensor.zoneId); + // console.log("(확인완료)", mock_details_sensor.zoneId); const mapSensorType = (sensorType) => { const sensorMap = { temp: "온도 센서", humid: "습도 센서" }; @@ -176,9 +187,17 @@ export default function ZoneDetail_2() {
{/* 근무자 현황 :: 스프린트2 */}
-
근무자 현황
+
+ 근무자 현황 + setRefreshWorkers((prev) => prev + 1)} + > + + +
- +
{/* 담당자 :: 스프린트2 */} From 8549a68fb296dbc8eabe8ab95789773d4847f28f Mon Sep 17 00:00:00 2001 From: YeIn kim <2076070@ewhain.net> Date: Thu, 22 May 2025 10:45:16 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat=20|=20sprint2=20|=20FRB-141=20|=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=84=EB=B3=84=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=B0=B0=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?|=20=EA=B9=80=EC=98=88=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API 연결 안 되어 있습니닷... 디자인도 확정은 아닙니다욧... --- src/components/WorkerTable.jsx | 149 ++++++++++++++--------- src/components/modal/WorkerInfoModal.jsx | 51 +++++++- src/pages/Safety.jsx | 35 ++++-- src/pages/Settings.jsx | 9 ++ src/pages/ZoneDetail_2.jsx | 142 ++++++++++++++++++--- src/styles/style.css | 74 ++++++++++- 6 files changed, 373 insertions(+), 87 deletions(-) diff --git a/src/components/WorkerTable.jsx b/src/components/WorkerTable.jsx index 5f55453..2892196 100644 --- a/src/components/WorkerTable.jsx +++ b/src/components/WorkerTable.jsx @@ -1,6 +1,12 @@ import { useState } from "react"; -export default function WorkerTable({ worker_list, isDetail = false }) { +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("전체"); @@ -16,75 +22,86 @@ export default function WorkerTable({ worker_list, isDetail = false }) { } return true; }); + + const directCall = (email, phone) => { + const confirmed = window.confirm(`작업자를 호출하시겠습니까?`); + if (confirmed) { + /* To-Do: 긴급 호출 기능 구현하면 됨!! */ + console.log("긴급 호출!!!!!", `${email} ${phone}`); + } + }; return ( <>
- - + - + + + )} {!isDetail && } + @@ -100,7 +117,21 @@ export default function WorkerTable({ worker_list, isDetail = false }) { {!isDetail && } - + + ); })} diff --git a/src/components/modal/WorkerInfoModal.jsx b/src/components/modal/WorkerInfoModal.jsx index 04bfe5c..586495b 100644 --- a/src/components/modal/WorkerInfoModal.jsx +++ b/src/components/modal/WorkerInfoModal.jsx @@ -1,3 +1,52 @@ -export default function WorkerInfoModal() { +import XIcon from "../../assets/x_icon.svg?react"; + +function ContactTable({ email, phone, id }) { + return ( +
+
-
- {/* 이름검색 라디오버튼 */} -
-
+
+ {/* 이름검색 라디오버튼 */} +
+ { - setSearchType("byName"); - setSearch(""); - }} - > - 이름으로 검색 - - setSearch(e.target.value)} - disabled={searchType !== "byName"} - /> + id="search" + name="search" + className="search-field" + value={search} + onChange={(e) => setSearch(e.target.value)} + disabled={searchType !== "byName"} + /> +
+ {/* 상태별 분류 라디오버튼 */} +
+ + +
- {/* 상태별 분류 라디오버튼 */} -
- - -
- -
상태 이름현재 위치웨어러블 ID연락처 호출
{worker.name}{worker.zone}{worker.wearableId}🚨 { + selectWorker(worker); + openModal(true); + }} + > + 조회 + directCall(worker.email, worker.phone)} + > + 🚨 +
+ + + + + + + + + + + + + + + + +
이메일{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 cefceba..745f398 100644 --- a/src/pages/Safety.jsx +++ b/src/pages/Safety.jsx @@ -1,10 +1,18 @@ import { useCallback, useEffect, useState } from "react"; import axiosInstance from "../api/axiosInstance"; -import WorkerInfoModal from "../components/modal/WorkerInfoModal"; import WorkerTable from "../components/WorkerTable"; +import WorkerInfoModal from "../components/modal/WorkerInfoModal"; export default function Safety() { const [workerList, setWorkerList] = useState([]); + + const [isOpen, setIsOpen] = useState(false); + const onClose = () => { + setSelectedWorker(); + setIsOpen(false); + }; + const [selectedWorkerInfo, setSelectedWorker] = useState(); + const mock_workers = [ { name: "김00", @@ -12,8 +20,8 @@ export default function Safety() { status: "위험", zone: "포장 구역 A", wearableId: "WEARABLE000111000", - email: "test@example.com", - phone: "01011112222", + email: "test1@example.com", + phone: "010111111111", }, { name: "윤00", @@ -21,8 +29,8 @@ export default function Safety() { status: "정상", zone: "휴게실", wearableId: "인식되지 않음", - email: "test@example.com", - phone: "01011112222", + email: "test2@example.com", + phone: "010222222222", }, { name: "정00", @@ -30,8 +38,8 @@ export default function Safety() { status: "정상", zone: "조립 구역 B", wearableId: "WEARABLE111111111", - email: "test@example.com", - phone: "01011112222", + email: "test3@example.com", + phone: "01033333333", }, ]; @@ -58,11 +66,18 @@ export default function Safety() { 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 f07ff28..6d88f8c 100644 --- a/src/pages/ZoneDetail_2.jsx +++ b/src/pages/ZoneDetail_2.jsx @@ -4,6 +4,71 @@ 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(); @@ -15,31 +80,25 @@ export default function ZoneDetail_2() { const [refreshWorkers, setRefreshWorkers] = useState(0); const [workerList, setWorkerList] = useState([]); - // 작업자 정보 + // Mock data 시작 const mock_workers = [ { name: "S00", - role: "사원", status: "위험", - // zone: "포장 구역 A", wearableId: "WEARABLE000111000", email: "test@example.com", - phone: "01011112222", + phone: "01022222222", }, { name: "Y00", - role: "공장장", status: "정상", - // zone: "휴게실", wearableId: "인식되지 않음", email: "test@example.com", - phone: "01011112222", + phone: "01033333333", }, { name: "J00", - role: "반장", status: "정상", - // zone: "조립 구역 B", wearableId: "WEARABLE111111111", email: "test@example.com", phone: "01011112222", @@ -79,6 +138,19 @@ export default function ZoneDetail_2() { }, ]; + 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 끝 + // 공간의 작업자 정보 받아오기 const fetchWorkers = () => { axiosInstance @@ -100,7 +172,7 @@ export default function ZoneDetail_2() { return () => clearInterval(interval); }, [refreshWorkers]); // 여기서 리프레시 버튼을 추가해도 좋을 것 같네요! - // 로그 상세조회 + // 로그 정보 받아오기 useEffect(() => { if (refreshLog) { axiosInstance @@ -113,13 +185,26 @@ export default function ZoneDetail_2() { } }, [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"]; // 원하는 센서 타입들 추가 - /* mock data */ - /* zoneId로 detail 정보를 요청하면, 아래 정보를 줬으면 좋겠다...*/ - const mock_details_sensor = { zoneId: zoneId, zoneName: "생산 라인 A", @@ -128,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: "습도 센서" }; @@ -155,8 +239,22 @@ export default function ZoneDetail_2() { }, 200); } }, [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 */}
@@ -197,14 +295,26 @@ export default function ZoneDetail_2() {
- +
{/* 담당자 :: 스프린트2 */}
담당자 정보
-

스프린트2에서 진행 예정

+
{/* 설비 현황 :: 스프린트3 */} diff --git a/src/styles/style.css b/src/styles/style.css index 324a381..efd2008 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -8,6 +8,8 @@ --box-color: #f6f6f6; --blue: #1d4a7a; --warning: #fde1ad; + --warning2: #f2c97e; + --warning3: #fcbc45; } #root { position: relative; @@ -375,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; @@ -437,6 +440,7 @@ p.sidebar-open { } .modal-box { background-color: var(--c4); + /* background-color: white; */ border-radius: 2rem; width: 40vw; height: auto; @@ -868,7 +872,7 @@ strong.normal { padding: 0.5rem; border: 1px solid #ccc; background-color: var(--c4); - width: 7.5%; + width: 10%; } .worker-table td, @@ -938,3 +942,71 @@ select.search-field { 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); +}