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

Merge branch 'feature/share' into 'main'

Feature/share

See merge request !36
parents 12ee4894 5eb8a114
Branches
No related tags found
1 merge request!36Feature/share
Pipeline #10784 passed
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 = [
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 @@
}
.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;
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;
background-color: var(--background-light);
color: var(--text-primary);
font-size: 14px;
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;
cursor: pointer;
transition: all 0.3s ease;
}
.pc-item::before {
content: none;
.edit-buttons button:first-child {
background-color: var(--primary-color);
color: white;
border: 1px solid var(--primary-color);
}
.pc-item:hover {
background-color: var(--background-light);
color: var(--primary-color);
.edit-buttons button:first-child:hover {
background-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);
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);
border: 1px solid var(--primary-color);
border-radius: 8px;
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 {
......@@ -86,6 +182,12 @@
display: flex;
flex-direction: column;
gap: 1rem;
transition: opacity 0.3s ease;
opacity: 1;
}
.part-list.loading {
opacity: 0.5;
}
.part-item {
......
......@@ -4,12 +4,18 @@ import getPartById from "@/api/parts/getPartById";
import { Link, useNavigate } from "react-router-dom";
import './MyCombinationPage.css';
import PartItem from "@/components/PartItem";
import updatePCName from "@/api/my/updatePCName";
const CertifiedCombination = () => {
const [pcs, setPcs] = useState([]);
const [selectedPc, setSelectedPc] = useState(null);
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 [isPartsLoading, setIsPartsLoading] = useState(false);
useEffect(() => {
const fetchPCs = async () => {
......@@ -30,6 +36,7 @@ const CertifiedCombination = () => {
useEffect(() => {
const fetchParts = async () => {
if (selectedPc && !selectedPc.parts.some(part => part.partType === "오류")) {
setIsPartsLoading(true);
const parts = await Promise.all(
selectedPc.parts.map(async (partId) => {
try {
......@@ -46,11 +53,13 @@ const CertifiedCombination = () => {
})
);
setPartsData(parts.filter((part) => part !== null));
setIsPartsLoading(false);
}
};
fetchParts();
}, [selectedPc]);
const handlePcClick = (pcId) => {
const selected = pcs.find((pc) => pc.id === pcId);
setSelectedPc(selected);
......@@ -60,6 +69,44 @@ const CertifiedCombination = () => {
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 (
<div className="layout">
<aside className="sidebar">
......@@ -68,10 +115,54 @@ const CertifiedCombination = () => {
{pcs.map((pc) => (
<li
key={pc.id}
onClick={() => handlePcClick(pc.id)}
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>
))}
</ul>
......@@ -81,7 +172,7 @@ const CertifiedCombination = () => {
</button>
</aside>
<main className="part-list">
<main className={`part-list ${isPartsLoading ? 'loading' : ''}`}>
{partsData.map((part, index) => (
<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: var(--primary-color);
color: white;
border: 1px solid var(--primary-color);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.share-button:hover {
background-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--primary-light);
transform: translateY(-1px);
}
.share-button:disabled {
background-color: var(--primary-light);
border-color: var(--primary-light);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.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 CombinationGrid from "@/components/CombinationGrid/CombinationGrid";
import getPartById from "@/api/parts/getPartById";
......@@ -39,9 +40,28 @@ const CombinationBox = ({ title, combination }) => {
fetchPartDetails();
}, [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 (
<div>
<div className="combination-box">
<div className="combination-header">
<h1 className="title">{title}</h1>
<button className="share-button" onClick={handleShare}>
공유하기
</button>
</div>
<CombinationGrid combination={partDetails} />
</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: var(--primary-color);
color: white;
border: 1px solid var(--primary-color);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.share-button:hover {
background-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--primary-light);
transform: translateY(-1px);
}
.share-button:disabled {
background-color: var(--primary-light);
border-color: var(--primary-light);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
\ 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