Skip to content

Commit

Permalink
Merge pull request #125 from ITZipProject/main
Browse files Browse the repository at this point in the history
next.js 스탠드얼론 전환
  • Loading branch information
jukrap2 authored Nov 19, 2024
2 parents 592d925 + 72fe4c2 commit 5b51028
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 86 deletions.
13 changes: 13 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { withSentryConfig } from '@sentry/nextjs';
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
poweredByHeader: false,
eslint: {
ignoreDuringBuilds: true,
},
output: 'standalone',
images: {
unoptimized: true,
remotePatterns: [
{
protocol: 'https',
Expand All @@ -19,6 +25,13 @@ const nextConfig = {
},
],
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};

export default withSentryConfig(nextConfig, {
Expand Down
10 changes: 7 additions & 3 deletions src/components/blog/main/BlogPostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { animated } from '@react-spring/web';
import Image from 'next/image';
import Link from 'next/link';
import React from 'react';

import { BlogPostCardProps } from '@/types/blog/common';

// BlogPostCardProps 인터페이스 제거
const BlogPostCard: React.FC<BlogPostCardProps> = ({
id,
title,
Expand All @@ -16,9 +16,13 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({
timeAgo,
imageUrl,
profileImageUrl,
style,
}) => {
return (
<div className="flex h-[400px] w-full max-w-[300px] flex-col overflow-hidden">
<animated.div
style={style}
className="flex h-[400px] w-full max-w-[300px] flex-col overflow-hidden"
>
<Link href={`/blog/post/${id}`} passHref className="relative h-[180px]">
<Image src={imageUrl} className="rounded-lg" alt={title} layout="fill" objectFit="cover" />
</Link>
Expand Down Expand Up @@ -68,7 +72,7 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({
<p className="text-xs text-gray-400">{timeAgo}</p>
</div>
</div>
</div>
</animated.div>
);
};

Expand Down
52 changes: 52 additions & 0 deletions src/components/blog/main/BlogPostCardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';

const BlogPostCardSkeleton: React.FC = () => {
return (
<div className="flex h-[400px] w-full max-w-[300px] flex-col overflow-hidden">
{/* Image placeholder - same size as the real image container */}
<div className="relative h-[180px] rounded-lg bg-slate-200" />

{/* Category and likes section */}
<div className="flex items-center justify-between py-4 text-sm">
<div className="flex items-center">
<div className="flex items-center gap-2 pr-1">
{/* Main category */}
<div className="h-5 w-16 rounded bg-slate-200" />
{/* Icon */}
<div className="size-4 rounded bg-slate-200" />
{/* Sub category */}
<div className="h-5 w-20 rounded bg-slate-200" />
</div>
</div>
{/* Likes count */}
<div className="ml-auto h-5 w-20 rounded bg-slate-200" />
</div>

{/* Title and content */}
<div className="grow">
{/* Title */}
<div className="mb-2 h-6 w-4/5 rounded bg-slate-200" />
{/* Content - 3 lines */}
<div className="space-y-2">
<div className="h-4 w-full rounded bg-slate-200" />
<div className="h-4 w-full rounded bg-slate-200" />
<div className="h-4 w-3/4 rounded bg-slate-200" />
</div>
</div>

{/* Author section */}
<div className="flex items-center py-4">
{/* Profile image */}
<div className="size-8 rounded-full bg-slate-200" />
<div className="ml-3">
{/* Author name */}
<div className="mb-1 h-4 w-20 rounded bg-slate-200" />
{/* Time ago */}
<div className="h-3 w-16 rounded bg-slate-200" />
</div>
</div>
</div>
);
};

export default BlogPostCardSkeleton;
129 changes: 48 additions & 81 deletions src/components/main/BlogPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,38 @@
'use client';
import { useTransition, animated } from '@react-spring/web';
import React, { useState, useEffect } from 'react';

import { getCategoryById } from '@/types/blog/category';
import { ApiResponse, BlogPost, AnimatedStyleProps } from '@/types/blog/common';

import BlogPostCard from '../blog/main/BlogPostCard';

interface PostPreviewResponse {
postId: string;
categoryId: string;
title: string;
content: string;
thumbnailImagePath: string;
likeCount: number;
profileImagePath: string;
author: string;
createDate: string;
links: Array<any>;
}

interface ApiResponse {
status: string;
msg: string;
data: {
content: PostPreviewResponse[];
links: Array<any>;
page: {
size: number;
totalElements: number;
totalPages: number;
number: number;
};
};
code: string;
}

interface Post {
id: string;
title: string;
content: string;
category: string;
subCategory: string;
likes: number;
saves: number;
author: string;
timeAgo: string;
imageUrl: string;
profileImageUrl: string;
}
import BlogPostCardSkeleton from '../blog/main/BlogPostCardSkeleton';

const combineURLs = (baseURL: string, relativeURL: string): string => {
return `${baseURL.replace(/\/+$/, '')}/${relativeURL.replace(/^\/+/, '')}`;
};

const AnimatedBlogPostCard = animated(BlogPostCard);

const BlogPreview: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [posts, setPosts] = useState<BlogPost[]>([]);
const [error, setError] = useState<string | null>(null);
const [postsToShow, setPostsToShow] = useState(4);
const [loading, setLoading] = useState(true);
const [initialFetchSize] = useState(4); // 초기 fetch 크기

const transitions = useTransition<BlogPost, AnimatedStyleProps>(
loading ? [] : posts.slice(0, postsToShow),
{
keys: (post) => post.id,
from: { opacity: 0, transform: 'translateY(20px)' },
enter: { opacity: 1, transform: 'translateY(0px)' },
trail: 100,
config: { tension: 300, friction: 20 },
},
);

// resize 이벤트 핸들러
useEffect(() => {
const handleResize = () => {
if (window.matchMedia('(min-width: 768px) and (max-width: 992px)').matches) {
Expand All @@ -68,27 +44,22 @@ const BlogPreview: React.FC = () => {
}
};

// 초기 실행
handleResize();

// resize 이벤트 리스너 등록
window.addEventListener('resize', handleResize);

// cleanup
return () => window.removeEventListener('resize', handleResize);
}, []);

// 데이터 fetch는 initialFetchSize만 의존
useEffect(() => {
const fetchPosts = async () => {
setLoading(true);
try {
if (!process.env.NEXT_PUBLIC_API_URL) {
throw new Error('API URL이 설정되지 않았습니다.');
}

const apiUrl = combineURLs(process.env.NEXT_PUBLIC_API_URL, 'tech-info/posts/preview');
const fullUrl = `${apiUrl}?sortType=LIKECOUNT&page=0&size=${postsToShow}`;

console.log('Fetching posts from:', fullUrl);
const fullUrl = `${apiUrl}?sortType=LIKECOUNT&page=0&size=${initialFetchSize}`;

const response = await fetch(fullUrl, {
headers: {
Expand All @@ -97,11 +68,6 @@ const BlogPreview: React.FC = () => {
},
});

console.log('Response status:', response.status);

const responseText = await response.text();
console.log('Raw response:', responseText);

if (!response.ok) {
switch (response.status) {
case 404:
Expand All @@ -117,24 +83,20 @@ const BlogPreview: React.FC = () => {
}
}

const responseText = await response.text();
let data: ApiResponse;
try {
data = JSON.parse(responseText) as ApiResponse;
} catch (parseError) {
console.error('JSON 파싱 에러:', parseError);
console.error('받은 응답:', responseText);
throw new Error('응답을 JSON으로 파싱할 수 없습니다. 서버 응답을 확인해주세요.');
}

if (!data.data.content || data.data.content.length === 0) {
console.log('No posts found in response');
setPosts([]);
return;
}

console.log('Posts found:', data.data.content.length);

const transformedPosts: Post[] = data.data.content.map((post) => {
const transformedPosts: BlogPost[] = data.data.content.map((post) => {
const categoryInfo = getCategoryById(post.categoryId);
const timeAgo = getTimeAgo(new Date(post.createDate));

Expand All @@ -145,7 +107,7 @@ const BlogPreview: React.FC = () => {
category: categoryInfo?.mainCategory || '기타',
subCategory: categoryInfo?.subCategory || '기타',
likes: post.likeCount,
saves: 0, // 기본값 설정
saves: 0,
author: post.author,
timeAgo,
imageUrl: post.thumbnailImagePath,
Expand All @@ -157,47 +119,52 @@ const BlogPreview: React.FC = () => {
} catch (err) {
console.error('Error details:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch posts');
} finally {
setLoading(false);
}
};

void fetchPosts();
}, [postsToShow]); // postsToShow가 변경될 때마다 새로 fetch

}, [initialFetchSize]);
if (error) {
return (
<div className="rounded-lg bg-red-50 p-4">
<div className="text-red-800">에러가 발생했습니다: {error}</div>
<div className="mt-2 text-sm text-red-600">
개발자 도구의 콘솔에서 자세한 에러 내용을 확인할 수 있습니다.
<div className="mx-auto max-w-7xl px-4 pb-36">
<div className="mx-auto max-w-7xl">
<h2 className="mb-8 ml-2 mt-24 text-3xl font-semibold leading-relaxed tracking-tight">
지금 핫한 블로그 글은?
</h2>
<div className="grid grid-cols-1 justify-items-center gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: postsToShow }).map((_, index) => (
<BlogPostCardSkeleton key={index} />
))}
</div>
</div>
</div>
);
}

if (posts.length === 0) {
return (
<div className="rounded-lg bg-blue-50 p-4">
<div className="text-blue-800">현재 등록된 게시글이 없습니다.</div>
</div>
);
}
return (
<div className="mx-auto max-w-7xl px-4 pb-36">
<div className="mx-auto max-w-7xl">
<h2 className="mb-8 ml-2 mt-24 text-3xl font-semibold leading-relaxed tracking-tight">
지금 핫한 블로그 글은?
</h2>
<div className="grid grid-cols-1 justify-items-center gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{posts.slice(0, postsToShow).map((post) => (
<BlogPostCard key={post.id} {...post} />
))}
{loading
? // 로딩 중일 때만 스켈레톤 표시
Array.from({ length: postsToShow }).map((_, index) => (
<BlogPostCardSkeleton key={`skeleton-${index}`} />
))
: // 데이터가 있을 때는 애니메이션된 카드 표시
transitions((style, post) => (
<AnimatedBlogPostCard style={style} key={post.id} {...post} />
))}
</div>
</div>
</div>
);
};

// 시간 경과 계산 헬퍼 함수
const getTimeAgo = (date: Date): string => {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
Expand Down
Loading

0 comments on commit 5b51028

Please sign in to comment.