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
74 changes: 38 additions & 36 deletions app/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import sharp from 'sharp';
import { put } from '@vercel/blob';

export async function POST(request: Request): Promise<NextResponse> {
const session = await getServerSession();
Expand All @@ -9,47 +10,48 @@ export async function POST(request: Request): Promise<NextResponse> {
return new NextResponse('Unauthorized', { status: 401 });
}

const body = (await request.json()) as HandleUploadBody;

try {
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (
pathname
/* clientPayload */
) => {
return {
allowedContentTypes: ['image/*'],
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');
}
},
const formData = await request.formData();
const file = formData.get('file') as File;

if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}

if (!file.type.startsWith('image/')) {
return NextResponse.json({ error: 'File must be an image' }, { status: 400 });
}

const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

const timestamp = Date.now();
const baseFilename = file.name.replace(/\.[^/.]+$/, '');
const pathname = `/images/${timestamp}-${baseFilename}.webp`;

// Convert to WebP with max 1920px width
const webpBuffer = await sharp(buffer)
.resize(1920, null, {
withoutEnlargement: true,
fit: 'inside',
})
.webp({ quality: 85 })
.toBuffer();

const blob = await put(pathname, webpBuffer, {
access: 'public',
contentType: 'image/webp',
});

return NextResponse.json(jsonResponse);
return NextResponse.json({
success: true,
url: blob.url,
});
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 400 } // The webhook will retry 5 times waiting for a 200
{ status: 500 }
);
}
}
9 changes: 6 additions & 3 deletions app/entities/post/write/BlogForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ const BlogForm = () => {

const [createSeriesOpen, setCreateSeriesOpen] = useState(false);
useBlockNavigate({ title: formData.title, content: formData.content || '' });

const handleFieldChange = (field: string, value: string | boolean | string[]) => {

const handleFieldChange = (
field: string,
value: string | boolean | string[]
) => {
setFormData({ [field]: value });
}
};

return (
<div className={'px-16'}>
Expand Down
27 changes: 18 additions & 9 deletions app/entities/post/write/UploadImageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { ChangeEvent, Dispatch, SetStateAction } from 'react';
import { FaImage } from 'react-icons/fa';
import UploadedImage from '@/app/entities/post/write/UploadedImage';
import { upload } from '@vercel/blob/client';

interface UploadImageContainerProps {
onClick: (link: string) => void;
Expand Down Expand Up @@ -34,14 +33,24 @@ const UploadImageContainer = ({
throw new Error('이미지 파일만 업로드할 수 있습니다.');
}

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

const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});

setUploadedImages((prev) => [...prev, newBlob.url]);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '업로드 실패');
}

const data = await response.json();

if (data.success && data.url) {
setUploadedImages((prev) => [...prev, data.url]);
}
}

return;
Expand Down Expand Up @@ -86,8 +95,8 @@ const UploadImageContainer = ({
'w-full border px-4 py-4 bg-gray-100 whitespace-nowrap space-x-4 overflow-x-scroll gap-2 min-h-40'
}
>
{uploadedImages.map((image, index) => (
<UploadedImage key={index} onClick={onClick} image={image} />
{uploadedImages.map((imageUrl, index) => (
<UploadedImage key={index} onClick={onClick} imageUrl={imageUrl} />
))}
</ul>
</div>
Expand Down
16 changes: 11 additions & 5 deletions app/entities/post/write/UploadedImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ import Image from 'next/image';

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

const UploadedImage = ({ onClick, image }: UploadedImageProps) => {
const UploadedImage = ({ onClick, imageUrl }: UploadedImageProps) => {
const markdownSyntax = `![이미지](${imageUrl})`;

const handleClick = () => {
onClick(markdownSyntax);
};

return (
<li
className={
'relative rounded-md overflow-hidden max-w-[240px] w-full h-full aspect-video inline-block hover:opacity-80 cursor-pointer hover:shadow-lg group'
}
onClick={() => onClick(image)}
onClick={handleClick}
>
<p
className={
Expand All @@ -22,10 +28,10 @@ const UploadedImage = ({ onClick, image }: UploadedImageProps) => {
</p>
<Image
className={'group object-cover'}
src={image}
src={imageUrl}
alt={'이미지'}
fill={true}
sizes={'400'}
sizes={'240px'}
/>
</li>
);
Expand Down