-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #57 from prgrms-be-devcourse/feature/J-00FE
[FEAT] [FE]: J-00FE
- Loading branch information
Showing
15 changed files
with
515 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import axios from 'axios'; | ||
|
||
export const axiosInstance = axios.create({ | ||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
withCredentials: true | ||
}); | ||
|
||
// 요청 인터셉터 | ||
axiosInstance.interceptors.request.use( | ||
(config) => { | ||
const token = localStorage.getItem('token'); | ||
if (token) { | ||
config.headers.Authorization = `Bearer ${token}`; | ||
} | ||
return config; | ||
}, | ||
(error) => { | ||
return Promise.reject(error); | ||
} | ||
); | ||
|
||
// 응답 인터셉터 | ||
axiosInstance.interceptors.response.use( | ||
(response) => response, | ||
(error) => { | ||
if (error.response?.status === 401) { | ||
// 인증 에러 처리 | ||
localStorage.removeItem('token'); | ||
} | ||
return Promise.reject(error); | ||
} | ||
); | ||
|
||
export default axiosInstance; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { ApiResponse } from '@/types/common/ApiResponse'; | ||
import { JobPostingSearchCondition } from '@/types/job-posting/JobPostingSearchCondition'; | ||
import { PageResponse } from '@/types/common/PageResponse'; | ||
import { JobPostingPageResponse } from '@/types/job-posting/JobPostingPageResponse'; | ||
import axiosInstance from './axios'; | ||
|
||
export const getJobPostings = async (params: JobPostingSearchCondition): Promise<ApiResponse<PageResponse<JobPostingPageResponse>>> => { | ||
const response = await axiosInstance.get('/api/v1/job-posting', { params }); | ||
return response.data; | ||
}; | ||
|
||
export const getJobPosting = async (id: number): Promise<ApiResponse<JobPostingPageResponse>> => { | ||
const response = await axiosInstance.get(`/api/v1/job-posting/${id}`); | ||
return response.data; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { ApiResponse } from '@/types/common/ApiResponse'; | ||
import { PageResponse } from '@/types/common/PageResponse'; | ||
import { PostResponse } from '@/types/post/PostResponse'; | ||
import axiosInstance from './axios'; | ||
|
||
interface GetPostsParams { | ||
categoryId?: number; | ||
keyword?: string; | ||
sort?: 'latest' | 'popular'; | ||
page?: number; | ||
size?: number; | ||
} | ||
|
||
export const getPosts = async (params: GetPostsParams = {}): Promise<ApiResponse<PageResponse<PostResponse>>> => { | ||
const response = await axiosInstance.get('/api/v1/post', { params }); | ||
return response.data; | ||
}; | ||
|
||
export const getPost = async (id: number): Promise<ApiResponse<PostResponse>> => { | ||
const response = await axiosInstance.get(`/api/v1/post/${id}`); | ||
return response.data; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
'use client'; | ||
|
||
import { useEffect, useState } from 'react'; | ||
import { JobPostingPageResponse } from '@/types/job-posting/JobPostingPageResponse'; | ||
import { getJobPosting } from '@/api/jobPosting'; | ||
import { BriefcaseIcon, BuildingOfficeIcon, CalendarIcon, AcademicCapIcon } from '@heroicons/react/24/outline'; | ||
import Link from 'next/link'; | ||
|
||
export default function JobPostingDetail({ params }: { params: { id: string } }) { | ||
const [posting, setPosting] = useState<JobPostingPageResponse | null>(null); | ||
const [isLoading, setIsLoading] = useState(true); | ||
|
||
useEffect(() => { | ||
const fetchJobPosting = async () => { | ||
try { | ||
const response = await getJobPosting(parseInt(params.id)); | ||
if (response.success) { | ||
setPosting(response.data); | ||
} | ||
} catch (error) { | ||
console.error('Failed to fetch job posting:', error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
fetchJobPosting(); | ||
}, [params.id]); | ||
|
||
if (isLoading) { | ||
return ( | ||
<div className="max-w-4xl mx-auto p-8 animate-pulse"> | ||
<div className="h-8 bg-gray-200 rounded w-3/4 mb-6"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-8"></div> | ||
<div className="space-y-4"> | ||
<div className="h-4 bg-gray-200 rounded w-full"></div> | ||
<div className="h-4 bg-gray-200 rounded w-full"></div> | ||
<div className="h-4 bg-gray-200 rounded w-2/3"></div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
if (!posting) { | ||
return ( | ||
<div className="max-w-4xl mx-auto p-8"> | ||
<p className="text-center text-gray-500">채용 공고를 찾을 수 없습니다.</p> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<div className="max-w-4xl mx-auto p-8"> | ||
<Link | ||
href="/" | ||
className="inline-flex items-center text-blue-600 hover:text-blue-800 mb-6" | ||
> | ||
← 목록으로 돌아가기 | ||
</Link> | ||
|
||
<div className="bg-white rounded-lg shadow-lg p-8"> | ||
<h1 className="text-3xl font-bold mb-4">{posting.subject}</h1> | ||
|
||
<div className="flex items-center text-gray-600 mb-6"> | ||
<BuildingOfficeIcon className="h-5 w-5 mr-2" /> | ||
<a | ||
href={posting.companyLink} | ||
target="_blank" | ||
className="text-blue-600 hover:text-blue-800" | ||
> | ||
{posting.companyName} | ||
</a> | ||
</div> | ||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> | ||
<div className="flex items-center"> | ||
<BriefcaseIcon className="h-5 w-5 text-gray-500 mr-2" /> | ||
<span> | ||
경력: {posting.experienceLevel.name} | ||
{posting.experienceLevel.min > 0 && ` (${posting.experienceLevel.min}년 이상)`} | ||
</span> | ||
</div> | ||
|
||
<div className="flex items-center"> | ||
<AcademicCapIcon className="h-5 w-5 text-gray-500 mr-2" /> | ||
<span>학력: {posting.requireEducate.name}</span> | ||
</div> | ||
|
||
<div className="flex items-center"> | ||
<CalendarIcon className="h-5 w-5 text-gray-500 mr-2" /> | ||
<span>마감일: {new Date(posting.closeDate).toLocaleDateString()}</span> | ||
</div> | ||
|
||
<div className="flex items-center"> | ||
<span className="font-semibold text-blue-600">{posting.salary.name}</span> | ||
</div> | ||
</div> | ||
|
||
<div className="mb-8"> | ||
<h2 className="text-lg font-semibold mb-3">필요 기술</h2> | ||
<div className="flex flex-wrap gap-2"> | ||
{posting.jobSkillList.map((skill) => ( | ||
<span | ||
key={skill.jobSkillId} | ||
className="px-3 py-1 bg-blue-50 text-blue-700 rounded-full text-sm" | ||
> | ||
{skill.name} | ||
</span> | ||
))} | ||
</div> | ||
</div> | ||
|
||
<a | ||
href={posting.url} | ||
target="_blank" | ||
className="inline-block w-full text-center bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors duration-200" | ||
> | ||
지원하기 | ||
</a> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,99 @@ | ||
import Image from "next/image"; | ||
'use client'; | ||
|
||
import { useEffect, useState } from 'react'; | ||
import { JobPostingPageResponse } from '@/types/job-posting/JobPostingPageResponse'; | ||
import { getJobPostings } from '@/api/jobPosting'; | ||
import { BriefcaseIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; | ||
import JobPostingCard from '@/components/job/JobPostingCard'; | ||
|
||
export default function Home() { | ||
const [jobPostings, setJobPostings] = useState<JobPostingPageResponse[]>([]); | ||
const [isLoading, setIsLoading] = useState(true); | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
|
||
useEffect(() => { | ||
const fetchData = async () => { | ||
try { | ||
const response = await getJobPostings({ | ||
pageNum: 0, | ||
pageSize: 100 | ||
}); | ||
|
||
console.log('Jobs Response:', response); | ||
|
||
if (response.success) { | ||
setJobPostings(response.data.content); | ||
} | ||
} catch (error) { | ||
console.error('Failed to fetch job postings:', error); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
fetchData(); | ||
}, []); | ||
|
||
return ( | ||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8"> | ||
{/* 게시글 영역 */} | ||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | ||
{/* 게시글 자리 표시자 */} | ||
{[1, 2, 3, 4, 5, 6].map((item) => ( | ||
<div | ||
key={item} | ||
className="bg-white p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-200 min-h-[200px] flex flex-col" | ||
> | ||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/4"></div> | ||
<div className="mt-auto pt-4 flex justify-between items-center"> | ||
<div className="h-4 bg-gray-200 rounded w-1/4"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/4"></div> | ||
<main> | ||
{/* 히어로 섹션 */} | ||
<div className="bg-gradient-to-r from-blue-600 to-blue-800 text-white"> | ||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> | ||
<div className="text-center"> | ||
<h1 className="text-4xl font-bold mb-4"> | ||
IT 채용 정보를 한눈에 | ||
</h1> | ||
<p className="text-xl text-blue-100 mb-8"> | ||
최신 개발자 채용 공고를 찾아보세요 | ||
</p> | ||
|
||
{/* 검색바 */} | ||
<div className="max-w-2xl mx-auto"> | ||
<div className="relative"> | ||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" /> | ||
<input | ||
type="text" | ||
placeholder="기술 스택, 회사명으로 검색" | ||
className="w-full pl-10 pr-4 py-3 rounded-lg text-gray-900 focus:ring-2 focus:ring-blue-500 focus:outline-none" | ||
value={searchTerm} | ||
onChange={(e) => setSearchTerm(e.target.value)} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> | ||
{/* 채용 공고 섹션 */} | ||
<div className="mb-12"> | ||
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2 mb-6"> | ||
<BriefcaseIcon className="h-6 w-6 text-blue-600" /> | ||
채용 공고 | ||
</h2> | ||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | ||
{isLoading ? ( | ||
[...Array(6)].map((_, index) => ( | ||
<div | ||
key={`job-skeleton-${index}`} | ||
className="bg-white p-6 rounded-lg shadow-lg animate-pulse" | ||
> | ||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/4"></div> | ||
</div> | ||
)) | ||
) : ( | ||
jobPostings.map((posting) => ( | ||
<div key={posting.id} className="bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-1"> | ||
<JobPostingCard posting={posting} /> | ||
</div> | ||
)) | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</main> | ||
); | ||
} |
Oops, something went wrong.