- 프론트엔드 개발자로서 React, Next.js, TypeScript를 주로 사용합니다.
- 항상 사용자 입장에서 생각하고, 성능 최적화에 관심이 많으며, 지속적인
+
+ Software Engineer로서 React, TypeScript를 주로 사용합니다. 항상
+ 확장성에 대해서 고민하고, 성능 최적화에 관심이 많으며, 지속적인
학습과 성장을 추구합니다.
diff --git a/app/entities/profile/Experience.tsx b/app/entities/profile/Experience.tsx
new file mode 100644
index 0000000..84afc8f
--- /dev/null
+++ b/app/entities/profile/Experience.tsx
@@ -0,0 +1,72 @@
+import { HiOutlineAcademicCap, HiOutlineBriefcase } from 'react-icons/hi';
+import SectionHeading from '../common/SectionHeading';
+
+type ExperienceType = 'work' | 'education';
+
+interface ExperienceItem {
+ company: string;
+ role: string;
+ period: string;
+ type: ExperienceType;
+ current?: boolean;
+}
+
+const experiences: ExperienceItem[] = [
+ {
+ company: 'CJ올리브영',
+ role: 'AI플랫폼팀 Intern',
+ period: '2025.09 ~',
+ type: 'work',
+ current: true,
+ },
+ {
+ company: '네이버 부스트캠프 9기 웹·모바일',
+ role: '웹풀스택 챌린지, 멤버십 수료',
+ period: '2024.07 ~ 2024.12',
+ type: 'education',
+ },
+];
+
+const Experience = () => {
+ return (
+
+
+
+ {experiences.map((exp) => (
+
+
+ {exp.type === 'work' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {exp.company}
+
+ {exp.current && (
+
+ 현재
+
+ )}
+
+
+ {exp.role}
+
+
+
+ {exp.period}
+
+
+ ))}
+
+
+ );
+};
+
+export default Experience;
diff --git a/app/entities/profile/FeaturedProjects.tsx b/app/entities/profile/FeaturedProjects.tsx
index 90cfc1f..1cdfe06 100644
--- a/app/entities/profile/FeaturedProjects.tsx
+++ b/app/entities/profile/FeaturedProjects.tsx
@@ -1,4 +1,5 @@
import { Project } from '@/app/types/Portfolio';
+import SectionHeading from '../common/SectionHeading';
import PortfolioPreview from '../portfolio/PortfolioPreview';
interface FeaturedProjectsProps {
@@ -7,14 +8,9 @@ interface FeaturedProjectsProps {
const FeaturedProjects = ({ projects }: FeaturedProjectsProps) => {
return (
-
-
-
- Featured Projects
-
-
-
-
+
+
+
{projects.map((project) => {
return
;
})}
diff --git a/app/entities/profile/HeroBanner.tsx b/app/entities/profile/HeroBanner.tsx
index 0a2a555..2f59b0d 100644
--- a/app/entities/profile/HeroBanner.tsx
+++ b/app/entities/profile/HeroBanner.tsx
@@ -1,10 +1,11 @@
import Image from 'next/image';
+import { RiDoubleQuotesR } from 'react-icons/ri';
import DecryptedText from '../bits/DecryptedText';
const HeroBanner = () => {
return (
-
+
{
className="object-cover bg-gray-100 w-full h-full transition-transform duration-700 group-hover:scale-105"
/>
-
+
-
+
{
encryptedClassName="text-neutral-200/90"
/>
-
+
Jeongwoo Seo
@@ -46,14 +47,20 @@ const HeroBanner = () => {
-
-
- 안녕하세요, 서정우입니다.
-
-
- 깔끔한 코드 작성에 중점을 두고, 확장성에 대해 고민하며 멈추지 않는
- 기술의 변화를 즐깁니다.
-
+
+
+
+
+
+
+ 안녕하세요, 서정우입니다.
+
+
+ 깔끔한 코드 작성에 중점을 두고, 확장성에 대해 고민하며 멈추지 않는
+ 기술의 변화를 즐깁니다.
+
+
+
);
};
diff --git a/app/entities/profile/LatestArticles.tsx b/app/entities/profile/LatestArticles.tsx
index 75dd3f5..ca1bf09 100644
--- a/app/entities/profile/LatestArticles.tsx
+++ b/app/entities/profile/LatestArticles.tsx
@@ -3,6 +3,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { Post } from '@/app/types/Post';
import ErrorBox from '../common/Error/ErrorBox';
+import SectionHeading from '../common/SectionHeading';
import Skeleton from '../common/Skeleton/Skeleton';
interface LatestArticlesProps {
@@ -17,13 +18,8 @@ const LatestArticles = ({
error = null,
}: LatestArticlesProps) => {
return (
-
-
-
- Latest Articles
-
-
-
+
+
{loading ? (
<>
@@ -39,7 +35,7 @@ const LatestArticles = ({
key={post._id}
className="group cursor-pointer bg-gradient-to-br from-gray-50 to-gray-100 dark:from-primary-rich rounded-2xl overflow-hidden shadow transition-all duration-300 border border-gray-200 dark:border-gray-700 hover:scale-[1.02]"
>
-
+
-
+
{post.title}
@@ -75,7 +71,7 @@ const LatestArticleSkeleton = () => {
'flex flex-col gap-3 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-primary-rich rounded-2xl overflow-hidden shadow-2xl border border-gray-200 dark:border-gray-700'
}
>
-
+
diff --git a/app/favicon.ico b/app/favicon.ico
index 5e7df3c..b7555a8 100644
Binary files a/app/favicon.ico and b/app/favicon.ico differ
diff --git a/app/hooks/useToast.ts b/app/hooks/useToast.ts
index b8c4662..f0583e8 100644
--- a/app/hooks/useToast.ts
+++ b/app/hooks/useToast.ts
@@ -1,25 +1,33 @@
import { useCallback, useEffect, useRef } from 'react';
import useToastStore from '@/app/stores/useToastStore';
+const DEFAULT_DURATION = 5000;
+
+interface ToastOptions {
+ title?: string;
+ duration?: number;
+}
+
const useToast = () => {
const { createToast, removeToast } = useToastStore();
- const DURATION = 5000;
const timerIDs = useRef
[]>([]);
const toast = useCallback(
- (message: string, type: 'success' | 'error') => {
+ (message: string, type: 'success' | 'error' | 'info', options?: ToastOptions) => {
+ const duration = options?.duration ?? DEFAULT_DURATION;
const newToast = {
id: new Date().getTime() + Math.floor(Math.random() * 200),
message,
+ title: options?.title,
type,
- duration: DURATION || 5000,
+ duration,
};
createToast(newToast);
const id = setTimeout(() => {
removeToast(newToast.id);
timerIDs.current = timerIDs.current.filter((timerId) => timerId !== id);
- }, DURATION);
+ }, duration);
},
[createToast, removeToast]
);
@@ -31,20 +39,27 @@ const useToast = () => {
}, []);
const success = useCallback(
- (message: string) => {
- toast(message, 'success');
+ (message: string, options?: ToastOptions) => {
+ toast(message, 'success', options);
},
[toast]
);
const error = useCallback(
- (message: string) => {
- toast(message, 'error');
+ (message: string, options?: ToastOptions) => {
+ toast(message, 'error', options);
+ },
+ [toast]
+ );
+
+ const info = useCallback(
+ (message: string, options?: ToastOptions) => {
+ toast(message, 'info', options);
},
[toast]
);
- return { success, error };
+ return { success, error, info };
};
export default useToast;
diff --git a/app/layout.tsx b/app/layout.tsx
index 226df5a..935e7b2 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -12,6 +12,22 @@ export const metadata: Metadata = {
description: '문제 해결 경험과 개발 지식을 공유하는 개발 블로그입니다.',
icons: {
icon: '/favicon.ico',
+ apple: '/assets/apple-touch-icon.png',
+ shortcut: '/favicon-16.png',
+ other: [
+ {
+ rel: 'icon',
+ url: '/favicon-32.png',
+ sizes: '32x32',
+ type: 'image/png',
+ },
+ {
+ rel: 'icon',
+ url: '/favicon-16.png',
+ sizes: '16x16',
+ type: 'image/png',
+ },
+ ],
},
keywords: [
'ShipFriend',
diff --git a/app/lib/constants/landingPageData.ts b/app/lib/constants/landingPageData.ts
index 618fd8a..59d802f 100644
--- a/app/lib/constants/landingPageData.ts
+++ b/app/lib/constants/landingPageData.ts
@@ -7,9 +7,10 @@ export const githubLink = 'https://github.com/ShipFriend0516';
export const projects: Project[] = [
{
title: 'PREVIEW',
- description: 'WebRTC 기반 화상 면접 스터디 플랫폼',
+ description:
+ '네이버 부스트캠프 9기 최종 프로젝트 WebRTC 기반 화상 면접 스터디 플랫폼',
image: '/images/logo/preview-logo.png',
- tags: ['React', 'WebRTC', 'Fullstack'],
+ tags: ['React', 'WebRTC'],
githubUrl: 'https://github.com/boostcampwm-2024/web27-Preview',
slug: 'preview',
},
@@ -17,7 +18,7 @@ export const projects: Project[] = [
title: 'ShipFriend TechBlog',
description: '개발 관련 글을 작성하고 공유하는 기술 블로그 플랫폼',
image: '/images/logo/shipfriend-logo.webp',
- tags: ['Next.js', 'TypeScript', 'MongoDB'],
+ tags: ['Next.js', 'TypeScript', 'MongoDB', 'Fullstack'],
demoUrl: 'https://shipfriend.dev',
githubUrl: 'https://github.com/ShipFriend0516/TechBlog',
slug: 'shipfriend',
diff --git a/app/page.tsx b/app/page.tsx
index b76b27e..99b1264 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,13 +1,12 @@
'use client';
-import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import useDataFetch from '@/app/hooks/common/useDataFetch';
-import useShortcut from '@/app/hooks/common/useShortcut';
import useFingerprint from '@/app/hooks/useFingerprint';
import useToast from '@/app/hooks/useToast';
import { projects } from '@/app/lib/constants/landingPageData';
import { Post } from '@/app/types/Post';
import AboutMe from './entities/profile/AboutMe';
+import Experience from './entities/profile/Experience';
import FeaturedProjects from './entities/profile/FeaturedProjects';
import HeroBanner from './entities/profile/HeroBanner';
import LatestArticles from './entities/profile/LatestArticles';
@@ -16,7 +15,6 @@ import MoreExplore from './entities/profile/MoreExplore';
export default function Home() {
const { fingerprint } = useFingerprint();
const toast = useToast();
- const router = useRouter();
const fetchConfig = {
method: 'GET' as const,
@@ -38,23 +36,11 @@ export default function Home() {
}
}, [fingerprint]);
- const goToWritePage = () => {
- toast.success('글쓰기 페이지로 이동합니다...');
- router.push('/admin/write');
- };
-
- const goToPostsPage = () => {
- toast.success('모든 글 목록 페이지로 이동합니다...');
- router.push('/posts');
- };
-
- useShortcut(goToWritePage, ['Alt', 'N'], true);
- useShortcut(goToPostsPage, ['Ctrl', ';'], true);
-
return (
-
+
+
diff --git a/app/posts/[slug]/page.tsx b/app/posts/[slug]/page.tsx
index 96f2944..7324240 100644
--- a/app/posts/[slug]/page.tsx
+++ b/app/posts/[slug]/page.tsx
@@ -5,6 +5,7 @@ import PostActionSection from '@/app/entities/post/detail/PostActionSection';
import PostDetail from '@/app/entities/post/detail/PostDetail';
import PostJSONLd from '@/app/entities/post/detail/PostJSONLd';
import PostRecommendation from '@/app/entities/post/detail/PostRecommendation';
+import SubscribeToast from '@/app/entities/post/detail/SubscribeToast';
import dbConnect from '@/app/lib/dbConnect';
import Post from '@/app/models/Post';
@@ -83,6 +84,7 @@ const BlogDetailPage = async ({ params }: { params: { slug: string } }) => {
currentPostId={post?._id}
seriesId={post?.seriesId}
/>
+
>
diff --git a/app/stores/useToastStore.ts b/app/stores/useToastStore.ts
index d10645e..3e229bb 100644
--- a/app/stores/useToastStore.ts
+++ b/app/stores/useToastStore.ts
@@ -1,9 +1,13 @@
import { create } from "zustand";
+type ToastType = "success" | "error" | "info";
+
interface Toast {
id: number;
message: string;
- type: "success" | "error";
+ title?: string;
+ type: ToastType;
+ duration?: number;
}
interface ToastState {
diff --git a/public/assets/android-chrome-192x192.png b/public/assets/android-chrome-192x192.png
new file mode 100644
index 0000000..792aeba
Binary files /dev/null and b/public/assets/android-chrome-192x192.png differ
diff --git a/public/assets/android-chrome-512x512.png b/public/assets/android-chrome-512x512.png
new file mode 100644
index 0000000..2dd49d2
Binary files /dev/null and b/public/assets/android-chrome-512x512.png differ
diff --git a/public/assets/apple-touch-icon.png b/public/assets/apple-touch-icon.png
new file mode 100644
index 0000000..2f9986b
Binary files /dev/null and b/public/assets/apple-touch-icon.png differ
diff --git a/public/assets/favicon-16x16.png b/public/assets/favicon-16x16.png
new file mode 100644
index 0000000..6625442
Binary files /dev/null and b/public/assets/favicon-16x16.png differ
diff --git a/public/assets/favicon-32x32.png b/public/assets/favicon-32x32.png
new file mode 100644
index 0000000..e8986cb
Binary files /dev/null and b/public/assets/favicon-32x32.png differ
diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico
new file mode 100644
index 0000000..b7555a8
Binary files /dev/null and b/public/assets/favicon.ico differ