Skip to content
Snippets Groups Projects
Commit af4f7269 authored by 민재 조's avatar 민재 조
Browse files

Merge branch 'refactor/share' into 'main'

refactor: uuid를 통해 공유할 수 있도록 변경

See merge request !42
parents f18a1823 9090253f
Branches
No related tags found
1 merge request!42refactor: uuid를 통해 공유할 수 있도록 변경
Pipeline #10838 passed
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
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
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
} }
.sidebar { .sidebar {
position: sticky;
top: 2rem;
flex: 1; flex: 1;
max-width: 300px; max-width: 300px;
padding: 1.5rem; padding: 1.5rem;
...@@ -195,16 +197,17 @@ ...@@ -195,16 +197,17 @@
align-items: center; align-items: center;
padding: 1.5rem; padding: 1.5rem;
border-radius: 12px; 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); box-shadow: var(--shadow-sm);
transition: all 0.3s ease; transition: all 0.3s ease;
border: 1px solid transparent; border: 1px solid rgba(var(--primary-rgb), 0.08);
} }
.part-item:hover { .part-item:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: var(--shadow-md); 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 { .part-image {
...@@ -261,6 +264,7 @@ ...@@ -261,6 +264,7 @@
} }
.sidebar { .sidebar {
position: static;
max-width: 100%; max-width: 100%;
} }
...@@ -307,3 +311,118 @@ ...@@ -307,3 +311,118 @@
transform: none; transform: none;
box-shadow: 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;
}
}
...@@ -7,6 +7,7 @@ import PartItem from "@/components/PartItem"; ...@@ -7,6 +7,7 @@ import PartItem from "@/components/PartItem";
import updatePCName from "@/api/my/updatePCName"; import updatePCName from "@/api/my/updatePCName";
import deletePC from "@/api/my/deletePC"; import deletePC from "@/api/my/deletePC";
import PCList from "@/components/PCList"; import PCList from "@/components/PCList";
import createUUID from "@/api/my/createUUID";
const CertifiedCombination = () => { const CertifiedCombination = () => {
const [pcs, setPcs] = useState([]); const [pcs, setPcs] = useState([]);
...@@ -92,6 +93,21 @@ const CertifiedCombination = () => { ...@@ -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 ( return (
<div className="layout"> <div className="layout">
<PCList <PCList
...@@ -103,6 +119,17 @@ const CertifiedCombination = () => { ...@@ -103,6 +119,17 @@ const CertifiedCombination = () => {
onAddPC={handleAddPcClick} onAddPC={handleAddPcClick}
/> />
<main className={`part-list ${isPartsLoading ? 'loading' : ''}`}> <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) => ( {partsData.map((part, index) => (
<PartItem key={index} part={part} /> <PartItem key={index} part={part} />
))} ))}
......
...@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; ...@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
import CombinationGrid from "@/components/CombinationGrid/CombinationGrid"; import CombinationGrid from "@/components/CombinationGrid/CombinationGrid";
import getPartById from "@/api/parts/getPartById"; import getPartById from "@/api/parts/getPartById";
const CombinationBox = ({ title, combination }) => { const CombinationBox = ({ title, combination, showShareButton = false }) => {
const [partDetails, setPartDetails] = useState([]); const [partDetails, setPartDetails] = useState([]);
useEffect(() => { useEffect(() => {
const fetchPartDetails = async () => { const fetchPartDetails = async () => {
...@@ -67,9 +67,11 @@ const CombinationBox = ({ title, combination }) => { ...@@ -67,9 +67,11 @@ const CombinationBox = ({ title, combination }) => {
<div className="combination-box"> <div className="combination-box">
<div className="combination-header"> <div className="combination-header">
<h1 className="title">{title}</h1> <h1 className="title">{title}</h1>
{showShareButton && (
<button className="share-button" onClick={handleShare}> <button className="share-button" onClick={handleShare}>
공유하기 공유하기
</button> </button>
)}
</div> </div>
<CombinationGrid combination={partDetails} /> <CombinationGrid combination={partDetails} />
</div> </div>
......
.shared-combination-page { .shared-combination-page {
padding: 2rem; padding: 2rem;
max-width: 1200px; display: flex;
margin: 0 auto; flex-direction: column;
gap: 1.5rem;
} }
.shared-loading, .shared-info {
.shared-error { background: linear-gradient(135deg, var(--background-white) 0%, rgba(var(--primary-rgb), 0.03) 100%);
text-align: center; padding: 1.5rem 2rem;
padding: 2rem; border-radius: 12px;
box-shadow: var(--shadow-sm);
border: 1px solid rgba(var(--primary-rgb), 0.08);
}
.shared-info p {
margin: 0.5rem 0;
font-size: 1.1rem;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.shared-info p::before {
content: '';
width: 6px;
height: 6px;
background-color: var(--primary-color);
border-radius: 50%;
}
.shared-info p:first-child {
color: var(--primary-color);
font-weight: 600;
font-size: 1.2rem; font-size: 1.2rem;
} }
.shared-error { .shared-loading {
color: #ff4444; display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
font-size: 1.2rem;
color: var(--text-secondary);
} }
.combination-header { .shared-error {
display: flex; display: flex;
justify-content: space-between; justify-content: center;
align-items: center; align-items: center;
margin-bottom: 1rem; 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 { @media (max-width: 768px) {
padding: 0.5rem 1rem; .shared-combination-page {
background-color: var(--primary-color); padding: 1rem;
color: white;
border: 1px solid var(--primary-color);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
} }
.share-button:hover { .shared-info {
background-color: var(--primary-color); padding: 1rem 1.5rem;
box-shadow: 0 0 0 3px var(--primary-light);
transform: translateY(-1px);
} }
.share-button:disabled { .shared-info p {
background-color: var(--primary-light); font-size: 1rem;
border-color: var(--primary-light); }
cursor: not-allowed;
transform: none; .shared-info p:first-child {
box-shadow: none; font-size: 1.1rem;
}
} }
\ No newline at end of file
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import CombinationBox from "../SearchCombinationPage/components/CombinationBox"; import CombinationBox from "../SearchCombinationPage/components/CombinationBox";
import getCombinationByUUID from "@/api/parts/getCombinationByUUID";
import getCombination from "@/api/parts/getCombination"; import getCombination from "@/api/parts/getCombination";
import "./SharedCombinationPage.css"; import "./SharedCombinationPage.css";
const SharedCombinationPage = () => { const SharedCombinationPage = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [combination, setCombination] = useState(null); const [combination, setCombination] = useState(null);
const [sharedInfo, setSharedInfo] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
const fetchSharedCombination = async () => { const fetchSharedCombination = async () => {
try { try {
const partsParam = searchParams.get('parts'); const uuid = searchParams.get('uuid');
if (!partsParam) { if (!uuid) {
throw new Error('부품 정보가 없습니다.'); throw new Error('공유 정보가 없습니다.');
} }
const partIds = partsParam.split(','); const sharedData = await getCombinationByUUID(uuid);
const data = await getCombination({ setSharedInfo(sharedData);
cpuId: partIds[0],
gpuId: partIds[1], const combinationData = await getCombination({
mbId: partIds[2], cpuId: sharedData.parts[0],
ramId: partIds[3], gpuId: sharedData.parts[1],
ssdId: partIds[4], mbId: sharedData.parts[2],
hddId: partIds[5], ramId: sharedData.parts[3],
ssdId: sharedData.parts[4],
hddId: sharedData.parts[5],
}); });
if (data && data.length > 0) { if (combinationData && combinationData.length > 0) {
setCombination(data[0]); setCombination(combinationData[0]);
} else { } else {
throw new Error('조합을 찾을 수 없습니다.'); throw new Error('조합을 찾을 수 없습니다.');
} }
...@@ -53,11 +57,18 @@ const SharedCombinationPage = () => { ...@@ -53,11 +57,18 @@ const SharedCombinationPage = () => {
return ( return (
<div className="shared-combination-page"> <div className="shared-combination-page">
{combination && ( {combination && sharedInfo && (
<>
<div className="shared-info">
<p>공유자: {sharedInfo.nickname}</p>
<p>등록일: {new Date(sharedInfo.created_at).toLocaleDateString()}</p>
<p>공유일: {new Date(sharedInfo.verified_at).toLocaleDateString()}</p>
</div>
<CombinationBox <CombinationBox
title="공유된 조합" title="공유된 조합"
combination={combination} combination={combination}
/> />
</>
)} )}
</div> </div>
); );
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment