Skip to content
Snippets Groups Projects
Commit 371489fb authored by 정원제's avatar 정원제 :guitar:
Browse files

feat: share 기능 프로토타입 + mypc rename 기능 추가

parent 12ee4894
Branches main
No related tags found
1 merge request!36Feature/share
import axios from '../axios';
const updatePCName = async (pcId, newName) => {
try {
// 입력값 검증
if (!newName || !newName.trim()) {
throw {
response: {
data: {
message: "잘못된 요청입니다",
statusCode: 400,
data: {
error: "이름은 필수 입력값입니다"
}
}
}
};
}
const response = await axios.patch(`/my/pc/${pcId}/name`, {
name: newName.trim()
});
// 성공 응답 형식에 맞춤
return {
message: "PC 이름이 성공적으로 변경되었습니다",
statusCode: 200,
data: {
id: pcId,
name: newName,
updatedAt: new Date().toISOString()
}
};
} catch (error) {
// 401 Unauthorized 에러 처리
if (error.response?.status === 401) {
throw {
message: "인증되지 않은 요청입니다",
statusCode: 401,
data: {}
};
}
// 400 Bad Request 에러 처리
if (error.response?.status === 400) {
throw {
message: "잘못된 요청입니다",
statusCode: 400,
data: {
error: "이름은 필수 입력값입니다"
}
};
}
// 기타 에러는 그대로 전달
throw error;
}
};
export default updatePCName;
\ No newline at end of file
...@@ -481,6 +481,43 @@ export const handlers = [ ...@@ -481,6 +481,43 @@ export const handlers = [
return new HttpResponse(null, { status: 500 }); return new HttpResponse(null, { status: 500 });
} }
}), }),
// PC 이름 변경
http.patch(`${api}/my/pc/:pcId/name`, async ({ request, params }) => {
const { name } = await request.json();
// 이름이 없거나 빈 문자열인 경우
if (!name || !name.trim()) {
return HttpResponse.json({
message: "잘못된 요청입니다",
statusCode: 400,
data: {
error: "이름은 필수 입력값입니다"
}
}, { status: 400 });
}
// Authorization 헤더가 없는 경우
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
return HttpResponse.json({
message: "인증되지 않은 요청입니다",
statusCode: 401,
data: {}
}, { status: 401 });
}
// 성공 응답
return HttpResponse.json({
message: "PC 이름이 성공적으로 변경되었습니다",
statusCode: 200,
data: {
id: params.pcId,
name: name.trim(),
updatedAt: new Date().toISOString()
}
});
}),
]; ];
// 부품 조회 함수 // 부품 조회 함수
......
...@@ -25,31 +25,127 @@ ...@@ -25,31 +25,127 @@
} }
.pc-item { .pc-item {
position: relative;
padding: 10px;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
}
.pc-item.active {
background-color: var(--primary-light);
border-radius: 8px;
border-bottom: 1px solid var(--primary-light);
}
.pc-item.active .pc-item-content span {
color: var(--primary-color);
font-weight: 500;
}
.pc-item-content {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer; cursor: pointer;
padding: 1rem 1.25rem; padding: 4px;
}
.edit-name-container {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px;
}
.edit-name-container input {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 8px;
background-color: var(--background-light); font-size: 14px;
color: var(--text-primary);
transition: all 0.3s ease; transition: all 0.3s ease;
border: 1px solid transparent; }
.edit-name-container input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.edit-buttons {
display: flex;
gap: 8px;
}
.edit-buttons button {
padding: 6px 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
} }
.pc-item::before { .edit-buttons button:first-child {
content: none; background-color: var(--primary-color);
color: white;
border: 1px solid var(--primary-color);
} }
.pc-item:hover { .edit-buttons button:first-child:hover {
background-color: var(--background-light); background-color: var(--primary-color);
color: var(--primary-color); box-shadow: 0 0 0 3px var(--primary-light);
transform: translateY(-1px);
}
.edit-buttons button:first-child:disabled {
background-color: var(--primary-light);
border-color: var(--primary-light); border-color: var(--primary-light);
transform: translateY(-2px); cursor: not-allowed;
transform: none;
box-shadow: none;
} }
.pc-item.active { .cancel-button {
background-color: var(--background-light);
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.cancel-button:hover {
background-color: var(--background-white);
border-color: var(--text-primary);
transform: translateY(-1px);
}
.cancel-button:disabled {
background-color: var(--background-light);
border-color: var(--border-color);
color: var(--text-secondary);
cursor: not-allowed;
transform: none;
}
.edit-button {
padding: 4px 12px;
font-size: 13px;
background-color: var(--primary-color); background-color: var(--primary-color);
border: 1px solid var(--primary-color);
border-radius: 8px;
color: white; color: white;
box-shadow: var(--shadow-sm); cursor: pointer;
transition: all 0.3s ease;
}
.edit-button:hover {
background-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--primary-light);
transform: translateY(-1px);
}
.error-message {
color: var(--error-color);
font-size: 12px;
margin-top: 4px;
} }
.add-pc-btn { .add-pc-btn {
...@@ -86,6 +182,12 @@ ...@@ -86,6 +182,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
transition: opacity 0.3s ease;
opacity: 1;
}
.part-list.loading {
opacity: 0.5;
} }
.part-item { .part-item {
......
...@@ -4,12 +4,18 @@ import getPartById from "@/api/parts/getPartById"; ...@@ -4,12 +4,18 @@ import getPartById from "@/api/parts/getPartById";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import './MyCombinationPage.css'; import './MyCombinationPage.css';
import PartItem from "@/components/PartItem"; import PartItem from "@/components/PartItem";
import updatePCName from "@/api/my/updatePCName";
const CertifiedCombination = () => { const CertifiedCombination = () => {
const [pcs, setPcs] = useState([]); const [pcs, setPcs] = useState([]);
const [selectedPc, setSelectedPc] = useState(null); const [selectedPc, setSelectedPc] = useState(null);
const [partsData, setPartsData] = useState([]); const [partsData, setPartsData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [editingPcId, setEditingPcId] = useState(null);
const [editingName, setEditingName] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const [isPartsLoading, setIsPartsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const fetchPCs = async () => { const fetchPCs = async () => {
...@@ -30,6 +36,7 @@ const CertifiedCombination = () => { ...@@ -30,6 +36,7 @@ const CertifiedCombination = () => {
useEffect(() => { useEffect(() => {
const fetchParts = async () => { const fetchParts = async () => {
if (selectedPc && !selectedPc.parts.some(part => part.partType === "오류")) { if (selectedPc && !selectedPc.parts.some(part => part.partType === "오류")) {
setIsPartsLoading(true);
const parts = await Promise.all( const parts = await Promise.all(
selectedPc.parts.map(async (partId) => { selectedPc.parts.map(async (partId) => {
try { try {
...@@ -46,11 +53,13 @@ const CertifiedCombination = () => { ...@@ -46,11 +53,13 @@ const CertifiedCombination = () => {
}) })
); );
setPartsData(parts.filter((part) => part !== null)); setPartsData(parts.filter((part) => part !== null));
setIsPartsLoading(false);
} }
}; };
fetchParts(); fetchParts();
}, [selectedPc]); }, [selectedPc]);
const handlePcClick = (pcId) => { const handlePcClick = (pcId) => {
const selected = pcs.find((pc) => pc.id === pcId); const selected = pcs.find((pc) => pc.id === pcId);
setSelectedPc(selected); setSelectedPc(selected);
...@@ -60,6 +69,44 @@ const CertifiedCombination = () => { ...@@ -60,6 +69,44 @@ const CertifiedCombination = () => {
navigate('/partscertification'); navigate('/partscertification');
}; };
const handleEditClick = (pc) => {
setError(null);
setEditingPcId(pc.id);
setEditingName(pc.name);
};
const handleCancel = () => {
setEditingPcId(null);
setError(null);
};
const handleNameSubmit = async (pcId) => {
if (!editingName.trim()) {
setError("PC 이름을 입력해주세요");
return;
}
setIsLoading(true);
setError(null);
try {
await updatePCName(pcId, editingName);
setPcs(pcs.map(pc =>
pc.id === pcId ? { ...pc, name: editingName } : pc
));
setEditingPcId(null);
} catch (error) {
setError("PC 이름 변경 중 오류가 발생했습니다");
console.error("PC 이름 변경 중 오류 발생:", error);
} finally {
setIsLoading(false);
}
};
const handleNameChange = (e) => {
setEditingName(e.target.value);
};
return ( return (
<div className="layout"> <div className="layout">
<aside className="sidebar"> <aside className="sidebar">
...@@ -68,10 +115,54 @@ const CertifiedCombination = () => { ...@@ -68,10 +115,54 @@ const CertifiedCombination = () => {
{pcs.map((pc) => ( {pcs.map((pc) => (
<li <li
key={pc.id} key={pc.id}
onClick={() => handlePcClick(pc.id)}
className={`pc-item ${selectedPc && selectedPc.id === pc.id ? 'active' : ''}`} className={`pc-item ${selectedPc && selectedPc.id === pc.id ? 'active' : ''}`}
> >
{pc.name} {editingPcId === pc.id ? (
<div className="edit-name-container">
<input
type="text"
value={editingName}
onChange={handleNameChange}
onClick={(e) => e.stopPropagation()}
placeholder="PC 이름 입력"
/>
<div className="edit-buttons">
<button
onClick={(e) => {
e.stopPropagation();
handleNameSubmit(pc.id);
}}
disabled={isLoading}
>
{isLoading ? '저장 중...' : '저장'}
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleCancel();
}}
className="cancel-button"
disabled={isLoading}
>
취소
</button>
</div>
{error && <div className="error-message">{error}</div>}
</div>
) : (
<div className="pc-item-content" onClick={() => handlePcClick(pc.id)}>
<span>{pc.name}</span>
<button
className="edit-button"
onClick={(e) => {
e.stopPropagation();
handleEditClick(pc);
}}
>
수정
</button>
</div>
)}
</li> </li>
))} ))}
</ul> </ul>
...@@ -81,7 +172,7 @@ const CertifiedCombination = () => { ...@@ -81,7 +172,7 @@ const CertifiedCombination = () => {
</button> </button>
</aside> </aside>
<main className="part-list"> <main className={`part-list ${isPartsLoading ? 'loading' : ''}`}>
{partsData.map((part, index) => ( {partsData.map((part, index) => (
<PartItem key={index} part={part} /> <PartItem key={index} part={part} />
))} ))}
......
.combination-box {
margin-bottom: 2rem;
}
.combination-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.share-button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.share-button:hover {
background-color: #45a049;
}
.title {
margin: 0;
font-size: 1.5rem;
color: #333;
}
\ No newline at end of file
import "./CombinationBox.css";
import React, { useEffect, useState } from "react"; 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";
...@@ -39,9 +40,28 @@ const CombinationBox = ({ title, combination }) => { ...@@ -39,9 +40,28 @@ const CombinationBox = ({ title, combination }) => {
fetchPartDetails(); fetchPartDetails();
}, [combination]); }, [combination]);
const handleShare = () => {
const partIds = combination.partids || combination.partIds;
const shareUrl = `${window.location.origin}/shared?parts=${partIds.join(',')}`;
navigator.clipboard.writeText(shareUrl)
.then(() => {
alert('공유 링크가 클립보드에 복사되었습니다!');
})
.catch((err) => {
console.error('클립보드 복사 실패:', err);
alert('링크 복사에 실패했습니다.');
});
};
return ( return (
<div> <div className="combination-box">
<div className="combination-header">
<h1 className="title">{title}</h1> <h1 className="title">{title}</h1>
<button className="share-button" onClick={handleShare}>
공유하기
</button>
</div>
<CombinationGrid combination={partDetails} /> <CombinationGrid combination={partDetails} />
</div> </div>
); );
......
.shared-combination-page {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.shared-loading,
.shared-error {
text-align: center;
padding: 2rem;
font-size: 1.2rem;
}
.shared-error {
color: #ff4444;
}
.combination-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.share-button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.share-button:hover {
background-color: #45a049;
}
\ No newline at end of file
import React, { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import CombinationBox from "../SearchCombinationPage/components/CombinationBox";
import getCombination from "@/api/parts/getCombination";
import "./SharedCombinationPage.css";
const SharedCombinationPage = () => {
const [searchParams] = useSearchParams();
const [combination, setCombination] = 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 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],
});
if (data && data.length > 0) {
setCombination(data[0]);
} else {
throw new Error('조합을 찾을 수 없습니다.');
}
} catch (error) {
setError(error.message);
} finally {
setIsLoading(false);
}
};
fetchSharedCombination();
}, [searchParams]);
if (isLoading) {
return <div className="shared-loading">로딩 중...</div>;
}
if (error) {
return <div className="shared-error">{error}</div>;
}
return (
<div className="shared-combination-page">
{combination && (
<CombinationBox
title="공유된 조합"
combination={combination}
/>
)}
</div>
);
};
export default SharedCombinationPage;
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment