Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev 브랜치 머지 (반응형 웹 구현) #4

Merged
merged 3 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading