Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eb3e27a
LIN-75 refactor: 테마 토글버튼 플로팅버튼으로 병합
10000jaeN Sep 10, 2025
f6cabe9
LIN-75 chore: 영화 추가버튼 툴팁 메세지 변경,,,
10000jaeN Sep 10, 2025
50dcfb7
LIN-75 chore: 텍스트에리어 라이트모드 적용
10000jaeN Sep 10, 2025
bb4ab93
LIN-75 refactor: 넥스트 컨피그에 도메인 추가
10000jaeN Sep 11, 2025
9b3eb39
LIN-75 refactor: 이미지 최적화
10000jaeN Sep 11, 2025
50edc9d
LIN-75 refactor: 리뷰 서버액션 no-store -> no-cache 로 변경
10000jaeN Sep 11, 2025
e03d8be
LIN-75 refactor: 파일인풋 사이즈 원복 및 폼 여백 재조정
10000jaeN Sep 11, 2025
70fb49d
LIN-75 refactor: 리뷰 이미지 클릭시 프리뷰 제공
10000jaeN Sep 11, 2025
ed6cd85
LIN-75 refactor: 리뷰 이미지 클릭시 프리뷰 제공 사이즈 조정
10000jaeN Sep 11, 2025
a2e7d95
LIN-75 refactor: 플로팅버튼 호버 색상 적용
10000jaeN Sep 11, 2025
5a4fad9
LIN-75 chore: 의존성배열 린트 경고 무시 주석 추가
10000jaeN Sep 11, 2025
3914d1e
LIN-75 chore: productPost 에러상태 핸들링 추가
10000jaeN Sep 11, 2025
fcfbbbb
LIN-75 chore: 플로팅버튼 원복
10000jaeN Sep 11, 2025
dbbbed6
LIN-75 chore: 헤더 로고사이즈 변경
10000jaeN Sep 11, 2025
3e7c823
LIN-75 chore: 라이트모드 제거
10000jaeN Sep 11, 2025
3c58aaa
LIN-75 chore: 라이트모드 제거
10000jaeN Sep 11, 2025
f930af4
LIN-75 chore: 레이아웃 충돌 해결
10000jaeN Sep 11, 2025
769b21e
Merge branch 'dev' into refactor/LIN-75-상세페이지-LCP-최적화
10000jaeN Sep 11, 2025
dfd8e55
Update layout.tsx
10000jaeN Sep 11, 2025
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
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const nextConfig: NextConfig = {
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'picsum.photos',
},
],
domains: ['mogazoa-api.vercel.app', 'example.com'],
},
Expand Down
6 changes: 3 additions & 3 deletions src/actions/review/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const postReview = async ({
'Content-Type': 'application/json',
},
body: JSON.stringify(newReview),
cache: 'no-store',
cache: 'no-cache',
});

revalidatePath(`/products/${productId}`);
Expand All @@ -84,7 +84,7 @@ export const patchReview = async ({ rating, content, images, reviewId }: ReviewP
'Content-Type': 'application/json',
},
body: JSON.stringify(newReview),
cache: 'no-store',
cache: 'no-cache',
});

revalidateTag('reviews');
Expand All @@ -101,7 +101,7 @@ export const deleteReview = async (reviewId: number, productId: number) => {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
cache: 'no-store',
cache: 'no-cache',
},
});

Expand Down
2 changes: 1 addition & 1 deletion src/app/_components/ProductPost/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ProductForm = ({ product, mode }: { product: ProductDetail; mode: 'create'
<FileInput value={field.value ?? []} onChange={field.onChange} maxFiles={1} />
)}
/>
<div className='flex flex-col gap-[10px] md:flex-1 md:gap-3'>
<div className='flex flex-col gap-[10px] md:flex-1 md:gap-4'>
<Input
placeholder='작품 제목'
{...register('name')}
Expand Down
4 changes: 1 addition & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { ThemeProvider } from 'next-themes';
import GlobalNav from '@/components/common/gnb/GlobalNav';
import ModalContainer from '@/components/common/ModalContainer';
import SonnerToast from '@/components/common/SonnerToast';
import ThemeToggle from '@/components/common/ThemeToggle';
import FloatingButton from '@/components/ui/FloatingButton';

import pretendard from '../lib/utils/fonts/pretendard';
Expand All @@ -20,7 +19,7 @@ export const metadata: Metadata = {

export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang='ko'>
<html lang='ko' suppressHydrationWarning>
<body className={pretendard.variable}>
<ThemeProvider
attribute='class' // Tailwind v4 class 전략
Expand All @@ -33,7 +32,6 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
{/* 서버 컴포넌트에서 세션 정보를 가져와 클라이언트 컴포넌트에 전달 */}
<GlobalNav />
{children}
<ThemeToggle />
<FloatingButton />
<SonnerToast />
<SpeedInsights />
Expand Down
28 changes: 28 additions & 0 deletions src/app/products/[productId]/components/PreviewModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import Image from 'next/image';

import Modal from '@/components/common/ModalUi';
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';

const PreviewModal = ({ image }: { image: string }) => {
return (
<Modal className='px-7 pb-7'>
<DialogHeader>
<DialogTitle>Preview</DialogTitle>
</DialogHeader>
<DialogDescription>클릭한 리뷰 이미지를 확대해서 보여주는 모달입니다.</DialogDescription>
<div className='flex h-full w-full justify-center'>
<Image
src={image}
alt='리뷰 이미지'
width={295}
height={295}
className='h-auto w-2/3 object-center'
/>
</div>
</Modal>
);
};

export default PreviewModal;
14 changes: 10 additions & 4 deletions src/app/products/[productId]/components/ReviewAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import Image from 'next/image';
import Link from 'next/link';

import RatingIcon from '@/assets/icon/Icon-star.svg';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Avatar } from '@/components/ui/avatar';
import { userType } from '@/types/review/review';

const ReviewAvatar = ({ user, rating }: { user: userType; rating: number }) => {
const maxRating = 5;

return (
<Link href={`/user/${user.id}`} className='flex w-60 gap-[10px]'>
<Avatar className='h-9 w-9 transition-normal duration-300 xl:h-11 xl:w-11'>
<AvatarImage src={user.image ?? undefined} alt={`profile image for ${user.nickname}`} />
<AvatarFallback>{user.nickname.charAt(0)}</AvatarFallback>
<Avatar className='bg-gray-9fa6b2 relative h-9 w-9 transition-normal duration-300 xl:h-11 xl:w-11'>
<Image
src={user.image ?? '/images/default-profile.png'}
alt={`profile image for ${user.nickname}`}
width={36}
height={36}
className='h-auto w-full object-cover'
/>
</Avatar>
<div className='flex shrink-0 flex-col gap-0.5'>
{user.nickname.slice(0, 10)}
Expand Down
15 changes: 13 additions & 2 deletions src/app/products/[productId]/components/ReviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ThumbChip from '@/components/ui/chips/ThumbChip';
import { useModalStore } from '@/store/modalStore';
import { ReviewCardProps } from '@/types/review/review';

import PreviewModal from './PreviewModal';
import ReviewAvatar from './ReviewAvatar';
import ReviewDeleteMessageModal from './ReviewDeleteMessageModal';
import ReviewModal from './ReviewModal';
Expand Down Expand Up @@ -39,6 +40,10 @@ const ReviewCard = ({ review }: ReviewCardProps) => {
return openModal({ component: ReviewDeleteMessageModal, props: { reviewId: review.id } });
};

const handleClickPreviewModal = (imageSrc: string) => {
return openModal({ component: PreviewModal, props: { image: imageSrc } });
};

return (
<div className='bg-black-252530 light:bg-white border-black-353542 flex w-full flex-col gap-5 rounded-[8px] border-[1px] p-5 transition-normal duration-300 md:flex-row'>
<ReviewAvatar user={review.user} rating={review.rating} />
Expand All @@ -49,9 +54,15 @@ const ReviewCard = ({ review }: ReviewCardProps) => {
{filteredImages.map((ri) => (
<div
key={ri.id}
className='relative h-15 w-15 overflow-hidden rounded-[12px] md:h-20 md:w-20 xl:h-25 xl:w-25'
onClick={() => handleClickPreviewModal(ri.source as string)}
className='relative h-15 w-15 cursor-pointer overflow-hidden rounded-[12px] md:h-20 md:w-20 xl:h-25 xl:w-25'
>
<Image src={ri.source as string} alt='리뷰 이미지' fill className='object-cover' />
<Image
src={(ri.source as string) ?? '/images/noImage.png'}
alt='리뷰 이미지'
fill
className='object-cover'
/>
</div>
))}
</div>
Expand Down
10 changes: 9 additions & 1 deletion src/app/products/[productId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ const ProductIdPage = async ({ params }: ProductIdPageProps) => {
<div className='mx-auto max-w-250 px-5 py-10'>
<header className='flex w-full flex-col md:max-h-[350px] md:flex-row'>
<div className='relative mx-auto aspect-[5/7] w-full shrink-0 transition-normal duration-300 md:max-w-[250px]'>
<Image src={posterImage} alt='영화 포스터' fill priority className='object-cover' />
<Image
src={posterImage}
alt='영화 포스터'
width={500}
height={700}
priority
fetchPriority='high'
className='h-full w-full object-cover'
/>
</div>
<div className='mt-5 flex w-full flex-col gap-[10px] md:mt-0 md:pl-5'>
<div className='flex justify-between'>
Expand Down
3 changes: 2 additions & 1 deletion src/components/common/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ const FileInput = ({ maxFiles = 1, value, onChange }: FileInputProps) => {
'flex shrink-0 cursor-pointer items-center justify-center',
'border-black-353542 bg-black-252530 border',
'aspect-square w-30 rounded-[8px]',
'md:w-40',
'md:w-[135px]',
'xl:w-40',
)}
onClick={() => fileInputRef.current?.click()}
>
Expand Down
51 changes: 0 additions & 51 deletions src/components/common/ThemeToggle.tsx

This file was deleted.

82 changes: 67 additions & 15 deletions src/components/ui/FloatingButton.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,88 @@
'use client';
import React from 'react';

import React, { useEffect, useState } from 'react';

import { Moon, Sun } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useTheme } from 'next-themes';

import ProductModal from '@/app/_components/ProductPost/ProductModal';
import AddIcon from '@/assets/icon/Icon-floating.svg';
import { cn } from '@/lib/utils';
import { useModalStore } from '@/store/modalStore';

const FloatingButton = () => {
const [mounted, setMounted] = useState(false); // SSR 깜빡임 방지
const { resolvedTheme, setTheme } = useTheme(); // enableSystem=false면 theme===resolvedTheme
const { data: session } = useSession();
const openModal = useModalStore((state) => state.openModal);

useEffect(() => setMounted(true), []);

const handleClickModal = () => {
return openModal({ component: ProductModal, props: { mode: 'create' } });
};

if (!session) return null;
const isDark = resolvedTheme === 'dark'; // 현재 적용 테마 판별

return (
<div className='group fixed right-10 bottom-10 z-50 flex items-center gap-4'>
<div className='bg-white-f1f1f5 relative ml-auto hidden max-w-xs rounded-2xl px-3 py-2 text-gray-900 shadow group-hover:block'>
상품 추가하기
<span className='border-l-white-f1f1f5 absolute top-1/2 -right-[7px] -mt-1 h-0 w-0 border-y-8 border-b-0 border-l-8 border-y-transparent'></span>
</div>
<button
type='button'
aria-label='영화 추가하기'
className='to-main-indigo hover:indigo-300 mr-[-20px] flex h-15 w-15 cursor-pointer items-center justify-center rounded-full bg-gradient-to-r from-sky-500 via-blue-500 text-8xl hover:bg-gradient-to-r hover:from-sky-400 hover:via-blue-400'
onClick={handleClickModal}
>
<AddIcon />
</button>
<div className='fixed right-5 bottom-10 z-50 flex shrink-0 flex-col gap-4'>
{/* 테마 변경 버튼 */}
{mounted && (
<div className='group/theme flex items-center gap-4'>
<div
className={cn(
'bg-white-f1f1f5 rounded-2xl text-gray-900 shadow group-hover/theme:block',
'absolute -left-45 ml-auto hidden max-w-xs px-3 py-2',
)}
>
{isDark ? 'Change LightMode' : 'Change DarkMode'}
<span
className={cn(
'border-l-white-f1f1f5 border-y-8 border-b-0 border-l-8 border-y-transparent',
'absolute top-1/2 -right-[7px] -mt-1 h-0 w-0',
)}
></span>
</div>
<button
type='button'
onClick={() => setTheme(isDark ? 'light' : 'dark')} // 라이트↔다크 전환
aria-pressed={isDark}
data-theme-applied={resolvedTheme} // 스타일 확장용 data-attr
className={cn(
'inline-flex h-15 w-15 items-center justify-center rounded-full',
'transition focus:outline-none',
'border border-transparent',
'light:bg-orange-500 light:hover:bg-orange-400 bg-indigo-800 text-white hover:bg-indigo-700',
)}
>
{isDark ? (
<Moon /> // 다크 아이콘
) : (
<Sun /> // 라이트 아이콘
)}
<span className='sr-only'>{isDark ? '다크 → 라이트' : '라이트 → 다크'}</span>
</button>
</div>
)}

{/* 영화 추가버튼 */}
{session && (
<div className='group/add flex items-center gap-4'>
<div className='bg-white-f1f1f5 absolute -left-32 ml-auto hidden max-w-xs rounded-2xl px-3 py-2 text-gray-900 shadow group-hover/add:block'>
영화 추가하기
<span className='border-l-white-f1f1f5 absolute top-1/2 -right-[7px] -mt-1 h-0 w-0 border-y-8 border-b-0 border-l-8 border-y-transparent'></span>
</div>
<button
type='button'
aria-label='영화 추가하기'
className='to-main-indigo hover:indigo-300 mr-[-20px] flex h-15 w-15 cursor-pointer items-center justify-center rounded-full bg-gradient-to-r from-sky-500 via-blue-500 text-8xl hover:bg-gradient-to-r hover:from-sky-400 hover:via-blue-400'
onClick={handleClickModal}
>
<AddIcon />
</button>
</div>
)}
</div>
);
};
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function Textarea({ hasError, ...props }: TextareaProps) {
return (
<div
className={cn(
'bg-black-353542 light:bg-gray-300',
'relative rounded-[8px] p-[1px]',
'focus-within:bg-main-gradation',
hasError ? 'bg-red-ff0000 focus-within:bg-red-ff0000' : '',
Expand All @@ -20,11 +21,10 @@ function Textarea({ hasError, ...props }: TextareaProps) {
<div
className={cn(
// Border
'border-black-353542 rounded-[8px] border',
'focus:border-black-353542 focus:ring-0 focus:outline-none',
'rounded-[8px]',

// Background & Text
'bg-black-252530 dark:bg-input/30',
'bg-black-252530 light:bg-white',
'placeholder:text-gray-6e6e82 text-base',

// Layout & Sizing
Expand Down
Loading