Skip to content

Commit

Permalink
Merge pull request #4 from YerangPark/dev
Browse files Browse the repository at this point in the history
Dev 브랜치 머지 (반응형 웹 구현)
  • Loading branch information
YerangPark authored Oct 17, 2024
2 parents 617d4f6 + 2790076 commit cd82348
Show file tree
Hide file tree
Showing 24 changed files with 365 additions and 235 deletions.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
86 changes: 77 additions & 9 deletions src/app/[username]/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>올바르지 않은 접근입니다.</div>
}
const { username } = params
const id = parseInt(params.id, 10)

return <PortfolioViewPage username={username} id={id} isPublic />
const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yrpark.duckdns.org:8080'

const getPortfolioData = async (): Promise<Portfolio | null> => {
const url = `${apiUrl}/api/${username}/${id}`
const headers: Record<string, string> = {}

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<string, string> = {}

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 <div>포트폴리오 데이터를 불러올 수 없습니다.</div>
}

return <PortfolioViewPage portfolioData={portfolioData} userData={userData} />
}
106 changes: 97 additions & 9 deletions src/app/portfolio/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<any>(null)
const [userData, setUserData] = useState<any>(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<string, string> = {}
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<string, string> = {}
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 <div>올바르지 않은 접근입니다.</div>
fetchData()
}, [id, router, apiUrl])

if (loading) {
return <div>Loading...</div>
}

if (!portfolioData || !userData) {
return <div>포트폴리오 데이터를 불러올 수 없습니다.</div>
}
const id = parseInt(params.id, 10)

return <PortfolioViewPage id={id} />
return <PortfolioViewPage portfolioData={portfolioData} userData={userData} />
}
4 changes: 2 additions & 2 deletions src/components/molecules/BlurryImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlurryImageProps> = ({
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/InputDateRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const InputDateRange: React.FC<DateRangeInputProps> = ({
return (
<FormControl mb={4}>
<Flex align="center">
<FormLabel mb="0" width="150px">
<FormLabel mb="0" width={[110, 130, 150]}>
{formLabel}
</FormLabel>
<Input
Expand Down
4 changes: 2 additions & 2 deletions src/components/molecules/InputFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FiFilePlus } from 'react-icons/fi'

interface InputFileProps {
formLabel: string
placeholder: string
placeholder?: string
onFileChange: (file: File | null) => void
allowedExtensions: string[] // 허용된 파일 확장자
}
Expand All @@ -30,7 +30,7 @@ const InputFile: React.FC<InputFileProps> = ({ formLabel, placeholder, onFileCha
return (
<FormControl mb={4}>
<Flex align="center">
<FormLabel mb="0" width="150px">
<FormLabel mb="0" width={[110, 130, 150]}>
{formLabel}
</FormLabel>
<InputGroup width="100%" flex="1">
Expand Down
4 changes: 2 additions & 2 deletions src/components/molecules/InputImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ const InputImage: React.FC<InputImageProps> = ({ formLabel, image, alt, onImageC
return (
<FormControl mb={4}>
<Box display="flex" alignItems="center">
<FormLabel mb="0" width="150px">
<FormLabel mb="0" width={[110, 130, 150]}>
{formLabel}
</FormLabel>
<Box display="flex" alignItems="center">
{image && (
<Image
src={image instanceof File ? URL.createObjectURL(image) : image} // 파일 객체를 URL로 변환하여 이미지 미리보기
alt={alt}
boxSize="150px"
boxSize={[100, 130, 150]}
mr={4}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/InputTextbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const InputTextbox: React.FC<InputTextBoxProps> = ({ formLabel, placeHolder, val
return (
<FormControl mb={4}>
<Flex align="center">
<FormLabel mb="0" width="150px">
<FormLabel mb="0" width={[110, 130, 150]} fontSize="base">
{formLabel}
</FormLabel>
<Input
Expand Down
4 changes: 2 additions & 2 deletions src/components/molecules/RoundButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ interface DefaultButtonProps {
color: string
size?: ResponsiveValue<string>
fontSize?: ResponsiveValue<string> // 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
}

Expand Down
7 changes: 6 additions & 1 deletion src/components/organisms/FindIdModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
AlertIcon,
AlertTitle,
keyframes,
Spinner,
} from '@chakra-ui/react'
import { useState, ChangeEvent } from 'react'
import Button from '../molecules/DefaultButton'
Expand Down Expand Up @@ -46,6 +47,7 @@ const FindIdModal: React.FC<FindIdModalProps> = ({ isOpen, onClose, openLoginMod
const [resultMessage, setResultMessage] = useState<string | null>(null)
const [isSuccess, setIsSuccess] = useState<boolean | null>(null)
const [shakeKey, setShakeKey] = useState<number>(0)
const [loading, setLoading] = useState(false)

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
Expand All @@ -54,6 +56,7 @@ const FindIdModal: React.FC<FindIdModalProps> = ({ isOpen, onClose, openLoginMod
const handleSubmit = async () => {
try {
// 서버에 이메일 전송 요청
setLoading(true)
const response = await fetch(`${apiUrl}/api/user/find-id`, {
method: 'POST',
headers: {
Expand All @@ -76,6 +79,8 @@ const FindIdModal: React.FC<FindIdModalProps> = ({ isOpen, onClose, openLoginMod
setIsSuccess(false)
setResultMessage('일치하는 정보가 없습니다.')
setShakeKey((prev) => prev + 1)
} finally {
setLoading(false)
}
}

Expand All @@ -99,7 +104,7 @@ const FindIdModal: React.FC<FindIdModalProps> = ({ isOpen, onClose, openLoginMod
<Input type="email" placeholder="이메일 입력" value={email} onChange={handleChange} />
</FormControl>

<Button width="100%" onClick={handleSubmit} label="아이디 찾기" />
{loading ? <Spinner size="lg" /> : <Button width="100%" onClick={handleSubmit} label="아이디 찾기" />}
{resultMessage && (
<Alert status={isSuccess ? 'success' : 'error'} animation={`${shake} 0.5s`} key={shakeKey}>
<AlertIcon />
Expand Down
Loading

0 comments on commit cd82348

Please sign in to comment.