Skip to content
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
48 changes: 48 additions & 0 deletions app/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import { NextResponse } from 'next/server';

export async function POST(request: Request): Promise<NextResponse> {
const body = (await request.json()) as HandleUploadBody;

try {
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (
pathname
/* clientPayload */
) => {
return {
allowedContentTypes: ['image/jpeg', 'image/png', 'image/gif'],
tokenPayload: JSON.stringify({
// optional, sent to your server on upload completion
// you could pass a user id from auth, or a value from clientPayload
}),
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
// Get notified of client upload completion
// ⚠️ This will not work on `localhost` websites,
// Use ngrok or similar to get the full upload flow

console.log('blob upload completed', blob, tokenPayload);

try {
// Run any logic after the file upload completed
// const { userId } = JSON.parse(tokenPayload);
// await db.update({ avatar: blob.url, userId });
return;
} catch (error) {
throw new Error('Could not update user');
}
},
});

return NextResponse.json(jsonResponse);
} catch (error) {
return NextResponse.json(
{ error: (error as Error).message },
{ status: 400 } // The webhook will retry 5 times waiting for a 200
);
}
}
30 changes: 16 additions & 14 deletions app/entities/common/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
'use client';
import Link from 'next/link';
import useToast from '@/app/hooks/useToast';

const Footer = () => {
const toast = useToast();

return (
<footer
className={'w-screen bg-gray-950 min-h-96 flex flex-col justify-between'}
className={
'w-screen bg-neutral-200 dark:bg-gray-950 min-h-96 flex flex-col justify-between'
}
>
<section
className={'footer w-full flex flex-col md:flex-row justify-center'}
>
<div className={'footer-col'}>
<b>BLOG</b>
<div>
<p
className={
'text-left text-gray-100 font-serif whitespace-pre-wrap'
}
>
<div className={'text-weak'}>
<p className={'text-left font-serif whitespace-pre-wrap'}>
a developer who never stops growing.
</p>
<p
className={
'text-left text-gray-100 font-serif whitespace-pre-wrap'
}
>
<p className={'text-left font-serif whitespace-pre-wrap'}>
성장을 멈추지 않는 개발자.
</p>
</div>
Expand All @@ -48,7 +46,7 @@ const Footer = () => {
<div className={'footer-col'}>
<b>Subscribe</b>
<form className={'flex flex-col gap-4'}>
<p className={'text-gray-300'}>새 글을 구독해보세요</p>
<p className={'text-default'}>새 글을 구독해보세요</p>
<input
className={
'border-b bg-transparent px-4 py-1.5 inset-3 outline-black'
Expand All @@ -63,9 +61,13 @@ const Footer = () => {
/>
<button
className={
'rounded-md border bg-transparent py-3 w-1/2 shadow-lg hover:bg-white hover:text-black transition'
'rounded-md border bg-transparent py-3 w-1/2 border-black hover:shadow-lg hover:bg-white hover:text-black transition'
}
aria-label={'구독 버튼'}
onClick={(e) => {
e.preventDefault();
toast.error('새 글 구독은 아직 지원하지 않는 기능입니다.');
}}
>
Subscribe
</button>
Expand Down
8 changes: 7 additions & 1 deletion app/entities/post/write/BlogForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Overlay from '@/app/entities/common/Overlay/Overlay';
import { FaPlus } from 'react-icons/fa6';
import CreateSeriesOverlayContainer from '@/app/entities/series/CreateSeriesOverlayContainer';
import { getAllSeriesData } from '@/app/entities/series/api/series';
import LoadingIndicator from '@/app/entities/common/Loading/LoadingIndicator';
import UploadImageContainer from '@/app/entities/post/write/UploadImageContainer';

const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });

Expand Down Expand Up @@ -135,6 +135,11 @@ const BlogForm = () => {
}
};

const handleLinkCopy = (image: string) => {
navigator.clipboard.writeText(image);
toast.success('이미지 링크가 복사되었습니다.');
};

return (
<div className={'px-16'}>
<input
Expand Down Expand Up @@ -193,6 +198,7 @@ const BlogForm = () => {
height={500}
visibleDragbar={false}
/>
<UploadImageContainer onClick={handleLinkCopy} />
{errors && (
<div className={'mt-2'}>
{errors.slice(0, 3).map((error, index) => (
Expand Down
80 changes: 80 additions & 0 deletions app/entities/post/write/UploadImageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';
import UploadedImage from '@/app/entities/post/write/UploadedImage';
import { FaImage } from 'react-icons/fa';
import { upload } from '@vercel/blob/client';
import { ChangeEvent, useState } from 'react';

interface UploadImageContainerProps {
onClick: (link: string) => void;
}
const UploadImageContainer = ({ onClick }: UploadImageContainerProps) => {
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
const uploadToBlob = async (event: ChangeEvent) => {
try {
event.preventDefault();
const target = event.target as HTMLInputElement;
if (!target.files) {
throw new Error('이미지가 선택되지 않았습니다.');
}

const file = target.files[0];

const timestamp = new Date().getTime();
const pathname = `/images/${timestamp}-${file.name}`;
const newBlob = await upload(pathname, file, {
access: 'public',
handleUploadUrl: '/api/upload',
});

setUploadedImages([...uploadedImages, newBlob.url]);
return;
} catch (error) {
console.error('업로드 실패:', error);
throw error;
}
};

return (
<div className={'w-full mt-4'}>
<div className={'flex justify-between my-1'}>
<div>
<span className={'text-xl font-bold'}>업로드된 이미지</span>
<p>클릭하여 링크 복사</p>
</div>
<div
className={
'cursor-pointer relative w-12 h-12 bg-emerald-500 rounded-md overflow-hidden'
}
>
<FaImage
className={
'absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 pointer-events-none'
}
/>
<input
type={'file'}
multiple={true}
placeholder={'이미지 업로드'}
onChange={uploadToBlob}
className={
'w-full h-full file:hidden text-transparent px-2 hover:bg-emerald-600'
}
accept={'image/*'}
></input>
</div>
</div>

<ul
className={
'w-full border-t border-b px-4 py-4 bg-gray-100 whitespace-nowrap space-x-4 overflow-x-scroll gap-2'
}
>
{uploadedImages.map((image, index) => (
<UploadedImage key={index} onClick={onClick} image={image} />
))}
</ul>
</div>
);
};

export default UploadImageContainer;
34 changes: 34 additions & 0 deletions app/entities/post/write/UploadedImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Image from 'next/image';

interface UploadedImageProps {
onClick: (link: string) => void;
image: string;
}

const UploadedImage = ({ onClick, image }: UploadedImageProps) => {
return (
<li
className={
'relative rounded-md overflow-hidden w-1/3 aspect-video inline-block hover:opacity-80 cursor-pointer hover:shadow-lg group'
}
onClick={() => onClick(image)}
>
<p
className={
' z-10 absolute opacity-0 group-hover:opacity-100 top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 text-xl font-bold text-black'
}
>
링크 복사
</p>
<Image
className={'group object-cover'}
src={image}
alt={'이미지'}
fill={true}
sizes={'400'}
/>
</li>
);
};

export default UploadedImage;
39 changes: 39 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
--text-default: #000000;
--text-overlay: #FFFFFF;
--bg-overlay: #1e201e;
--text-weak: #474b4e;
}
.dark {
--background: #1e201e;
Expand All @@ -19,6 +20,7 @@
--text-default: #FFFFFF;
--text-overlay: #000000;
--bg-overlay: #ededed;
--text-weak: rgba(255, 255, 255, 0.7);
}


Expand Down Expand Up @@ -87,6 +89,16 @@ article.post .post-body {
line-height: 150%;
}

article.post .post-body p:has(img) {
text-align: center;
}

article.post .post-body img {
max-width: 100%;
height: auto;
margin: 1em 0;
}

article.post h3 {
font-size: 1.25rem;
font-weight: bold;
Expand Down Expand Up @@ -118,6 +130,11 @@ section.footer > div.footer-col b {
font-weight: bold;
}

section.footer input {
background-color: white;
border-radius: 4px;
}

section.footer a:hover {
border-bottom: 1px solid white;
}
Expand All @@ -132,3 +149,25 @@ section.footer .disabled {
transition-delay: unset;
transition-timing-function: ease-in-out;
}

/* HR Style */
article.post .post-body hr {
border: 0px solid;
height: 1px;
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 100, 0, 0.75), rgba(0, 0, 0, 0));
margin: 32px 0px;
display: block;
}

article.post .post-body hr::before {
position: absolute;
background-color: #efefef;
border: 10px solid #006400;
border-top: 10px solid transparent;
border-radius: 2px;
padding: 0px;
transform: rotate(180deg);
left: 50%;
margin: -5px 0px 0px -21px;
content: "";
}
17 changes: 8 additions & 9 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export default function Home() {
getPosts();
}, []);

const linkedinLink =
'https://www.linkedin.com/in/%EC%A0%95%EC%9A%B0-%EC%84%9C-9a0b79312/';
const githubLink = 'https://github.com/ShipFriend0516';

return (
<main className="w-full max-w-4xl mx-auto grid gap-16 p-8">
{/* Hero Section */}
Expand Down Expand Up @@ -66,16 +70,11 @@ export default function Home() {
학습과 성장을 추구합니다.
</p>
<div className="flex gap-4">
<a href={'https://github.com/ShipFriend0516'} target={'_blank'}>
<FaGithub className="w-5 h-5 text-default hover:text-gray-100 hover:scale-125 transition cursor-pointer" />
<a href={githubLink} target={'_blank'}>
<FaGithub className="w-5 h-5 text-default hover:scale-125 transition cursor-pointer" />
</a>
<a
href={
'https://www.linkedin.com/in/%EC%A0%95%EC%9A%B0-%EC%84%9C-9a0b79312/'
}
target={'_blank'}
>
<FaLinkedin className="w-5 h-5 text-default hover:text-gray-100 hover:scale-125 transition cursor-pointer" />
<a href={linkedinLink} target={'_blank'}>
<FaLinkedin className="w-5 h-5 text-default hover:scale-125 transition cursor-pointer" />
</a>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/series/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const SeriesListPage = () => {
}

return (
<section className={'w-full p-10'}>
<section className={'w-full p-10 max-w-5xl mx-auto'}>
<h1 className={'text-4xl font-bold mt-4'}>시리즈</h1>
<p className={'text-lg mb-4'}>시리즈별로 글을 확인해보세요.</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
Expand Down
2 changes: 2 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ const config: Config = {
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class',
theme: {
extend: {
colors: {
default: 'var(--text-default)',
background: 'var(--background)',
foreground: 'var(--foreground)',
overlay: 'var(--bg-overlay)',
weak: 'var(--text-weak)',
},
textColor: {
overlay: 'var(--text-overlay)',
Expand Down
Loading