Skip to content

Commit

Permalink
Merge pull request #63 from prgrms-be-devcourse/feature/J-00FE
Browse files Browse the repository at this point in the history
[FEAT] [FE] : J-00FE
  • Loading branch information
sungyeong98 authored Feb 6, 2025
2 parents 9cd7790 + 82717f6 commit 6ec2e1f
Show file tree
Hide file tree
Showing 10 changed files with 743 additions and 102 deletions.
14 changes: 13 additions & 1 deletion frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ const nextConfig = {
// app 디렉토리 사용 명시
experimental: {
appDir: true
}
},
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_FRONTEND_URL: process.env.NEXT_PUBLIC_FRONTEND_URL,
},
async rewrites() {
return [
{
source: '/api/:path*',
destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`,
},
];
},
}

module.exports = nextConfig
30 changes: 16 additions & 14 deletions frontend/src/api/axios.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import axios from 'axios';

export const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
headers: {
'Content-Type': 'application/json',
},
// 인증이 필요한 요청을 위한 인스턴스
export const privateApi = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true
});

// 요청 인터셉터
axiosInstance.interceptors.request.use(
// 인증이 필요없는 요청을 위한 인스턴스
export const publicApi = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true
});

// 인증이 필요한 요청에만 토큰 추가
privateApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
Expand All @@ -22,16 +26,14 @@ axiosInstance.interceptors.request.use(
}
);

// 응답 인터셉터
axiosInstance.interceptors.response.use(
privateApi.interceptors.response.use(
(response) => response,
(error) => {
async (error) => {
if (error.response?.status === 401) {
// 인증 에러 처리
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);

export default axiosInstance;
export default publicApi; // 기본 export는 인증이 필요없는 인스턴스
8 changes: 8 additions & 0 deletions frontend/src/api/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiResponse } from '@/types/common/ApiResponse';
import { Category } from '@/types/post/Category';
import publicApi from './axios';

export const getCategories = async (): Promise<ApiResponse<Category[]>> => {
const response = await publicApi.get('/api/v1/category');
return response.data;
};
17 changes: 14 additions & 3 deletions frontend/src/api/post.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ApiResponse } from '@/types/common/ApiResponse';
import { PageResponse } from '@/types/common/PageResponse';
import { PostResponse } from '@/types/post/PostResponse';
import axiosInstance from './axios';
import publicApi from './axios';

interface GetPostsParams {
categoryId?: number;
Expand All @@ -12,11 +12,22 @@ interface GetPostsParams {
}

export const getPosts = async (params: GetPostsParams = {}): Promise<ApiResponse<PageResponse<PostResponse>>> => {
const response = await axiosInstance.get('/api/v1/post', { params });
const response = await publicApi.get('/api/v1/posts', { params });
return response.data;
};

export const getPost = async (id: number): Promise<ApiResponse<PostResponse>> => {
const response = await axiosInstance.get(`/api/v1/post/${id}`);
const response = await publicApi.get(`/api/v1/posts/${id}`);
return response.data;
};

interface CreatePostRequest {
subject: string;
content: string;
categoryId: number;
}

export const createPost = async (data: CreatePostRequest): Promise<ApiResponse<PostResponse>> => {
const response = await publicApi.post('/api/v1/posts', data);
return response.data;
};
128 changes: 113 additions & 15 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,65 @@
'use client';

import { useEffect, useState } from 'react';
import { PostResponse } from '@/types/post/PostResponse';
import { JobPostingPageResponse } from '@/types/job-posting/JobPostingPageResponse';
import { getPosts } from '@/api/post';
import { getJobPostings } from '@/api/jobPosting';
import { BriefcaseIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { formatDate } from '@/utils/dateUtils';
import { BriefcaseIcon, ChatBubbleLeftIcon, ArrowRightIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import JobPostingCard from '@/components/job/JobPostingCard';
import { Category } from '@/types/post/Category';
import { getCategories } from '@/api/category';

export default function Home() {
const [posts, setPosts] = useState<PostResponse[]>([]);
const [jobPostings, setJobPostings] = useState<JobPostingPageResponse[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [categories, setCategories] = useState<Category[]>([]);

useEffect(() => {
const fetchCategories = async () => {
try {
const response = await getCategories();
if (response.success) {
setCategories(response.data);
}
} catch (error) {
console.error('Failed to fetch categories:', error);
}
};

fetchCategories();
}, []);

useEffect(() => {
const fetchData = async () => {
try {
const response = await getJobPostings({
pageNum: 0,
pageSize: 100
const postsResponse = await getPosts({
page: 0,
size: 5, // 5개만 가져오기
sort: 'latest'
});

console.log('Jobs Response:', response);
if (postsResponse.success) {
setPosts(postsResponse.data.content);
}

if (response.success) {
setJobPostings(response.data.content);
// 최신 채용공고 5개만 가져오기
const jobsResponse = await getJobPostings({
pageNum: 0,
pageSize: 5
});

console.log('Jobs Response:', jobsResponse); // 응답 확인

if (jobsResponse.success) {
setJobPostings(jobsResponse.data.content);
}
} catch (error) {
console.error('Failed to fetch job postings:', error);
console.error('Failed to fetch data:', error);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -67,14 +101,23 @@ export default function Home() {
<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="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<BriefcaseIcon className="h-6 w-6 text-blue-600" />
최신 채용 공고
</h2>
<Link
href="/job-posting"
className="text-blue-600 hover:text-blue-800 flex items-center gap-1"
>
더 보기
<ArrowRightIcon className="h-4 w-4" />
</Link>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{isLoading ? (
[...Array(6)].map((_, index) => (
[...Array(3)].map((_, index) => (
<div
key={`job-skeleton-${index}`}
className="bg-white p-6 rounded-lg shadow-lg animate-pulse"
Expand All @@ -86,14 +129,69 @@ export default function Home() {
))
) : (
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">
<div key={posting.id} className="bg-white rounded-lg shadow-lg">
<JobPostingCard posting={posting} />
</div>
))
)}
</div>
</div>

{/* 게시글 섹션 */}
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<ChatBubbleLeftIcon className="h-6 w-6 text-blue-600" />
최신 게시글
</h2>
<Link
href="/post"
className="text-blue-600 hover:text-blue-800 flex items-center gap-1"
>
더 보기
<ArrowRightIcon className="h-4 w-4" />
</Link>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{isLoading ? (
[...Array(3)].map((_, index) => (
<div
key={`main-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>
))
) : (
posts.map((post) => (
<Link
key={`main-post-${post.id}`}
href={`/post/${post.id}`}
className="block bg-white rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:-translate-y-1 h-full"
>
<div className="p-6 flex flex-col h-full justify-between">
<div>
<h2 className="text-lg font-semibold mb-2 line-clamp-2">{post.subject}</h2>
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-blue-600">
{categories.find(cat => cat.id === String(post.categoryId))?.name}
</span>
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>익명</span>
<span>{formatDate(post.createdAt)}</span>
</div>
</div>
</Link>
))
)}
</div>
</div>
</div>
</main>
);
}
}
Loading

0 comments on commit 6ec2e1f

Please sign in to comment.