-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/explain4cut #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/explain4cut #172
Changes from all commits
f2a45b4
5dfdb0e
e130623
a514e9d
c6b38f5
665c854
08647e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { X } from 'lucide-react'; | ||
| import { use4CutAiSummary } from '../hooks/use4CutAiSummary'; | ||
|
|
||
| interface Container4CutExplanationProps { | ||
| albumId: string; | ||
| eventName?: string; | ||
| eventDate?: string; | ||
| scale?: number; | ||
| width?: number; | ||
| isFinalized?: boolean; | ||
| onClose?: () => void; | ||
| } | ||
|
|
||
| const BASE_WIDTH = 216; | ||
| const BASE_HEIGHT = 384; | ||
| const BASE_ASPECT_RATIO = BASE_HEIGHT / BASE_WIDTH; | ||
|
|
||
| export default function Container4CutExplanation({ | ||
| albumId, | ||
| eventName, | ||
| eventDate, | ||
| scale = 1, | ||
| width, | ||
| isFinalized = false, | ||
| onClose, | ||
| }: Container4CutExplanationProps) { | ||
| const calculatedWidth = width ?? BASE_WIDTH * scale; | ||
| const calculatedHeight = calculatedWidth * BASE_ASPECT_RATIO; | ||
| const { aiSummary, isLoading } = use4CutAiSummary(albumId); | ||
|
|
||
| return ( | ||
| <div | ||
| className='bg-element-letter relative overflow-hidden font-medium' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| style={{ | ||
| width: `${calculatedWidth}px`, | ||
| height: `${calculatedHeight}px`, | ||
| borderRadius: '8px', | ||
| ...(isFinalized && { | ||
| boxShadow: '0px 0px 25px 5px rgba(0, 0, 0, 0.08)', | ||
| }), | ||
| }} | ||
|
Comment on lines
+38
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| > | ||
| {/* 헤더: 제목 + 날짜 + X 버튼 */} | ||
| <div className='flex justify-between p-4'> | ||
| <div className='flex flex-col gap-1'> | ||
| <h2 className='text-text-basic typo-body-lg-semibold'> | ||
| {eventName || '인생네컷'} | ||
| </h2> | ||
| {eventDate && ( | ||
| <p className='text-text-subtler typo-caption-sm-medium'> | ||
| {eventDate} | ||
| </p> | ||
| )} | ||
| </div> | ||
| {/* X 버튼 */} | ||
| <button | ||
| onClick={onClose} | ||
| className='rounded-full p-1 transition-colors hover:bg-gray-100' | ||
| aria-label='닫기' | ||
| > | ||
| <X className='text-text-secondary h-5 w-5' /> | ||
| </button> | ||
| </div> | ||
|
|
||
| {/* Body: 텍스트 설명 */} | ||
| <div className='flex h-full flex-col justify-center p-4'> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| <div className='bg-surface-white rounded-lg p-4'> | ||
| {isLoading ? ( | ||
| <div className='flex items-center justify-center py-8'> | ||
| <div className='text-text-secondary text-sm'> | ||
| AI 요약 생성 중... | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <p className='text-text-basic text-sm leading-relaxed whitespace-pre-wrap'> | ||
| {aiSummary} | ||
| </p> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,9 +21,11 @@ import dynamic from 'next/dynamic'; | |
| import Link from 'next/link'; | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import { useGetAlbumInfo } from '../../detail/hooks/useGetAlbumInfo'; | ||
| import { use4CutAiSummary } from '../hooks/use4CutAiSummary'; | ||
| import { use4CutFixed } from '../hooks/use4CutFixed'; | ||
| import { use4CutPreviewQuery } from '../hooks/use4CutPreviewQuery'; | ||
| import Container4Cut from './Container4Cut'; | ||
| import Container4CutExplanation from './Container4CutExplanation'; | ||
| const Capture4CutPortal = dynamic(() => import('./Capture4CutPortal'), { | ||
| ssr: false, | ||
| }); | ||
|
|
@@ -36,10 +38,12 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) { | |
| const queryClient = useQueryClient(); | ||
| const [isCaptureVisible, setIsCaptureVisible] = useState(false); | ||
| const [isDownloading, setIsDownloading] = useState(false); | ||
| const [showExplanation, setShowExplanation] = useState(false); | ||
| const captureRef = useRef<HTMLDivElement>(null); | ||
| const { data } = useGetAlbumInfo(albumId); | ||
| const { data: albumInformData } = useGetAlbumInform({ code: albumId }); | ||
| const { data: { name } = {} } = useGetUserMe(); | ||
| const { isCompleted } = use4CutAiSummary(albumId); | ||
|
|
||
| // TODO : openapi type이 이상해서 임시 any처리. 백엔드랑 협의 필요 | ||
| const { | ||
|
|
@@ -98,6 +102,13 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) { | |
| } | ||
| }; | ||
|
|
||
| const handleFlipCard = () => { | ||
| if (!isCompleted && !showExplanation) { | ||
| return; | ||
| } | ||
| setShowExplanation(!showExplanation); | ||
| }; | ||
|
|
||
| const handleDownload = async () => { | ||
| trackGaEvent(GA_EVENTS.click_download_4cut, { | ||
| album_id: albumId, | ||
|
|
@@ -196,15 +207,64 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) { | |
| {!isFinalized && ( | ||
| <div className='typo-body-lg-semibold mb-2'>현재 TOP 4 사진</div> | ||
| )} | ||
| <div> | ||
| <Container4Cut | ||
| albumId={albumId} | ||
| eventName={data?.title} | ||
| eventDate={ | ||
| data?.eventDate ? data.eventDate.replace(/-/g, '.') : '' | ||
| } | ||
| scale={1} | ||
| /> | ||
| <div | ||
| className='relative cursor-pointer' | ||
| style={{ | ||
| perspective: '1000px', | ||
| }} | ||
| onClick={isFinalized ? handleFlipCard : undefined} | ||
| > | ||
| <div | ||
| style={{ | ||
| transformStyle: 'preserve-3d', | ||
| transition: 'transform 0.6s', | ||
| transform: showExplanation | ||
| ? 'rotateY(180deg)' | ||
| : 'rotateY(0deg)', | ||
| }} | ||
| > | ||
| {/* 앞면 - 4컷 사진 */} | ||
| <div | ||
| style={{ | ||
| backfaceVisibility: 'hidden', | ||
| WebkitBackfaceVisibility: 'hidden', | ||
| }} | ||
| > | ||
| <Container4Cut | ||
| albumId={albumId} | ||
| eventName={data?.title} | ||
| eventDate={ | ||
| data?.eventDate ? data.eventDate.replace(/-/g, '.') : '' | ||
| } | ||
| scale={isFinalized ? 1.5 : 1} | ||
| isFinalized={isFinalized} | ||
| /> | ||
| </div> | ||
| {/* 뒷면 - 설명 */} | ||
| {isFinalized && ( | ||
| <div | ||
| style={{ | ||
| position: 'absolute', | ||
| top: 0, | ||
| left: 0, | ||
| backfaceVisibility: 'hidden', | ||
| WebkitBackfaceVisibility: 'hidden', | ||
| transform: 'rotateY(180deg)', | ||
| }} | ||
| > | ||
| <Container4CutExplanation | ||
| albumId={albumId} | ||
| eventName={data?.title} | ||
| eventDate={ | ||
| data?.eventDate ? data.eventDate.replace(/-/g, '.') : '' | ||
| } | ||
| scale={1.5} | ||
| isFinalized={isFinalized} | ||
| onClose={handleFlipCard} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
Comment on lines
+217
to
+267
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| </div> | ||
| </section> | ||
| {!is4CutPreviewPending && ( | ||
|
|
@@ -286,7 +346,7 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) { | |
| )}{' '} | ||
| </main>{' '} | ||
| {isDownloading && ( | ||
| <div className='fixed inset-0 z-[9999] flex items-center justify-center bg-black/40 backdrop-blur-[2px]'> | ||
| <div className='fixed inset-0 z-9999 flex items-center justify-center bg-black/40 backdrop-blur-[2px]'> | ||
| <div className='flex items-center gap-3 rounded-2xl bg-white px-4 py-3 shadow-lg'> | ||
| <Loader2 className='text-primary h-5 w-5 animate-spin' /> | ||
| <span className='typo-body-lg-semibold text-text-basic'> | ||
|
|
@@ -301,6 +361,7 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) { | |
| albumId={albumId} | ||
| eventName={data?.title} | ||
| eventDate={data?.eventDate ? data.eventDate.replace(/-/g, '.') : ''} | ||
| isFinalized={isFinalized} | ||
| /> | ||
| </> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { ApiReturns, EP } from '@/global/api/ep'; | ||
| import { api } from '@/global/utils/api'; | ||
| import { useQuery } from '@tanstack/react-query'; | ||
|
|
||
| const fetchAiSummary = async (albumId: string) => { | ||
| const response = await api.get<ApiReturns['cheese4cut.cheese4cutAiSummary']>({ | ||
| path: EP.cheese4cut.cheese4cutAiSummary(albumId), | ||
| }); | ||
|
|
||
| return response.result; | ||
| }; | ||
|
|
||
| export function use4CutAiSummary(albumId: string) { | ||
| const query = useQuery({ | ||
| queryKey: [EP.cheese4cut.cheese4cutAiSummary(albumId)], | ||
| queryFn: () => fetchAiSummary(albumId), | ||
| refetchInterval: (query) => { | ||
| // COMPLETED 상태면 polling 중단 | ||
| if (query.state.data?.status === 'COMPLETED') { | ||
| return false; | ||
| } | ||
| // 아니면 30초마다 polling | ||
| return 30000; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }, | ||
| refetchIntervalInBackground: false, | ||
| }); | ||
|
|
||
| return { | ||
| ...query, | ||
| aiSummary: query.data?.content || '', | ||
| isCompleted: query.data?.status === 'COMPLETED', | ||
| title: query.data?.title || '', | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인라인 스타일 대신 Tailwind CSS 클래스를 사용하여 조건부 스타일을 적용하는 것이 유지보수 측면에서 더 좋습니다.
boxShadow를 별도의 클래스로 정의하고isFinalized값에 따라 동적으로 클래스를 적용하는 것을 고려해보세요. 예를 들어,clsx와 같은 유틸리티를 사용할 수 있습니다.