o2o-castad-frontend/src/pages/Dashboard/AssetManagementContent.tsx

253 lines
9.1 KiB
TypeScript
Executable File

import React, { useRef, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageItem, ImageUrlItem } from '../../types/api';
import { uploadImages } from '../../utils/api';
interface AssetManagementContentProps {
onNext: (imageTaskId: string) => void;
onBack?: () => void;
imageList: ImageItem[];
onRemoveImage: (index: number) => void;
onAddImages: (files: File[]) => void;
}
type VideoRatio = 'vertical' | 'horizontal';
const IMAGES_PER_PAGE = 12;
const AssetManagementContent: React.FC<AssetManagementContentProps> = ({
onNext,
onBack,
imageList,
onRemoveImage,
onAddImages,
}) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const [videoRatio, setVideoRatio] = useState<VideoRatio>('vertical');
const [displayCount, setDisplayCount] = useState(IMAGES_PER_PAGE);
useEffect(() => {
const savedRatio = localStorage.getItem('castad_video_ratio') as VideoRatio;
if (savedRatio === 'vertical' || savedRatio === 'horizontal') {
setVideoRatio(savedRatio);
}
}, []);
const handleVideoRatioChange = (ratio: VideoRatio) => {
setVideoRatio(ratio);
localStorage.setItem('castad_video_ratio', ratio);
};
const getImageSrc = (item: ImageItem): string => {
return item.type === 'url' ? item.url : item.preview;
};
const handleNextWithUpload = async () => {
if (imageList.length === 0) return;
setIsUploading(true);
setUploadError(null);
try {
const urlImages: ImageUrlItem[] = imageList
.filter((item: ImageItem): item is ImageItem & { type: 'url' } => item.type === 'url')
.map((item) => ({ url: item.url }));
const fileImages: File[] = imageList
.filter((item: ImageItem): item is ImageItem & { type: 'file' } => item.type === 'file')
.map((item) => item.file);
if (urlImages.length > 0 || fileImages.length > 0) {
const response = await uploadImages(urlImages, fileImages);
onNext(response.task_id);
}
} catch (error) {
console.error('Image upload failed:', error);
setUploadError(error instanceof Error ? error.message : t('assetManagement.uploadFailed'));
} finally {
setIsUploading(false);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer.files).filter((file: File) =>
file.type.startsWith('image/')
);
if (files.length > 0) onAddImages(files);
};
const handleFileSelect = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
onAddImages(Array.from(files));
e.target.value = '';
}
};
const visibleImages = imageList.slice(0, displayCount);
const hasMore = imageList.length > displayCount;
return (
<main className="asset-page">
{/* Fixed Header - 뒤로가기 버튼 */}
<div className="asset-sticky-header">
{onBack && (
<button onClick={onBack} className="asset-back-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
{t('assetManagement.back')}
</button>
)}
</div>
{/* Single Scrollable Content Area */}
<div className="asset-scroll-area">
<h1 className="asset-title">{t('assetManagement.title')}</h1>
<div className="asset-container">
{/* Left Column - Selected Images */}
<div className="asset-column asset-column-left">
<div className="asset-section-header">
<div className="asset-section-header-left">
<h3 className="asset-section-title">{t('assetManagement.selectedImages')}</h3>
<span className="asset-section-subtitle">{t('assetManagement.minImages')}</span>
</div>
<button onClick={handleFileSelect} className="asset-mobile-upload-btn">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<line x1="8" y1="2" x2="8" y2="14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<line x1="2" y1="8" x2="14" y2="8" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
{t('assetManagement.imageUpload')}
</button>
</div>
<div className="asset-image-list">
{visibleImages.length > 0 && (
<div className="asset-image-grid">
{visibleImages.map((item, i) => (
<div key={i} className="asset-image-item">
<img
src={getImageSrc(item)}
alt={`${t('assetManagement.imageAlt')} ${i + 1}`}
/>
{item.type === 'file' && (
<div className="asset-image-badge">{t('assetManagement.uploadBadge')}</div>
)}
<button
onClick={() => onRemoveImage(i)}
className="asset-image-remove"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
))}
</div>
)}
</div>
{hasMore && (
<button
className="asset-load-more"
onClick={() => setDisplayCount(prev => prev + IMAGES_PER_PAGE)}
>
{t('assetManagement.loadMore')}
</button>
)}
</div>
{/* Right Column - Upload and Video Ratio */}
<div className="asset-column asset-column-right">
{/* Image Upload Section (desktop only) */}
<div className="asset-upload-section">
<h3 className="asset-section-title">{t('assetManagement.imageUpload')}</h3>
<div
onClick={handleFileSelect}
onDragOver={handleDragOver}
onDrop={handleDrop}
className="asset-upload-zone"
>
<p className="asset-upload-text">
{t('assetManagement.dragAndDrop').split('\n').map((line, i) => (
<React.Fragment key={i}>{i > 0 && <br/>}{line}</React.Fragment>
))}
</p>
</div>
</div>
{/* Video Ratio Section */}
<div className="asset-ratio-section">
<h3 className="asset-section-title">{t('assetManagement.videoRatio')}</h3>
<div className="asset-ratio-buttons">
<button
onClick={() => handleVideoRatioChange('vertical')}
className={`asset-ratio-button ${videoRatio === 'vertical' ? 'active' : ''}`}
>
<div className="asset-ratio-icon asset-ratio-icon-vertical">
<div className="asset-ratio-box"></div>
</div>
<span className="asset-ratio-label">9:16</span>
<span className="asset-ratio-subtitle">{t('assetManagement.youtubeShorts')}</span>
</button>
<button
onClick={() => handleVideoRatioChange('horizontal')}
className={`asset-ratio-button ${videoRatio === 'horizontal' ? 'active' : ''}`}
>
<div className="asset-ratio-icon asset-ratio-icon-horizontal">
<div className="asset-ratio-box"></div>
</div>
<span className="asset-ratio-label">16:9</span>
<span className="asset-ratio-subtitle">{t('assetManagement.youtubeVideo')}</span>
</button>
</div>
</div>
</div>
</div>
</div>
{/* Fixed Footer - 다음 단계 버튼 */}
<div className="asset-sticky-footer">
{uploadError && (
<p className="text-red-500 text-sm mb-2">{uploadError}</p>
)}
<button
onClick={handleNextWithUpload}
disabled={imageList.length === 0 || isUploading}
className="asset-next-button"
>
{isUploading ? t('assetManagement.uploading') : t('assetManagement.nextStep')}
</button>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={handleFileChange}
className="hidden"
/>
</main>
);
};
export default AssetManagementContent;