diff --git a/frontend/src/assets/icons/betweenStepIcon.svg b/frontend/src/assets/icons/betweenStepIcon.svg new file mode 100644 index 0000000000..2e48b79198 --- /dev/null +++ b/frontend/src/assets/icons/betweenStepIcon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/icons/findStepIcon.svg b/frontend/src/assets/icons/findStepIcon.svg new file mode 100644 index 0000000000..f238bb768a --- /dev/null +++ b/frontend/src/assets/icons/findStepIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/searchStepIcon.svg b/frontend/src/assets/icons/searchStepIcon.svg new file mode 100644 index 0000000000..fe14d37615 --- /dev/null +++ b/frontend/src/assets/icons/searchStepIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/guardianMain/CustomSteps.js b/frontend/src/components/guardianMain/CustomSteps.js index fe84a5b081..4319f9b5c3 100644 --- a/frontend/src/components/guardianMain/CustomSteps.js +++ b/frontend/src/components/guardianMain/CustomSteps.js @@ -1,6 +1,9 @@ import styled from "styled-components"; -import { LoadingOutlined, SmileOutlined, SolutionOutlined, UserOutlined } from "@ant-design/icons"; import { useEffect, useState } from "react"; +import { LoadingOutlined, SmileOutlined, SolutionOutlined, UserOutlined } from "@ant-design/icons"; +import SearchStepIcon from "../../assets/icons/searchStepIcon.svg"; +import BetweenStepIcon from "../../assets/icons/betweenStepIcon.svg"; +import FindStepIcon from "../../assets/icons/findStepIcon.svg"; import { StepContents } from "./StepContents"; export const CustomSteps = ({ currentStep }) => { const [current, setCurrent] = useState(0); @@ -9,16 +12,16 @@ export const CustomSteps = ({ currentStep }) => { const [state, setState] = useState("current"); const iconList = [ - { title: "1차 탐색", icon: }, - { title: "이미지 선별", icon: }, - { title: "2차 탐색", icon: }, - { title: "실종자 수색", icon: }, + { title: "1차 탐색", icon: SearchStepIcon }, + { title: "이미지 선별", icon: BetweenStepIcon }, + { title: "2차 탐색", icon: SearchStepIcon }, + { title: "실종자 수색", icon: FindStepIcon }, ]; useEffect(() => { setCurrent(currentStep); setView(currentStep); - }, []); + }, [currentStep]); const clickItem = (idx) => { console.log("clickItem:", idx); @@ -34,7 +37,9 @@ export const CustomSteps = ({ currentStep }) => { {idx != 3 && = current ? "#E7E7E7" : view == current ? "#1890FF" : "#A9D6FF"}>} {idx == view ? ( - current ? "#E7E7E7" : "#1890FF"}> + current ? "#E7E7E7" : "#1890FF"}> + + ) : ( @@ -96,11 +101,17 @@ const IconContainer = styled.div` const IconWrapper = styled.div` display: flex; justify-content: center; + align-items: center; width: 11.5rem; height: 11.5rem; margin-top: 1.5rem; border-radius: 50%; background-color: ${(props) => props.color || "#1890FF"}; + + img { + width: 6rem; + height: 6rem; + } `; const DotWrapper = styled.div` width: 4rem; diff --git a/frontend/src/components/guardianMain/StepContents.js b/frontend/src/components/guardianMain/StepContents.js index 6761bb9586..73fcffad72 100644 --- a/frontend/src/components/guardianMain/StepContents.js +++ b/frontend/src/components/guardianMain/StepContents.js @@ -22,31 +22,49 @@ export const StepContents = ({ index, current }) => { title: "1차 탐색", wait: { content: "지능형 탐색을 시작합니다." }, process: { content: "등록된 정보로 지능형 탐색 중입니다." }, - finish: { content: "탐색 완료했습니다.", buttonText: "탐색 결과 확인하기", callBack: () => {} }, + finish: { + content: "탐색 완료했습니다.", + buttonText: "탐색 결과 확인하기", + callBack: () => { + navigate("/result", { state: { title: "1차 탐색 결과", step: "first" } }); + }, + }, }, { step: 1, title: "탐색 이미지 선별", wait: { content: - "1차 탐색 결과 중 실종자와 유사한 이미지를 선택해주세요. 선택된 이미지는 2차 탐색에 활용되니 신중하고 빠른 선택 부탁드립니다.", + "1차 탐색 결과 이미지 중 실종자와 유사한 이미지를 선택해주세요. 선택된 이미지는 2차 탐색에 활용되니 신중한 선택 부탁드립니다.", }, process: { content: - "1차 탐색 결과 중 실종자와 유사한 이미지를 선택해주세요. 선택된 이미지는 2차 탐색에 활용되니 신중하고 빠른 선택 부탁드립니다.", + "1차 탐색 결과 이미지 중 실종자와 유사한 이미지를 선택해주세요. 선택된 이미지는 2차 탐색에 활용되니 신중한 선택 부탁드립니다.", buttonText: "이미지 선별하기", callBack: () => { navigate("/select"); }, }, - finish: { content: "이미지 선택 완료했습니다.", buttonText: "선별 이미지 확인하기", callBack: () => {} }, + finish: { + content: "이미지 선택 완료했습니다.", + buttonText: "선별 이미지 확인하기", + callBack: () => { + navigate("/result", { state: { title: "선별 이미지 확인", step: "between" } }); + }, + }, }, { step: 2, title: "2차 탐색", wait: { content: "선별된 이미지를 바탕으로 지능형 탐색을 시작합니다." }, process: { content: "등록된 정보로 지능형 탐색 중입니다." }, - finish: { content: "탐색 완료했습니다.", buttonText: "탐색 결과 확인하기", callBack: () => {} }, + finish: { + content: "탐색 완료했습니다.", + buttonText: "탐색 결과 확인하기", + callBack: () => { + navigate("/result", { state: { title: "2차 탐색 결과", step: "second" } }); + }, + }, }, { step: 3, @@ -101,7 +119,7 @@ const StStepContents = styled.div` align-items: center; gap: 3.5rem; margin-top: 4.75rem; - width: 75.5rem; + width: 77.5rem; `; const Title = styled.p` font-size: 5rem; diff --git a/frontend/src/components/guardianSelect/AllResultList.js b/frontend/src/components/guardianSelect/AllResultList.js index 1f01916399..38691bcb68 100644 --- a/frontend/src/components/guardianSelect/AllResultList.js +++ b/frontend/src/components/guardianSelect/AllResultList.js @@ -1,8 +1,10 @@ import { List, Image, Button } from "antd"; +import { useState } from "react"; import styled from "styled-components"; +import { CloseOutlined } from "@ant-design/icons"; +/*1차 탐색 결과를 보여주는 리스트 */ export const AllResultList = ({ onSelect, data, selectedList }) => { - console.log("data", data); return ( 1차 탐색 결과 @@ -13,30 +15,46 @@ export const AllResultList = ({ onSelect, data, selectedList }) => { }} dataSource={data} renderItem={(item) => ( - - ( - - onSelect(item)}> - {selectedList.includes(item) ? "선택해제" : "선택"} - - - ), - }} - /> - + )} /> ); }; +/*각 이미지 아이템 */ +const ImageItem = ({ item, onSelect, isSelected }) => { + const [isPreviewVisible, setPreviewVisible] = useState(false); + + return ( + + , + onVisibleChange: (visible) => setPreviewVisible(visible), + toolbarRender: () => ( + e.stopPropagation()}> + { + e.stopPropagation(); + onSelect(item); + setPreviewVisible(false); + }}> + {isSelected ? "선택해제" : "선택"} + + + ), + }} + onClick={() => setPreviewVisible(true)} + /> + + ); +}; const StAllResultList = styled.div` display: flex; flex-direction: column; @@ -70,11 +88,23 @@ const ItemImage = styled(Image)` width: 27.5rem; height: 45rem; border-radius: 2.5rem; - border: ${(props) => (props.select == "true" ? "0.8rem solid #0580F1" : "none")}; + border: ${(props) => (props.select == "true" ? "1rem solid #0580F1" : "none")}; &.ant-image-preview { width: 100%; background-color: #fff; } + &.ant-image-preview-footer { + width: 50%; + right: 0; + } + } + &.custom-image.ant-image-preview-close > .anticon { + width: 60rem; + font-size: 60rem; + } + &.custom-image.ant-image-preview-footer { + width: 50%; + right: 0; } `; @@ -90,6 +120,7 @@ const BottomContainer = styled.div` const BottomButton = styled(Button)` width: 31.5rem; height: 12.5rem; + border: none; border-radius: 2.5rem; background: #0580f1; diff --git a/frontend/src/components/guardianSelect/SelectImgList.js b/frontend/src/components/guardianSelect/SelectImgList.js index 7221ffd93c..c45b7f0060 100644 --- a/frontend/src/components/guardianSelect/SelectImgList.js +++ b/frontend/src/components/guardianSelect/SelectImgList.js @@ -1,5 +1,5 @@ -import { List, Image, Button, Modal } from "antd"; -import { useState } from "react"; +import { List, Image, Button, Modal, Empty } from "antd"; +import { useEffect, useState } from "react"; import styled from "styled-components"; export const SelectImgList = ({ onSelect, data }) => { @@ -11,47 +11,35 @@ export const SelectImgList = ({ onSelect, data }) => { const handleClose = () => { setOpenPreview(false); }; + useEffect(() => {}, [data]); return ( 선택한 이미지({data.length}) - {data.map((item) => ( - <> + {data && data.length > 0 ? ( + data.map((item) => ( setOpenPreview(true)} - preview={false} - // preview={{ - // width: 900, - // toolbarRender: () => ( - // - // onSelect(item)}>선택해제 - // - // ), - // }} - />{" "} - onSelect(item)}>선택해제} - closeIcon={null} - width={1000}> - setOpenPreview(true)} - preview={false} - />{" "} - - > - ))} + // preview={false} + preview={{ + width: 900, + toolbarRender: () => ( + + onSelect(item)}>선택해제 + + ), + }} + /> + )) + ) : ( + + + + )} ); @@ -166,3 +154,11 @@ const BottomButton = styled(Button)` line-height: 5.5rem; right: 0; `; +const EmptyContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; +`; diff --git a/frontend/src/core/api/index.js b/frontend/src/core/api/index.js index 81bf2da72c..d8f6a996d0 100644 --- a/frontend/src/core/api/index.js +++ b/frontend/src/core/api/index.js @@ -207,6 +207,7 @@ export const getGuardianMissingPerson = async () => { { headers: { Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, "Content-Type": "application/json", }, }, @@ -233,6 +234,7 @@ export const getGuardianMissingPersonStep = async () => { { headers: { Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, "Content-Type": "application/json", }, }, @@ -260,6 +262,7 @@ export const postProfileImg = async (value) => { { headers: { Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, "Content-Type": "multipart/form-data", }, }, @@ -286,6 +289,7 @@ export const getGuardianSelectImage = async () => { { headers: { Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, "Content-Type": "application/json", }, }, @@ -312,6 +316,7 @@ export const postGuardianSelectImage = async (value) => { { headers: { Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, "Content-Type": "application/json", }, }, @@ -328,3 +333,57 @@ export const postGuardianSelectImage = async (value) => { }); return data; }; + +/*의뢰인용 탐색 결과 보여주기 - 선별된 이미지 (Get) */ +export const getGuardianSelectedResult = async () => { + const data = axios + .get( + `${process.env.REACT_APP_API_ROOT}/api/guardian/between-result`, + + { + headers: { + Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, + "Content-Type": "application/json", + }, + }, + ) + .then(function (response) { + console.log("response:", response.data); + return response.data.data; + }) + .catch(function (e) { + // 실패 시 처리 + console.error(e); + console.log(e.response.data); + alert("선별된 이미지 가져오기 실패."); + }); + return data; +}; + +/*의뢰인용 탐색 결과 보여주기 - 2차 탐색 이미지 (Get) */ +export const getGuardianSecondResult = async () => { + const data = axios + .get( + `${process.env.REACT_APP_API_ROOT}/api/guardian/second`, + + { + headers: { + Authorization: `${getCookie("authToken")}`, + // Authorization: `${process.env.REACT_APP_TOKEN}`, + "Content-Type": "application/json", + }, + }, + ) + .then(function (response) { + console.log("response:", response.data); + return response.data.data; + }) + .catch(function (e) { + // 실패 시 처리 + console.error(e); + console.log(e.response.data); + alert("2차 탐색 결과 가져오기 실패."); + }); + return data; +}; diff --git a/frontend/src/core/router.js b/frontend/src/core/router.js index 7b4ddd5600..84fe677781 100644 --- a/frontend/src/core/router.js +++ b/frontend/src/core/router.js @@ -6,7 +6,7 @@ import MissingPersonReportPage from "../pages/MissingPersonReportPage"; import MissingPersonListPage from "../pages/MissingPersonListPage"; import GuardianMainPage from "../pages/GuardianMainPage"; import GuardianSelectImgPage from "../pages/GuardianSelectImgPage"; - +import GuardianShowResultPage from "../pages/GuardianShowResultPage"; function Router() { return ( @@ -20,6 +20,7 @@ function Router() { } /> } /> + } /> ); diff --git a/frontend/src/pages/GuardianMainPage.js b/frontend/src/pages/GuardianMainPage.js index 8241b32c06..3828310fc4 100644 --- a/frontend/src/pages/GuardianMainPage.js +++ b/frontend/src/pages/GuardianMainPage.js @@ -26,6 +26,7 @@ function GuardianMainPage() { //실종자 진행현황 가져오기 getGuardianMissingPersonStep().then((data) => { + console.log("getGuardianMissingPersonStep:", data.step); switch (data.step) { case "FIRST": setStep(0); @@ -40,16 +41,16 @@ function GuardianMainPage() { setStep(3); break; default: - setStep(1); + setStep(0); } }); }, []); return ( - + {/* - + */} @@ -61,6 +62,7 @@ function GuardianMainPage() { } style={{ width: "12.5rem", @@ -74,6 +76,7 @@ function GuardianMainPage() { export default GuardianMainPage; const StGuardianMainPage = styled(Layout)` + padding-top: 3rem; background-color: white; `; const MainHeader = styled(Header)` @@ -97,11 +100,17 @@ const ProfileSection = styled(Form)` align-items: center; justify-content: center; width: 100%; + margin-bottom: 5rem; `; const FloatButtonContainer = styled(FloatButton)` - &.ant-float-btn .ant-float-btn-body .ant-float-btn-content { - width: 12.5rem; - height: 12.5rem; + &.custom-float-btn.ant-float-btn .ant-float-btn-body .ant-float-btn-content .ant-float-btn-icon { + width: 6rem; + height: 6rem; + margin: 0; + svg { + font-size: 6rem; + color: #848484; + } } `; diff --git a/frontend/src/pages/GuardianSelectImgPage.js b/frontend/src/pages/GuardianSelectImgPage.js index 90da4d7c41..90c8e6b950 100644 --- a/frontend/src/pages/GuardianSelectImgPage.js +++ b/frontend/src/pages/GuardianSelectImgPage.js @@ -1,9 +1,12 @@ import styled from "styled-components"; import { SelectImgList } from "../components/guardianSelect/SelectImgList"; import { useEffect, useState } from "react"; +import { CloseOutlined } from "@ant-design/icons"; import { AllResultList } from "../components/guardianSelect/AllResultList"; -import { Button } from "antd"; +import { Button, Modal } from "antd"; import { getGuardianSelectImage, postGuardianSelectImage } from "../core/api"; +import Icon from "@ant-design/icons/lib/components/Icon"; +import { useNavigate } from "react-router-dom"; // 더미 데이터 const dummyData = [ @@ -80,9 +83,12 @@ const dummyData = [ ]; function GuardianSelectImgPage() { + const navigate = useNavigate(); const [selectedImg, setSelectedImg] = useState([]); const [selctedImgId, setSelectedImgId] = useState([]); const [data, setData] = useState([]); + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); + const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false); useEffect(() => { getGuardianSelectImage().then((data) => { @@ -90,6 +96,15 @@ function GuardianSelectImgPage() { }); }, []); + // JavaScript로 화면 캡처 방지 설정 + document.addEventListener("contextmenu", (e) => e.preventDefault()); + document.addEventListener("keydown", (e) => { + if (e.key === "PrintScreen" || (e.ctrlKey && e.key === "p")) { + e.preventDefault(); + alert("Screen capture is disabled"); + } + }); + const onSelect = (select) => { setSelectedImg((prev) => { if (prev.includes(select)) { @@ -100,6 +115,37 @@ function GuardianSelectImgPage() { }); }; + const handleClose = { + // 버튼 클릭 이벤트 + showModal: () => { + setIsCancelModalOpen(true); + }, + + selectOK: () => { + setIsCancelModalOpen(false); + navigate("/m"); + }, + selectCancel: () => { + setIsCancelModalOpen(false); + }, + }; + + const handleSubmit = { + // 버튼 클릭 이벤트 + showModal: () => { + setIsSubmitModalOpen(true); + }, + + selectOK: () => { + setIsSubmitModalOpen(false); + onFinish(); + window.location.replace("/m"); + }, + selectCancel: () => { + setIsSubmitModalOpen(false); + }, + }; + const onFinish = () => { const value = [ selectedImg.map((item) => { @@ -113,11 +159,38 @@ function GuardianSelectImgPage() { return ( + + handleClose.showModal()}> + + + 탐색 이미지 선별 + - onFinish()}>제출 + handleSubmit.showModal()}>제출 + + 중단하면 선택했던 이미지 정보는 삭제됩니다. + + + 제출 후에는 선택한 이미지를 변경할 수 없어요. + ); } @@ -132,6 +205,50 @@ const StGuardianSelectImgPage = styled.div` height: 100vh; `; +const HeaderContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + height: 14rem; + top: 0; + z-index: 1; + + padding: 2rem 5rem; + + background-color: white; + box-shadow: 0 1rem 1rem 0 rgba(0, 0, 0, 0.1); +`; + +const CloseButton = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 11rem; + height: 11rem; + + &:hover { + background-color: #f2f2f2; + border-radius: 2rem; + } +`; + +const CloseIcon = styled(Icon)` + font-size: 5rem; +`; + +const Title = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + font-size: 5.5rem; + font-weight: 600; + color: black; + margin-right: 5rem; +`; + const BottomContainer = styled.div` display: flex; flex-direction: row; @@ -156,3 +273,81 @@ const BottomButton = styled(Button)` font-weight: 500; line-height: 5.5rem; `; + +const ModalContainer = styled(Modal)` + &.custom-modal { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + } + + &.ant-modal .ant-modal-content { + display: flex; + flex-direction: column; + align-items: center; + height: 57.5rem; + min-width: 86.5rem; + max-width: 125rem; + min-height: 57.5rem; + max-height: 75rem; + + margin-bottom: 60rem; + padding: 4rem 4.5rem; + + box-shadow: none; + border-radius: 5rem; + } + + &.ant-modal .ant-modal-body { + width: 100%; + padding: 0; + display: flex; + justify-content: center; + width: 48rem; + margin-bottom: 7rem; + p { + font-size: 3.75rem; + font-style: normal; + font-weight: 400; + line-height: 140%; + text-align: center; + word-break: keep-all; + } + } + + &.ant-modal .ant-modal-title { + width: 45rem; + margin-bottom: 2.5rem; + font-size: 5rem; + font-style: normal; + font-weight: 600; + line-height: 140%; + text-align: center; + word-break: keep-all; + } + + &.ant-modal .ant-modal-footer { + display: flex; + justify-content: space-around; + align-items: center; + width: 100%; + } + &.ant-modal .ant-modal-footer .ant-btn { + width: 35rem; + height: 12.5rem; + border-radius: 2.5rem; + span { + font-size: 4rem; + font-style: normal; + font-weight: 500; + line-height: 5.5rem; + } + } + + &.ant-modal .ant-modal-footer .ant-btn-default { + background: #dedede; + } +`; diff --git a/frontend/src/pages/GuardianShowResultPage.js b/frontend/src/pages/GuardianShowResultPage.js new file mode 100644 index 0000000000..575b9f25c8 --- /dev/null +++ b/frontend/src/pages/GuardianShowResultPage.js @@ -0,0 +1,155 @@ +import { Image, List } from "antd"; +import styled from "styled-components"; +import { CloseOutlined } from "@ant-design/icons"; + +import Icon from "@ant-design/icons/lib/components/Icon"; +import { getGuardianSecondResult, getGuardianSelectImage, getGuardianSelectedResult } from "../core/api"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; + +function GuardianShowResultPage() { + const location = useLocation(); + + const [data, setData] = useState([]); + const handleClose = () => { + window.history.back(); + }; + + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => {}, [data]); + + const fetchData = () => { + if (location.state.step === "first") { + getGuardianSelectImage().then((res) => { + console.log("res", res); + setData(res); + }); + } else if (location.state.step === "second") { + getGuardianSecondResult().then((res) => { + setData(res); + }); + } else { + getGuardianSelectedResult().then((res) => { + setData(res); + }); + } + }; + return ( + + + handleClose()}> + + + {location.state.title} + + + ( + + + + )} + /> + + + ); +} +export default GuardianShowResultPage; + +const StGuardianShowResultPage = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + overflow: hidden; +`; +const HeaderContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + height: 14rem; + top: 0; + z-index: 1; + + padding: 2rem 5rem; + + background-color: white; + box-shadow: 0 1rem 1rem 0 rgba(0, 0, 0, 0.1); +`; + +const CloseButton = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 11rem; + height: 11rem; + + &:hover { + background-color: #f2f2f2; + border-radius: 2rem; + } +`; + +const CloseIcon = styled(Icon)` + font-size: 5rem; +`; + +const Title = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + font-size: 5.5rem; + font-weight: 600; + color: black; + margin-right: 5rem; +`; + +const ContentsContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 0 5rem; + background-color: white; + border-radius: 1rem; +`; +const ListContainer = styled(List)` + width: 100%; + height: 100%; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; + scroll-snap-align: center; +`; + +const ItemImage = styled(Image)` + &.custom-image { + width: 27.5rem; + height: 45rem; + border-radius: 2.5rem; + border: ${(props) => (props.select == "true" ? "0.8rem solid #0580F1" : "none")}; + &.ant-image-preview { + width: 100%; + background-color: #fff; + } + } +`;
중단하면 선택했던 이미지 정보는 삭제됩니다.
제출 후에는 선택한 이미지를 변경할 수 없어요.