diff --git a/Week6/package.json b/Week6/package.json index bc57f457..e32d82e0 100644 --- a/Week6/package.json +++ b/Week6/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@hookform/resolvers": "^5.0.1", - "@tanstack/react-query": "^5.72.1", + "@tanstack/react-query": "^5.76.1", "@tanstack/react-query-devtools": "^5.75.2", "axios": "^1.8.4", "clsx": "^2.1.1", @@ -21,6 +21,7 @@ "react-content-loader": "^7.0.2", "react-dom": "^19.0.0", "react-hook-form": "^7.55.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.4.1", "zod": "^3.24.2" }, diff --git a/Week6/public/Lp.png b/Week6/public/Lp.png new file mode 100644 index 00000000..ef0004a3 Binary files /dev/null and b/Week6/public/Lp.png differ diff --git a/Week6/public/closed-eyes.png b/Week6/public/closed-eyes.png new file mode 100644 index 00000000..4807ea7a Binary files /dev/null and b/Week6/public/closed-eyes.png differ diff --git a/Week6/public/eyes.png b/Week6/public/eyes.png new file mode 100644 index 00000000..f1ad3793 Binary files /dev/null and b/Week6/public/eyes.png differ diff --git a/Week6/public/profile.png b/Week6/public/profile.png new file mode 100644 index 00000000..10cfb2f0 Binary files /dev/null and b/Week6/public/profile.png differ diff --git a/Week6/src/App.tsx b/Week6/src/App.tsx index 16fe89eb..a3852074 100644 --- a/Week6/src/App.tsx +++ b/Week6/src/App.tsx @@ -6,7 +6,7 @@ import { import HomePage from './pages/Home/HomePage'; import Layout from './layout/Layout'; import SignupPage from './pages/SignUp/SignupPage'; -import Mypage from './pages/Mypage'; +import Mypage from './pages/Mypage/Mypage'; import LoginPage from './pages/Login/LoginPage'; import ProtectedLayout from './layout/ProtectedLayout'; import GoogleLoginRedirectPage from './pages/Login/GoogleLoginRedirectPage'; diff --git a/Week6/src/apis/image.ts b/Week6/src/apis/image.ts new file mode 100644 index 00000000..6c332271 --- /dev/null +++ b/Week6/src/apis/image.ts @@ -0,0 +1,23 @@ +import { privateAxios, publicAxios } from './axiosInstance'; +import { API_UPLOADS } from '../constants/api'; +import { ResponseImageDto } from '../types/image'; + +export const postImagePrivate = async ( + file: File, +): Promise => { + const formData = new FormData(); + formData.append('file', file); // file타입 + + const { data } = await privateAxios.post( + API_UPLOADS.PRIVATE_UPLOAD, + formData, + ); + + return data; +}; + +export const postImagePublic = async () => { + const { data } = await publicAxios.post(API_UPLOADS.PUBLIC_UPLOAD); + + return data; +}; diff --git a/Week6/src/apis/lp.ts b/Week6/src/apis/lp.ts index d47d71f3..4274b29a 100644 --- a/Week6/src/apis/lp.ts +++ b/Week6/src/apis/lp.ts @@ -1,9 +1,13 @@ -import { API_COMMENTS, API_LPS } from '../constants/api'; +import { API_COMMENTS, API_LIKES, API_LPS } from '../constants/api'; import { SortOrder } from '../constants/sort'; import { ResponseLpDto, ResponseLpDetailDto, ResponseCommentDto, + CreateLpDto, + ResponsePostCommentDto, + ResponsePatchCommentDto, + patchLpDto, } from '../types/lp'; import { privateAxios, publicAxios } from './axiosInstance'; @@ -25,6 +29,7 @@ export const getLpInfo = async ({ order, }, }); + console.log(data); return data; }; @@ -36,7 +41,7 @@ export const getLpDetail = async ( }; interface GetCommentsParms extends GetLpInfoParams { - lpId: number | string; + lpId: number; } export const getComments = async ({ @@ -51,3 +56,99 @@ export const getComments = async ({ ); return data; }; + +export const postComment = async ({ + lpId, + content, +}: { + lpId: number; + content: string; +}) => { + const { data } = await privateAxios.post( + API_COMMENTS.CREATE(lpId), + { content }, + ); + return data; +}; + +export const patchComment = async ({ + lpId, + commentId, + content, +}: { + lpId: number; + commentId: number; + content: string; +}) => { + const { data } = await privateAxios.patch( + API_COMMENTS.UPDATE(lpId, commentId), + { content }, + ); + return data; +}; + +export const deleteComment = async ({ + lpId, + commentId, +}: { + lpId: number; + commentId: number; +}) => { + const { data } = await privateAxios.delete( + API_COMMENTS.DELETE(lpId, commentId), + ); + + return data; +}; + +export const postLp = async ({ + title, + content, + thumbnail, + tags, + published = true, +}: CreateLpDto) => { + const { data } = await privateAxios.post(API_LPS.CREATE, { + title, + content, + thumbnail, + tags, + published, + }); + + return data; +}; + +export const patchLp = async ({ + lpId, + title, + content, + thumbnail, + tags, + published = true, +}: patchLpDto) => { + const { data } = await privateAxios.patch(API_LPS.UPDATE(lpId), { + title, + content, + thumbnail, + tags, + published, + }); + + return data; +}; + +export const deleteLp = async (lpId: number) => { + const { data } = await privateAxios.delete(API_LPS.DELETE(lpId)); + return data; +}; + +export const postLike = async (lpId: number) => { + const { data } = await privateAxios.post(API_LIKES.LIKE(lpId)); + return data; +}; + +export const deleteLike = async (lpId: number) => { + const { data } = await privateAxios.delete(API_LIKES.UNLIKE(lpId)); + return data; +}; diff --git a/Week6/src/apis/users.ts b/Week6/src/apis/users.ts new file mode 100644 index 00000000..d80a5f16 --- /dev/null +++ b/Week6/src/apis/users.ts @@ -0,0 +1,24 @@ +import { API_USERS } from '../constants/api'; +import { privateAxios } from './axiosInstance'; + +interface patchUsersProps { + name: string; + bio: string; + avatar: string; +} + +export const patchUsers = async ({ name, bio, avatar }: patchUsersProps) => { + const { data } = await privateAxios.patch(API_USERS.UPDATE_USER, { + name, + bio, + avatar, + }); + + return data; +}; + +export const deleteUser = async () => { + const { data } = await privateAxios.delete(API_USERS.DELETE_USER); + + return data; +}; diff --git a/Week6/src/components/LogoutButton/LogoutButton.tsx b/Week6/src/components/LogoutButton/LogoutButton.tsx new file mode 100644 index 00000000..78750d24 --- /dev/null +++ b/Week6/src/components/LogoutButton/LogoutButton.tsx @@ -0,0 +1,16 @@ +import { useAuth } from '../../context/AuthContext'; + +function LogoutButton() { + const { logout } = useAuth(); + + return ( + + ); +} + +export default LogoutButton; diff --git a/Week6/src/components/Modal.tsx b/Week6/src/components/Modal.tsx new file mode 100644 index 00000000..fce59dd6 --- /dev/null +++ b/Week6/src/components/Modal.tsx @@ -0,0 +1,56 @@ +import { PropsWithChildren } from 'react'; +import { createPortal } from 'react-dom'; + +interface ModalProps extends PropsWithChildren { + message: string; + onConfirm: () => void; + onCancel: () => void; + /** 버튼 레이블(기본값: “예”) */ + confirmText?: string; + /** 버튼 레이블(기본값: “아니오”) */ + cancelText?: string; +} + +export default function Modal({ + message, + onConfirm, + onCancel, + confirmText = '예', + cancelText = '아니오', +}: ModalProps) { + // DOM 계층 구조 바깥에 렌더링 + // createPortal(ReactNode, DOMElement) + return createPortal( +
+ {/* overlay */} +
+ {/* dialog */} +
+ {/* 메시지 */} +

{message}

+ + {/* 버튼 그룹 */} +
+ + +
+
+
, + document.body, + ); +} diff --git a/Week6/src/constants/api.ts b/Week6/src/constants/api.ts index b34178fa..e11bb67d 100644 --- a/Week6/src/constants/api.ts +++ b/Week6/src/constants/api.ts @@ -20,24 +20,24 @@ export const API_LPS = { CREATE: '/v1/lps', LIST_BY_USER_ID: (userId: string) => `/v1/lps/user/${userId}`, LIST_MY: '/v1/lps/user', - DETAIL: (lpId: string | number) => `/v1/lps/${lpId}`, - UPDATE: (lpId: string | number) => `/v1/lps/${lpId}`, - DELETE: (lpId: string) => `/v1/lps/${lpId}`, + DETAIL: (lpId: number) => `/v1/lps/${lpId}`, + UPDATE: (lpId: number) => `/v1/lps/${lpId}`, + DELETE: (lpId: number) => `/v1/lps/${lpId}`, LIST_BY_TAG: (tagName: string) => `/v1/lps/tag/${tagName}`, }; export const API_COMMENTS = { - LIST: (lpId: string | number) => `/v1/lps/${lpId}/comments`, - CREATE: (lpId: string) => `/v1/lps/${lpId}/comments`, - UPDATE: (lpId: string, commentId: string) => + LIST: (lpId: number) => `/v1/lps/${lpId}/comments`, + CREATE: (lpId: number) => `/v1/lps/${lpId}/comments`, + UPDATE: (lpId: number, commentId: number) => `/v1/lps/${lpId}/comments/${commentId}`, - DELETE: (lpId: string, commentId: string) => + DELETE: (lpId: number, commentId: number) => `/v1/lps/${lpId}/comments/${commentId}`, }; export const API_LIKES = { - LIKE: (lpId: string) => `/v1/lps/${lpId}/likes`, - UNLIKE: (lpId: string) => `/v1/lps/${lpId}/likes`, + LIKE: (lpId: number) => `/v1/lps/${lpId}/likes`, + UNLIKE: (lpId: number) => `/v1/lps/${lpId}/likes`, LIST_MY_LIKES: '/v1/lps/likes/me', LIST_USER_LIKES: (userId: string) => `/v1/lps/likes/${userId}`, }; diff --git a/Week6/src/constants/images.ts b/Week6/src/constants/images.ts index b424a0f3..ac0beb2c 100644 --- a/Week6/src/constants/images.ts +++ b/Week6/src/constants/images.ts @@ -1,6 +1,10 @@ export const IMAGE_PATH = { GOOGLE_LOGO: '/google-logo.png', - PROFILE: '/my.png', + MY: '/my.png', SEARCH_ICON: '/search.png', LOGO: '/Logo.png', + EYE: '/eyes.png', + CLOSED_EYE: '/closed-eyes.png', + LP: '/Lp.png', + PROFILE: '/profile.png', } as const; diff --git a/Week6/src/constants/key.ts b/Week6/src/constants/key.ts index e042a314..4128d9ee 100644 --- a/Week6/src/constants/key.ts +++ b/Week6/src/constants/key.ts @@ -2,3 +2,10 @@ export const LOCAL_STORAGE_KEY = { accessToken: 'accessToken', refreshToken: 'refreshToken', } as const; + +export const QUERY_KEY = { + users: 'users', + lpInfo: 'lpInfo', + lpDetail: 'lpDetail', + comments: 'comments', +} as const; diff --git a/Week6/src/context/AuthContext.tsx b/Week6/src/context/AuthContext.tsx index ac6d6f86..4a9b10df 100644 --- a/Week6/src/context/AuthContext.tsx +++ b/Week6/src/context/AuthContext.tsx @@ -1,18 +1,16 @@ -import { - createContext, - PropsWithChildren, - useContext, - useEffect, - useState, -} from 'react'; +import { createContext, PropsWithChildren, useContext, useState } from 'react'; import { RequestSigninDto, ResponseMyInfoDto } from '../types/auth'; -import { postLogout, postSignin, getMyInfo } from '../apis/auth'; +import { postLogout, postSignin } from '../apis/auth'; import { tokenStorage } from '../utils/tokenStorage'; +import ROUTES from '../constants/routes'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useGetUsers } from './useGetUsers'; +import { QUERY_KEY } from '../constants/key'; interface AuthContextType { accessToken: string | null; refreshToken: string | null; - myInfo: ResponseMyInfoDto | null; + myInfo: ResponseMyInfoDto | undefined; login: (signinData: RequestSigninDto) => Promise; logout: () => Promise; } @@ -20,69 +18,71 @@ interface AuthContextType { export const AuthContext = createContext({ accessToken: null, refreshToken: null, - myInfo: null, + myInfo: undefined, login: async () => {}, logout: async () => {}, }); export const AuthProvider = ({ children }: PropsWithChildren) => { + const queryClient = useQueryClient(); + const [accessToken, setAccessToken] = useState( tokenStorage.getAccessToken(), ); const [refreshToken, setRefreshToken] = useState( tokenStorage.getRefreshToken(), ); - const [myInfo, setMyInfo] = useState(null); - - const loginWithToken = async () => { - try { - const userInfo = await getMyInfo(); - setMyInfo(userInfo); - } catch (error) { - console.error('유저 정보 불러오기 실패', error); - } - }; - const login = async (signinData: RequestSigninDto) => { - try { - const { data } = await postSignin(signinData); + const { data: myInfo } = useGetUsers(); - if (data) { - const newAccessToken = data.accessToken; - const newRefreshToken = data.refreshToken; + const { mutateAsync: useLogin } = useMutation({ + mutationFn: postSignin, + onSuccess: (data) => { + const newAccessToken = data.data.accessToken; + const newRefreshToken = data.data.refreshToken; - tokenStorage.setAccessToken(newAccessToken); - tokenStorage.setRefreshToken(newRefreshToken); + tokenStorage.setAccessToken(newAccessToken); + tokenStorage.setRefreshToken(newRefreshToken); - setAccessToken(newAccessToken); - setRefreshToken(newRefreshToken); + setAccessToken(newAccessToken); + setRefreshToken(newRefreshToken); - await loginWithToken(); - } - } catch (error) { - console.error('로그인 실패', error); - } - }; + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.users] }); - const logout = async () => { - try { - await postLogout(); + alert('로그인 성공'); + window.location.replace(ROUTES.HOME); + }, + onError: (e) => { + alert('로그인 실패'); + console.error(e); + }, + }); + + const login = async (signinData: RequestSigninDto): Promise => { + await useLogin(signinData); + }; + const { mutateAsync: useLogout } = useMutation({ + mutationFn: postLogout, + onSuccess: () => { tokenStorage.clear(); setAccessToken(null); setRefreshToken(null); - setMyInfo(null); - } catch (error) { - console.error('로그아웃 실패', error); - } - }; - useEffect(() => { - if (accessToken) { - loginWithToken(); - } - }, [accessToken]); + queryClient.removeQueries({ queryKey: [QUERY_KEY.users] }); + + alert('로그아웃되었습니다.'); + }, + onError: (e) => { + alert('로그아웃실패'); + console.error(e); + }, + }); + + const logout = async (): Promise => { + await useLogout(); + }; return ( { + const { accessToken } = useAuth(); + + return useQuery({ + queryKey: [QUERY_KEY.users], + queryFn: getMyInfo, + enabled: !!accessToken, // 토큰 있을 때만 실행 + staleTime: 1000 * 60 * 30, // 30분 동안 fresh + retry: 1, // 실패 시 한 번 재시도 + }); +}; diff --git a/Week6/src/index.css b/Week6/src/index.css index 387b05ce..7aa2ccdb 100644 --- a/Week6/src/index.css +++ b/Week6/src/index.css @@ -12,6 +12,6 @@ -moz-osx-font-smoothing: grayscale; } -main { +/* main { scrollbar-gutter: stable; -} +} */ diff --git a/Week6/src/layout/Layout.tsx b/Week6/src/layout/Layout.tsx index 5523bf64..8d6a9376 100644 --- a/Week6/src/layout/Layout.tsx +++ b/Week6/src/layout/Layout.tsx @@ -2,7 +2,6 @@ import Navbar from './components/Navbar/Navbar'; import Sidebar from './components/Sidebar'; import { Outlet } from 'react-router-dom'; import { useState, useEffect } from 'react'; - function Layout() { const [isSidebarOpen, setIsSidebarOpen] = useState(false); @@ -18,12 +17,21 @@ function Layout() { const closeSidebar = () => setIsSidebarOpen(false); return ( -
+
-
+ + {/* 오버레이 */} + {isSidebarOpen && ( +
+ )} + +
setIsSidebarOpen((prev) => !prev)} /> diff --git a/Week6/src/layout/components/Navbar/ui/UserMenu.tsx b/Week6/src/layout/components/Navbar/ui/UserMenu.tsx index 86f87db9..e214986e 100644 --- a/Week6/src/layout/components/Navbar/ui/UserMenu.tsx +++ b/Week6/src/layout/components/Navbar/ui/UserMenu.tsx @@ -1,20 +1,16 @@ -import { useAuth } from '../../../../context/AuthContext'; +import LogoutButton from '../../../../components/LogoutButton/LogoutButton'; +import { useGetUsers } from '../../../../context/useGetUsers'; function UserMenu() { - const { logout, myInfo } = useAuth(); + const { data: myInfo } = useGetUsers(); return (
- + {myInfo?.data.name} 님 반갑습니다. - +
); } diff --git a/Week6/src/layout/components/Sidebar.tsx b/Week6/src/layout/components/Sidebar.tsx index 83a19b69..2e1a186d 100644 --- a/Week6/src/layout/components/Sidebar.tsx +++ b/Week6/src/layout/components/Sidebar.tsx @@ -1,17 +1,21 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; import { Link } from 'react-router-dom'; import ROUTES from '../../constants/routes'; +import Modal from '../../components/Modal'; +import { useDeleteUser } from '../hooks/useDeleteUser'; -const Sidebar = ({ - isSidebarOpen, - closeSidebar, -}: { +interface SidebarProps { isSidebarOpen: boolean; closeSidebar: () => void; -}) => { +} + +const Sidebar = ({ isSidebarOpen, closeSidebar }: SidebarProps) => { + const [isModalOpen, setIsModalOpen] = useState(false); const ref = useRef(null); + const { mutate: deleteUser } = useDeleteUser(); + useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( @@ -27,6 +31,9 @@ const Sidebar = ({ document.removeEventListener('mousedown', handleClickOutside); }, [closeSidebar]); + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + return (
마이페이지 + + {isModalOpen && ( + + )}
); }; diff --git a/Week6/src/layout/hooks/useDeleteUser.ts b/Week6/src/layout/hooks/useDeleteUser.ts new file mode 100644 index 00000000..180c4a08 --- /dev/null +++ b/Week6/src/layout/hooks/useDeleteUser.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { deleteUser } from '../../apis/users'; +import { QUERY_KEY } from '../../constants/key'; + +export const useDeleteUser = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteUser, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.users], + }); + }, + onError: (err) => { + console.error('댓글 생성 실패', err); + }, + }); +}; diff --git a/Week6/src/pages/Home/component/LpCardSkeleton.tsx b/Week6/src/pages/Home/component/LpCardSkeleton.tsx index 1037739e..3c64d5ae 100644 --- a/Week6/src/pages/Home/component/LpCardSkeleton.tsx +++ b/Week6/src/pages/Home/component/LpCardSkeleton.tsx @@ -9,6 +9,7 @@ function LpCardSkeleton() { backgroundColor="#e0e0e0" foregroundColor="#bdbdbd" className="w-full h-full" + //비율무시 preserveAspectRatio="none" > diff --git a/Week6/src/pages/Login/GoogleLoginRedirectPage.tsx b/Week6/src/pages/Login/GoogleLoginRedirectPage.tsx index 5bfaf673..915fd830 100644 --- a/Week6/src/pages/Login/GoogleLoginRedirectPage.tsx +++ b/Week6/src/pages/Login/GoogleLoginRedirectPage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { LOCAL_STORAGE_KEY } from '../../constants/key'; -import { localStorageUtil } from '../../utils/localStorageUtil'; import ROUTES from '../../constants/routes'; +import { tokenStorage } from '../../utils/tokenStorage'; const GoogleLoginRedirectPage = () => { useEffect(() => { @@ -11,14 +11,8 @@ const GoogleLoginRedirectPage = () => { const refreshToken = urlParams.get(LOCAL_STORAGE_KEY.refreshToken); if (accessToken && refreshToken) { - localStorageUtil.setItem( - LOCAL_STORAGE_KEY.accessToken, - accessToken, - ); - localStorageUtil.setItem( - LOCAL_STORAGE_KEY.refreshToken, - refreshToken, - ); + tokenStorage.setAccessToken(accessToken); + tokenStorage.setRefreshToken(refreshToken); window.location.href = ROUTES.HOME; } }, []); diff --git a/Week6/src/pages/Login/LoginPage.tsx b/Week6/src/pages/Login/LoginPage.tsx index 8564fdbc..2cb445a6 100644 --- a/Week6/src/pages/Login/LoginPage.tsx +++ b/Week6/src/pages/Login/LoginPage.tsx @@ -4,7 +4,6 @@ import { loginSchema, LoginFields } from '../../schemas/login.schema'; import { useAuth } from '../../context/AuthContext'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import ROUTES from '../../constants/routes'; import { API_AUTH } from '../../constants/api'; function LoginPage() { @@ -28,8 +27,6 @@ function LoginPage() { const onSubmit = async (data: LoginFields) => { await login(data); - alert('로그인성공'); - navigate(ROUTES.HOME); }; const navigateToGoogleLogin = () => { diff --git a/Week6/src/pages/Lp/Lp.tsx b/Week6/src/pages/Lp/Lp.tsx index 5fbce5a1..15ad6592 100644 --- a/Week6/src/pages/Lp/Lp.tsx +++ b/Week6/src/pages/Lp/Lp.tsx @@ -1,54 +1,202 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; +import { HiHeart, HiOutlineHeart } from 'react-icons/hi2'; + import { getLpDetail } from '../../apis/lp'; +import { postImagePrivate } from '../../apis/image'; import LoadingSpinner from '../../components/LoadingSpinner'; import ErrorMessage from '../../components/ErrorMessage'; import Comments from './components/comments'; +import { usePatchLp } from './hooks/usePatchLp'; +import { useDeleteLp } from './hooks/useDeleteLp'; +import { useAuth } from '../../context/AuthContext'; +import ROUTES from '../../constants/routes'; +import { useToggleLike } from './hooks/useToggleLike'; +import { QUERY_KEY } from '../../constants/key'; function Lp() { + const navigate = useNavigate(); + const { myInfo } = useAuth(); const { lpId } = useParams<{ lpId: string }>(); const id = Number(lpId); - const { data, isLoading, isError } = useQuery({ - queryKey: ['lpDetail', id], + const { data, isPending, isError } = useQuery({ + queryKey: [QUERY_KEY.lpDetail, id], queryFn: () => getLpDetail(id), enabled: Number.isFinite(id), }); + const { mutate: patchLp } = usePatchLp(); + const { mutate: deleteLp } = useDeleteLp(); + const { mutate: toggleLike } = useToggleLike(); + + const [isEditing, setIsEditing] = useState(false); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(''); + if (!Number.isFinite(id)) return ; - if (isLoading) return ; + if (isPending) return ; if (isError || !data?.data) return ; const lp = data.data; + const isMyLp = lp.authorId === myInfo?.data.id; + const alreadyLiked = lp.likes.some((u) => u.userId === myInfo?.data.id); + + const startEdit = () => { + setIsEditing(true); + setTitle(lp.title); + setContent(lp.content); + setPreviewUrl(lp.thumbnail); + setFile(null); + }; + + const handleDeleteLp = () => { + deleteLp(lp.id, { + onSuccess: () => navigate(ROUTES.HOME), + }); + }; + + const handleToggleLike = () => { + toggleLike({ lpId: id, isLiked: alreadyLiked ?? false }); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const f = e.target.files?.[0]; + if (!f) return; + setFile(f); + //미리보기 url 생성 함수. revokeObjectURL로 해제해줘야 함 + setPreviewUrl(URL.createObjectURL(f)); + }; + + const handleEditSubmit = async () => { + let thumbnailUrl = lp.thumbnail; + if (file) { + const res = await postImagePrivate(file); + thumbnailUrl = res.data.imageUrl; + } + + patchLp( + { + lpId: lp.id, + title: title.trim(), + content: content.trim(), + thumbnail: thumbnailUrl, + tags: lp.tags.map((tag) => tag.name), // 기존 태그 유지 + published: true, + }, + { + onSuccess: () => setIsEditing(false), + }, + ); + }; + + console.log(lp.likes); return (
+ {isMyLp && ( +
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+ )} +
{lp.title}
-

- {lp.title} -

+ {isEditing ? ( + setTitle(e.target.value)} + className="w-full px-4 py-2 mt-6 text-3xl font-bold text-center border rounded-md" + /> + ) : ( +

+ {lp.title} +

+ )} +

{new Date(lp.createdAt).toLocaleDateString()}

-

- {lp.content} -

+ {isEditing ? ( +