From e321594f35e3cf2139dc53c89c2076488fbcc50b Mon Sep 17 00:00:00 2001 From: chaeyoungwon Date: Sun, 22 Sep 2024 22:19:13 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Add=20Portfolio=20API=20and=20fi?= =?UTF-8?q?x=20Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #233 포트폴리오 api 연결 및 오류 수정 --- .../src/pages/Portfolio/UsePortfolio.jsx | 186 +++++++++--------- .../src/pages/ProfilePage/ProfilePage.jsx | 102 +++++++++- .../pages/ProfilePage/ProfilePageStyled.jsx | 22 ++- gongjakso/src/router/Router.jsx | 5 + gongjakso/src/service/portfolio_service.js | 20 +- 5 files changed, 228 insertions(+), 107 deletions(-) diff --git a/gongjakso/src/pages/Portfolio/UsePortfolio.jsx b/gongjakso/src/pages/Portfolio/UsePortfolio.jsx index 183a04a..806bc2f 100644 --- a/gongjakso/src/pages/Portfolio/UsePortfolio.jsx +++ b/gongjakso/src/pages/Portfolio/UsePortfolio.jsx @@ -1,102 +1,115 @@ import * as S from './Portfolio.Styled'; import { useRef, useState, useEffect } from 'react'; import { getMyInfo } from '../../service/profile_service'; -import { postExistPortfolio } from '../../service/portfolio_service'; -import { useNavigate } from 'react-router-dom'; +import { + getExistPortfolio, + postExistPortfolio, + editExistPortfolio, +} from '../../service/portfolio_service'; +import { useNavigate, useParams } from 'react-router-dom'; const UsePortfolio = () => { const [data, setProfileData] = useState(); const navigate = useNavigate(); + const { id } = useParams(); // 포트폴리오 ID 가져오기 const fileInput = useRef(null); - const [snsLinks, setSnsLinks] = useState([{ id: 1, link: '' }]); + const [isEdit, setIsEdit] = useState(false); + const [snsLink, setSnsLink] = useState(''); // 단일 노션 링크 const [error, setError] = useState(''); // State for error messages - const [files, setFiles] = useState([]); // 여러 파일을 저장하는 배열 + const [file, setFile] = useState(null); // 단일 파일 + const [existingFile, setExistingFile] = useState(null); // 기존 파일 URI 상태 + + useEffect(() => { + // 기존 포트폴리오 데이터를 가져와서 snsLink와 file 상태를 설정 + const fetchPortfolio = async () => { + try { + const response = await getExistPortfolio(id); // 포트폴리오 상세 데이터 가져오기 + const portfolioData = response?.data.data; + + // 기존 노션 링크 및 파일이 있으면 상태에 설정 + if (portfolioData) { + setSnsLink(portfolioData.notionUri || ''); // 노션 링크 설정 + console.log(portfolioData); + if (portfolioData.fileUri) { + const fileName = portfolioData.fileUri.split('/').pop(); + setExistingFile({ + name: fileName, + uri: portfolioData.fileUri, + }); // 파일 이름과 URI 설정 + } + } + } catch (error) {} + }; + + fetchPortfolio(); + getMyInfo().then(response => { + setProfileData(response?.data); + }); + }, [id]); const handleButtonClick = () => { fileInput.current.click(); }; - const addSNSLink = () => { - setSnsLinks([...snsLinks, { id: new Date(), link: '' }]); - }; const handleChange = e => { - const selectedFiles = Array.from(e.target.files); // 선택된 파일들을 배열로 변환 + const selectedFile = e.target.files[0]; // 첫 번째 파일만 선택 const maxSize = 10 * 1024 * 1024; // 10MB 제한 - let totalSize = 0; - selectedFiles.forEach(file => { - totalSize += file.size; - }); - - if (totalSize > maxSize) { + if (selectedFile.size > maxSize) { setError( - '전체 파일 크기가 10MB를 초과했습니다. 파일을 다시 선택해 주세요.', + '파일 크기가 10MB를 초과했습니다. 다른 파일을 선택해 주세요.', ); } else { setError(''); - - // 기존 파일과 새로 선택한 파일들을 합쳐서 업데이트 - const updatedFiles = [...files, ...selectedFiles]; - - // 중복 파일 제거 (파일 이름 기준) - const uniqueFiles = updatedFiles.filter( - (file, index, self) => - index === self.findIndex(f => f.name === file.name), - ); - - setFiles(uniqueFiles); // 중복 제거 후 파일 상태 업데이트 + setFile(selectedFile); // 선택된 파일 설정 + setExistingFile(null); // 새 파일을 선택한 경우 기존 파일 초기화 } }; // 파일 삭제 - const handleFileDelete = index => { - const updatedFiles = files.filter((_, i) => i !== index); // 선택된 파일 삭제 - setFiles(updatedFiles); // 파일 상태 업데이트 - }; - - // 노션 링크 삭제 - const handleSNSDelete = id => { - setSnsLinks(snsLinks.filter(link => link.id !== id)); // 링크 삭제 + const handleFileDelete = () => { + setFile(null); // 파일 상태 초기화 + setExistingFile(null); // 기존 파일도 초기화 }; - const handleSubmit = async () => { const formData = new FormData(); - // 파일 배열을 순회하여 각 파일을 'file' 필드로 추가 (단일 파일씩 추가) - files.forEach(file => { + // 파일이 새로 선택되었으면 FormData에 추가 + if (file) { formData.append('file', file); - console.log('파일 추가됨:', file); - }); + console.log('새 파일 추가됨:', file); + } else if (existingFile) { + // 새 파일이 없고 기존 파일이 있으면 서버에 기존 파일을 유지하게 알림 + formData.append('existingFileUri', existingFile.uri); + console.log('기존 파일 유지됨:', existingFile.name); + } - // notionUri 배열을 JSON 문자열로 FormData에 추가 - if (snsLinks.length > 0) { - const notionUris = snsLinks.map(link => link.link).filter(Boolean); // 빈 링크 필터링 - if (notionUris.length > 0) { - formData.append('notionUri', JSON.stringify(notionUris)); // 노션 링크 배열을 JSON으로 추가 - } + // 노션 링크가 빈 문자열이 아니면 추가 + if (snsLink && snsLink.trim() !== '') { + formData.append('notionUri', snsLink); // 노션 링크가 있을 때만 추가 } - if (files.length === 0 && snsLinks.every(link => !link.link)) { + // 파일이나 노션 링크가 없으면 에러 메시지 표시 (기존 파일도 포함) + if (!file && !existingFile && !snsLink) { setError('파일 또는 노션 링크 중 하나는 필수로 입력해야 합니다.'); return; } - for (let pair of formData.entries()) { - console.log(pair[0], pair[1]); - } + try { - await postExistPortfolio(formData); + // 새 포트폴리오를 올리는 경우: file이 있으면 무조건 새 포트폴리오라고 가정 + if (!id) { + // ID가 없을 경우에만 포스트 API 호출 + await postExistPortfolio(formData); + } else { + // 기존 포트폴리오 수정하는 경우 편집 API 호출 + await editExistPortfolio(id, formData); + } navigate('/profile'); } catch (err) { setError('포트폴리오 업로드 중 오류가 발생했습니다.'); } }; - useEffect(() => { - getMyInfo().then(response => { - setProfileData(response?.data); - }); - }, []); - return (
@@ -108,7 +121,9 @@ const UsePortfolio = () => { - 포트폴리오 파일 업로드하기 + + 포트폴리오 파일 {isEdit ? '수정' : '업로드'}하기 + PDF 형식으로 업로드 해주세요.
@@ -123,28 +138,27 @@ const UsePortfolio = () => { - {/* 파일 리스트 표시 */} - {files.length > 0 && ( + {/* 파일이 선택되었거나 기존 파일이 있을 때만 파일 정보 표시 */} + {(file || existingFile) && ( - {files.map((file, index) => ( - - {file.name} + + + {file ? file.name : existingFile?.name} + + {file && file.size && ( {(file.size / (1024 * 1024)).toFixed(2)}{' '} MB - handleFileDelete(index)} - /> - - ))} + )} + + )} @@ -152,36 +166,24 @@ const UsePortfolio = () => { {error && {error}} - 노션 포트폴리오 공유하기 - + 노션 포트폴리오 링크 입력하기 노션 공유에서 ‘웹에 게시’ 여부를 확인해주세요! 게시가 허용되지 않았을 경우 링크 확인이 불가능해요. - {/* 노션 링크 리스트 */} - {snsLinks.map((section, index) => ( - - - - { - const updatedLinks = [...snsLinks]; - updatedLinks[index].link = e.target.value; - setSnsLinks(updatedLinks); - }} - /> - {snsLinks.length > 1 && ( - handleSNSDelete(section.id)} - /> - )} - - - ))} + {/* 단일 노션 링크 입력 필드 */} + + + + setSnsLink(e.target.value)} + /> + + navigate(-1)}>돌아가기 diff --git a/gongjakso/src/pages/ProfilePage/ProfilePage.jsx b/gongjakso/src/pages/ProfilePage/ProfilePage.jsx index d2164cd..455405d 100644 --- a/gongjakso/src/pages/ProfilePage/ProfilePage.jsx +++ b/gongjakso/src/pages/ProfilePage/ProfilePage.jsx @@ -13,7 +13,8 @@ import SelectPortfolio from '../../features/modal/SelectPortfolio'; import { getAllPortfolio, deletePortfolio, // Assuming you have a service for deleting portfolios - updatePortfolio, // Assuming you have a service for updating portfolios + getExistPortfolio, + deleteExistPortfolio, } from '../../service/portfolio_service'; const MAX_PORTFOLIOS = 3; @@ -24,6 +25,7 @@ const ProfilePage = () => { const [postContent2, setPostContent2] = useState(); const [postContent3, setPostContent3] = useState(); const [showModal, setShowModal] = useState(false); // 모달 상태 + const [portfolioDetails, setPortfolioDetails] = useState({}); // 상세정보 저장 const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedPortfolioId, setSelectedPortfolioId] = useState(null); const [selectedPortfolioName, setSelectedPortfolioName] = useState(''); @@ -97,7 +99,23 @@ const ProfilePage = () => { setPostContent3(response?.data.content); }); }, []); + useEffect(() => { + if (selectedPortfolioId) { + fetchPortfolioDetails(selectedPortfolioId); // 포트폴리오가 선택되면 상세 정보 불러오기 + } + }, [selectedPortfolioId]); + const fetchPortfolioDetails = async portfolioId => { + try { + const details = await getExistPortfolio(portfolioId); + setPortfolioDetails(prevState => ({ + ...prevState, + [portfolioId]: details.data, // ID별로 저장 + })); + } catch (error) { + console.error('Error fetching portfolio details:', error); + } + }; useEffect(() => { const fetchPortfolios = async () => { try { @@ -110,6 +128,12 @@ const ProfilePage = () => { } else { setPortfolioExists(false); } + + portfolios.forEach(portfolio => { + if (portfolio.isExistedPortfolio) { + fetchPortfolioDetails(portfolio.PortfolioId); + } + }); } catch (error) { console.error('Error fetching portfolios:', error); } @@ -126,7 +150,12 @@ const ProfilePage = () => { setSelectedPortfolioName(portfolioName); // 포트폴리오 이름 저장 setShowDeleteModal(true); // 삭제 모달 열기 }; - + const extractFileName = fileUri => { + if (fileUri) { + return fileUri.split('/').pop(); // '/'로 분리한 배열의 마지막 요소를 가져옴 + } + return '등록된 파일 없음'; + }; const confirmDelete = async () => { try { await deletePortfolio(selectedPortfolioId); @@ -144,14 +173,24 @@ const ProfilePage = () => { setShowDeleteModal(false); // 삭제 후 모달 닫기 } }; - - const handleEditPortfolio = portfolioId => { - // Find the portfolio to be edited + const handleEditPortfolio = async portfolioId => { const portfolioToEdit = portfolioList.find( portfolio => portfolio.PortfolioId === portfolioId, ); if (portfolioToEdit) { - navigate(`/profile/makeportfolio/${portfolioId}`); + const isExistedPortfolio = portfolioToEdit.isExistedPortfolio; + const editUrl = isExistedPortfolio + ? `/profile/useportfolio/${portfolioId}` // 다른 포트폴리오 형식의 편집 경로 + : `/profile/makeportfolio/${portfolioId}`; // 기본 편집 경로 + + navigate(editUrl); + + // 포트폴리오 수정 후 최신 정보 가져오기 + const updatedPortfolio = await getExistPortfolio(portfolioId); + setPortfolioDetails(prevDetails => ({ + ...prevDetails, + [portfolioId]: updatedPortfolio.data, // 최신 포트폴리오 정보로 업데이트 + })); } }; @@ -205,8 +244,57 @@ const ProfilePage = () => { key={portfolio.PortfolioId} > - {portfolio.PortfolioName} + {portfolio.isExistedPortfolio ? ( + portfolioDetails[ + portfolio.PortfolioId + ] ? ( + + {/* 노션 링크가 null이거나 빈 문자열이 아니면 표시 */} + {portfolioDetails[ + portfolio.PortfolioId + ]?.data.notionUri?.trim() && ( + <> + 노션 링크 등록 + 중  + + { + portfolioDetails[ + portfolio + .PortfolioId + ]?.data + .notionUri + } + + + )} + + {/* PDF 파일이 있는 경우에만 표시 */} + {portfolioDetails[ + portfolio.PortfolioId + ]?.data.fileUri && ( + <> + PDF 파일 등록 + 중  + + {extractFileName( + portfolioDetails[ + portfolio + .PortfolioId + ]?.data + .fileUri, + )} + + + )} + + ) : ( + <>로딩 중... + ) + ) : ( + portfolio.PortfolioName + )} + diff --git a/gongjakso/src/pages/ProfilePage/ProfilePageStyled.jsx b/gongjakso/src/pages/ProfilePage/ProfilePageStyled.jsx index 32fef21..6371508 100644 --- a/gongjakso/src/pages/ProfilePage/ProfilePageStyled.jsx +++ b/gongjakso/src/pages/ProfilePage/ProfilePageStyled.jsx @@ -202,7 +202,7 @@ export const DeletePortfolioButton = styled.button` export const PortfolioContainer = styled.div` width: 1000px; - height: 6rem; + min-height: 6rem; display: flex; flex-direction: row; justify-content: space-between; @@ -210,7 +210,7 @@ export const PortfolioContainer = styled.div` border-radius: 24px; border: 2px solid #c3e9ff; background: #e5f5ff; - padding: 3rem 3.5rem; + padding: 1.5rem 3rem; box-sizing: border-box; `; @@ -236,3 +236,21 @@ export const Plus = styled.div` cursor: pointer; margin-bottom: 25px; `; + +export const LinkInfo = styled.div` + display: flex; + flex-direction: column; + font-family: 'Pretendard'; + font-size: ${({ theme }) => theme.fontSize.mdd}; + font-weight: 600; + text-align: left; + gap: 5px; +`; + +export const LinkDetail = styled.div` + color: #8e8e93; + font-family: 'Pretendard'; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: 500; + text-align: left; +`; diff --git a/gongjakso/src/router/Router.jsx b/gongjakso/src/router/Router.jsx index bb3a9b7..e86e58c 100644 --- a/gongjakso/src/router/Router.jsx +++ b/gongjakso/src/router/Router.jsx @@ -115,6 +115,11 @@ const Router = () => { path="/profile/makeportfolio/:id" element={} /> + + } + /> diff --git a/gongjakso/src/service/portfolio_service.js b/gongjakso/src/service/portfolio_service.js index 9a20363..c9d5d28 100644 --- a/gongjakso/src/service/portfolio_service.js +++ b/gongjakso/src/service/portfolio_service.js @@ -26,18 +26,22 @@ export const postPortfolio = async portfolioData => { console.error('Error posting portfolio:', error); } }; - // 존재 포트폴리오 post -export const postExistPortfolio = async formData => { - const reqURL = `mypage/portfolio/exist-portfolio`; +export const postExistPortfolio = async (formData, accessToken) => { + const reqURL = 'mypage/portfolio/exist-portfolio'; try { - const response = await axiosInstanceV2.post(reqURL, formData); + const response = await axiosInstanceV2.post(reqURL, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${accessToken}`, + }, + }); return response.data; } catch (error) { console.error('Error posting portfolio:', error); + throw error; // 오류 발생 시 호출자에게 전달 } }; - // 입력 포트폴리오 상세 get export const getPortfolio = async id => { const reqURL = `mypage/portfolio/${id}`; @@ -83,7 +87,11 @@ export const editExistPortfolio = async (id, portfolioData) => { const reqURL = `mypage/portfolio/exist-portfolio/${id}`; try { - const response = await axiosInstanceV2.patch(reqURL, portfolioData); + const response = await axiosInstanceV2.patch(reqURL, portfolioData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); return response.data; } catch (error) { console.error('Error editing portfolio:', error);