diff --git a/next.config.mjs b/next.config.mjs
index 72a796a..26a0ecb 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -3,9 +3,9 @@ const nextConfig = {
output: "standalone",
images: {
formats: ["image/avif", "image/webp"],
- minimumCacheTTL: 60,
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
- imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
+ minimumCacheTTL: 60 * 60 * 24 * 30,
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920],
+ imageSizes: [16, 32, 64, 96, 128, 256],
remotePatterns: [
{
protocol: "https",
@@ -27,17 +27,22 @@ const nextConfig = {
// 정적 이미지 최적화
webpack(config) {
config.module.rules.push({
- test: /\.(webp)$/i,
+ test: /\.(webp|avif)$/i,
use: [
{
loader: 'image-webpack-loader',
options: {
webp: {
- quality: 75,
+ quality: 50,
lossless: false,
progressive: true,
optimizationLevel: 3,
},
+ avif: {
+ quality: 50,
+ lossless: false,
+ speed: 5,
+ },
},
},
],
diff --git a/src/app/albums/[id]/page.tsx b/src/app/albums/[id]/page.tsx
index 9d4a0d6..ffdf7b3 100644
--- a/src/app/albums/[id]/page.tsx
+++ b/src/app/albums/[id]/page.tsx
@@ -14,7 +14,6 @@ async function getAlbum(id: string) {
"Content-Type": "application/json",
},
credentials: 'include',
- cache: 'no-store',
});
if (!response.ok) {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 1acc346..a100596 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,3 +1,4 @@
+import { cache } from 'react';
import "./globals.css";
import type { Metadata } from "next";
import { AuthProvider } from "@/contexts/auth/AuthContext";
@@ -6,25 +7,13 @@ import { Header } from "@/components/layout/Header";
import { BackgroundImage } from "@/components/layout/BackgroundImage";
import { getAuthCookie } from "@/lib/server-auth";
import { ThemeProvider } from "@/contexts/theme/ThemeContext";
+import { UserProvider } from "@/contexts/auth/UserContext";
-export const metadata: Metadata = {
- title: "SOFO",
- description: "SOund FOrest",
- icons: {
- icon: "/images/logo.svg",
- },
-};
-
-export const viewport = {
- width: 'device-width',
- initialScale: 1,
-};
-
-async function getUser() {
+// getUser를 layout 레벨에서 캐싱
+const getUser = cache(async () => {
const accessToken = getAuthCookie();
if (!accessToken) {
- console.log('No access token found');
return null;
}
@@ -33,42 +22,53 @@ async function getUser() {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
- Cookie: `access_token=${accessToken}`,
},
credentials: "include",
- cache: 'no-store',
});
if (!response.ok) return null;
- const userData = await response.json();
- return userData;
+ return response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
-}
+});
+
+export const metadata: Metadata = {
+ title: "SOFO",
+ description: "SOund FOrest",
+ icons: {
+ icon: "/images/logo.svg",
+ },
+};
+
+export const viewport = {
+ width: 'device-width',
+ initialScale: 1,
+};
export default async function RootLayout({
children,
-}: Readonly<{
+}: {
children: React.ReactNode;
-}>) {
+}) {
+ // 캐시된 getUser 함수 사용
const user = await getUser();
return (
-
-
+
+
-
+
diff --git a/src/components/albums/AlbumInfo.tsx b/src/components/albums/AlbumInfo.tsx
index 892ad03..c4ed71e 100644
--- a/src/components/albums/AlbumInfo.tsx
+++ b/src/components/albums/AlbumInfo.tsx
@@ -6,6 +6,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { User } from "lucide-react";
import { AlbumActions } from "./AlbumActions";
import { useAuth } from "@/contexts/auth/AuthContext";
+import { useUser } from "@/contexts/auth/UserContext";
import { useState } from "react";
import { EditAlbumModal } from "./EditAlbumModal";
@@ -25,9 +26,10 @@ interface AlbumInfoProps {
}
export function AlbumInfo({ album, artist }: AlbumInfoProps) {
- const { user } = useAuth();
+ const { isAuthenticated } = useAuth();
+ const { user } = useUser();
const [showEditModal, setShowEditModal] = useState(false);
- const isOwner = user?.uuid === artist.uuid;
+ const isOwner = isAuthenticated && user?.uuid === artist.uuid;
return (
diff --git a/src/components/home/AlbumSection.tsx b/src/components/home/AlbumSection.tsx
index 0a85e9a..0f60f6d 100644
--- a/src/components/home/AlbumSection.tsx
+++ b/src/components/home/AlbumSection.tsx
@@ -7,30 +7,42 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import { formatDate } from "@/lib/format";
import { Skeleton } from "@/components/ui/skeleton";
+import { Button } from "@/components/ui/button";
+import { RefreshCcw } from "lucide-react";
+import { useToast } from "@/hooks/use-toast";
export function AlbumSection() {
const [albums, setAlbums] = useState
([]);
const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const { toast } = useToast();
- useEffect(() => {
- const fetchAlbums = async () => {
- try {
- const response = await fetch(
- `${process.env.NEXT_PUBLIC_API_BASE_URL}/albums?page=0&pageSize=6`,
- {
- credentials: 'include',
- }
- );
- if (!response.ok) throw new Error('앨범 목록을 불러오는데 실패했습니다.');
- const data = await response.json();
- setAlbums(data);
- } catch (error) {
- console.error('Failed to fetch albums:', error);
- } finally {
- setIsLoading(false);
- }
- };
+ const fetchAlbums = async () => {
+ try {
+ setError(null);
+ setIsLoading(true);
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_API_BASE_URL}/albums?page=0&pageSize=6`,
+ {
+ credentials: 'include',
+ }
+ );
+ if (!response.ok) throw new Error('앨범 목록을 불러오는데 실패했습니다.');
+ const data = await response.json();
+ setAlbums(data);
+ } catch (error) {
+ console.error('Failed to fetch albums:', error);
+ setError(error instanceof Error ? error.message : "앨범 목록을 불러오는데 실패했습니다.");
+ toast({
+ variant: "destructive",
+ description: error instanceof Error ? error.message : "앨범 목록을 불러오는데 실패했습니다.",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ useEffect(() => {
fetchAlbums();
}, []);
@@ -51,6 +63,25 @@ export function AlbumSection() {
);
}
+ if (error) {
+ return (
+
+ 최신 앨범
+
+
{error}
+
+
+
+ );
+ }
+
return (
최신 앨범
diff --git a/src/components/layout/BackgroundImage.tsx b/src/components/layout/BackgroundImage.tsx
index c8bbb95..d078a55 100644
--- a/src/components/layout/BackgroundImage.tsx
+++ b/src/components/layout/BackgroundImage.tsx
@@ -1,30 +1,52 @@
+"use client";
+
import Image from "next/image";
+import { useTheme } from "@/contexts/theme/ThemeContext";
+import { useState } from "react";
+import { cn } from "@/lib/utils";
export function BackgroundImage() {
+ const { theme } = useTheme();
+ const [isLoading, setIsLoading] = useState(true);
+
+ const handleImageLoad = () => {
+ setIsLoading(false);
+ };
+
return (
-
);
}
\ No newline at end of file
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index 80cc2f6..dd7ce26 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -6,6 +6,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import Image from "next/image";
import { useAuth } from "@/contexts/auth/AuthContext";
+import { useUser } from "@/contexts/auth/UserContext";
import {
Tooltip,
TooltipContent,
@@ -18,7 +19,8 @@ import { LoginModal } from "@/components/auth/LoginModal";
export function Sidebar() {
const pathname = usePathname();
- const { user, isAuthenticated } = useAuth();
+ const { isAuthenticated } = useAuth();
+ const { user } = useUser();
const [showLoginModal, setShowLoginModal] = useState(false);
const sidebarItems = [
@@ -31,7 +33,7 @@ export function Sidebar() {
fill
priority
sizes="16px"
- quality={90}
+ quality={50}
className="object-contain"
/>
@@ -39,9 +41,9 @@ export function Sidebar() {
label: "홈",
href: "/"
},
- { icon: Search, label: "검색", href: "/" },
+ { icon: Search, label: "검색", href: "" },
{ icon: Upload, label: "업로드", href: "/upload" },
- { icon: Bell, label: "알림", href: "/" },
+ { icon: Bell, label: "알림", href: "" },
{
icon: ({ className }: { className?: string }) =>
isAuthenticated ? (
diff --git a/src/components/playlists/PlaylistInfo.tsx b/src/components/playlists/PlaylistInfo.tsx
index a63500d..103d785 100644
--- a/src/components/playlists/PlaylistInfo.tsx
+++ b/src/components/playlists/PlaylistInfo.tsx
@@ -5,6 +5,7 @@ import { User, ListMusic } from "lucide-react";
import Link from "next/link";
import { PlaylistActions } from "./PlaylistActions";
import { useAuth } from "@/contexts/auth/AuthContext";
+import { useUser } from "@/contexts/auth/UserContext";
interface PlaylistInfoProps {
playlist: {
@@ -24,9 +25,10 @@ interface PlaylistInfoProps {
}
export function PlaylistInfo({ playlist }: PlaylistInfoProps) {
- const { user } = useAuth();
- const creator = playlist.playlistItemResponseDtos[0]?.trackGetResponseDto.artistResponseDto;
- const isOwner = user?.uuid === creator?.uuid;
+ const { isAuthenticated } = useAuth();
+ const { user } = useUser();
+
+ const isOwner = isAuthenticated && user?.uuid === playlist.uuid;
return (
@@ -56,18 +58,18 @@ export function PlaylistInfo({ playlist }: PlaylistInfoProps) {
)}
- {creator && (
+ {playlist.playlistItemResponseDtos[0]?.trackGetResponseDto.artistResponseDto && (
-
+
- {creator.name}
+ {playlist.playlistItemResponseDtos[0]?.trackGetResponseDto.artistResponseDto.name}
)}
diff --git a/src/components/profile/AlbumList.tsx b/src/components/profile/AlbumList.tsx
index 8fdc2a3..031f175 100644
--- a/src/components/profile/AlbumList.tsx
+++ b/src/components/profile/AlbumList.tsx
@@ -57,10 +57,10 @@ export function AlbumList({ albums }: AlbumListProps) {
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
25vw"
- priority={index < 4} // 처음 4개 이미지는 우선 로딩
- loading={index < 4 ? "eager" : "lazy"}
+ priority={index < 3} // 처음 3개 이미지는 우선 로딩
+ loading={index < 3 ? "eager" : "lazy"}
className="object-cover transition-transform duration-500 group-hover:scale-105"
- quality={75} // 품질 조정
+ quality={50} // 품질 조정
/>
{/* 그라데이션 오버레이 */}
diff --git a/src/components/profile/EditProfileModal.tsx b/src/components/profile/EditProfileModal.tsx
index 42727e3..73a9208 100644
--- a/src/components/profile/EditProfileModal.tsx
+++ b/src/components/profile/EditProfileModal.tsx
@@ -10,10 +10,11 @@ import {
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
-import { useAuth } from "@/contexts/auth/AuthContext";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { User, Upload } from "lucide-react";
import { checkAuth } from "@/lib/auth";
+import { useUser } from "@/contexts/auth/UserContext";
+import { convertToWebP } from "@/lib/image";
interface EditProfileModalProps {
isOpen: boolean;
@@ -25,7 +26,7 @@ const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg'
export function EditProfileModal({ isOpen, onClose }: EditProfileModalProps) {
const { toast } = useToast();
- const { user, updateUser } = useAuth();
+ const { user, updateUser } = useUser();
const [isLoading, setIsLoading] = useState(false);
const [previewImage, setPreviewImage] = useState(null);
@@ -43,11 +44,14 @@ export function EditProfileModal({ isOpen, onClose }: EditProfileModalProps) {
setIsLoading(true);
validateFile(file);
+ // WebP로 변환
+ const optimizedFile = await convertToWebP(file);
+
const { accessToken } = await checkAuth();
// 1. S3에 이미지 업로드
const formData = new FormData();
- formData.append('file', file);
+ formData.append('file', optimizedFile); // 변환된 파일 사용
const uploadResponse = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/uploads/images`, {
method: 'POST',
@@ -60,13 +64,13 @@ export function EditProfileModal({ isOpen, onClose }: EditProfileModalProps) {
// 2. 프로필 이미지 업데이트
const updateResponse = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/artists/profile-image`, {
- method: 'PATCH',
+ method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
- "imageUrl": imageUrl as string,
+ "imageUrl": imageUrl,
}),
credentials: 'include',
});
diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx
index 6236c1b..9c23cd8 100644
--- a/src/components/profile/ProfileHeader.tsx
+++ b/src/components/profile/ProfileHeader.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useUser } from "@/contexts/auth/UserContext";
import { useAuth } from "@/contexts/auth/AuthContext";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { User, MoreVertical, Edit2 } from "lucide-react";
@@ -18,19 +19,17 @@ interface ProfileHeaderProps {
}
export function ProfileHeader({ userId }: ProfileHeaderProps) {
- const { user } = useAuth();
+ const { user } = useUser();
+ const { isAuthenticated } = useAuth();
const [showEditModal, setShowEditModal] = useState(false);
- const isOwner = user?.uuid === userId;
+ const isOwner = isAuthenticated && user?.uuid === userId;
return (
- {/* 배경 이미지 */}
- {/* 프로필 정보 */}
- {/* 프로필 이미지 */}
@@ -40,11 +39,9 @@ export function ProfileHeader({ userId }: ProfileHeaderProps) {
- {/* 유저 정보 */}
{user?.name}
- {/* 더보기 메뉴 */}
{isOwner && (
@@ -68,9 +65,9 @@ export function ProfileHeader({ userId }: ProfileHeaderProps) {
-
setShowEditModal(false)}
+ setShowEditModal(false)}
/>
);
diff --git a/src/components/upload/TrackUpload.tsx b/src/components/upload/TrackUpload.tsx
index cd88cb1..84c3c2e 100644
--- a/src/components/upload/TrackUpload.tsx
+++ b/src/components/upload/TrackUpload.tsx
@@ -9,9 +9,9 @@ import { useState, useEffect } from "react";
import { useToast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation";
import Link from "next/link";
-import { useAuth } from "@/contexts/auth/AuthContext";
import { AlbumSelect } from "./AlbumSelect";
import { checkAuth } from "@/lib/auth";
+import { useUser } from "@/contexts/auth/UserContext";
interface Album {
uuid: string;
@@ -29,7 +29,7 @@ interface TrackForm {
export function TrackUpload() {
const router = useRouter();
const { toast } = useToast();
- const { user } = useAuth();
+ const { user } = useUser();
const [isLoading, setIsLoading] = useState(false);
const [albums, setAlbums] = useState
([]);
const [selectedAlbum, setSelectedAlbum] = useState("");
diff --git a/src/contexts/auth/AuthContext.tsx b/src/contexts/auth/AuthContext.tsx
index 3ac240e..6e94847 100644
--- a/src/contexts/auth/AuthContext.tsx
+++ b/src/contexts/auth/AuthContext.tsx
@@ -1,91 +1,76 @@
"use client";
-import { createContext, useContext, useEffect, useState } from "react";
-import { AuthContextType, User } from "./types";
-import { checkAuth } from "@/lib/auth";
+import { createContext, useContext, useCallback, useMemo, useReducer } from "react";
+import { AuthContextType, AuthState } from "./types";
-const AuthContext = createContext(null);
+const initialState: AuthState = {
+ isAuthenticated: false,
+ isLoading: false,
+ error: null,
+};
-interface AuthProviderProps {
- children: React.ReactNode;
- initialUser: User | null;
-}
+type AuthAction =
+ | { type: "SET_LOADING"; payload: boolean }
+ | { type: "SET_AUTH"; payload: boolean }
+ | { type: "SET_ERROR"; payload: string | null };
-export function AuthProvider({ children, initialUser }: AuthProviderProps) {
- const [user, setUser] = useState(initialUser);
- const [isLoading, setIsLoading] = useState(false);
+function authReducer(state: AuthState, action: AuthAction): AuthState {
+ switch (action.type) {
+ case "SET_LOADING":
+ return { ...state, isLoading: action.payload };
+ case "SET_AUTH":
+ return { ...state, isAuthenticated: action.payload };
+ case "SET_ERROR":
+ return { ...state, error: action.payload };
+ default:
+ return state;
+ }
+}
- // 인증 상태 확인
- useEffect(() => {
- const verifyAuth = async () => {
- setIsLoading(true);
- try {
- const { isAuthenticated } = await checkAuth();
- if (!isAuthenticated && user) {
- setUser(null);
- } else {
- setUser(initialUser);
- }
- } catch (error) {
- console.error('Auth verification failed:', error);
- } finally {
- setIsLoading(false);
- }
- };
+const AuthContext = createContext(null);
- verifyAuth();
- }, [user, initialUser]);
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [state, dispatch] = useReducer(authReducer, initialState);
- const login = () => {
- setIsLoading(true);
- const googleOAuthUrl = process.env.NEXT_PUBLIC_GOOGLE_OAUTH_URL;
- if (!googleOAuthUrl) {
- console.error('OAuth URL is not defined');
- setIsLoading(false);
- return;
+ const login = useCallback(async () => {
+ dispatch({ type: "SET_LOADING", payload: true });
+ try {
+ const googleOAuthUrl = process.env.NEXT_PUBLIC_GOOGLE_OAUTH_URL;
+ if (!googleOAuthUrl) throw new Error('OAuth URL is not defined');
+ window.location.href = googleOAuthUrl;
+ } catch (error) {
+ dispatch({ type: "SET_ERROR", payload: "로그인에 실패했습니다" });
+ } finally {
+ dispatch({ type: "SET_LOADING", payload: false });
}
- window.location.href = googleOAuthUrl;
- };
+ }, []);
- const logout = async () => {
+ const logout = useCallback(async () => {
+ dispatch({ type: "SET_LOADING", payload: true });
try {
- setIsLoading(true);
-
- // 1. 서버에 로그아웃 요청
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
-
if (!response.ok) throw new Error('Logout failed');
-
- // 2. 클라이언트 상태 초기화
- setUser(null);
- // 3. 페이지 새로고침 (선택사항)
+ dispatch({ type: "SET_AUTH", payload: false });
window.location.href = '/';
} catch (error) {
- console.error('Logout error:', error);
+ dispatch({ type: "SET_ERROR", payload: "로그아웃에 실패했습니다" });
} finally {
- setIsLoading(false);
+ dispatch({ type: "SET_LOADING", payload: false });
}
- };
+ }, []);
- const updateUser = (newUser: User) => {
- setUser(newUser);
- };
+ const value = useMemo(() => ({
+ ...state,
+ login,
+ logout,
+ }), [state, login, logout]);
return (
-
+
{children}
);
diff --git a/src/contexts/auth/UserContext.tsx b/src/contexts/auth/UserContext.tsx
new file mode 100644
index 0000000..2411662
--- /dev/null
+++ b/src/contexts/auth/UserContext.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { createContext, useContext, useCallback, useMemo, useReducer } from "react";
+import { UserContextType, UserState, User } from "./types";
+
+type UserAction =
+ | { type: "SET_USER"; payload: User | null }
+ | { type: "CLEAR_USER" };
+
+function userReducer(state: UserState, action: UserAction): UserState {
+ switch (action.type) {
+ case "SET_USER":
+ return { ...state, user: action.payload };
+ case "CLEAR_USER":
+ return { ...state, user: null };
+ default:
+ return state;
+ }
+}
+
+const UserContext = createContext(null);
+
+interface UserProviderProps {
+ children: React.ReactNode;
+ initialUser: User | null;
+}
+
+export function UserProvider({ children, initialUser }: UserProviderProps) {
+ const [state, dispatch] = useReducer(userReducer, {
+ user: initialUser,
+ });
+
+ const updateUser = useCallback((user: User) => {
+ dispatch({ type: "SET_USER", payload: user });
+ }, []);
+
+ const clearUser = useCallback(() => {
+ dispatch({ type: "CLEAR_USER" });
+ }, []);
+
+ const value = useMemo(() => ({
+ ...state,
+ updateUser,
+ clearUser,
+ }), [state, updateUser, clearUser]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useUser = () => {
+ const context = useContext(UserContext);
+ if (!context) {
+ throw new Error("useUser must be used within a UserProvider");
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/contexts/auth/types.ts b/src/contexts/auth/types.ts
index 7bfb7b8..305c361 100644
--- a/src/contexts/auth/types.ts
+++ b/src/contexts/auth/types.ts
@@ -6,11 +6,25 @@ export interface User {
artistImage: string;
}
-export interface AuthContextType {
- user: User | null;
+export interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
+ error: string | null;
+}
+
+export interface AuthActions {
login: () => void;
logout: () => void;
+}
+
+export interface UserState {
+ user: User | null;
+}
+
+export interface UserActions {
updateUser: (user: User) => void;
-}
\ No newline at end of file
+ clearUser: () => void;
+}
+
+export interface AuthContextType extends AuthState, AuthActions {}
+export interface UserContextType extends UserState, UserActions {}
\ No newline at end of file
diff --git a/src/hooks/use-local-storage.ts b/src/hooks/use-local-storage.ts
new file mode 100644
index 0000000..4a5ff74
--- /dev/null
+++ b/src/hooks/use-local-storage.ts
@@ -0,0 +1,33 @@
+"use client";
+
+import { useState, useEffect } from "react";
+
+export function useLocalStorage(key: string, initialValue: T) {
+ // 초기값 설정
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ if (typeof window === "undefined") {
+ return initialValue;
+ }
+
+ const item = window.localStorage.getItem(key);
+ return item ? JSON.parse(item) : initialValue;
+ } catch (error) {
+ console.error(error);
+ return initialValue;
+ }
+ });
+
+ // 값이 변경될 때마다 localStorage 업데이트
+ useEffect(() => {
+ try {
+ if (typeof window !== "undefined") {
+ window.localStorage.setItem(key, JSON.stringify(storedValue));
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }, [key, storedValue]);
+
+ return [storedValue, setStoredValue] as const;
+}
\ No newline at end of file
diff --git a/src/lib/image.ts b/src/lib/image.ts
new file mode 100644
index 0000000..07f5cfd
--- /dev/null
+++ b/src/lib/image.ts
@@ -0,0 +1,42 @@
+export async function convertToWebP(file: File): Promise {
+ // 이미 WebP 형식이면 그대로 반환
+ if (file.type === 'image/webp') {
+ return file;
+ }
+
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ reject(new Error('Canvas context creation failed'));
+ return;
+ }
+
+ ctx.drawImage(img, 0, 0);
+
+ canvas.toBlob(
+ (blob) => {
+ if (!blob) {
+ reject(new Error('Blob creation failed'));
+ return;
+ }
+
+ const webpFile = new File([blob], file.name.replace(/\.[^/.]+$/, '.webp'), {
+ type: 'image/webp'
+ });
+ resolve(webpFile);
+ },
+ 'image/webp',
+ 0.8 // 품질 설정 (0.8 = 80%)
+ );
+ };
+
+ img.onerror = () => reject(new Error('Image loading failed'));
+ img.src = URL.createObjectURL(file);
+ });
+}
\ No newline at end of file
diff --git a/src/lib/server-auth.ts b/src/lib/server-auth.ts
index a19da53..5a71fa8 100644
--- a/src/lib/server-auth.ts
+++ b/src/lib/server-auth.ts
@@ -10,8 +10,5 @@ export function getAuthCookie() {
// 서버 컴포넌트에서 로그아웃 처리
export async function serverLogout() {
const cookieStore = cookies();
- cookieStore.set('access_token', '', {
- expires: new Date(0),
- path: '/',
- });
+ cookieStore.delete('access_token');
}
\ No newline at end of file