solorpower_app/components/UploadModal.js

326 lines
11 KiB
JavaScript

/**
* UploadModal.js - 과거 발전 데이터 엑셀 업로드 모달
*/
import React, { useState } from 'react';
// Force Update Check
import {
Modal,
View,
Text,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
import * as DocumentPicker from 'expo-document-picker';
const API_URL = 'https://solorpower.dadot.net';
export default function UploadModal({ visible, onClose, plantId, onUploadSuccess }) {
const [uploading, setUploading] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const [uploadType, setUploadType] = useState('daily'); // 'daily' | 'monthly'
// 파일 선택
const pickDocument = async () => {
try {
const result = await DocumentPicker.getDocumentAsync({
type: [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel', // .xls
],
copyToCacheDirectory: true,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
setSelectedFile(result.assets[0]);
}
} catch (error) {
Alert.alert('오류', '파일 선택 중 오류가 발생했습니다.');
}
};
// 업로드 실행
const handleUpload = async () => {
if (!selectedFile) {
Alert.alert('알림', '먼저 파일을 선택해주세요.');
return;
}
if (!plantId) {
Alert.alert('알림', '발전소를 선택해주세요.');
return;
}
setUploading(true);
try {
const formData = new FormData();
// 파일 추가
formData.append('file', {
uri: selectedFile.uri,
name: selectedFile.name,
type: selectedFile.mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
// uploadType에 따라 엔드포인트 분기
const endpoint = uploadType === 'daily'
? `/plants/${plantId}/upload`
: `/plants/${plantId}/upload/monthly`;
console.log(`🚀 Uploading ${uploadType} data`);
console.log(" URL:", `${API_URL}${endpoint}`);
const response = await fetch(`${API_URL}${endpoint}`, {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
const result = await response.json();
if (response.ok) {
Alert.alert('성공', result.message || '업로드가 완료되었습니다.');
setSelectedFile(null);
onUploadSuccess?.();
onClose();
} else {
Alert.alert('오류', result.detail || '업로드에 실패했습니다.');
}
} catch (error) {
Alert.alert('오류', `업로드 중 오류가 발생했습니다: ${error.message}`);
} finally {
setUploading(false);
}
};
// 모달 닫기
const handleClose = () => {
setSelectedFile(null);
setUploadType('daily');
onClose();
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={handleClose}
>
<View style={styles.overlay}>
<View style={styles.modalContainer}>
{/* 헤더 */}
<View style={styles.header}>
<Text style={styles.title}>📂 과거 데이터 업로드</Text>
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
{/* 타입 선택 탭 */}
<View style={{ flexDirection: 'row', marginBottom: 16, backgroundColor: '#F3F4F6', borderRadius: 8, padding: 4 }}>
<TouchableOpacity
style={{
flex: 1,
paddingVertical: 8,
alignItems: 'center',
borderRadius: 6,
backgroundColor: uploadType === 'daily' ? '#FFFFFF' : 'transparent',
shadowColor: uploadType === 'daily' ? '#000' : 'transparent',
shadowOpacity: uploadType === 'daily' ? 0.1 : 0,
shadowRadius: 2,
elevation: uploadType === 'daily' ? 2 : 0,
}}
onPress={() => setUploadType('daily')}
>
<Text style={{ fontWeight: uploadType === 'daily' ? '600' : '400', color: '#374151' }}>일간 (Daily)</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
flex: 1,
paddingVertical: 8,
alignItems: 'center',
borderRadius: 6,
backgroundColor: uploadType === 'monthly' ? '#FFFFFF' : 'transparent',
shadowColor: uploadType === 'monthly' ? '#000' : 'transparent',
shadowOpacity: uploadType === 'monthly' ? 0.1 : 0,
shadowRadius: 2,
elevation: uploadType === 'monthly' ? 2 : 0,
}}
onPress={() => setUploadType('monthly')}
>
<Text style={{ fontWeight: uploadType === 'monthly' ? '600' : '400', color: '#374151' }}>월간 (Monthly)</Text>
</TouchableOpacity>
</View>
{/* 안내 */}
<Text style={styles.description}>
{uploadType === 'daily'
? "필수 컬럼: date (날짜), generation (발전량) 또는 year, month, day, kwh"
: "필수 컬럼: year (연도), month (월), kwh (발전량)\n* 합계/평균 행 자동 제외"
}
</Text>
{/* 발전소 ID 표시 */}
<View style={styles.infoBox}>
<Text style={styles.infoLabel}>발전소 ID:</Text>
<Text style={styles.infoValue}>{plantId || '선택 안됨'}</Text>
</View>
{/* 파일 선택 */}
<TouchableOpacity style={styles.fileButton} onPress={pickDocument}>
<Text style={styles.fileButtonText}>
{selectedFile ? '📄 ' + selectedFile.name : '📁 엑셀 파일 선택 (.xlsx)'}
</Text>
</TouchableOpacity>
{/* 선택된 파일 정보 */}
{selectedFile && (
<View style={styles.fileInfo}>
<Text style={styles.fileInfoText}>
크기: {(selectedFile.size / 1024).toFixed(1)} KB
</Text>
</View>
)}
{/* 업로드 버튼 */}
<TouchableOpacity
style={[
styles.uploadButton,
(!selectedFile || uploading) && styles.uploadButtonDisabled,
]}
onPress={handleUpload}
disabled={!selectedFile || uploading}
>
{uploading ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.uploadButtonText}>
{uploadType === 'daily' ? '일간 데이터 업로드' : '월간 데이터 업로드'}
</Text>
)}
</TouchableOpacity>
{/* 취소 버튼 */}
<TouchableOpacity style={styles.cancelButton} onPress={handleClose}>
<Text style={styles.cancelButtonText}>취소</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContainer: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 24,
width: '100%',
maxWidth: 400,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 16,
elevation: 8,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
title: {
fontSize: 20,
fontWeight: '700',
color: '#1F2937',
},
closeButton: {
padding: 4,
},
closeButtonText: {
fontSize: 20,
color: '#6B7280',
},
description: {
fontSize: 14,
color: '#6B7280',
marginBottom: 16,
lineHeight: 20,
},
infoBox: {
flexDirection: 'row',
backgroundColor: '#F3F4F6',
padding: 12,
borderRadius: 8,
marginBottom: 16,
},
infoLabel: {
fontSize: 14,
color: '#6B7280',
marginRight: 8,
},
infoValue: {
fontSize: 14,
fontWeight: '600',
color: '#1F2937',
},
fileButton: {
backgroundColor: '#E5E7EB',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginBottom: 8,
borderWidth: 2,
borderColor: '#D1D5DB',
borderStyle: 'dashed',
},
fileButtonText: {
fontSize: 16,
color: '#374151',
},
fileInfo: {
alignItems: 'center',
marginBottom: 16,
},
fileInfoText: {
fontSize: 12,
color: '#6B7280',
},
uploadButton: {
backgroundColor: '#3B82F6',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginBottom: 8,
},
uploadButtonDisabled: {
backgroundColor: '#9CA3AF',
},
uploadButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#FFFFFF',
},
cancelButton: {
padding: 12,
alignItems: 'center',
},
cancelButtonText: {
fontSize: 14,
color: '#6B7280',
},
});