diff --git a/package-lock.json b/package-lock.json index 4eac7311..9a967c67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2040,31 +2040,12 @@ "url": "https://opencollective.com/eslint" } }, -<<<<<<< HEAD "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", -======= - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", ->>>>>>> 66f3ba6e6e7435aa827db8a2a9ce4b80c9b9d177 "dependencies": { "estraverse": "^5.1.0" }, @@ -2076,11 +2057,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", -<<<<<<< HEAD "dev": true, "license": "BSD-2-Clause", -======= ->>>>>>> 66f3ba6e6e7435aa827db8a2a9ce4b80c9b9d177 "dependencies": { "estraverse": "^5.2.0" }, @@ -3298,4 +3276,4 @@ } } } - +} diff --git a/src/App.jsx b/src/App.jsx index ed3f3226..92608742 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,48 +1,61 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import GlobalStyle from './styles/GlobalStyle'; import { ThemeProvider } from 'styled-components'; +import { AuthProvider } from './context/AuthContext'; import theme from './styles/theme'; import Header from './components/Layout/Header'; import HomePage from './components/pages/HomePage'; import LoginPage from './components/pages/LoginPage'; +import SignupPage from './components/pages/SignupPage'; import MarketPage from './components/pages/MarketPage/MarketPage'; import AddItemPage from './components/pages/AddItemPage/AddItemPage'; import CommunityFeedPage from './components/pages/CommunityFeedPage'; +import ItemDetailPage from './components/pages/ItemDetailPage/ItemDetailPage'; function App() { return ( <> - - - -
+ + + + +
-
- - } - /> - } - /> - } - /> - } - /> - } - /> - -
- - +
+ + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + +
+ + + ); } diff --git a/src/api/api.js b/src/api/api.js index 7a7b0aec..3b72e59f 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -13,3 +13,112 @@ export const getProducts = async ({ page = 1, pageSize = 10, orderBy = 'recent', throw error; } }; + +export const getProductDetail = async (productId) => { + try { + const res = await axios.get(`${baseURL}/products/${productId}`); + return res.data; + } catch (error) { + console.log('상품 상세 정보 api 호출 실패 :', error.message); + throw error; + } +}; + +export const postProduct = async (product) => { + try { + const token = localStorage.getItem('token'); + const res = await axios.post(`${baseURL}/products`, product, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + return res.data; + } catch (error) { + console.log('상품 등록 api 호출 실패 :', error.message); + throw error; + } +}; + +export const postComment = async (productId, content) => { + try { + const token = localStorage.getItem('token'); + const res = await axios.post( + `${baseURL}/products/${productId}/comments`, + { content: content }, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ); + return res.data; + } catch (error) { + console.log('상품 댓글 등록 api 호출 실패 :', error.message); + throw error; + } +}; + +export const getComments = async (productId, limit = 10, cursor = null) => { + try { + const res = await axios.get(`${baseURL}/products/${productId}/comments`, { + params: { + limit, + cursor, + }, + }); + return res.data; + } catch (error) { + console.log('상품 댓글 목록 api 호출 실패 :', error.message); + throw error; + } +}; + +export const patchComment = async (commentId, content) => { + try { + const res = await axios.patch(`${baseURL}/comments/${commentId}`, { content: content }); + return res.data; + } catch (error) { + console.log('상품 댓글 수정 api 호출 실패 :', error.message); + throw error; + } +}; + +export const deleteComment = async (commentId) => { + try { + const res = await axios.delete(`${baseURL}/comments/${commentId}`); + return res.data; + } catch (error) { + console.log('상품 댓글 삭제 api 호출 실패 :', error.message); + throw error; + } +}; + +export const postSignup = async (email, password, nickname, passwordConfirmation) => { + try { + const res = await axios.post(`${baseURL}/auth/signup`, { + email, + password, + nickname, + passwordConfirmation, + }); + return res.data; + } catch (error) { + console.log('회원가입 api 호출 실패 :', error.message); + throw error; + } +}; + +export const postLogin = async (email, password) => { + try { + const res = await axios.post(`${baseURL}/auth/signin`, { + email, + password, + }); + return res.data; + } catch (error) { + console.log('로그인 api 호출 실패 :', error.message); + throw error; + } +}; diff --git a/src/assets/images/icons/ic_back.png b/src/assets/images/icons/ic_back.png new file mode 100644 index 00000000..6c5cbec2 Binary files /dev/null and b/src/assets/images/icons/ic_back.png differ diff --git a/src/assets/images/icons/ic_google.png b/src/assets/images/icons/ic_google.png new file mode 100644 index 00000000..53575dc0 Binary files /dev/null and b/src/assets/images/icons/ic_google.png differ diff --git a/src/assets/images/icons/ic_kakao.png b/src/assets/images/icons/ic_kakao.png new file mode 100644 index 00000000..000d07e3 Binary files /dev/null and b/src/assets/images/icons/ic_kakao.png differ diff --git a/src/assets/images/icons/ic_kebab.svg b/src/assets/images/icons/ic_kebab.svg new file mode 100644 index 00000000..dd7ed7f5 --- /dev/null +++ b/src/assets/images/icons/ic_kebab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/icons/ic_nocomment.png b/src/assets/images/icons/ic_nocomment.png new file mode 100644 index 00000000..25019b5a Binary files /dev/null and b/src/assets/images/icons/ic_nocomment.png differ diff --git a/src/assets/images/icons/ic_visibility_off.png b/src/assets/images/icons/ic_visibility_off.png new file mode 100644 index 00000000..8e80ce22 Binary files /dev/null and b/src/assets/images/icons/ic_visibility_off.png differ diff --git a/src/assets/images/icons/ic_visibility_on.png b/src/assets/images/icons/ic_visibility_on.png new file mode 100644 index 00000000..9920a1ef Binary files /dev/null and b/src/assets/images/icons/ic_visibility_on.png differ diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx index 97abc131..42e44160 100644 --- a/src/components/Layout/Header.jsx +++ b/src/components/Layout/Header.jsx @@ -3,61 +3,75 @@ import styled from 'styled-components'; import { FontTypes, ColorTypes } from '../../styles/theme'; import { applyFontStyles } from '../../styles/mixins'; +import { useAuth } from '../../context/AuthContext'; import logo from '../../assets/images/logo/logo.svg'; import textLogo from '../../assets/images/logo/textlogo.svg'; import profile from '../../assets/images/icons/ic_profile.png'; function Header() { + const { isAuthenticated, logout } = useAuth(); const location = useLocation(); const isMarketActive = location.pathname === '/items' || location.pathname === '/additem'; return ( - - + + - - - - - - 로그인 - - + + + {isAuthenticated ? ( + + 로그아웃 + + 마이페이지 + + + ) : ( + + 로그인 + + )} + ); } export default Header; -const HeaderContainer = styled.header` +const StyledHeaderContainer = styled.header` display: flex; justify-content: space-between; align-items: center; @@ -74,20 +88,20 @@ const HeaderContainer = styled.header` } `; -const HeaderLeft = styled.div` +const StyledHeaderLeft = styled.div` display: flex; align-items: center; gap: 10px; `; -const Ul = styled.ul` +const StyledUl = styled.ul` display: flex; align-items: center; gap: 8px; margin-top: 4px; `; -const Li = styled.li` +const StyledLi = styled.li` ${applyFontStyles(FontTypes.BOLD16, ColorTypes.SECONDARY_GRAY_600)}; &:hover { @@ -95,7 +109,7 @@ const Li = styled.li` } `; -const StNavLink = styled(NavLink)` +const StyledNavLink = styled(NavLink)` color: ${({ $isActive, theme }) => $isActive ? theme.colors[ColorTypes.PRIMARY_100] : theme.colors[ColorTypes.SECONDARY_GRAY_600]}; @@ -104,7 +118,7 @@ const StNavLink = styled(NavLink)` } `; -const TextLogo = styled.img` +const StyledTextLogo = styled.img` width: 81px; display: none; @@ -113,7 +127,7 @@ const TextLogo = styled.img` } `; -const Imglogo = styled.img` +const StyledImglogo = styled.img` width: 153px; display: block; @@ -121,3 +135,16 @@ const Imglogo = styled.img` display: none; } `; + +const StyledLogoutButtonContainer = styled.div` + display: flex; + align-items: center; + gap: 16px; +`; + +const StyledLogoutButton = styled.button` + padding: 10px 20px; + border-radius: 40px; + background-color: ${({ theme }) => theme.colors[ColorTypes.PRIMARY_100]}; + ${applyFontStyles(FontTypes.SEMIBOLD14, ColorTypes.SECONDARY_WHITE)}; +`; diff --git a/src/components/UI/CommentEditList.jsx b/src/components/UI/CommentEditList.jsx new file mode 100644 index 00000000..6e465ac7 --- /dev/null +++ b/src/components/UI/CommentEditList.jsx @@ -0,0 +1,80 @@ +import styled from 'styled-components'; +import { useState } from 'react'; + +import kebab from '../../assets/images/icons/ic_kebab.svg'; +import { FontTypes, ColorTypes } from '../../styles/theme'; +import { applyFontStyles } from '../../styles/mixins'; + +function CommentEditList({ onEditClick, commentId, onDeleteClick }) { + const [isOpen, setIsOpen] = useState(false); + + const handleEdit = () => { + onEditClick(commentId); + setIsOpen(false); + }; + + const handleDelete = () => { + onDeleteClick(commentId); + setIsOpen(false); + }; + + return ( + + setIsOpen((prev) => !prev)}> + kebab + + + {isOpen && ( + + 수정하기 + 삭제하기 + + )} + + ); +} + +export default CommentEditList; + +const StyledContainer = styled.div` + position: relative; + display: flex; + flex-direction: column; + gap: 10px; +`; + +const StyledEditButton = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +`; + +const StyledEditList = styled.ul` + position: absolute; + top: 34px; + right: 0px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + + width: 138px; + height: 96px; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.colors[ColorTypes.SECONDARY_GRAY_200]}; + background-color: #ffffff; + z-index: 1; +`; + +const StyledEditItem = styled.li` + display: flex; + cursor: pointer; + + ${applyFontStyles(FontTypes.REGULAR16, ColorTypes.SECONDARY_GRAY_500)} +`; diff --git a/src/components/UI/ImageUpload.jsx b/src/components/UI/ImageUpload.jsx index dd1fb785..26c9d349 100644 --- a/src/components/UI/ImageUpload.jsx +++ b/src/components/UI/ImageUpload.jsx @@ -15,20 +15,21 @@ function ImageUpload() { const file = e.target.files?.[0]; if (!file) return; - const preview = URL.createObjectURL(file); - setPreviewUrl(preview); + if (file) { + const preview = URL.createObjectURL(file); + setPreviewUrl(preview); - if (previewUrl) { - setError('*이미지 등록은 최대 1개까지 가능합니다.'); + if (previewUrl) { + setError('*이미지 등록은 최대 1개까지 가능합니다.'); + } e.target.value = ''; return; } - - setError(''); }; const handleImageRemove = () => { setPreviewUrl(null); + setError(''); }; useEffect(() => { @@ -40,19 +41,19 @@ function ImageUpload() { }, [previewUrl]); return ( - + - - + - - + + plus 이미지 등록 - - + + {previewUrl && ( - - + - - + )} - + - {error && {error}} - + {error && {error}} + ); } export default ImageUpload; -const Container = styled.div` +const StyledContainer = styled.div` display: flex; flex-direction: column; gap: 16px; `; -const Wrapper = styled.div` +const StyledWrapper = styled.div` position: relative; display: flex; gap: 24px; `; -const ImageWrapper = styled.div` +const StyledImageWrapper = styled.div` display: flex; `; -const StInput = styled.input` +const StyledInput = styled.input` position: absolute; opacity: 0; width: 0; @@ -111,7 +112,7 @@ const StInput = styled.input` overflow: hidden; `; -const StLabel = styled.label` +const StyledLabel = styled.label` display: flex; align-items: center; justify-content: center; @@ -131,22 +132,22 @@ const StLabel = styled.label` } `; -const PreviewImage = styled.div` +const StyledPreviewImage = styled.div` position: relative; `; -const StImage = styled.img` +const StyledImage = styled.img` width: 168px; height: 168px; border-radius: 12px; `; -const StXIcon = styled.img` +const StyledXIcon = styled.img` position: absolute; top: 14px; right: 13px; `; -const ErrorMessage = styled.span` +const StyledErrorMessage = styled.span` ${applyFontStyles(FontTypes.REGULAR16, ColorTypes.ERROR)} `; diff --git a/src/components/UI/InputField.jsx b/src/components/UI/InputField.jsx index 648eb88e..28d9b71c 100644 --- a/src/components/UI/InputField.jsx +++ b/src/components/UI/InputField.jsx @@ -1,35 +1,109 @@ import styled from 'styled-components'; +import { useState } from 'react'; +import closeEye from '../../assets/images/icons/ic_visibility_off.png'; +import openEye from '../../assets/images/icons/ic_visibility_on.png'; +import { applyFontStyles } from '../../styles/mixins'; +import { ColorTypes, FontTypes } from '../../styles/theme'; -function InputField({ label, type, placeholder, isTextArea, value, onChange }) { - return isTextArea ? ( - - -