diff --git a/src/feature/album/4cut/components/Capture4CutPortal.tsx b/src/feature/album/4cut/components/Capture4CutPortal.tsx
index 8deff1da..5f98b13f 100644
--- a/src/feature/album/4cut/components/Capture4CutPortal.tsx
+++ b/src/feature/album/4cut/components/Capture4CutPortal.tsx
@@ -10,6 +10,7 @@ interface Capture4CutPortalProps {
albumId: string;
eventName?: string;
eventDate?: string;
+ isFinalized?: boolean;
}
const Capture4CutPortal = ({
@@ -18,6 +19,7 @@ const Capture4CutPortal = ({
albumId,
eventName,
eventDate,
+ isFinalized = false,
}: Capture4CutPortalProps) => (
diff --git a/src/feature/album/4cut/components/Container4Cut.tsx b/src/feature/album/4cut/components/Container4Cut.tsx
index b3a71ca3..c5fcbab1 100644
--- a/src/feature/album/4cut/components/Container4Cut.tsx
+++ b/src/feature/album/4cut/components/Container4Cut.tsx
@@ -9,6 +9,7 @@ interface Container4CutProps {
eventDate?: string;
scale?: number;
width?: number;
+ isFinalized?: boolean;
}
const BASE_WIDTH = 216;
@@ -30,6 +31,7 @@ export default function Container4Cut({
eventName,
scale = 1,
width,
+ isFinalized = false,
}: Container4CutProps) {
// TODO : openapi type이 이상해서 임시 any처리. 백엔드랑 협의 필요
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -64,7 +66,12 @@ export default function Container4Cut({
return (
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 (
+
+ {/* 헤더: 제목 + 날짜 + X 버튼 */}
+
+
+
+ {eventName || '인생네컷'}
+
+ {eventDate && (
+
+ {eventDate}
+
+ )}
+
+ {/* X 버튼 */}
+
+
+
+ {/* Body: 텍스트 설명 */}
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {aiSummary}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/feature/album/4cut/components/ScreenAlbum4Cut.tsx b/src/feature/album/4cut/components/ScreenAlbum4Cut.tsx
index ec43df54..1787d3cb 100644
--- a/src/feature/album/4cut/components/ScreenAlbum4Cut.tsx
+++ b/src/feature/album/4cut/components/ScreenAlbum4Cut.tsx
@@ -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(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 && (
현재 TOP 4 사진
)}
-
-
+
+
+ {/* 앞면 - 4컷 사진 */}
+
+
+
+ {/* 뒷면 - 설명 */}
+ {isFinalized && (
+
+
+
+ )}
+
{!is4CutPreviewPending && (
@@ -286,7 +346,7 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) {
)}{' '}
{' '}
{isDownloading && (
-
+
@@ -301,6 +361,7 @@ export default function ScreenAlbum4Cut({ albumId }: ScreenAlbum4CutProps) {
albumId={albumId}
eventName={data?.title}
eventDate={data?.eventDate ? data.eventDate.replace(/-/g, '.') : ''}
+ isFinalized={isFinalized}
/>
>
);
diff --git a/src/feature/album/4cut/hooks/use4CutAiSummary.ts b/src/feature/album/4cut/hooks/use4CutAiSummary.ts
new file mode 100644
index 00000000..7f534966
--- /dev/null
+++ b/src/feature/album/4cut/hooks/use4CutAiSummary.ts
@@ -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({
+ 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;
+ },
+ refetchIntervalInBackground: false,
+ });
+
+ return {
+ ...query,
+ aiSummary: query.data?.content || '',
+ isCompleted: query.data?.status === 'COMPLETED',
+ title: query.data?.title || '',
+ };
+}
diff --git a/src/feature/album/4cut/hooks/use4CutFixed.ts b/src/feature/album/4cut/hooks/use4CutFixed.ts
index d5b6eeee..28766c98 100644
--- a/src/feature/album/4cut/hooks/use4CutFixed.ts
+++ b/src/feature/album/4cut/hooks/use4CutFixed.ts
@@ -8,8 +8,8 @@ interface Cheese4CutFixedProps {
}
const fetchData = async ({ albumId, photoIds }: Cheese4CutFixedProps) => {
- const res = await api.post({
- path: EP.cheese4cut.finalize(albumId),
+ const res = await api.post({
+ path: EP.cheese4cut.cheese4cutFixedAi(albumId),
body: { photoIds },
});
return res.result;
diff --git a/src/feature/album/detail/components/ScreenAlbumDetail.tsx b/src/feature/album/detail/components/ScreenAlbumDetail.tsx
index 3512342c..1a693412 100644
--- a/src/feature/album/detail/components/ScreenAlbumDetail.tsx
+++ b/src/feature/album/detail/components/ScreenAlbumDetail.tsx
@@ -250,5 +250,6 @@ function mapLikedPhotosToPhotoList(
isLiked: item.isLiked ?? false,
isDownloaded: item.isDownloaded,
isRecentlyDownloaded: item.isRecentlyDownloaded,
+ canDelete: false,
}));
}
diff --git a/src/global/api/ep.ts b/src/global/api/ep.ts
index b695f366..cbdac0af 100644
--- a/src/global/api/ep.ts
+++ b/src/global/api/ep.ts
@@ -45,7 +45,9 @@ export const EP = {
"reportUploadResult": () => `/v1/photo/report`,
},
cheese4cut: {
+ "cheese4cutAiSummary": (code: string | number) => `/v1/cheese4cut/${code}/ai-summary`,
"finalize": (code: string | number) => `/v1/cheese4cut/${code}/fixed`,
+ "cheese4cutFixedAi": (code: string | number) => `/v1/cheese4cut/${code}/fixed/ai`,
"preview": (code: string | number) => `/v1/cheese4cut/${code}/preview`,
},
internal: {
@@ -101,10 +103,12 @@ export interface Cheese4cutResponseSchema { "finalized"?: boolean; }
export interface CommonResponseCheese4cutResponseSchema { "isSuccess"?: boolean; "code"?: number; "message"?: string; "result"?: Cheese4cutFinalResponseSchema | Cheese4cutPreviewResponseSchema; }
export interface FinalPhotoInfoSchema { "photoId": number; "imageUrl": string; "photoRank": number; }
export interface PreviewPhotoInfoSchema { "photoId": number; "imageUrl": string; "photoRank": number; }
+export interface Cheese4cutAiResponseSchema { "status"?: string; "title"?: string; "content"?: string; }
+export interface CommonResponseCheese4cutAiResponseSchema { "isSuccess"?: boolean; "code"?: number; "message"?: string; "result"?: Cheese4cutAiResponseSchema; }
export interface AuthExchangeResponseSchema { "accessToken": string; "refreshToken": string; "isOnboarded": boolean; "userId": number; "name": string; "email": string; }
export interface CommonResponseAuthExchangeResponseSchema { "isSuccess"?: boolean; "code"?: number; "message"?: string; "result"?: AuthExchangeResponseSchema; }
export interface CommonResponsePhotoPageResponseSchema { "isSuccess"?: boolean; "code"?: number; "message"?: string; "result"?: PhotoPageResponseSchema; }
-export interface PhotoListResponseSchema { "name"?: string; "photoId": number; "profileImage": string; "imageUrl"?: string; "thumbnailUrl": string; "likeCnt": number; "isLiked": boolean; "isDownloaded": boolean; "isRecentlyDownloaded": boolean; "canDelete"?: boolean; }
+export interface PhotoListResponseSchema { "name"?: string; "photoId": number; "uploaderId"?: number; "profileImage": string; "imageUrl"?: string; "thumbnailUrl": string; "likeCnt": number; "isLiked": boolean; "isDownloaded": boolean; "isRecentlyDownloaded": boolean; "canDelete": boolean; }
export interface PhotoPageResponseSchema { "responses": PhotoListResponseSchema[]; "listSize": number; "isFirst": boolean; "isLast": boolean; "hasNext": boolean; }
export interface CommonResponsePhotoDetailResponseSchema { "isSuccess"?: boolean; "code"?: number; "message"?: string; "result"?: PhotoDetailResponseSchema; }
export interface PhotoDetailResponseSchema { "name": string; "profileImage": string; "photoId": number; "imageUrl": string; "thumbnailUrl": string; "likesCnt": number; "isLiked": boolean; "isDownloaded": boolean; "isRecentlyDownloaded": boolean; "canDelete"?: boolean; "captureTime"?: string; "createdAt"?: string; }
@@ -164,7 +168,9 @@ export type PhotoUnlikeResponse = CommonResponseVoidSchema["result"];
export type PhotoPresignedDownloadResponse = CommonResponsePhotoDownloadResponseSchema["result"];
export type PhotoPresignedUploadResponse = CommonResponsePhotoPresignedUrlResponseSchema["result"];
export type PhotoReportUploadResultResponse = CommonResponseVoidSchema["result"];
+export type Cheese4cutCheese4cutAiSummaryResponse = CommonResponseCheese4cutAiResponseSchema["result"];
export type Cheese4cutFinalizeResponse = CommonResponseVoidSchema["result"];
+export type Cheese4cutCheese4cutFixedAiResponse = CommonResponseVoidSchema["result"];
export type Cheese4cutPreviewResponse = CommonResponseCheese4cutResponseSchema["result"];
export type InternalThumbnailCompleteResponse = CommonResponseVoidSchema["result"];
@@ -201,7 +207,9 @@ export interface ApiReturns {
"photo.presignedDownload": PhotoPresignedDownloadResponse; // POST /v1/photo/download-url
"photo.presignedUpload": PhotoPresignedUploadResponse; // POST /v1/photo/presigned-url
"photo.reportUploadResult": PhotoReportUploadResultResponse; // POST /v1/photo/report
+ "cheese4cut.cheese4cutAiSummary": Cheese4cutCheese4cutAiSummaryResponse; // GET /v1/cheese4cut/{code}/ai-summary
"cheese4cut.finalize": Cheese4cutFinalizeResponse; // POST /v1/cheese4cut/{code}/fixed
+ "cheese4cut.cheese4cutFixedAi": Cheese4cutCheese4cutFixedAiResponse; // POST /v1/cheese4cut/{code}/fixed/ai
"cheese4cut.preview": Cheese4cutPreviewResponse; // GET /v1/cheese4cut/{code}/preview
"internal.thumbnailComplete": InternalThumbnailCompleteResponse; // POST /internal/thumbnail/complete
}