diff --git a/package.json b/package.json index fcef1c9..923fbc6 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "eslint-plugin-react": "^7.37.3", "globals": "^15.14.0", "husky": "^9.1.7", - "prettier": "^3.4.2" + "prettier": "^3.4.2", + "typescript": "^5.7.3" } } diff --git a/src/App.js b/src/App.js index ec9d6fd..a5e9624 100644 --- a/src/App.js +++ b/src/App.js @@ -1,14 +1,18 @@ -import './App.css'; -import Layout from './components/layout/Layout'; -import Router from './Router'; -import { UserInfoProvider } from './contexts/UserInfoContext'; +import "./App.css"; +import Layout from "./components/layout/Layout"; +import Router from "./Router"; +import React from 'react'; +import { UserInfoProvider } from "./contexts/UserInfoContext"; +import { ReviewProvider } from "./contexts/ReviewInfoContext"; function App() { return ( + + ); } diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/Router.js b/src/Router.js index 9bac72d..01111f2 100644 --- a/src/Router.js +++ b/src/Router.js @@ -1,26 +1,32 @@ -import { Routes, Route } from 'react-router-dom'; -import HomePage from './pages/HomePage'; -import AlbaSearchPage from './pages/AlbaSearchPage'; -import MyPage from './pages/MyPage/MyPage'; -import ResumePage from './pages/ResumePage'; +import { Routes, Route } from "react-router-dom"; +import HomePage from "./pages/HomePage"; +import AlbaSearchPage from "./pages/AlbaSearchPage"; +import AlbaReviewPage from "./pages/AlbaReviewPage"; +import MyPage from "./pages/MyPage/MyPage"; +import ResumePage from "./pages/ResumePage"; +import ChattingPage from "./pages/ChattingPage"; +import React from 'react'; import MyStatusPage from './pages/MyPage/MyStatusPage'; -import ChattingPage from './pages/ChattingPage'; import MyResume from './pages/MyResume/MyResume'; import SignUpPage from './pages/SignUpPage'; -import ContactDetailPage from './pages/MyPage/ContactDetailPage'; +import MyReviewPage from "./pages/MyPage/MyReviewPage"; +import SupportPage from './pages/Support/SupportPage'; const AppRouter = () => { return ( } /> } /> + }/> } /> } /> } /> + } /> } /> } /> - } /> - } /> + } /> + } /> + } /> ); }; diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..7d1253b Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/components/common/Accordion.jsx b/src/components/common/Accordion.jsx new file mode 100644 index 0000000..59b3c9b --- /dev/null +++ b/src/components/common/Accordion.jsx @@ -0,0 +1,57 @@ +import React from "react"; +import styled from "styled-components"; +import { IoIosArrowDown } from "react-icons/io"; + +const Accordion = ({ title, children, isOpen, onToggle }) => { + return ( + + +
Q.

{title}

+ +
+ {isOpen && {children}} +
+ ); +}; + +export default Accordion; + +const AccordionContainer = styled.div` + border-bottom: 1px solid #ddd; + font-size: 16px; + + &:first-of-type { + border-top: 1px solid #ddd; /* 첫 번째 박스만 위쪽 선 표시 */ + } +`; + +const AccordionHeader = styled.div` + background-color: #ffffff; + padding: 22px 15px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + color: #000000; + + div { + display: flex; + gap: 8px; + } + + span { + font-weight: 600; + color: ${(props) => (props.isOpen ? "#D90000" : "#000000")}; + } +`; + +const Arrow = styled.span` + transform: rotate(${(props) => (props.isOpen ? "180deg" : "0deg")}); + transition: transform 0.3s ease; +`; + +const AccordionContent = styled.div` + padding: 22px 42px; + background-color: #f9f9f9; + border-top: 1px solid #ddd; +`; \ No newline at end of file diff --git a/src/components/common/AccordionTable.jsx b/src/components/common/AccordionTable.jsx new file mode 100644 index 0000000..28888a6 --- /dev/null +++ b/src/components/common/AccordionTable.jsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; + +const AccordionTable = ({ data }) => { + const [openIndex, setOpenIndex] = useState(null); // 열려 있는 행의 인덱스 + + const toggleAccordion = (index) => { + const selectedItem = data[index]; + if (!selectedItem.response) { + // 답변이 없는 경우 alert 창 표시 + alert('문의 답변은 2-3일 정도 소요됩니다. 조금만 기다려 주세요!'); + return; + } + // 답변이 있는 경우 아코디언 열기 + setOpenIndex(openIndex === index ? null : index); + }; + + return ( + + + + + + + + + + + + + {data.map((item, index) => ( + + toggleAccordion(index)}> + + + + + + + {openIndex === index && ( + + + + )} + + ))} + +
문의일문의 유형문의 제목처리 상태답변일
{item.date}{item.type}{item.title}{item.status}{item.answerDate !== '-' ? item.answerDate : '-'}
+ {item.response} +
+
+ ); +}; + + +export default AccordionTable; + +const TableContainer = styled.div` + width: 100%; + overflow-x: auto; +`; + +const Table = styled.table` + width: 100%; + border-collapse: collapse; + margin: 5px 0; + font-size: 16px; + + th, + td { + border: 1px solid #ddd; + text-align: left; + padding: 8px; + } + + th { + background-color: #f4f4f4; + } + + tr { + cursor: pointer; + } + + tr:hover { + background-color: #f9f9f9; + } +`; + +const AccordionContent = styled.div` + padding: 10px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; +`; \ No newline at end of file diff --git a/src/components/common/DropDown.jsx b/src/components/common/DropDown.jsx index a8e72eb..62f1e57 100644 --- a/src/components/common/DropDown.jsx +++ b/src/components/common/DropDown.jsx @@ -1,10 +1,10 @@ +/*eslint-disable*/ import React, { useState } from 'react'; import styled from 'styled-components'; import KeyboardArrowDown from '../../assets/icons/keyboard_arrow_down.svg'; -const Dropdown = ({ label, options, isActive, onToggle, onSelect }) => { +const Dropdown = ({ label, options, isActive, onToggle, onSelect,selectedOption }) => { const [isOpen, setIsOpen] = useState(isActive); - const [selectedOption, setSelectedOption] = useState(label); const toggleDropdown = () => { setIsOpen(!isOpen); @@ -12,7 +12,6 @@ const Dropdown = ({ label, options, isActive, onToggle, onSelect }) => { }; const handleSelect = (option) => { - setSelectedOption(option); onSelect(option); setIsOpen(false); }; @@ -20,7 +19,7 @@ const Dropdown = ({ label, options, isActive, onToggle, onSelect }) => { return ( - {selectedOption} + {selectedOption||label} {isOpen && ( @@ -96,4 +95,4 @@ const DropdownItem = styled.div` } `; -export default Dropdown; +export default Dropdown; \ No newline at end of file diff --git a/src/components/common/ReviewForm.jsx b/src/components/common/ReviewForm.jsx index bb9ca64..e9358b7 100644 --- a/src/components/common/ReviewForm.jsx +++ b/src/components/common/ReviewForm.jsx @@ -1,73 +1,97 @@ -import React, { useState,useEffect } from "react"; +import React, { useState, useEffect } from "react"; import styled from "styled-components"; import StarRating from "./StarRating"; +import { useReviewInfo } from "../../contexts/useReviewInfo"; -const mockMappingData = { - "가게1": ["홍길동", "박지훈", "장미숙", "홍유진", "최수빈", "김서연"], - "가게2": ["김민수", "최유나", "김하늘", "오지훈", "이수연", "박진영"], - "가게3": ["이영희", "정수빈", "박성민", "김지수", "박준호"], -}; - -const ReviewForm = ({ onClose, addReview,initialData }) => { - const [review,setReview]=useState(initialData ||{}); +const ReviewForm = ({ onClose, initialData }) => { + const isEditing=initialData; + const [reviewId,setReviewId]=useState(Date.now()); const [starPoint, setStarPoint] = useState(0); const [content, setContent] = useState(""); + const [reviewCount,setReviewCount]=useState(1); + const [reviewDate,setReviewDate]=useState(new Date()); const [selectedStore, setSelectedStore] = useState(""); const [selectedAlba, setSelectedAlba] = useState(""); const [selectedTag, setSelectedTag] = useState([]); - const [selectedRCount,setSelectedRCount]=useState(0); - const [reviewTag,setReviewTag]=useState([ - '일을 잘해요', - '시간 엄수를 잘해요', - '일이 서툴러요', - '근무시간을 못 지켰어요', - '성실해요', - '꼼꼼해요', - '신뢰가 가요', - '또 같이 일하고 싶어요']); + const [reviewTag] = useState([ + "일을 잘해요", + "시간 엄수를 잘해요", + "일이 서툴러요", + "근무시간을 못 지켰어요", + "성실해요", + "꼼꼼해요", + "신뢰가 가요", + "또 같이 일하고 싶어요" + ]); + + const {addReview,editReview}=useReviewInfo(); + + // 더미 가게 및 알바 목록 + const storeData = { + "크리스피 크림도넛 경성대점": ["홍길동", "유재석", "하하"], + "할리스커피 부경대점": ["김철수", "이영희", "신동엽"], + "GS25 대연점": ["박민수", "강호동"] + }; useEffect(() => { if (initialData) { - setReview(initialData); + setReviewId(initialData.id||Date.now()); setStarPoint(initialData.starPoint || 0); setContent(initialData.content || ""); + setReviewCount(initialData.reviewCount||1); + setReviewDate(initialData.date||""); setSelectedStore(initialData.storeID || ""); setSelectedAlba(initialData.albaID || ""); setSelectedTag(initialData.tags || []); } }, [initialData]); - const handleSubmit = () => { const newReview = { - id: Date.now(), + id: reviewId, storeID: selectedStore, albaID: selectedAlba, starPoint: starPoint, - reviewCount: selectedRCount+1, - date: new Date().toISOString().split("T")[0], + reviewCount: reviewCount, + date:reviewDate, content: content, - tags: selectedTag, // 태그 추가 + tags: selectedTag, }; + + if (!selectedStore || !selectedAlba || !starPoint || !content) { + let missingFields = []; + + if (!selectedStore) missingFields.push("가게"); + if (!selectedAlba) missingFields.push("알바생"); + if (!starPoint) missingFields.push("평점"); + if (!content) missingFields.push("한줄평"); - addReview(newReview); // 부모 컴포넌트에 리뷰 전달 - onClose(); // 모달 닫기 + alert(`${missingFields.join(", ")}을(를) 입력해 주세요.`); + return; // 입력 받지 않은 항목에 대해 alert 메시지 + } + + if (initialData) { + editReview(newReview); + } else { + addReview(newReview); + } + + onClose(); }; const toggleTagSelection = (tag) => { if (selectedTag.includes(tag)) { - setSelectedTag((prev) => prev.filter((t) => t !== tag)); // 태그 해제 + setSelectedTag((prev) => prev.filter((t) => t !== tag)); } else { - setSelectedTag((prev) => [...prev, tag]); // 태그 선택 + setSelectedTag((prev) => [...prev, tag]); } }; const handleStoreChange = (e) => { const store = e.target.value; setSelectedStore(store); - setSelectedAlba(""); // 가게 변경 시, 알바 선택 초기화 + setSelectedAlba(""); // 가게 변경 시 알바 리스트 새로 불러오기 }; - return ( @@ -78,29 +102,34 @@ const ReviewForm = ({ onClose, addReview,initialData }) => { - - - - - + {Object.keys(storeData).map((store) => ( + + ))} - +