diff --git a/src/api/my/createUUID.js b/src/api/my/createUUID.js new file mode 100644 index 0000000000000000000000000000000000000000..545c6be4ea8580045942a59a6d36e19ca215eb02 --- /dev/null +++ b/src/api/my/createUUID.js @@ -0,0 +1,12 @@ +import api from "../axios"; + +const createUUID = async (pcId) => { + try { + const response = await api.post(`/my/pc/${pcId}/uuid`); + return response.data.data.uuid.uuid; + } catch (error) { + throw error; + } +}; + +export default createUUID; \ No newline at end of file diff --git a/src/api/parts/getCombinationByUUID.js b/src/api/parts/getCombinationByUUID.js new file mode 100644 index 0000000000000000000000000000000000000000..48d885aa71b86608f777e5941d819d181e6f77c2 --- /dev/null +++ b/src/api/parts/getCombinationByUUID.js @@ -0,0 +1,12 @@ +import api from "../axios"; + +const getCombinationByUUID = async (uuid) => { + try { + const response = await api.get(`/parts/combination/by-uuid/${uuid}`); + return response.data.data; + } catch (error) { + throw error; + } +}; + +export default getCombinationByUUID; \ No newline at end of file diff --git a/src/pages/MyCombinationPage/MyCombinationPage.css b/src/pages/MyCombinationPage/MyCombinationPage.css index 3fa221b6e78a4a4d427415e491d1d73b13c64a2c..2d961c96438a38418b427c124ae0e269902abe9b 100644 --- a/src/pages/MyCombinationPage/MyCombinationPage.css +++ b/src/pages/MyCombinationPage/MyCombinationPage.css @@ -7,6 +7,8 @@ } .sidebar { + position: sticky; + top: 2rem; flex: 1; max-width: 300px; padding: 1.5rem; @@ -195,16 +197,17 @@ align-items: center; padding: 1.5rem; border-radius: 12px; - background-color: var(--background-white); + background: linear-gradient(135deg, var(--background-white) 0%, rgba(var(--primary-rgb), 0.03) 100%); box-shadow: var(--shadow-sm); transition: all 0.3s ease; - border: 1px solid transparent; + border: 1px solid rgba(var(--primary-rgb), 0.08); } .part-item:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); - border-color: var(--primary-light); + background: linear-gradient(135deg, var(--background-white) 0%, rgba(var(--primary-rgb), 0.07) 100%); + border-color: rgba(var(--primary-rgb), 0.15); } .part-image { @@ -261,6 +264,7 @@ } .sidebar { + position: static; max-width: 100%; } @@ -307,3 +311,118 @@ transform: none; box-shadow: none; } + +.part-list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + margin-bottom: 1.5rem; + background: linear-gradient(135deg, var(--background-white) 0%, var(--primary-light) 100%); + border-radius: 12px; + box-shadow: var(--shadow-sm); + border: 1px solid rgba(var(--primary-rgb), 0.1); + position: relative; + overflow: hidden; +} + +/* 배경에 부드러운 장식 효과 추가 */ +.part-list-header::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 200px; + height: 200px; + background: radial-gradient(circle, rgba(var(--primary-rgb), 0.1) 0%, transparent 70%); + transform: translate(30%, -30%); +} + +.part-list-header h2 { + margin: 0; + font-size: 1.8rem; + font-weight: 600; + color: var(--text-primary); + position: relative; /* 텍스트를 장식 위에 표시 */ +} + +.share-button { + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark, #0056b3) 100%); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1.1rem; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 4px 15px rgba(var(--primary-rgb), 0.2); + position: relative; + overflow: hidden; +} + +/* 버튼에 빛나는 효과 추가 */ +.share-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 120deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent + ); + transition: 0.5s; +} + +.share-button:hover { + background: linear-gradient(135deg, var(--primary-dark, #0056b3) 0%, var(--primary-color) 100%); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.3); +} + +.share-button:hover::before { + left: 100%; +} + +.share-button:active { + transform: translateY(0); +} + +/* 부품 아이템 카드도 파스텔톤으로 개선 */ +.part-item { + display: flex; + align-items: center; + padding: 1.5rem; + border-radius: 12px; + background: linear-gradient(135deg, var(--background-white) 0%, rgba(var(--primary-rgb), 0.03) 100%); + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; + border: 1px solid rgba(var(--primary-rgb), 0.08); +} + +.part-item:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + background: linear-gradient(135deg, var(--background-white) 0%, rgba(var(--primary-rgb), 0.07) 100%); + border-color: rgba(var(--primary-rgb), 0.15); +} + +/* 모바일 대응 */ +@media (max-width: 768px) { + .part-list-header { + padding: 1.2rem; + } + + .part-list-header h2 { + font-size: 1.5rem; + } + + .share-button { + padding: 0.5rem 1rem; + font-size: 1rem; + } +} diff --git a/src/pages/MyCombinationPage/MyCombinationPage.jsx b/src/pages/MyCombinationPage/MyCombinationPage.jsx index b6507f7942de453cf22503c07cc607a74dd04eb9..0d12dcf41059cf0a9a48d3dfd4d34339bb2f44e8 100644 --- a/src/pages/MyCombinationPage/MyCombinationPage.jsx +++ b/src/pages/MyCombinationPage/MyCombinationPage.jsx @@ -7,6 +7,7 @@ import PartItem from "@/components/PartItem"; import updatePCName from "@/api/my/updatePCName"; import deletePC from "@/api/my/deletePC"; import PCList from "@/components/PCList"; +import createUUID from "@/api/my/createUUID"; const CertifiedCombination = () => { const [pcs, setPcs] = useState([]); @@ -92,6 +93,21 @@ const CertifiedCombination = () => { } }; + const handleShare = async () => { + if (!selectedPc) return; + + try { + const uuid = await createUUID(selectedPc.id); + const shareUrl = `${window.location.origin}/shared?uuid=${uuid}`; + + await navigator.clipboard.writeText(shareUrl); + alert('공유 링크가 클립보드에 복사되었습니다.'); + } catch (error) { + console.error("공유 링크 생성 중 오류 발생:", error); + alert("공유 링크 생성에 실패했습니다."); + } + }; + return ( <div className="layout"> <PCList @@ -103,6 +119,17 @@ const CertifiedCombination = () => { onAddPC={handleAddPcClick} /> <main className={`part-list ${isPartsLoading ? 'loading' : ''}`}> + {selectedPc && ( + <div className="part-list-header"> + <h2>{selectedPc.name}</h2> + <button + className="share-button" + onClick={handleShare} + > + 공유하기 + </button> + </div> + )} {partsData.map((part, index) => ( <PartItem key={index} part={part} /> ))} diff --git a/src/pages/SearchCombinationPage/components/CombinationBox.jsx b/src/pages/SearchCombinationPage/components/CombinationBox.jsx index 3a5947b41ac74336eec2529f62a2a1b765b09169..79c547908b5a0dd55ef03d6803bc15c5c334363f 100644 --- a/src/pages/SearchCombinationPage/components/CombinationBox.jsx +++ b/src/pages/SearchCombinationPage/components/CombinationBox.jsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; import CombinationGrid from "@/components/CombinationGrid/CombinationGrid"; import getPartById from "@/api/parts/getPartById"; -const CombinationBox = ({ title, combination }) => { +const CombinationBox = ({ title, combination, showShareButton = false }) => { const [partDetails, setPartDetails] = useState([]); useEffect(() => { const fetchPartDetails = async () => { @@ -67,9 +67,11 @@ const CombinationBox = ({ title, combination }) => { <div className="combination-box"> <div className="combination-header"> <h1 className="title">{title}</h1> - <button className="share-button" onClick={handleShare}> - 공유하기 - </button> + {showShareButton && ( + <button className="share-button" onClick={handleShare}> + 공유하기 + </button> + )} </div> <CombinationGrid combination={partDetails} /> </div> diff --git a/src/pages/SharedCombinationPage/SharedCombinationPage.css b/src/pages/SharedCombinationPage/SharedCombinationPage.css index e4a4ac725ff86719951bc210b0234667f20f5d8e..dfae220bba72689afa9b38094fabf3cf134f4a74 100644 --- a/src/pages/SharedCombinationPage/SharedCombinationPage.css +++ b/src/pages/SharedCombinationPage/SharedCombinationPage.css @@ -1,48 +1,77 @@ .shared-combination-page { padding: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -.shared-loading, -.shared-error { - text-align: center; - padding: 2rem; - font-size: 1.2rem; + display: flex; + flex-direction: column; + gap: 1.5rem; } -.shared-error { - color: #ff4444; +.shared-info { + background: linear-gradient(135deg, var(--background-white) 0%, rgba(var(--primary-rgb), 0.03) 100%); + padding: 1.5rem 2rem; + border-radius: 12px; + box-shadow: var(--shadow-sm); + border: 1px solid rgba(var(--primary-rgb), 0.08); } -.combination-header { +.shared-info p { + margin: 0.5rem 0; + font-size: 1.1rem; + color: var(--text-primary); display: flex; - justify-content: space-between; align-items: center; - margin-bottom: 1rem; + gap: 0.5rem; } -.share-button { - padding: 0.5rem 1rem; +.shared-info p::before { + content: ''; + width: 6px; + height: 6px; background-color: var(--primary-color); - color: white; - border: 1px solid var(--primary-color); - border-radius: 8px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s ease; + border-radius: 50%; } -.share-button:hover { - background-color: var(--primary-color); - box-shadow: 0 0 0 3px var(--primary-light); - transform: translateY(-1px); +.shared-info p:first-child { + color: var(--primary-color); + font-weight: 600; + font-size: 1.2rem; +} + +.shared-loading { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; + font-size: 1.2rem; + color: var(--text-secondary); +} + +.shared-error { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; + font-size: 1.2rem; + color: var(--error-color); + background-color: var(--background-white); + border-radius: 12px; + padding: 2rem; + box-shadow: var(--shadow-sm); } -.share-button:disabled { - background-color: var(--primary-light); - border-color: var(--primary-light); - cursor: not-allowed; - transform: none; - box-shadow: none; +@media (max-width: 768px) { + .shared-combination-page { + padding: 1rem; + } + + .shared-info { + padding: 1rem 1.5rem; + } + + .shared-info p { + font-size: 1rem; + } + + .shared-info p:first-child { + font-size: 1.1rem; + } } \ No newline at end of file diff --git a/src/pages/SharedCombinationPage/SharedCombinationPage.jsx b/src/pages/SharedCombinationPage/SharedCombinationPage.jsx index 906f5fd2353883f46d24632f03f2a51f2bb363ed..2507a56e1f49e05033dbfb43f56f6b7053855f5b 100644 --- a/src/pages/SharedCombinationPage/SharedCombinationPage.jsx +++ b/src/pages/SharedCombinationPage/SharedCombinationPage.jsx @@ -1,35 +1,39 @@ import React, { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import CombinationBox from "../SearchCombinationPage/components/CombinationBox"; +import getCombinationByUUID from "@/api/parts/getCombinationByUUID"; import getCombination from "@/api/parts/getCombination"; import "./SharedCombinationPage.css"; const SharedCombinationPage = () => { const [searchParams] = useSearchParams(); const [combination, setCombination] = useState(null); + const [sharedInfo, setSharedInfo] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchSharedCombination = async () => { try { - const partsParam = searchParams.get('parts'); - if (!partsParam) { - throw new Error('부품 정보가 없습니다.'); + const uuid = searchParams.get('uuid'); + if (!uuid) { + throw new Error('공유 정보가 없습니다.'); } - const partIds = partsParam.split(','); - const data = await getCombination({ - cpuId: partIds[0], - gpuId: partIds[1], - mbId: partIds[2], - ramId: partIds[3], - ssdId: partIds[4], - hddId: partIds[5], + const sharedData = await getCombinationByUUID(uuid); + setSharedInfo(sharedData); + + const combinationData = await getCombination({ + cpuId: sharedData.parts[0], + gpuId: sharedData.parts[1], + mbId: sharedData.parts[2], + ramId: sharedData.parts[3], + ssdId: sharedData.parts[4], + hddId: sharedData.parts[5], }); - if (data && data.length > 0) { - setCombination(data[0]); + if (combinationData && combinationData.length > 0) { + setCombination(combinationData[0]); } else { throw new Error('조합을 찾을 수 없습니다.'); } @@ -53,11 +57,18 @@ const SharedCombinationPage = () => { return ( <div className="shared-combination-page"> - {combination && ( - <CombinationBox - title="공유된 조합" - combination={combination} - /> + {combination && sharedInfo && ( + <> + <div className="shared-info"> + <p>공유자: {sharedInfo.email}</p> + <p>등록일: {new Date(sharedInfo.created_at).toLocaleDateString()}</p> + <p>공유일: {new Date(sharedInfo.verified_at).toLocaleDateString()}</p> + </div> + <CombinationBox + title="공유된 조합" + combination={combination} + /> + </> )} </div> );