diff --git a/package-lock.json b/package-lock.json
index 36024c4..9276df8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,11 +13,13 @@
"date-fns": "^4.1.0",
"framer-motion": "^11.9.0",
"next": "^14.2.15",
+ "next-redux-wrapper": "^8.1.0",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
+ "redux": "^5.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0"
},
@@ -6969,6 +6971,17 @@
}
}
},
+ "node_modules/next-redux-wrapper": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/next-redux-wrapper/-/next-redux-wrapper-8.1.0.tgz",
+ "integrity": "sha512-2hIau0hcI6uQszOtrvAFqgc0NkZegKYhBB7ZAKiG3jk7zfuQb4E7OV9jfxViqqojh3SEHdnFfPkN9KErttUKuw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "next": ">=9",
+ "react": "*",
+ "react-redux": "*"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
diff --git a/package.json b/package.json
index c8a5a32..b0dd8e9 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,13 @@
"date-fns": "^4.1.0",
"framer-motion": "^11.9.0",
"next": "^14.2.15",
+ "next-redux-wrapper": "^8.1.0",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
+ "redux": "^5.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0"
},
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/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
)}
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
-
+ {loading ? : }
{resultMessage && (
diff --git a/src/components/organisms/FindPasswordModal.tsx b/src/components/organisms/FindPasswordModal.tsx
index 5e9d4e0..2cf56c3 100644
--- a/src/components/organisms/FindPasswordModal.tsx
+++ b/src/components/organisms/FindPasswordModal.tsx
@@ -14,6 +14,7 @@ import {
AlertIcon,
AlertTitle,
keyframes,
+ Spinner,
} from '@chakra-ui/react'
import { useState, ChangeEvent } from 'react'
import Button from '../molecules/DefaultButton'
@@ -47,6 +48,7 @@ const FindPasswordModal: React.FC = ({ isOpen, onClose,
const [resultMessage, setResultMessage] = useState(null)
const [isSuccess, setIsSuccess] = useState(null)
const [shakeKey, setShakeKey] = useState(0)
+ const [loading, setLoading] = useState(false)
const handleChangeEmail = (e: ChangeEvent) => {
setEmail(e.target.value)
@@ -59,6 +61,7 @@ const FindPasswordModal: React.FC = ({ isOpen, onClose,
const handleSubmit = async () => {
try {
// 서버에 아이디와 이메일 전송 요청
+ setLoading(true)
const response = await fetch(`${apiUrl}/api/user/find-pw`, {
method: 'POST',
headers: {
@@ -81,6 +84,8 @@ const FindPasswordModal: React.FC = ({ isOpen, onClose,
setIsSuccess(false)
setResultMessage('일치하는 정보가 없습니다.')
setShakeKey((prev) => prev + 1)
+ } finally {
+ setLoading(false)
}
}
@@ -115,7 +120,8 @@ const FindPasswordModal: React.FC = ({ isOpen, onClose,
-
+ {loading ? : }
+
{resultMessage && (
diff --git a/src/components/organisms/Footer.tsx b/src/components/organisms/Footer.tsx
index 8366d8e..105abd8 100644
--- a/src/components/organisms/Footer.tsx
+++ b/src/components/organisms/Footer.tsx
@@ -7,7 +7,7 @@ const Footer: React.FC = () => {
-
+
© 2024 Folio
All rights reserved.
diff --git a/src/components/organisms/MainBanner.tsx b/src/components/organisms/MainBanner.tsx
index b2ad145..7b46b00 100644
--- a/src/components/organisms/MainBanner.tsx
+++ b/src/components/organisms/MainBanner.tsx
@@ -10,16 +10,16 @@ import BlurryImage from '../molecules/BlurryImage'
const MainBanner: React.FC<{ openSignupModal: () => void }> = ({ openSignupModal }) => {
return (
-
-
-
-
+
+
+
+
개발자
만을 위한
-
+
{' '}
포트폴리오
@@ -29,10 +29,10 @@ const MainBanner: React.FC<{ openSignupModal: () => void }> = ({ openSignupModal
diff --git a/src/components/organisms/MainContents.tsx b/src/components/organisms/MainContents.tsx
index cdd458c..f4fdc3d 100644
--- a/src/components/organisms/MainContents.tsx
+++ b/src/components/organisms/MainContents.tsx
@@ -60,14 +60,14 @@ const MainContents: React.FC<{ openSignupModal: () => void }> = ({ openSignupMod
}}
mt={index === 0 ? 0 : 150}
>
-
-
+
+
))}
-
+
나만의 포트폴리오
@@ -77,10 +77,10 @@ const MainContents: React.FC<{ openSignupModal: () => void }> = ({ openSignupMod
diff --git a/src/components/organisms/MarkdownModal.tsx b/src/components/organisms/MarkdownModal.tsx
index df64064..04af2a5 100644
--- a/src/components/organisms/MarkdownModal.tsx
+++ b/src/components/organisms/MarkdownModal.tsx
@@ -26,7 +26,6 @@ const fetchMarkdownContent = async (mdFilePath: string): Promise => {
const MarkdownModal: React.FC = ({ isOpen, onClose, mdFilePath }) => {
const [markdownContent, setMarkdownContent] = useState('')
- console.log(`mdFilePath : ${mdFilePath}`)
// .md 파일 읽어오기
useEffect(() => {
if (isOpen && mdFilePath) {
diff --git a/src/components/organisms/PortfolioInputForm.tsx b/src/components/organisms/PortfolioInputForm.tsx
index 729addf..7ab0575 100644
--- a/src/components/organisms/PortfolioInputForm.tsx
+++ b/src/components/organisms/PortfolioInputForm.tsx
@@ -12,7 +12,6 @@ import {
WrapItem,
TagLabel,
TagCloseButton,
- HStack,
InputGroup,
InputRightElement,
Icon,
@@ -130,7 +129,7 @@ const PortfolioInputForm: React.FC = ({
}
return (
-
+
{/* 기본 설정 */}
기본 설정
@@ -156,7 +155,7 @@ const PortfolioInputForm: React.FC = ({
image={image}
alt="대표 사진 미리보기"
onImageChange={setImage}
- buttonLabel="대표 사진 추가"
+ buttonLabel="사진 추가"
/>
{/* 임베드 링크 */}
@@ -189,7 +188,7 @@ const PortfolioInputForm: React.FC = ({
{/* 기술 스택 popular */}
-
+
{popularSkills.map((skill) => {
const isSelected = selectedTechStack.includes(skill.id)
return (
@@ -202,12 +201,15 @@ const PortfolioInputForm: React.FC = ({
onClick={() => handleSkillClick(skill.id)}
rightIcon={isSelected ? : }
fontWeight="normal"
+ whiteSpace="nowrap"
+ width="auto"
+ px={[3, 4, 5]}
>
{skill.name}
)
})}
-
+
{/* 기술 스택 검색 */}
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}
+
+
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 5dc6e16..bb4ca15 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: '',
@@ -159,7 +159,7 @@ const ProjectInputForm: React.FC = ({ projects, setProjec
onImageChange={(image) =>
setProjects((prev) => prev.map((p) => (p.id === project.id ? { ...p, image } : p)))
}
- buttonLabel="대표 사진 추가"
+ buttonLabel="사진 추가"
/>
= ({ projects, setProjec
/>
-
+
사용 기술 스택
= ({ 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([])
+ const [projects, setProjects] = useState([])
const checkRequiredFields = () => {
const missingFields: string[] = []
@@ -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 ad434cf..a0b9672 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,118 +33,27 @@ 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) {
+ if (portfolioData && reduxSkills) {
setCategorizedSkill(groupPortfolioSkillsByCategory(portfolioData.skills, reduxSkills))
const tmp = portfolioData.projects.map((project: any) =>
groupPortfolioSkillsByCategory(project.skills, reduxSkills),
)
setCategorizedProjectSkill(tmp)
}
- }, [portfolioData])
+ }, [portfolioData, reduxSkills]) // portfolioData나 reduxSkills가 변경될 때만 실행
if (!portfolioData) {
return Loading...
@@ -229,15 +115,23 @@ const PortfolioViewPage: React.FC<{ username?: string; id: number; isPublic?: bo
{portfolioData.projects.map((project: any, index: number) => (
-
-
-
+
+
+ {project.name}
+
+
+
-
-
- {project.name}
-
+
{project.description}
@@ -248,7 +142,7 @@ const PortfolioViewPage: React.FC<{ username?: string; id: number; isPublic?: bo
skills={categorizedProjectSkill[index]}
/>
{project.readmeFile && (
-
+