From 54230c278069fceaff8bdf691e825ec3ce2f9c46 Mon Sep 17 00:00:00 2001 From: YerangPark Date: Tue, 15 Oct 2024 16:39:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9E=AC?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20for=20SSR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[username]/[id]/page.tsx | 86 +++++++++-- src/app/portfolio/[id]/page.tsx | 106 +++++++++++-- src/components/organisms/ProjectInputForm.tsx | 8 +- .../templates/PortfolioFormPage.tsx | 4 +- .../templates/PortfolioViewPage.tsx | 140 ++---------------- src/types/data.ts | 44 +++++- 6 files changed, 230 insertions(+), 158 deletions(-) diff --git a/src/app/[username]/[id]/page.tsx b/src/app/[username]/[id]/page.tsx index 80f40c8..3d182fe 100644 --- a/src/app/[username]/[id]/page.tsx +++ b/src/app/[username]/[id]/page.tsx @@ -1,15 +1,83 @@ -'use client' - import PortfolioViewPage from '@/components/templates/PortfolioViewPage' -import { useParams } from 'next/navigation' +import { Portfolio } from '@/types/data' -export default function Page() { - const params = useParams() - if (Array.isArray(params.id) || !params.id || Array.isArray(params.username) || !params.username) { +export default async function Page({ params }: { params: { username: string; id: string } }) { + const { username, id } = params + if (Array.isArray(id) || !id || Array.isArray(username) || !username) { return
올바르지 않은 접근입니다.
} - const { username } = params - const id = parseInt(params.id, 10) - return + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yrpark.duckdns.org:8080' + + const getPortfolioData = async (): Promise => { + const url = `${apiUrl}/api/${username}/${id}` + const headers: Record = {} + + const response = await fetch(url, { + method: 'GET', + headers, + }) + + const res = await response.json() + if (response.ok) { + const portfolio = res.data + + // API에서 얻은 데이터를 포트폴리오 state에 저장 + return { + portfolioName: portfolio.file_name, + title: portfolio.title, + description: portfolio.description, + githubLink: portfolio.github_link, + blogLink: portfolio.blog_link, + image: `${apiUrl}/uploads/${portfolio.image}`, + skills: portfolio.portfolioSkills.map((skill: any) => skill.skill_id), + // eslint-disable-next-line no-underscore-dangle + projects: portfolio.__projects__.map((project: any) => ({ + id: project.id, + name: project.name, + description: project.description, + githubLink: project.github_link, + siteLink: project.site_link, + startDate: project.start_date ? project.start_date.split('-').slice(0, 2).join('-') : '', + endDate: project.end_date ? project.end_date.split('-').slice(0, 2).join('-') : '', + image: project.image ? `${apiUrl}/uploads/${project.image}` : null, + readmeFile: project.readme_file ? `${apiUrl}/uploads/${project.readme_file}` : null, + skills: project.projectSkills.map((skill: any) => skill.skill_id), + })), + } + } + console.error('포트폴리오 불러오기 실패:', res.message) + return null + } + + const getUserData = async () => { + const url = `${apiUrl}/api/user/public/${username}` + const headers: Record = {} + + const response = await fetch(url, { + method: 'GET', + headers, + }) + + const res = await response.json() + if (response.ok) { + const user = res.data + return { + name: user.name, + email: user.email, + birthdate: user.birthdate, + } + } + console.error('포트폴리오 불러오기 실패:', res.message) + return null + } + + const portfolioData = await getPortfolioData() + const userData = await getUserData() + + if (!portfolioData || !userData) { + return
포트폴리오 데이터를 불러올 수 없습니다.
+ } + + return } diff --git a/src/app/portfolio/[id]/page.tsx b/src/app/portfolio/[id]/page.tsx index 4cb8696..2a04cf1 100644 --- a/src/app/portfolio/[id]/page.tsx +++ b/src/app/portfolio/[id]/page.tsx @@ -1,26 +1,114 @@ 'use client' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' import PortfolioViewPage from '@/components/templates/PortfolioViewPage' -import { useParams, useRouter } from 'next/navigation' import isTokenExpired from '@/utils/TokenExpiredChecker' -import { useEffect } from 'react' -export default function Page() { +export default function Page({ params }: { params: { id: string } }) { + const { id } = params const router = useRouter() + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yrpark.duckdns.org:8080' + + const [portfolioData, setPortfolioData] = useState(null) + const [userData, setUserData] = useState(null) + const [loading, setLoading] = useState(true) useEffect(() => { const token = localStorage.getItem('token') if (!token || isTokenExpired(token)) { localStorage.removeItem('token') router.push('/') + return + } + + const fetchPortfolioData = async () => { + const url = `${apiUrl}/api/portfolio/${id}` + const headers: Record = {} + headers.Authorization = `Bearer ${token}` + + const response = await fetch(url, { + method: 'GET', + headers, + }) + + const res = await response.json() + if (response.status === 401) { + localStorage.removeItem('token') + router.push('/') + return + } + + if (response.ok) { + const portfolio = res.data + setPortfolioData({ + portfolioName: portfolio.file_name, + title: portfolio.title, + description: portfolio.description, + githubLink: portfolio.github_link, + blogLink: portfolio.blog_link, + image: `${apiUrl}/uploads/${portfolio.image}`, + skills: portfolio.portfolioSkills.map((skill: any) => skill.skill_id), + // eslint-disable-next-line no-underscore-dangle + projects: portfolio.__projects__.map((project: any) => ({ + id: project.id, + name: project.name, + description: project.description, + githubLink: project.github_link, + siteLink: project.site_link, + startDate: project.start_date ? project.start_date.split('-').slice(0, 2).join('-') : '', + endDate: project.end_date ? project.end_date.split('-').slice(0, 2).join('-') : '', + image: project.image ? `${apiUrl}/uploads/${project.image}` : null, + readmeFile: project.readme_file ? `${apiUrl}/uploads/${project.readme_file}` : null, + skills: project.projectSkills.map((skill: any) => skill.skill_id), + })), + }) + } + } + + const fetchUserData = async () => { + const url = `${apiUrl}/api/user` + const headers: Record = {} + headers.Authorization = `Bearer ${token}` + + const response = await fetch(url, { + method: 'GET', + headers, + }) + + const res = await response.json() + if (response.status === 401) { + localStorage.removeItem('token') + router.push('/') + return + } + + if (response.ok) { + const user = res.data + setUserData({ + name: user.name, + email: user.email, + birthdate: user.birthdate, + }) + } + } + + const fetchData = async () => { + await fetchPortfolioData() + await fetchUserData() + setLoading(false) // 데이터가 모두 로드되면 로딩을 종료합니다. } - }, [router]) - const params = useParams() - if (Array.isArray(params.id) || !params.id) { - return
올바르지 않은 접근입니다.
+ fetchData() + }, [id, router, apiUrl]) + + if (loading) { + return
Loading...
+ } + + if (!portfolioData || !userData) { + return
포트폴리오 데이터를 불러올 수 없습니다.
} - const id = parseInt(params.id, 10) - return + return } diff --git a/src/components/organisms/ProjectInputForm.tsx b/src/components/organisms/ProjectInputForm.tsx index 5dc6e16..79d0712 100644 --- a/src/components/organisms/ProjectInputForm.tsx +++ b/src/components/organisms/ProjectInputForm.tsx @@ -21,7 +21,7 @@ import { IconButton, } from '@chakra-ui/react' import { FiPlus, FiDelete } from 'react-icons/fi' -import { Skill, Project } from '@/types/data' +import { Skill, ProjectInputProps } from '@/types/data' import { useSelector } from 'react-redux' import { RootState } from '@/store' import { FaSearch } from 'react-icons/fa' @@ -32,8 +32,8 @@ import InputDateRange from '../molecules/InputDateRange' import InputFile from '../molecules/InputFile' interface ProjectInputFormProps { - projects: Project[] - setProjects: React.Dispatch> + projects: ProjectInputProps[] + setProjects: React.Dispatch> } const ProjectInputForm: React.FC = ({ projects, setProjects }) => { @@ -75,7 +75,7 @@ const ProjectInputForm: React.FC = ({ projects, setProjec } const handleAddProject = () => { - const newProject: Project = { + const newProject: ProjectInputProps = { id: projects.length + 1, name: '', description: '', diff --git a/src/components/templates/PortfolioFormPage.tsx b/src/components/templates/PortfolioFormPage.tsx index 7aa8013..56cf226 100644 --- a/src/components/templates/PortfolioFormPage.tsx +++ b/src/components/templates/PortfolioFormPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' import { Box, Divider } from '@chakra-ui/react' -import { Project } from '@/types/data' +import { ProjectInputProps } from '@/types/data' import { useRouter } from 'next/navigation' import DashboardNavBar from '../organisms/DashboardNavBar' import { Heading } from '../atoms/Text' @@ -38,7 +38,7 @@ const PortfolioFormPage: React.FC = ({ id }) => { const [selectedTechStack, setSelectedTechStack] = useState([]) // Project Page States - const [projects, setProjects] = useState([]) + const [projects, setProjects] = useState([]) const checkRequiredFields = () => { const missingFields: string[] = [] diff --git a/src/components/templates/PortfolioViewPage.tsx b/src/components/templates/PortfolioViewPage.tsx index ad434cf..c9daddd 100644 --- a/src/components/templates/PortfolioViewPage.tsx +++ b/src/components/templates/PortfolioViewPage.tsx @@ -1,9 +1,11 @@ +'use client' + import React, { useEffect, useState } from 'react' import { Box, Heading, Text, Flex, Image, VStack, Link, Icon, Button } from '@chakra-ui/react' import { FaGithub, FaBlog } from 'react-icons/fa' -import { useRouter } from 'next/navigation' import { useSelector } from 'react-redux' import { RootState } from '@/store' +import { PortfolioViewPageProps } from '@/types/data' import PortfolioMainImageText from '../organisms/PortfolioMainImageText' import PortfolioNavBar from '../organisms/PortfolioNavBar' import ProjectInformation from '../organisms/ProjectInformation' @@ -11,32 +13,7 @@ import SkillCategories from '../organisms/SkillCategories' import Footer from '../organisms/Footer' import MarkdownModal from '../organisms/MarkdownModal' -const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yrpark.duckdns.org:8080' - -const PortfolioViewPage: React.FC<{ username?: string; id: number; isPublic?: boolean }> = ({ - username, - id, - isPublic = false, -}) => { - const router = useRouter() - const [portfolioData, setPortfolioData] = useState(null) - const [userData, setUserData] = useState<{ name: string; birthdate: string; email: string }>({ - name: '', - birthdate: '', - email: '', - }) - const reduxSkills = useSelector((state: RootState) => state.skill.skills) - const [categorizedSkill, setCategorizedSkill] = useState(null) - const [categorizedProjectSkill, setCategorizedProjectSkill] = useState([]) - const [projectModalOpen, setProjectModalOpen] = useState(false) - const [selectedProjectMdFile, setSelectedProjectMdFile] = useState('') - const closeProjectModal = () => { - setProjectModalOpen(false) - } - const openProjectModal = () => { - setProjectModalOpen(true) - } - +const PortfolioViewPage: React.FC = ({ portfolioData, userData }) => { const groupPortfolioSkillsByCategory = (skillIds: number[], skillDefines: any[]) => { return skillIds.reduce( (acc, skillId) => { @@ -56,109 +33,18 @@ const PortfolioViewPage: React.FC<{ username?: string; id: number; isPublic?: bo ) } - const getPortfolioData = async () => { - const token = localStorage.getItem('token') - if (!token && !isPublic) { - console.error('토큰이 만료되었습니다.') - router.push('/') - throw new Error('Token not found') - } - - const url = isPublic ? `${apiUrl}/api/${username}/${id}` : `${apiUrl}/api/portfolio/${id}` - const headers: Record = {} - if (!isPublic) { - headers.Authorization = `Bearer ${token}` - } - - const response = await fetch(url, { - method: 'GET', - headers, - }) - - const res = await response.json() - if (response.status === 401) { - console.error('토큰이 만료되었습니다.') - localStorage.removeItem('token') - router.push('/') - } - - if (response.ok) { - const portfolio = res.data - - // API에서 얻은 데이터를 포트폴리오 state에 저장 - setPortfolioData({ - portfolioName: portfolio.file_name, - title: portfolio.title, - description: portfolio.description, - githubLink: portfolio.github_link, - blogLink: portfolio.blog_link, - image: portfolio.image ? `${apiUrl}/uploads/${portfolio.image}` : null, - skills: portfolio.portfolioSkills.map((skill: any) => skill.skill_id), - // eslint-disable-next-line no-underscore-dangle - projects: portfolio.__projects__.map((project: any) => ({ - id: project.id, - name: project.name, - description: project.description, - githubLink: project.github_link, - siteLink: project.site_link, - startDate: project.start_date ? project.start_date.split('-').slice(0, 2).join('-') : '', - endDate: project.end_date ? project.end_date.split('-').slice(0, 2).join('-') : '', - image: project.image ? `${apiUrl}/uploads/${project.image}` : null, - readmeFile: project.readme_file ? `${apiUrl}/uploads/${project.readme_file}` : null, - skills: project.projectSkills.map((skill: any) => skill.skill_id), - })), - }) - } else { - console.error('포트폴리오 불러오기 실패:', res.message) - } + const reduxSkills = useSelector((state: RootState) => state.skill.skills) + const [categorizedSkill, setCategorizedSkill] = useState(null) + const [categorizedProjectSkill, setCategorizedProjectSkill] = useState([]) + const [projectModalOpen, setProjectModalOpen] = useState(false) + const [selectedProjectMdFile, setSelectedProjectMdFile] = useState('') + const closeProjectModal = () => { + setProjectModalOpen(false) } - - const getUserData = async () => { - const token = localStorage.getItem('token') - if (!token && !isPublic) { - console.error('토큰이 만료되었습니다.') - router.push('/') - throw new Error('Token not found') - } - - const url = isPublic ? `${apiUrl}/api/user/public/${username}` : `${apiUrl}/api/user` - const headers: Record = {} - if (!isPublic) { - headers.Authorization = `Bearer ${token}` - } - - const response = await fetch(url, { - method: 'GET', - headers, - }) - - const res = await response.json() - if (response.status === 401) { - console.error('토큰이 만료되었습니다.') - localStorage.removeItem('token') - router.push('/') - } - - if (response.ok) { - const user = res.data - setUserData({ - name: user.name, - email: user.email, - birthdate: user.birthdate, - }) - } else { - console.error('포트폴리오 불러오기 실패:', res.message) - } + const openProjectModal = () => { + setProjectModalOpen(true) } - useEffect(() => { - const fetchData = async () => { - await getPortfolioData() - await getUserData() - } - fetchData() - }, []) - useEffect(() => { if (portfolioData) { setCategorizedSkill(groupPortfolioSkillsByCategory(portfolioData.skills, reduxSkills)) diff --git a/src/types/data.ts b/src/types/data.ts index 985d5bb..b14e93e 100644 --- a/src/types/data.ts +++ b/src/types/data.ts @@ -1,11 +1,6 @@ -interface Portfolio { - id: number - file_name: string -} - export interface DashboardProps { data: PortfolioBrief[] - onHover: (portfoil: Portfolio) => void + onHover: (portfolio: { id: number; file_name: string }) => void openDeletePortfolioModal: () => void handleExport: (arg: number) => void } @@ -23,7 +18,7 @@ export interface Skill { category?: string } -export interface Project { +export interface ProjectInputProps { id: number name: string description: string @@ -35,3 +30,38 @@ export interface Project { selectedTechStack: number[] readmeFile: File | null } + +export interface Project { + id: number + name: string + description: string + githubLink?: string + siteLink?: string + startDate: string + endDate: string + image?: string + readmeFile?: string + skills: number[] +} + +export interface Portfolio { + portfolioName: string + title: string + description: string + githubLink: string + blogLink: string + image: string + skills: number[] + projects: Project[] +} + +export interface User { + name: string + email: string + birthdate: string +} + +export interface PortfolioViewPageProps { + portfolioData: Portfolio + userData: User +} From 48021075efabd7b365701560c0b2908ade357189 Mon Sep 17 00:00:00 2001 From: YerangPark Date: Wed, 16 Oct 2024 23:19:46 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EC=9B=B9=EC=9D=84=20=EC=9C=84=ED=95=B4=EC=84=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=B0=94=EC=9D=BC=20=ED=99=98=EA=B2=BD=20=ED=98=B8=ED=99=98?= =?UTF-8?q?=EC=84=B1=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/molecules/BlurryImage.tsx | 4 +- src/components/molecules/InputDateRange.tsx | 2 +- src/components/molecules/InputFile.tsx | 4 +- src/components/molecules/InputImage.tsx | 4 +- src/components/molecules/InputTextbox.tsx | 2 +- src/components/molecules/RoundButton.tsx | 4 +- src/components/organisms/FindIdModal.tsx | 7 ++- .../organisms/FindPasswordModal.tsx | 8 ++- src/components/organisms/Footer.tsx | 2 +- src/components/organisms/MainBanner.tsx | 16 ++--- src/components/organisms/MainContents.tsx | 12 ++-- src/components/organisms/MarkdownModal.tsx | 1 - .../organisms/PortfolioInputForm.tsx | 12 ++-- .../organisms/PortfolioMainImageText.tsx | 12 ++-- .../organisms/ProjectInformation.tsx | 3 +- src/components/organisms/ProjectInputForm.tsx | 4 +- .../templates/PortfolioFormPage.tsx | 62 +++++++++++++------ .../templates/PortfolioViewPage.tsx | 24 ++++--- src/types/style.ts | 6 +- 19 files changed, 116 insertions(+), 73 deletions(-) diff --git a/src/components/molecules/BlurryImage.tsx b/src/components/molecules/BlurryImage.tsx index a8a4648..3c68916 100644 --- a/src/components/molecules/BlurryImage.tsx +++ b/src/components/molecules/BlurryImage.tsx @@ -6,8 +6,8 @@ interface BlurryImageProps { src: string alt: string overlayOpacity?: number - width?: string | number - height?: string | number + width?: string | number | (string | number)[] + height?: string | number | (string | number)[] } const BlurryImage: React.FC = ({ diff --git a/src/components/molecules/InputDateRange.tsx b/src/components/molecules/InputDateRange.tsx index eb17041..74937bb 100644 --- a/src/components/molecules/InputDateRange.tsx +++ b/src/components/molecules/InputDateRange.tsx @@ -21,7 +21,7 @@ const InputDateRange: React.FC = ({ return ( - + {formLabel} void allowedExtensions: string[] // 허용된 파일 확장자 } @@ -30,7 +30,7 @@ const InputFile: React.FC = ({ formLabel, placeholder, onFileCha return ( - + {formLabel} diff --git a/src/components/molecules/InputImage.tsx b/src/components/molecules/InputImage.tsx index dd12286..8ce9438 100644 --- a/src/components/molecules/InputImage.tsx +++ b/src/components/molecules/InputImage.tsx @@ -25,7 +25,7 @@ const InputImage: React.FC = ({ formLabel, image, alt, onImageC return ( - + {formLabel} @@ -33,7 +33,7 @@ const InputImage: React.FC = ({ formLabel, image, alt, onImageC {alt} )} diff --git a/src/components/molecules/InputTextbox.tsx b/src/components/molecules/InputTextbox.tsx index 58824f2..ddc721c 100644 --- a/src/components/molecules/InputTextbox.tsx +++ b/src/components/molecules/InputTextbox.tsx @@ -12,7 +12,7 @@ const InputTextbox: React.FC = ({ formLabel, placeHolder, val return ( - + {formLabel} fontSize?: ResponsiveValue // fontSize를 추가 - px?: number | string // 커스텀 padding-x - py?: number | string // 커스텀 padding-y + px?: number | string | (number | string)[] // 커스텀 padding-x + py?: number | string | (number | string)[] // 커스텀 padding-y fontWeight?: string } diff --git a/src/components/organisms/FindIdModal.tsx b/src/components/organisms/FindIdModal.tsx index db38def..a805dba 100644 --- a/src/components/organisms/FindIdModal.tsx +++ b/src/components/organisms/FindIdModal.tsx @@ -14,6 +14,7 @@ import { AlertIcon, AlertTitle, keyframes, + Spinner, } from '@chakra-ui/react' import { useState, ChangeEvent } from 'react' import Button from '../molecules/DefaultButton' @@ -46,6 +47,7 @@ const FindIdModal: React.FC = ({ isOpen, onClose, openLoginMod const [resultMessage, setResultMessage] = useState(null) const [isSuccess, setIsSuccess] = useState(null) const [shakeKey, setShakeKey] = useState(0) + const [loading, setLoading] = useState(false) const handleChange = (e: ChangeEvent) => { setEmail(e.target.value) @@ -54,6 +56,7 @@ const FindIdModal: React.FC = ({ isOpen, onClose, openLoginMod const handleSubmit = async () => { try { // 서버에 이메일 전송 요청 + setLoading(true) const response = await fetch(`${apiUrl}/api/user/find-id`, { method: 'POST', headers: { @@ -76,6 +79,8 @@ const FindIdModal: React.FC = ({ isOpen, onClose, openLoginMod setIsSuccess(false) setResultMessage('일치하는 정보가 없습니다.') setShakeKey((prev) => prev + 1) + } finally { + setLoading(false) } } @@ -99,7 +104,7 @@ const FindIdModal: React.FC = ({ isOpen, onClose, openLoginMod - ) })} - + {/* 기술 스택 검색 */} diff --git a/src/components/organisms/PortfolioMainImageText.tsx b/src/components/organisms/PortfolioMainImageText.tsx index 6aadbe9..cc605bb 100644 --- a/src/components/organisms/PortfolioMainImageText.tsx +++ b/src/components/organisms/PortfolioMainImageText.tsx @@ -8,11 +8,13 @@ import BlurryImage from '../molecules/BlurryImage' const PortfolioMainImageText: React.FC<{ image: string; text: string }> = ({ image, text }) => { return ( - - - - {text} - + + + + + {text} + + scroll diff --git a/src/components/organisms/ProjectInformation.tsx b/src/components/organisms/ProjectInformation.tsx index 7369b13..e51bcc1 100644 --- a/src/components/organisms/ProjectInformation.tsx +++ b/src/components/organisms/ProjectInformation.tsx @@ -17,7 +17,6 @@ const ProjectInformation: React.FC = ({ blogLink, skills, }) => { - console.log(skills) return ( <> @@ -28,7 +27,7 @@ const ProjectInformation: React.FC = ({ Object.entries(skills).map(([category, skillNames]) => ( - + {'>'} {category.charAt(0).toUpperCase() + category.slice(1)} diff --git a/src/components/organisms/ProjectInputForm.tsx b/src/components/organisms/ProjectInputForm.tsx index 79d0712..b7bfcef 100644 --- a/src/components/organisms/ProjectInputForm.tsx +++ b/src/components/organisms/ProjectInputForm.tsx @@ -215,7 +215,7 @@ const ProjectInputForm: React.FC = ({ projects, setProjec - + {searchQuery && (searchResults.length > 0 ? ( @@ -239,7 +239,7 @@ const ProjectInputForm: React.FC = ({ projects, setProjec - + = ({ id }) => { const [githubLink, setGithubLink] = useState('') const [blogLink, setBlogLink] = useState('') const [selectedTechStack, setSelectedTechStack] = useState([]) + const [loading, setLoading] = useState(false) // Project Page States const [projects, setProjects] = useState([]) @@ -113,26 +114,31 @@ const PortfolioFormPage: React.FC = ({ id }) => { } const updatePortfolio = async (token: any, formData: any) => { - const response = await fetch(`${apiUrl}/api/portfolio/${id}`, { - method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, - body: formData, - }) + try { + setLoading(true) + const response = await fetch(`${apiUrl}/api/portfolio/${id}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }) - const res = await response.json() - if (response.status === 401) { - console.error('토큰이 만료되었습니다.') - localStorage.removeItem('token') - router.push('/') - } + const res = await response.json() + if (response.status === 401) { + console.error('토큰이 만료되었습니다.') + localStorage.removeItem('token') + router.push('/') + } - if (response.ok) { - console.log('포트폴리오 저장 성공:', res) - router.push('/dashboard') - } else { - console.error('포트폴리오 저장 실패:', res.message) + if (response.ok) { + console.log('포트폴리오 저장 성공:', res) + router.push('/dashboard') + } else { + console.error('포트폴리오 저장 실패:', res.message) + } + } finally { + setLoading(false) } } @@ -230,9 +236,25 @@ const PortfolioFormPage: React.FC = ({ id }) => { return (
+ {loading && ( + + + + )}
- + {isEditMode ? ( ) : ( diff --git a/src/components/templates/PortfolioViewPage.tsx b/src/components/templates/PortfolioViewPage.tsx index c9daddd..b662266 100644 --- a/src/components/templates/PortfolioViewPage.tsx +++ b/src/components/templates/PortfolioViewPage.tsx @@ -115,15 +115,23 @@ const PortfolioViewPage: React.FC = ({ portfolioData, us {portfolioData.projects.map((project: any, index: number) => ( - - - + + + {project.name} + + + {project.name} - - - {project.name} - + {project.description} @@ -134,7 +142,7 @@ const PortfolioViewPage: React.FC = ({ portfolioData, us skills={categorizedProjectSkill[index]} /> {project.readmeFile && ( - +