diff --git a/.gitignore b/.gitignore index b8e96bd..9aa7806 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,12 @@ env/ *.db-journal .env .env.* +!.env.example secrets.json +# Runtime output +*.log + # IDEs and Editors .vscode/ .idea/ diff --git a/AGENTS.md b/AGENTS.md index bca2907..922c6db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,19 +43,17 @@ SolarPower is an integrated solar power monitoring system consisting of: - **Run (Prod)**: Managed via systemd: `sudo systemctl restart solar-api` - **Docs**: Access Swagger UI at `http://localhost:8000/docs` - **Test**: - - No automated test suite (pytest) currently. - - Verify endpoints manually using `curl` or Swagger UI. + - Run `python -m unittest discover -s tests -p "test_*.py" -v`. + - Verify deployed endpoints using `curl` or Swagger UI. ### ๐Ÿ Crawler (`crawler/`) - **Environment**: Python 3.10+ -- **Install**: Manual dependency installation (requests, python-dotenv, supabase). +- **Install**: `pip install -r requirements.txt` - **Run (Manual)**: `python main.py` - Runs in `LEARNING` or `OPTIMIZED` mode based on `crawler_manager.db`. - **Run (Force)**: `python main.py --force` (Ignores scheduler constraints) - **GUI Tool**: `python crawler_gui.py` (For historical data recovery and log cleaning) -- **Testing**: - - No standard test runner. Use standalone scripts in `tests/`. - - **Run Single Test**: `python tests/debug_kremc.py` (or similar script for specific site). +- **Testing**: `python -m unittest discover -s tests -p "test_*.py" -v` ## 4. Code Style & Standards @@ -99,7 +97,7 @@ SolarPower is an integrated solar power monitoring system consisting of: ### โ˜๏ธ Server Deployment 1. **Push**: Commit and push changes to the remote Git repository. 2. **Pull**: SSH into the server and pull changes. - - Command: `ssh user@server "cd ~/solorpower_server && git pull"` + - Command: `ssh user@server "cd ~/solorpower && git pull"` 3. **Restart**: Restart the API service. - Command: `sudo systemctl restart solar-api` diff --git a/README.md b/README.md index 781b9c7..f4771b8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ํƒœ์–‘๊ด‘ ๋ฐœ์ „์†Œ์˜ ์‹ค์‹œ๊ฐ„ ๋ฐœ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ํ†ตํ•ฉ ๊ด€์ œ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. +๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ [๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ](docs/development_and_testing.md), ๋””๋ ‰ํ„ฐ๋ฆฌ ์ฑ…์ž„๊ณผ ์ƒ์„ฑ ํŒŒ์ผ ์ •์ฑ…์€ [์ €์žฅ์†Œ ๊ตฌ์กฐ ๋ฌธ์„œ](docs/repository_structure.md)๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. + --- ## ๐Ÿ“‹ ๋ชฉ์ฐจ @@ -114,7 +116,7 @@ flowchart TD ### ์œ„์น˜ ``` d:\dev\etc\SolorPower\crawler\ (Local PC) -/home/ubuntu/SolorPower/crawler/ (Oracle Cloud) +/home/ubuntu/solorpower/crawler/ (Oracle Cloud) ``` ### ๋„คํŠธ์›Œํฌ ๊ตฌ์„ฑ (Tailscale) @@ -157,7 +159,7 @@ crawler/ ### ์ž๋™ ์ˆ˜์ง‘ (Cron) ```bash # 30๋ถ„๋งˆ๋‹ค ์‹คํ–‰ (Oracle Cloud ํ™˜๊ฒฝ) -*/30 * * * * cd /home/ubuntu/SolorPower/crawler && /home/ubuntu/SolorPower/crawler/venv/bin/python main.py >> cron.log 2>&1 +*/10 * * * * cd /home/ubuntu/solorpower/crawler && /usr/bin/python3 main.py >> cron.log 2>&1 ``` ### ํฌ๋กค๋Ÿฌ๋ณ„ ๊ฐœ์„  ์‚ฌํ•ญ (2026.01) @@ -173,7 +175,7 @@ crawler/ ### ์œ„์น˜ ``` -/home/ubuntu/SolorPower/api_server/ +/home/ubuntu/solorpower/api_server/ ``` ### ํ™˜๊ฒฝ ์„ค์ • (.env) @@ -219,6 +221,8 @@ d:\dev\etc\SolorPower\app\ - ๋ณ„์นญ(`๋ฐœ์ „๋Ÿ‰`, `kwh` ๋“ฑ) ๋ฐ ์…€ ๋ณ‘ํ•ฉ ์ฒ˜๋ฆฌ ์ง€์›. - **๋ฐ˜์‘ํ˜• ์›น ์ง€์›**: PC/๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ € ์ตœ์ ํ™”. +API ์ฃผ์†Œ์™€ ์กฐํšŒ ์—…์ฒด๋Š” `EXPO_PUBLIC_API_BASE_URL`, `EXPO_PUBLIC_COMPANY_ID`๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‹œ๋Š” `app/.env.example`์„ ์ฐธ๊ณ ํ•˜๋ฉฐ ๊ณต๊ฐœ ์„ค์ • ํŒŒ์ผ์— ์ธ์ฆ์ •๋ณด๋ฅผ ๋„ฃ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + --- ## ์›น ์•ฑ ๋ฐฐํฌ ๊ฐ€์ด๋“œ (Web Deployment) @@ -228,7 +232,8 @@ React Native(Expo) ์•ฑ์„ ์›น์œผ๋กœ ๋นŒ๋“œํ•˜๊ณ  Nginx ์„œ๋ฒ„์— ๋ฐฐํฌํ•˜๋Š” ### 1. ์›น ๋นŒ๋“œ (Local PC) ```bash cd d:\dev\etc\SolorPower\app -npm run export:web +npm ci +npm run build:web ``` - `dist` ํด๋”์— ์ •์  ์›น ํŒŒ์ผ๋“ค์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. diff --git a/api_server/app/routers/stats.py b/api_server/app/routers/stats.py index ead1cae..f979daa 100644 --- a/api_server/app/routers/stats.py +++ b/api_server/app/routers/stats.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query from supabase import Client -from typing import Literal, Optional +from typing import Annotated, Literal, Optional from datetime import date as date_type import calendar import re @@ -63,7 +63,10 @@ def get_all_plants_comparison( date: Optional[str] = Query(None, description="๋‚ ์งœ (YYYY-MM-DD)"), year: Optional[int] = Query(None, ge=MIN_STATS_YEAR, le=MAX_STATS_YEAR, description="์—ฐ๋„"), month: Optional[int] = Query(None, ge=1, le=12, description="์›”"), - db: Client = Depends(get_db) + db: Client = Depends(get_db), + company_id: Annotated[ + Optional[int], Query(ge=1, description="์—…์ฒด ID") + ] = None, ) -> ComparisonStatsResponse: """ ์ „์ฒด ๋ฐœ์ „์†Œ ๋ฐœ์ „๋Ÿ‰ ๋น„๊ต ํ†ต๊ณ„ ์กฐํšŒ @@ -75,8 +78,12 @@ def get_all_plants_comparison( target_year = year if year is not None else target_date.year target_month = month if month is not None else target_date.month - # 1. ๋ชจ๋“  ๋ฐœ์ „์†Œ ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ (์ด๋ฆ„, ์šฉ๋Ÿ‰) - plants_res = db.table("plants").select("id, name, capacity").execute() + # ์—…์ฒด๊ฐ€ ์ง€์ •๋˜๋ฉด ํ•ด๋‹น ์—…์ฒด ๋ฐœ์ „์†Œ๋งŒ ๋น„๊ตํ•œ๋‹ค. ๋ฏธ์ง€์ • ํ˜ธ์ถœ์€ ๊ธฐ์กด + # ์ „์ฒด ๋น„๊ต ๊ณ„์•ฝ์„ ์œ ์ง€ํ•˜์—ฌ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅํ•œ๋‹ค. + plants_query = db.table("plants").select("id, name, capacity") + if company_id is not None: + plants_query = plants_query.eq("company_id", company_id) + plants_res = plants_query.execute() plants = {p['id']: p for p in plants_res.data} # ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™” diff --git a/api_server/tests/test_api_contract.py b/api_server/tests/test_api_contract.py index 90b2aaa..5c911b5 100644 --- a/api_server/tests/test_api_contract.py +++ b/api_server/tests/test_api_contract.py @@ -53,6 +53,13 @@ class ApiContractTest(unittest.TestCase): self.assertEqual(400, response.status_code) + def test_invalid_comparison_company_returns_422(self): + response = self.client.get( + "/plants/stats/comparison?period=day&company_id=0" + ) + + self.assertEqual(422, response.status_code) + def test_invalid_month_returns_422(self): response = self.client.get( "/plants/nrems-03/stats?period=day&year=2026&month=13" diff --git a/api_server/tests/test_stats_timezone.py b/api_server/tests/test_stats_timezone.py index d0c1fd6..74f961c 100644 --- a/api_server/tests/test_stats_timezone.py +++ b/api_server/tests/test_stats_timezone.py @@ -84,6 +84,17 @@ class StatsTimezoneTest(unittest.TestCase): self.assertIn(("lt", "created_at", "2026-01-01T15:00:00+00:00"), query.filters) self.assertEqual(123.4, result.data[0].generation) + def test_comparison_filters_plants_by_company_when_requested(self): + db = FakeDb() + + stats.get_all_plants_comparison( + period="day", date="2026-01-01", year=None, month=None, + db=db, company_id=7, + ) + + query = db.latest_query("plants") + self.assertIn(("eq", "company_id", 7), query.filters) + def test_plant_today_query_has_upper_bound(self): db = FakeDb([{"today_kwh": 77.0}]) diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..016bc63 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,3 @@ +# Expo exposes EXPO_PUBLIC_* values to the app bundle. Do not put secrets here. +EXPO_PUBLIC_API_BASE_URL=https://solorpower.dadot.net +EXPO_PUBLIC_COMPANY_ID=1 diff --git a/app/App.js b/app/App.js index 34e3c6b..c3e650d 100644 --- a/app/App.js +++ b/app/App.js @@ -16,8 +16,9 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import PlantDetailScreen from './screens/PlantDetailScreen'; import ComparisonScreen from './screens/ComparisonScreen'; +import { appConfig } from './config/appConfig'; +import { getPlants, updatePlantAlerts } from './services/solarApi'; -const API_URL = 'https://solorpower.dadot.net/plants/1'; const Stack = createNativeStackNavigator(); // ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด @@ -33,11 +34,7 @@ function DashboardScreen({ navigation }) { const fetchData = useCallback(async () => { try { setError(null); - const response = await fetch(API_URL); - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status}`); - } - const result = await response.json(); + const result = await getPlants(); setData(result); } catch (err) { setError(err.message); @@ -62,17 +59,7 @@ function DashboardScreen({ navigation }) { return newData; }); - const response = await fetch(`https://solorpower.dadot.net/plants/${plant.company_id || 1}/${plant.id}/alerts`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ alerts_enabled: newValue }), - }); - - if (!response.ok) { - throw new Error('Failed to update alert settings'); - } + await updatePlantAlerts(plant.company_id, plant.id, newValue); } catch (err) { console.error(err); setData((prev) => { @@ -196,7 +183,7 @@ function DashboardScreen({ navigation }) { โ˜€๏ธ ํƒœ์–‘๊ด‘ ๋ฐœ์ „ ํ˜„ํ™ฉ navigation.navigate('Comparison')} + onPress={() => navigation.navigate('Comparison', { companyId: appConfig.companyId })} > ๐Ÿ“Š ์ „์ฒด ๋น„๊ต diff --git a/app/components/StatsPeriodControls.js b/app/components/StatsPeriodControls.js new file mode 100644 index 0000000..69bf5a2 --- /dev/null +++ b/app/components/StatsPeriodControls.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +const TABS = [ + { key: 'today', label: '์˜ค๋Š˜' }, + { key: 'day', label: '์ผ๊ฐ„' }, + { key: 'month', label: '์›”๊ฐ„' }, + { key: 'year', label: '์—ฐ๊ฐ„' }, +]; + +export default function StatsPeriodControls({ + dateLabel, + onMoveDate, + onPeriodChange, + period, +}) { + return ( + <> + + onMoveDate(-1)} style={styles.arrowButton}> + โ—€ + + {dateLabel} + onMoveDate(1)} style={styles.arrowButton}> + โ–ถ + + + + {TABS.map((tab) => ( + onPeriodChange(tab.key)} + > + + {tab.label} + + + ))} + + + ); +} + +const styles = StyleSheet.create({ + dateControl: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginVertical: 12, + gap: 16, + }, + arrowButton: { padding: 8 }, + arrowText: { fontSize: 20, color: '#4B5563' }, + dateText: { fontSize: 16, fontWeight: 'bold', color: '#1F2937' }, + tabContainer: { + flexDirection: 'row', + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 4, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + tab: { + flex: 1, + paddingVertical: 12, + alignItems: 'center', + borderRadius: 8, + }, + tabActive: { backgroundColor: '#3B82F6' }, + tabText: { fontSize: 13, fontWeight: '600', color: '#6B7280' }, + tabTextActive: { color: '#FFFFFF' }, +}); diff --git a/app/components/UploadModal.js b/app/components/UploadModal.js index 28fe989..a596629 100644 --- a/app/components/UploadModal.js +++ b/app/components/UploadModal.js @@ -14,8 +14,7 @@ import { Alert, } from 'react-native'; import * as DocumentPicker from 'expo-document-picker'; - -const API_URL = 'https://solorpower.dadot.net'; +import { uploadGenerationData } from '../services/solarApi'; export default function UploadModal({ visible, onClose, plantId, onUploadSuccess }) { const [uploading, setUploading] = useState(false); @@ -70,29 +69,11 @@ export default function UploadModal({ visible, onClose, plantId, onUploadSuccess }); } - // 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, - }); - - const result = await response.json(); - - if (response.ok) { - Alert.alert('์„ฑ๊ณต', result.message || '์—…๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - setSelectedFile(null); - onUploadSuccess?.(); - onClose(); - } else { - Alert.alert('์˜ค๋ฅ˜', result.detail || '์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); - } + const result = await uploadGenerationData(plantId, uploadType, formData); + Alert.alert('์„ฑ๊ณต', result.message || '์—…๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + setSelectedFile(null); + onUploadSuccess?.(); + onClose(); } catch (error) { Alert.alert('์˜ค๋ฅ˜', `์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message}`); } finally { diff --git a/app/config/appConfig.js b/app/config/appConfig.js new file mode 100644 index 0000000..89452a6 --- /dev/null +++ b/app/config/appConfig.js @@ -0,0 +1,20 @@ +const DEFAULT_API_BASE_URL = 'https://solorpower.dadot.net'; +const DEFAULT_COMPANY_ID = 1; + +const normalizeBaseUrl = (value) => value.replace(/\/+$/, ''); + +const parseCompanyId = (value) => { + const companyId = Number.parseInt(value, 10); + return Number.isInteger(companyId) && companyId > 0 + ? companyId + : DEFAULT_COMPANY_ID; +}; + +// This is the single tenant-selection boundary until the UI supports switching +// companies. Deployments can select another company without changing source. +export const appConfig = Object.freeze({ + apiBaseUrl: normalizeBaseUrl( + process.env.EXPO_PUBLIC_API_BASE_URL || DEFAULT_API_BASE_URL, + ), + companyId: parseCompanyId(process.env.EXPO_PUBLIC_COMPANY_ID), +}); diff --git a/app/hooks/usePlantStats.js b/app/hooks/usePlantStats.js new file mode 100644 index 0000000..ee39bd9 --- /dev/null +++ b/app/hooks/usePlantStats.js @@ -0,0 +1,73 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { getHourlyPlantStats, getPlantStats } from '../services/solarApi'; +import { + formatHourlyChartData, + formatPeriodChartData, + toLocalIsoDate, +} from '../utils/statsChart'; + +const buildPeriodParams = (period, selectedDate) => { + const year = selectedDate.getFullYear(); + const month = selectedDate.getMonth() + 1; + + if (period === 'day') return { period, year, month }; + return { period, year }; +}; + +const formatUpdatedAt = (value) => { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return '-'; + return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }); +}; + +export default function usePlantStats(plant, period, selectedDate) { + const [chartData, setChartData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const reload = useCallback(async () => { + try { + setLoading(true); + setError(null); + + if (period === 'today') { + const result = await getHourlyPlantStats(plant.id, toLocalIsoDate(selectedDate)); + setChartData(formatHourlyChartData(result?.data || [], selectedDate)); + } else { + const result = await getPlantStats( + plant.id, + buildPeriodParams(period, selectedDate), + ); + setChartData(formatPeriodChartData(period, result?.data || [], selectedDate)); + } + } catch (requestError) { + setError(requestError.message); + setChartData([]); + } finally { + setLoading(false); + } + }, [period, plant.id, selectedDate]); + + useEffect(() => { + reload(); + }, [reload]); + + const todayData = useMemo(() => { + if (period !== 'today' || toLocalIsoDate(selectedDate) !== toLocalIsoDate(new Date())) { + return null; + } + + const latestLog = plant.latest_log; + if (!latestLog) return null; + + return { + currentKw: Number(latestLog.current_kw) || 0, + todayKwh: Number(latestLog.today_kwh) || 0, + updatedAt: formatUpdatedAt(latestLog.created_at), + }; + }, [period, plant.latest_log, selectedDate]); + + return { chartData, loading, error, reload, todayData }; +} diff --git a/app/screens/ComparisonScreen.js b/app/screens/ComparisonScreen.js index d6b09bc..c8efd3a 100644 --- a/app/screens/ComparisonScreen.js +++ b/app/screens/ComparisonScreen.js @@ -10,10 +10,10 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { BarChart } from 'react-native-gifted-charts'; +import { appConfig } from '../config/appConfig'; +import { getComparisonStats } from '../services/solarApi'; -const API_BASE_URL = 'https://solorpower.dadot.net'; - -export default function ComparisonScreen({ navigation }) { +export default function ComparisonScreen({ navigation, route }) { const [period, setPeriod] = useState('day'); const [statsData, setStatsData] = useState([]); const [loading, setLoading] = useState(true); @@ -58,19 +58,17 @@ export default function ComparisonScreen({ navigation }) { const d = currentDate.getDate(); const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; - let query = `period=${period}`; + const query = { company_id: route.params?.companyId || appConfig.companyId, period }; if (period === 'day') { - query += `&date=${dateStr}`; + query.date = dateStr; } else if (period === 'month') { - query += `&year=${y}&month=${m}`; + query.year = y; + query.month = m; } else if (period === 'year') { - query += `&year=${y}`; + query.year = y; } - const response = await fetch(`${API_BASE_URL}/plants/stats/comparison?${query}`); - if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); - - const result = await response.json(); + const result = await getComparisonStats(query); setStatsData(result.data || []); } catch (err) { @@ -79,7 +77,7 @@ export default function ComparisonScreen({ navigation }) { } finally { setLoading(false); } - }, [period, currentDate]); + }, [period, currentDate, route.params?.companyId]); useEffect(() => { fetchStats(); diff --git a/app/screens/PlantDetailScreen.js b/app/screens/PlantDetailScreen.js index 324b444..fbce335 100644 --- a/app/screens/PlantDetailScreen.js +++ b/app/screens/PlantDetailScreen.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import { StyleSheet, View, @@ -6,27 +6,26 @@ import { TouchableOpacity, ActivityIndicator, ScrollView, - Alert, useWindowDimensions, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { BarChart } from 'react-native-gifted-charts'; +import StatsPeriodControls from '../components/StatsPeriodControls'; import UploadModal from '../components/UploadModal'; - -const API_BASE_URL = 'https://solorpower.dadot.net'; +import usePlantStats from '../hooks/usePlantStats'; export default function PlantDetailScreen({ route, navigation }) { const { plant } = route.params; const [period, setPeriod] = useState('today'); - const [chartData, setChartData] = useState([]); - const [loading, setLoading] = useState(true); const [uploadVisible, setUploadVisible] = useState(false); - const [error, setError] = useState(null); - const [todayData, setTodayData] = useState(null); - const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, label: '', value: 0 }); const { width } = useWindowDimensions(); const [currentDate, setCurrentDate] = useState(new Date()); + const { chartData, loading, error, reload: fetchStats, todayData } = usePlantStats( + plant, + period, + currentDate, + ); // ํƒญ ๋ณ€๊ฒฝ ์‹œ ๋‚ ์งœ ์ดˆ๊ธฐํ™” useEffect(() => { @@ -51,144 +50,6 @@ export default function PlantDetailScreen({ route, navigation }) { setCurrentDate(newDate); }; - // ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ - const fetchStats = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const y = currentDate.getFullYear(); - const m = currentDate.getMonth() + 1; - const d = currentDate.getDate(); - const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`; - - if (period === 'today') { - // ์˜ค๋Š˜ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋Š” ์ด๋ฏธ ์ „๋‹ฌ๋ฐ›์€ plant.latest_log ์‚ฌ์šฉ (์ƒ๋‹จ ์นด๋“œ์šฉ) - // ๋งŒ์•ฝ ๊ณผ๊ฑฐ ๋‚ ์งœ๋ฅผ ์„ ํƒํ–ˆ๋‹ค๋ฉด ์ƒ๋‹จ ์นด๋“œ๋Š” ์ˆจ๊ธฐ๊ฑฐ๋‚˜ ํ•ด๋‹น ๋‚ ์งœ์˜ ์š”์•ฝ์œผ๋กœ ๋Œ€์ฒดํ•ด์•ผ ํ•จ. - // ์—ฌ๊ธฐ์„  '์˜ค๋Š˜'์ธ ๊ฒฝ์šฐ์—๋งŒ ์ƒ๋‹จ ์นด๋“œ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์‹์œผ๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ. - - // ์„œ๋ฒ„์—์„œ ์‹œ๊ฐ„๋ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ - const response = await fetch(`${API_BASE_URL}/plants/${plant.id}/stats/today?date=${dateStr}`); - if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); - const result = await response.json(); - const hourlyData = result.data || []; - - // 24์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํฌ๋งทํŒ… - const formattedData = []; - for (let i = 0; i <= 24; i++) { - if (i === 24) { - formattedData.push({ value: 0, label: '24์‹œ', frontColor: 'transparent' }); - continue; - } - - const item = hourlyData.find(d => d.hour === i) || {}; - // ํ˜„์žฌ ์‹œ๊ฐ„์ธ์ง€ ํ™•์ธ (์˜ค๋Š˜ ๋‚ ์งœ์ด๊ณ  ํ˜„์žฌ ์‹œ๊ฐ„์ธ ๊ฒฝ์šฐ) - const isToday = new Date().toDateString() === currentDate.toDateString(); - const isCurrentHour = isToday && (i === new Date().getHours()); - const hasData = item.has_data || item.current_kw > 0; - - let color = '#E5E7EB'; - if (isCurrentHour) { - color = '#10B981'; - } else if (hasData) { - color = '#3B82F6'; - } - - formattedData.push({ - value: item.current_kw || 0, - label: i % 2 === 0 ? `${i}์‹œ` : '', - frontColor: color, - onPress: () => i < 24 && showTooltip(item.current_kw || 0, `${i}์‹œ`), - }); - } - setChartData(formattedData); - - // Today ํƒญ์ด์ง€๋งŒ ๊ณผ๊ฑฐ ๋‚ ์งœ์ธ ๊ฒฝ์šฐ ์ƒ๋‹จ ์š”์•ฝ ์นด๋“œ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  ํ•„์š” - // API ๊ฒฐ๊ณผ์— daily total์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ฑฐ๋‚˜, hourly sum์„ ํ•ด์•ผ ํ•จ. - // ํ˜„์žฌ API(stats/today)๋Š” today_kwh๋ฅผ ์ฃผ์ง€๋งŒ, ์ด๋Š” ๊ทธ ์‹œ๊ฐ„๋Œ€๊นŒ์ง€์˜ ๋ˆ„์ ์ž„. - // ๋งˆ์ง€๋ง‰ ์‹œ๊ฐ„๋Œ€์˜ today_kwh๊ฐ€ ๊ทธ๋‚ ์˜ ์ด ๋ฐœ์ „๋Ÿ‰. - // ํŽธ์˜์ƒ ์ฐจํŠธ ๋ฐ์ดํ„ฐ๋งŒ ๊ฐฑ์‹ . - - } else { - // ๊ธฐ์กด ์ผ๊ฐ„/์›”๊ฐ„/์—ฐ๊ฐ„ ์กฐํšŒ - // ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ - let query = `period=${period}`; - if (period === 'day') { - // period=day -> year, month ํ•„์š” - query += `&year=${y}&month=${m}`; - } else if (period === 'month') { - // period=month -> year ํ•„์š” - query += `&year=${y}`; - } else if (period === 'year') { - // period=year -> year ํ•„์š” (๊ธฐ์ค€ ์—ฐ๋„) - query += `&year=${y}`; - } - - const response = await fetch(`${API_BASE_URL}/plants/${plant.id}/stats?${query}`); - if (!response.ok) throw new Error(`HTTP Error: ${response.status}`); - const result = await response.json(); - - const rawData = result.data || []; - const formattedData = []; - - if (period === 'day') { - // ์„ ํƒ๋œ ๋‹ฌ์˜ ๋งˆ์ง€๋ง‰ ๋‚  - const lastDay = new Date(y, m, 0).getDate(); - const dataMap = {}; - rawData.forEach(item => { dataMap[item.label] = item.value; }); - - for (let day = 1; day <= lastDay; day++) { - const ds = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - const val = dataMap[ds] || 0; - const showLabel = (day === 1 || day % 5 === 0); - - formattedData.push({ - value: val, - label: showLabel ? `${day}` : '', - labelTextStyle: { fontSize: 10, color: '#6B7280', width: 40, textAlign: 'center', shiftX: -10 }, - frontColor: val > 0 ? '#3B82F6' : '#E5E7EB', - }); - } - } else if (period === 'month') { - // 1์›” ~ 12์›” - const dataMap = {}; - rawData.forEach(item => { dataMap[item.label] = item.value; }); - - for (let month = 1; month <= 12; month++) { - const ms = `${y}-${String(month).padStart(2, '0')}`; - const val = dataMap[ms] || 0; - - formattedData.push({ - value: val, - label: `${month}์›”`, - labelTextStyle: { fontSize: 10, color: '#6B7280', width: 40, textAlign: 'center', shiftX: -5 }, - frontColor: val > 0 ? '#3B82F6' : '#E5E7EB', - renderTooltip: renderTooltip, - }); - } - } else { - // year (๊ธฐ์กด ์œ ์ง€) - rawData.forEach(item => { - formattedData.push({ - value: item.value || 0, - label: formatLabel(item.label, period), - labelTextStyle: { fontSize: 10, color: '#6B7280', width: 40, textAlign: 'center', shiftX: -10 }, - frontColor: item.value > 0 ? '#3B82F6' : '#E5E7EB', - renderTooltip: renderTooltip, - }); - }); - } - - setChartData(formattedData); - } - } catch (err) { - setError(err.message); - setChartData([]); - } finally { - setLoading(false); - } - }, [plant.id, period, currentDate]); - // ๋‚ ์งœ ํฌ๋งทํŒ… (YYYY๋…„ MM์›” DD์ผ ๋“ฑ) const formatDateDisplay = () => { const y = currentDate.getFullYear(); @@ -202,36 +63,8 @@ export default function PlantDetailScreen({ route, navigation }) { return `${y}๋…„ ๊ธฐ์ค€`; }; - // ๊ธฐ๊ฐ„ ์„ค๋ช… (์ˆ˜์ •) - const getPeriodDescription = () => { - return formatDateDisplay(); - }; - - // ํƒญ ๋ Œ๋”๋ง (์•„๋ž˜ ๋‚ ์งœ ์ปจํŠธ๋กค ์ถ”๊ฐ€) - // ... renderTabs๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ , ํ˜ธ์ถœํ•˜๋Š” ๊ณณ์—์„œ ์ปจํŠธ๋กค ์ถ”๊ฐ€ - - // ๋‚ ์งœ ์ปจํŠธ๋กค ๋ Œ๋”๋ง - const renderDateControl = () => ( - - moveDate(-1)} style={{ padding: 8 }}> - โ—€ - - - {formatDateDisplay()} - - moveDate(1)} style={{ padding: 8 }}> - โ–ถ - - - ); - - useEffect(() => { - fetchStats(); - }, [fetchStats]); - - // ํˆดํŒ ํ‘œ์‹œ // ํˆดํŒ ๋ Œ๋”๋ง (์ปค์Šคํ…€) - const renderTooltip = (item, index) => { + const renderTooltip = (item) => { return ( { - if (!label) return ''; - - if (period === 'day') { - const parts = label.split('-'); - return parts.length >= 3 ? `${parseInt(parts[2], 10)}` : label; - } else if (period === 'month') { - const parts = label.split('-'); - // ์•ž์— 0 ์ œ๊ฑฐํ•˜๊ณ  '์›”' ์ถ”๊ฐ€ (์˜ˆ: 01 -> 1์›”) - return parts.length >= 2 ? `${parseInt(parts[1], 10)}์›”` : label; - } else { - return label.length === 4 ? `${label.slice(2)}๋…„` : label; - } - }; - // ์ด ๋ฐœ์ „๋Ÿ‰ ๊ณ„์‚ฐ const getTotalGeneration = () => { return chartData.reduce((sum, item) => sum + (item.value || 0), 0); }; - // ํƒญ ๋ Œ๋”๋ง - const renderTabs = () => ( - - {[ - { key: 'today', label: '์˜ค๋Š˜' }, - { key: 'day', label: '์ผ๊ฐ„' }, - { key: 'month', label: '์›”๊ฐ„' }, - { key: 'year', label: '์—ฐ๊ฐ„' }, - ].map((tab) => ( - setPeriod(tab.key)} - > - - {tab.label} - - - ))} - - ); - - // ๊ธฐ๊ฐ„ ์„ค๋ช… (์ˆ˜์ • - ์œ„์—์„œ ์ด๋ฏธ ์ •์˜๋จ, ์ค‘๋ณต ์ œ๊ฑฐ) - // const getPeriodDescription = ... (removed) - const chartWidth = Math.min(width - 60, 600); // ๋ง‰๋Œ€ ๋„ˆ๋น„ ๋ฐ ๊ฐ„๊ฒฉ ๊ณ„์‚ฐ @@ -329,11 +121,12 @@ export default function PlantDetailScreen({ route, navigation }) { - {/* ๋‚ ์งœ ์ปจํŠธ๋กค */} - {renderDateControl()} - - {/* ํƒญ */} - {renderTabs()} + {/* ์˜ค๋Š˜ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ ์นด๋“œ (today ํƒญ์ผ ๋•Œ๋งŒ) */} {period === 'today' && todayData && ( @@ -360,7 +153,7 @@ export default function PlantDetailScreen({ route, navigation }) { - ๐Ÿ“Š {period === 'today' ? '์‹œ๊ฐ„๋Œ€๋ณ„ ์ถœ๋ ฅ' : '๋ฐœ์ „๋Ÿ‰ ์ถ”์ด'} ({getPeriodDescription()}) + ๐Ÿ“Š {period === 'today' ? '์‹œ๊ฐ„๋Œ€๋ณ„ ์ถœ๋ ฅ' : '๋ฐœ์ „๋Ÿ‰ ์ถ”์ด'} ({formatDateDisplay()}) ๋ง‰๋Œ€๋ฅผ ํ„ฐ์น˜ํ•˜๋ฉด ์ƒ์„ธ ์ •๋ณด @@ -503,36 +296,6 @@ const styles = StyleSheet.create({ gap: 16, }, - // Tabs - tabContainer: { - flexDirection: 'row', - backgroundColor: '#FFFFFF', - borderRadius: 12, - padding: 4, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - tab: { - flex: 1, - paddingVertical: 12, - alignItems: 'center', - borderRadius: 8, - }, - tabActive: { - backgroundColor: '#3B82F6', - }, - tabText: { - fontSize: 13, - fontWeight: '600', - color: '#6B7280', - }, - tabTextActive: { - color: '#FFFFFF', - }, - // Today Card todayCard: { backgroundColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', diff --git a/app/server.log b/app/server.log deleted file mode 100644 index c4a5fe1..0000000 --- a/app/server.log +++ /dev/null @@ -1,33 +0,0 @@ -Starting up http-server, serving dist - -http-server version: 14.1.1 - -http-server settings: -CORS: disabled -Cache: 3600 seconds -Connection Timeout: 120 seconds -Directory Listings: visible -AutoIndex: visible -Serve GZIP Files: false -Serve Brotli Files: false -Default File Extension: none - -Available on: - http://172.31.160.1:3004 - http://192.168.45.31:3004 - http://127.0.0.1:3004 -Hit CTRL-C to stop the server - -[2026-02-12T01:44:51.238Z] "HEAD /" "curl/8.12.1" -(node:1368) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated -(Use `node --trace-deprecation ...` to show where the warning was created) -[2026-02-12T01:46:00.070Z] "GET /" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:46:00.119Z] "GET /_expo/static/js/web/AppEntry-943aad0f5c4e74ff22d7c538b45fdd54.js" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:46:00.399Z] "GET /favicon.ico" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:46:00.400Z] "GET /favicon.ico" Error (404): "Not found" -[2026-02-12T01:51:33.504Z] "GET /" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:52:17.173Z] "HEAD /" "curl/8.12.1" -[2026-02-12T01:52:33.791Z] "GET /" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:52:33.821Z] "GET /_expo/static/js/web/AppEntry-8dbed585399d63adcee0c00c59fe829b.js" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:52:34.014Z] "GET /favicon.ico" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" -[2026-02-12T01:52:34.015Z] "GET /favicon.ico" Error (404): "Not found" diff --git a/app/services/solarApi.js b/app/services/solarApi.js new file mode 100644 index 0000000..1fe2102 --- /dev/null +++ b/app/services/solarApi.js @@ -0,0 +1,63 @@ +import { appConfig } from '../config/appConfig'; + +const buildQuery = (params) => { + const entries = Object.entries(params) + .filter(([, value]) => value !== undefined && value !== null && value !== '') + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + + return entries.length > 0 ? `?${entries.join('&')}` : ''; +}; + +const requestJson = async (path, options = {}) => { + const response = await fetch(`${appConfig.apiBaseUrl}${path}`, options); + let payload = null; + + try { + payload = await response.json(); + } catch { + // Preserve the HTTP status when an upstream proxy returns a non-JSON body. + } + + if (!response.ok) { + const message = payload?.detail || payload?.message || `HTTP Error: ${response.status}`; + throw new Error(message); + } + + return payload; +}; + +export const getPlants = (companyId = appConfig.companyId) => ( + requestJson(`/plants/${companyId}`) +); + +export const updatePlantAlerts = (companyId, plantId, alertsEnabled) => { + if (!companyId) { + throw new Error('๋ฐœ์ „์†Œ์˜ ์—…์ฒด ID๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'); + } + + return requestJson(`/plants/${companyId}/${plantId}/alerts`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ alerts_enabled: alertsEnabled }), + }); +}; + +export const getHourlyPlantStats = (plantId, date) => ( + requestJson(`/plants/${plantId}/stats/today${buildQuery({ date })}`) +); + +export const getPlantStats = (plantId, params) => ( + requestJson(`/plants/${plantId}/stats${buildQuery(params)}`) +); + +export const getComparisonStats = (params) => ( + requestJson('/plants/stats/comparison' + buildQuery(params)) +); + +export const uploadGenerationData = (plantId, uploadType, formData) => { + const suffix = uploadType === 'monthly' ? '/monthly' : ''; + return requestJson(`/plants/${plantId}/upload${suffix}`, { + method: 'POST', + body: formData, + }); +}; diff --git a/app/utils/statsChart.js b/app/utils/statsChart.js new file mode 100644 index 0000000..8c68c94 --- /dev/null +++ b/app/utils/statsChart.js @@ -0,0 +1,87 @@ +const EMPTY_BAR_COLOR = '#E5E7EB'; +const DATA_BAR_COLOR = '#3B82F6'; +const CURRENT_BAR_COLOR = '#10B981'; + +export const toLocalIsoDate = (value) => { + const year = value.getFullYear(); + const month = String(value.getMonth() + 1).padStart(2, '0'); + const day = String(value.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +}; + +const labelTextStyle = (shiftX) => ({ + fontSize: 10, + color: '#6B7280', + width: 40, + textAlign: 'center', + shiftX, +}); + +const formatYearLabel = (label) => ( + label?.length === 4 ? `${label.slice(2)}๋…„` : (label || '') +); + +export const formatHourlyChartData = (hourlyData, selectedDate, now = new Date()) => { + const byHour = new Map(hourlyData.map((item) => [item.hour, item])); + const isToday = toLocalIsoDate(now) === toLocalIsoDate(selectedDate); + + return Array.from({ length: 25 }, (_, hour) => { + if (hour === 24) { + return { value: 0, label: '24์‹œ', frontColor: 'transparent' }; + } + + const item = byHour.get(hour) || {}; + const hasData = item.has_data || item.current_kw > 0; + const isCurrentHour = isToday && hour === now.getHours(); + + return { + value: item.current_kw || 0, + label: hour % 2 === 0 ? `${hour}์‹œ` : '', + frontColor: isCurrentHour + ? CURRENT_BAR_COLOR + : (hasData ? DATA_BAR_COLOR : EMPTY_BAR_COLOR), + }; + }); +}; + +export const formatPeriodChartData = (period, rawData, selectedDate) => { + const year = selectedDate.getFullYear(); + const month = selectedDate.getMonth() + 1; + const dataMap = new Map(rawData.map((item) => [item.label, item.value || 0])); + + if (period === 'day') { + const lastDay = new Date(year, month, 0).getDate(); + return Array.from({ length: lastDay }, (_, index) => { + const day = index + 1; + const key = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const value = dataMap.get(key) || 0; + return { + value, + label: day === 1 || day % 5 === 0 ? `${day}` : '', + labelTextStyle: labelTextStyle(-10), + frontColor: value > 0 ? DATA_BAR_COLOR : EMPTY_BAR_COLOR, + }; + }); + } + + if (period === 'month') { + return Array.from({ length: 12 }, (_, index) => { + const itemMonth = index + 1; + const key = `${year}-${String(itemMonth).padStart(2, '0')}`; + const value = dataMap.get(key) || 0; + return { + value, + label: `${itemMonth}์›”`, + labelTextStyle: labelTextStyle(-5), + frontColor: value > 0 ? DATA_BAR_COLOR : EMPTY_BAR_COLOR, + }; + }); + } + + return rawData.map((item) => ({ + value: item.value || 0, + label: formatYearLabel(item.label), + labelTextStyle: labelTextStyle(-10), + frontColor: item.value > 0 ? DATA_BAR_COLOR : EMPTY_BAR_COLOR, + })); +}; diff --git a/crawler/DEVELOPMENT.md b/crawler/DEVELOPMENT.md index 79d2730..0122cc2 100644 --- a/crawler/DEVELOPMENT.md +++ b/crawler/DEVELOPMENT.md @@ -6,14 +6,12 @@ NAS ๋ฐฐํฌ ์ „ Windows ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ## 2. ๊ฐ€์ƒํ™˜๊ฒฝ (Windows) -์œˆ๋„์šฐ์šฉ ๊ฐ€์ƒํ™˜๊ฒฝ์€ `venv_win` ํด๋”์— ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. +๊ฐ€์ƒํ™˜๊ฒฝ์€ ์ €์žฅ์†Œ์— ํฌํ•จํ•˜์ง€ ์•Š๊ณ  ๊ฐœ๋ฐœ PC๋งˆ๋‹ค `.venv`๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ณ ์ • ์˜์กด์„ฑ์€ `requirements.txt`๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ```powershell -# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” -.\venv_win\Scripts\activate - -# ์˜์กด์„ฑ ์„ค์น˜ -pip install -r requirements.txt +python -m venv .venv +.\.venv\Scripts\python -m pip install -r requirements.txt +.\.venv\Scripts\python -m unittest discover -s tests -p "test_*.py" -v ``` ## 3. ๊ฐœ๋ฐœ ๋„๊ตฌ (tools ํด๋”) @@ -35,16 +33,16 @@ pip install -r requirements.txt python tools/recover_from_log.py ``` -## 4. ์‹คํ–‰ ๋ฐ ํ…Œ์ŠคํŠธ +## 4. ์‹คํ–‰ ๋ฉ”์ธ ํฌ๋กค๋Ÿฌ ์‹คํ–‰: ```powershell -python main.py +.\.venv\Scripts\python main.py ``` GUI ๋Œ€์‹œ๋ณด๋“œ ์‹คํ–‰ (ํ…Œ์ŠคํŠธ์šฉ): ```powershell -python crawler_gui.py +.\.venv\Scripts\python crawler_gui.py ``` ## 5. ๋ฐฐํฌ ์‹œ ์ฃผ์˜์‚ฌํ•ญ -- `crawler_manager.py`์˜ `site_data.db`๋Š” ๋กœ์ปฌ์— ์ƒ์„ฑ๋˜๋ฏ€๋กœ ๋ฐฐํฌ ์‹œ ์ œ์™ธํ•˜๊ฑฐ๋‚˜ ์ดˆ๊ธฐํ™” ์ƒํƒœ๋กœ ๋ฐฐํฌํ•˜์„ธ์š”. +- `crawler_manager.py`์˜ `crawler_manager.db`๋Š” ๋Ÿฐํƒ€์ž„์— ์ƒ์„ฑ๋˜๋ฉฐ Git์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ์šด์˜ ์ƒํƒœ DB๋ฅผ ๋ฐฐํฌ ํŒŒ์ผ๋กœ ๋ฎ์–ด์“ฐ์ง€ ๋งˆ์„ธ์š”. - `.env` ํŒŒ์ผ์˜ API ํ‚ค๊ฐ€ ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. diff --git a/crawler/crawlers/cmsolar_old.py b/crawler/crawlers/cmsolar_old.py deleted file mode 100644 index d10d002..0000000 --- a/crawler/crawlers/cmsolar_old.py +++ /dev/null @@ -1,319 +0,0 @@ -# ========================================== -# crawlers/cmsolar.py - CMSolar ํฌ๋กค๋Ÿฌ (10ํ˜ธ๊ธฐ) -# ========================================== - -import requests -from .base import create_session - -def fetch_data(plant_info): - """ - CMSolar ๋ฐœ์ „์†Œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - company_name = plant_info.get('company_name', 'ํƒœ์–‘๊ณผ๋ฐ”๋žŒ') - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('login_id', '') - login_pw = auth.get('login_pw', '') - site_no = auth.get('site_no', '') - base_url = system.get('base_url', '') - - session = create_session() - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/143.0.0.0 Safari/537.36', - 'Referer': f'{base_url}/plant/index.php' - } - - # 1. ๋กœ๊ทธ์ธ - try: - login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'} - session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers) - except: - return [] - - # 2. ์‚ฌ์ดํŠธ ์„ ํƒ - try: - session.get(f"{base_url}/change.php?site={site_no}", headers=headers) - except: - return [] - - # 3. ๋ฐ์ดํ„ฐ ์š”์ฒญ - target_url = f"{base_url}/plant/sub/idx_ok.php?mode=getPlant" - - try: - res = session.get(target_url, headers=headers) - res.encoding = 'utf-8' - - data = res.json() - plant_data = data[0]['plant'] - - # ๋‹จ์œ„ ๋ณ€ํ™˜ (W -> kW, Wh -> kWh) - curr_kw = float(plant_data.get('now', 0)) / 1000 - today_kwh = float(plant_data.get('today', 0)) / 1000 - - is_error = int(plant_data.get('inv_error', 0)) - status = "๐ŸŸข ์ •์ƒ" if is_error == 0 else "๐Ÿ”ด ์ ๊ฒ€/๊ณ ์žฅ" - - print(f" [CMSolar] {plant_name} ์ˆ˜์ง‘ ์™„๋ฃŒ: {round(curr_kw, 2)} kW") - return [{ - 'id': plant_id, - 'name': f'{company_name} {plant_name}', - 'kw': round(curr_kw, 2), - 'today': round(today_kwh, 2), - 'status': status - }] - - except Exception as e: - print(f"โŒ {plant_name} ์—๋Ÿฌ: {e}") - return [] - - -def fetch_history_daily(plant_info, start_date, end_date): - """ - CMSolar ๋ฐœ์ „์†Œ์˜ ์ผ๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - from datetime import datetime, timedelta - from .base import safe_float - - results = [] - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('login_id', '') - login_pw = auth.get('login_pw', '') - site_no = auth.get('site_no', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[CMSolar Daily] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Referer': f'{base_url}/plant/index.php' - } - - try: - login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'} - session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers) - session.get(f"{base_url}/change.php?site={site_no}", headers=headers) - print(" โœ“ Login successful") - except Exception as e: - print(f" โœ— Login failed: {e}") - return results - - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - - # ์ผ๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - daily_url = f"{base_url}/plant/sub/daily_data.php?date={date_str}" - - try: - res = session.get(daily_url, headers=headers, timeout=10) - res.encoding = 'utf-8' - - if res.status_code == 200: - data = res.json() - daily_kwh = safe_float(data.get('today', data.get('daily', 0))) / 1000.0 - - results.append({ - 'plant_id': plant_id, - 'date': date_str, - 'generation_kwh': daily_kwh - }) - print(f" โœ“ {date_str}: {daily_kwh}kWh") - - except Exception as e: - print(f" โœ— {date_str}: {e}") - - current_date += timedelta(days=1) - - print(f"[Total] Collected {len(results)} daily records\n") - return results - - -def fetch_history_monthly(plant_info, start_month, end_month): - """ - CMSolar ๋ฐœ์ „์†Œ์˜ ์›”๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - from datetime import datetime - from dateutil.relativedelta import relativedelta - from .base import safe_float - - results = [] - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('login_id', '') - login_pw = auth.get('login_pw', '') - site_no = auth.get('site_no', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[CMSolar Monthly] {plant_name} ({start_month} ~ {end_month})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Referer': f'{base_url}/plant/index.php' - } - - try: - login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'} - session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers) - session.get(f"{base_url}/change.php?site={site_no}", headers=headers) - print(" โœ“ Login successful") - except Exception as e: - print(f" โœ— Login failed: {e}") - return results - - current_month = datetime.strptime(start_month, '%Y-%m') - end_month_dt = datetime.strptime(end_month, '%Y-%m') - - while current_month <= end_month_dt: - month_str = current_month.strftime('%Y-%m') - - # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - monthly_url = f"{base_url}/plant/sub/monthly_data.php?month={month_str}" - - try: - res = session.get(monthly_url, headers=headers, timeout=10) - res.encoding = 'utf-8' - - if res.status_code == 200: - data = res.json() - monthly_kwh = safe_float(data.get('month', data.get('monthly', 0))) / 1000.0 - - results.append({ - 'plant_id': plant_id, - 'month': month_str, - 'generation_kwh': monthly_kwh - }) - print(f" โœ“ {month_str}: {monthly_kwh}kWh") - - except Exception as e: - print(f" โœ— {month_str}: {e}") - - current_month += relativedelta(months=1) - - print(f"[Total] Collected {len(results)} monthly records\n") - return results - - -def fetch_history_hourly(plant_info, start_date, end_date): - """ - CMSolar ๋ฐœ์ „์†Œ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - - Args: - plant_info: dict, ๋ฐœ์ „์†Œ ์ •๋ณด - start_date: str, ์‹œ์ž‘์ผ (YYYY-MM-DD) - end_date: str, ์ข…๋ฃŒ์ผ (YYYY-MM-DD) - - Returns: - list: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ๋ ˆ์ฝ”๋“œ - """ - from datetime import datetime, timedelta - from .base import safe_float - - results = [] - - # ์„ค์ • ์ถ”์ถœ - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('login_id', '') - login_pw = auth.get('login_pw', '') - site_no = auth.get('site_no', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[CMSolar History] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - # ๋กœ๊ทธ์ธ - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/143.0.0.0 Safari/537.36', - 'Referer': f'{base_url}/plant/index.php' - } - - try: - login_data = {'id': login_id, 'pw': login_pw, 'commit': 'Login'} - session.post(f"{base_url}/login_ok.php", data=login_data, headers=headers) - - # ์‚ฌ์ดํŠธ ์„ ํƒ - session.get(f"{base_url}/change.php?site={site_no}", headers=headers) - print(f" โœ“ Login successful") - - except Exception as e: - print(f" โœ— Login failed: {e}") - return results - - # ๋‚ ์งœ ๋ฒ”์œ„ ๋ฐ˜๋ณต - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - print(f"\n[Processing Date] {date_str}") - - # ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - hourly_url = f"{base_url}/plant/sub/hourly_data.php?site={site_no}&date={date_str}" - - try: - res = session.get(hourly_url, headers=headers, timeout=10) - res.encoding = 'utf-8' - - if res.status_code == 200: - data = res.json() - hourly_data = data if isinstance(data, list) else data.get('hourly', []) - - if hourly_data and len(hourly_data) > 0: - print(f" โœ“ Found {len(hourly_data)} hourly records") - - for item in hourly_data: - hour = str(item.get('hour', item.get('time', '00'))).zfill(2) - generation_wh = safe_float(item.get('energy', item.get('now', 0))) - generation_kwh = generation_wh / 1000.0 if generation_wh > 1000 else generation_wh - current_kw = safe_float(item.get('power', 0)) / 1000.0 - - timestamp = f"{date_str} {hour}:00:00" - - results.append({ - 'plant_id': plant_id, - 'timestamp': timestamp, - 'generation_kwh': generation_kwh, - 'current_kw': current_kw - }) - else: - print(f" โš  No hourly data for {date_str}") - else: - print(f" โœ— HTTP {res.status_code}") - - except Exception as e: - print(f" โœ— Error: {e}") - - # ๋‹ค์Œ ๋‚ ์งœ๋กœ - current_date += timedelta(days=1) - - print(f"\n{'='*60}") - print(f"[Total] Collected {len(results)} hourly records") - print(f"{'='*60}\n") - - return results diff --git a/crawler/crawlers/cmsolar_old2.py b/crawler/crawlers/cmsolar_old2.py deleted file mode 100644 index 83dc02c..0000000 --- a/crawler/crawlers/cmsolar_old2.py +++ /dev/null @@ -1,427 +0,0 @@ -# ========================================== -# crawlers/cmsolar.py - CMSolar ํฌ๋กค๋Ÿฌ (10ํ˜ธ๊ธฐ) -# ========================================== - -import requests -from .base import create_session, safe_float - -def fetch_data(plant_info): - """ - CMSolar ๋ฐœ์ „์†Œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - company_name = plant_info.get('company_name', 'ํ•จ์•ˆํ–‡๋น›๋ฐœ์ „์†Œ') - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('payload_id', '') - login_pw = auth.get('payload_pw', '') - site_no = auth.get('site_no', '') - login_url = system.get('login_url', '') - data_url = system.get('data_url', '') - - session = create_session() - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded' - } - - # ๋กœ๊ทธ์ธ - login_data = { - 'login_id': login_id, - 'login_pw': login_pw, - 'site_no': site_no - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code != 200: - return [] - except Exception as e: - print(f"โŒ {plant_name} ์ ‘์† ์—๋Ÿฌ: {e}") - return [] - - # ๋ฐ์ดํ„ฐ ์š”์ฒญ - try: - res = session.get(data_url, headers=headers) - - if res.status_code == 200: - data = res.json() - curr_kw = safe_float(data.get('current', data.get('power', 0))) - today_kwh = safe_float(data.get('today', data.get('generation', 0))) - status = "๐ŸŸข ์ •์ƒ" if curr_kw > 0 else "๐Ÿ’ค ๋Œ€๊ธฐ" - - return [{ - 'id': plant_id, - 'name': f'{company_name} {plant_name}', - 'kw': curr_kw, - 'today': today_kwh, - 'status': status - }] - else: - return [] - - except Exception as e: - print(f"โŒ {plant_name} ์—๋Ÿฌ: {e}") - return [] - - -def fetch_history_hourly(plant_info, start_date, end_date): - """ - CMSolar ๋ฐœ์ „์†Œ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - - ์‹ค์ œ ์—”๋“œํฌ์ธํŠธ: /plant/sub/report_ok.php - ํŒŒ๋ผ๋ฏธํ„ฐ: mode=getPowers&type=daily&device=total&start=YYYY-MM-DD&money= - """ - from datetime import datetime, timedelta - - results = [] - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('payload_id', '') - login_pw = auth.get('payload_pw', '') - site_no = auth.get('site_no', '') - login_url = system.get('login_url', '') - - # ์‹ค์ œ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ - base_url = system.get('api_url', 'http://www.cmsolar2.kr') - data_url = f"{base_url}/plant/sub/report_ok.php" - - session = create_session() - - print(f"\n{'='*60}") - print(f"[CMSolar History] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = { - 'login_id': login_id, - 'login_pw': login_pw, - 'site_no': site_no - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - # ์‚ฌ์ดํŠธ ์„ ํƒ (ํ•„์ˆ˜!) - try: - change_url = f"{base_url}/change.php?site={site_no}" - session.get(change_url, headers=headers) - print(" โœ“ Site selected") - except Exception as e: - print(f" โœ— Site selection error: {e}") - return results - - # ๋‚ ์งœ ๋ฐ˜๋ณต - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - - # ์‹ค์ œ ํ™•์ธ๋œ ์‹œ๊ฐ„๋ณ„ ์—”๋“œํฌ์ธํŠธ (type=daily๋Š” ํ•˜๋ฃจ ์น˜ ์‹œ๊ฐ„๋ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜) - params = { - 'mode': 'getPowers', - 'type': 'daily', - 'device': 'total', - 'start': date_str, - 'money': '' - } - - try: - res = session.get(data_url, params=params, headers=headers, timeout=10) - - if res.status_code == 200: - data = res.json() - # ์‹œ๊ฐ„๋ณ„ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ - hourly_data = data.get('data', []) or data.get('list', []) or data.get('powers', []) - - if isinstance(hourly_data, list) and len(hourly_data) > 0: - print(f" โœ“ Found {len(hourly_data)} hourly records for {date_str}") - - for item in hourly_data: - hour = str(item.get('hour', item.get('time', '00'))).zfill(2) - generation_kwh = safe_float(item.get('power', item.get('generation', item.get('kwh', 0)))) - current_kw = safe_float(item.get('kw', 0)) - - timestamp = f"{date_str} {hour}:00:00" - - results.append({ - 'plant_id': plant_id, - 'timestamp': timestamp, - 'generation_kwh': generation_kwh, - 'current_kw': current_kw - }) - else: - print(f" โš  No data for {date_str}") - else: - print(f" โœ— HTTP {res.status_code} for {date_str}") - - except Exception as e: - print(f" โœ— Error: {e}") - - current_date += timedelta(days=1) - - print(f"\n{'='*60}") - print(f"[Total] Collected {len(results)} hourly records") - print(f"{'='*60}\n") - - return results - - -def fetch_history_daily(plant_info, start_date, end_date): - """ - CMSolar ๋ฐœ์ „์†Œ์˜ ์ผ๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - - ์‹ค์ œ ์—”๋“œํฌ์ธํŠธ: /plant/sub/report_ok.php - ํŒŒ๋ผ๋ฏธํ„ฐ: mode=getPowers&type=month&device=total&start=YYYY-MM-DD&money= - """ - from datetime import datetime - from dateutil.relativedelta import relativedelta - - results = [] - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('payload_id', '') - login_pw = auth.get('payload_pw', '') - site_no = auth.get('site_no', '') - login_url = system.get('login_url', '') - - # ์‹ค์ œ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ - base_url = system.get('api_url', 'http://www.cmsolar2.kr') - data_url = f"{base_url}/plant/sub/report_ok.php" - - session = create_session() - - print(f"\n{'='*60}") - print(f"[CMSolar Daily] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = { - 'login_id': login_id, - 'login_pw': login_pw, - 'site_no': site_no - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - # ์‚ฌ์ดํŠธ ์„ ํƒ (ํ•„์ˆ˜!) - try: - change_url = f"{base_url}/change.php?site={site_no}" - session.get(change_url, headers=headers) - print(" โœ“ Site selected") - except Exception as e: - print(f" โœ— Site selection error: {e}") - return results - - # ์›” ๋‹จ์œ„๋กœ ๋ฐ˜๋ณต (type=month๋Š” ํ•œ ๋‹ฌ ์น˜ ์ผ๋ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜) - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - month_start = current_date.strftime('%Y-%m-01') - - # ์‹ค์ œ ํ™•์ธ๋œ ์ผ๋ณ„ ์—”๋“œํฌ์ธํŠธ (type=month) - params = { - 'mode': 'getPowers', - 'type': 'month', - 'device': 'total', - 'start': month_start, - 'money': '' - } - - try: - res = session.get(data_url, params=params, headers=headers, timeout=10) - - if res.status_code == 200: - data = res.json() - # ์ผ๋ณ„ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ - daily_data = data.get('data', []) or data.get('list', []) or data.get('powers', []) - - if isinstance(daily_data, list) and len(daily_data) > 0: - print(f" โœ“ Found {len(daily_data)} daily records for {month_start[:7]}") - - for item in daily_data: - date_str = item.get('date', item.get('day', '')) - generation_kwh = safe_float(item.get('power', item.get('generation', item.get('kwh', 0)))) - current_kw = safe_float(item.get('kw', 0)) - - # ๋‚ ์งœ ๋ฒ”์œ„ ํ•„ํ„ฐ๋ง - if date_str >= start_date and date_str <= end_date: - results.append({ - 'plant_id': plant_id, - 'date': date_str, - 'generation_kwh': generation_kwh, - 'current_kw': current_kw - }) - print(f" โœ“ {date_str}: {generation_kwh:.2f}kWh") - else: - print(f" โœ— HTTP {res.status_code} for {month_start[:7]}") - - except Exception as e: - print(f" โœ— Error: {e}") - - # ๋‹ค์Œ ๋‹ฌ๋กœ ์ด๋™ - current_date = (current_date.replace(day=1) + relativedelta(months=1)) - - print(f"[Total] Collected {len(results)} daily records\n") - return results - - -def fetch_history_monthly(plant_info, start_month, end_month): - """ - CMSolar ๋ฐœ์ „์†Œ์˜ ์›”๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - - ์‹ค์ œ ์—”๋“œํฌ์ธํŠธ: /plant/sub/report_ok.php - ํŒŒ๋ผ๋ฏธํ„ฐ: mode=getPowers&type=year&device=total&start=YYYY-MM-DD&money= - """ - from datetime import datetime - from dateutil.relativedelta import relativedelta - - results = [] - plant_id = plant_info.get('id', 'cmsolar-10') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '10ํ˜ธ๊ธฐ') - - login_id = auth.get('payload_id', '') - login_pw = auth.get('payload_pw', '') - site_no = auth.get('site_no', '') - login_url = system.get('login_url', '') - - # ์‹ค์ œ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ - base_url = system.get('api_url', 'http://www.cmsolar2.kr') - data_url = f"{base_url}/plant/sub/report_ok.php" - - session = create_session() - - print(f"\n{'='*60}") - print(f"[CMSolar Monthly] {plant_name} ({start_month} ~ {end_month})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = { - 'login_id': login_id, - 'login_pw': login_pw, - 'site_no': site_no - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - # ์‚ฌ์ดํŠธ ์„ ํƒ (ํ•„์ˆ˜!) - try: - change_url = f"{base_url}/change.php?site={site_no}" - session.get(change_url, headers=headers) - print(" โœ“ Site selected") - except Exception as e: - print(f" โœ— Site selection error: {e}") - return results - # ์—ฐ๋„๋ณ„๋กœ ๋ฐ˜๋ณต (type=year๋Š” ํ•œ ํ•ด ์น˜ ์›”๋ณ„ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜) - current_date = datetime.strptime(start_month + '-01', '%Y-%m-%d') - end_date = datetime.strptime(end_month + '-01', '%Y-%m-%d') - - years_processed = set() - - while current_date <= end_date: - year_start = current_date.strftime('%Y-01-01') - year = current_date.year - - # ์ค‘๋ณต ์—ฐ๋„ ์Šคํ‚ต - if year in years_processed: - current_date += relativedelta(months=1) - continue - - years_processed.add(year) - - # ์‹ค์ œ ํ™•์ธ๋œ ์›”๋ณ„ ์—”๋“œํฌ์ธํŠธ (type=year) - params = { - 'mode': 'getPowers', - 'type': 'year', - 'device': 'total', - 'start': year_start, - 'money': '' - } - - try: - res = session.get(data_url, params=params, headers=headers, timeout=10) - - if res.status_code == 200: - data = res.json() - # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ - monthly_data = data.get('data', []) or data.get('list', []) or data.get('powers', []) - - if isinstance(monthly_data, list) and len(monthly_data) > 0: - print(f" โœ“ Found {len(monthly_data)} monthly records for {year}") - - for item in monthly_data: - month_str = item.get('month', item.get('date', '')) - generation_kwh = safe_float(item.get('power', item.get('generation', item.get('kwh', 0)))) - - # YYYY-MM ํ˜•์‹์œผ๋กœ ์ •๊ทœํ™” - if len(month_str) >= 7: - month_str = month_str[:7] - - # ์›” ๋ฒ”์œ„ ํ•„ํ„ฐ๋ง - if month_str >= start_month and month_str <= end_month: - results.append({ - 'plant_id': plant_id, - 'month': month_str, - 'generation_kwh': generation_kwh - }) - print(f" โœ“ {month_str}: {generation_kwh:.1f}kWh") - else: - print(f" โœ— HTTP {res.status_code} for {year}") - - except Exception as e: - print(f" โœ— Error: {e}") - - current_date += relativedelta(months=1) - - print(f"[Total] Collected {len(results)} monthly records\n") - return results diff --git a/crawler/crawlers/sun_wms.py.backup b/crawler/crawlers/sun_wms.py.backup deleted file mode 100644 index ad76f41..0000000 --- a/crawler/crawlers/sun_wms.py.backup +++ /dev/null @@ -1,343 +0,0 @@ -# ========================================== -# crawlers/sun_wms.py - Sun-WMS ํฌ๋กค๋Ÿฌ (6ํ˜ธ๊ธฐ) -# ========================================== - -import requests -import re -import time -from .base import create_session - -def fetch_data(plant_info): - """ - Sun-WMS ๋ฐœ์ „์†Œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - company_name = plant_info.get('company_name', 'ํƒœ์–‘๊ณผ๋ฐ”๋žŒ') - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - data_url = system.get('data_url', '') - - session = create_session() - - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'http://tb6.sun-wms.com/public/main/login.php', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - # 1. ๋กœ๊ทธ์ธ - login_data = { - 'act': 'loginChk', - 'user_id': payload_id, - 'user_pass': payload_pw - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code != 200: - return [] - except Exception as e: - print(f"โŒ {plant_name} ์ ‘์† ์—๋Ÿฌ: {e}") - return [] - - # 2. ๋ฐ์ดํ„ฐ ์š”์ฒญ - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{data_url}?time={timestamp}", headers=headers) - res.encoding = 'euc-kr' - - content = res.text - - match_kw = re.search(r"id=['\"]cur_power['\"].*?value=['\"]([^'\"]+)['\"]", content) - curr_kw = float(match_kw.group(1)) if match_kw else 0.0 - - match_today = re.search(r"id=['\"]today_power['\"].*?value=['\"]([^'\"]+)['\"]", content) - today_kwh = float(match_today.group(1)) if match_today else 0.0 - - status = "๐ŸŸข ์ •์ƒ" if curr_kw > 0 else "๐Ÿ’ค ๋Œ€๊ธฐ" - - return [{ - 'id': plant_id, - 'name': f'{company_name} {plant_name}', - 'kw': curr_kw, - 'today': today_kwh, - 'status': status - }] - - except Exception as e: - print(f"โŒ {plant_name} ์—๋Ÿฌ: {e}") - return [] - - -def fetch_history_daily(plant_info, start_date, end_date): - """ - Sun-WMS ๋ฐœ์ „์†Œ์˜ ์ผ๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - from datetime import datetime, timedelta - from .base import safe_float - import time - - results = [] - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[Sun-WMS Daily] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw} - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - - # ์ผ๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - daily_url = f"{base_url}/public/chart/getDailyData.php?date={date_str}" - - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{daily_url}&time={timestamp}", headers=headers, timeout=10) - res.encoding = 'euc-kr' - - if res.status_code == 200: - data = res.json() - daily_kwh = safe_float(data.get('daily', data.get('today', 0))) - - results.append({ - 'plant_id': plant_id, - 'date': date_str, - 'generation_kwh': daily_kwh - }) - print(f" โœ“ {date_str}: {daily_kwh}kWh") - - except Exception as e: - print(f" โœ— {date_str}: {e}") - - current_date += timedelta(days=1) - - print(f"[Total] Collected {len(results)} daily records\n") - return results - - -def fetch_history_monthly(plant_info, start_month, end_month): - """ - Sun-WMS ๋ฐœ์ „์†Œ์˜ ์›”๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - from datetime import datetime - from dateutil.relativedelta import relativedelta - from .base import safe_float - import time - - results = [] - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[Sun-WMS Monthly] {plant_name} ({start_month} ~ {end_month})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw} - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - current_month = datetime.strptime(start_month, '%Y-%m') - end_month_dt = datetime.strptime(end_month, '%Y-%m') - - while current_month <= end_month_dt: - month_str = current_month.strftime('%Y-%m') - - # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - monthly_url = f"{base_url}/public/chart/getMonthlyData.php?month={month_str}" - - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{monthly_url}&time={timestamp}", headers=headers, timeout=10) - res.encoding = 'euc-kr' - - if res.status_code == 200: - data = res.json() - monthly_kwh = safe_float(data.get('monthly', data.get('month', 0))) - - results.append({ - 'plant_id': plant_id, - 'month': month_str, - 'generation_kwh': monthly_kwh - }) - print(f" โœ“ {month_str}: {monthly_kwh}kWh") - - except Exception as e: - print(f" โœ— {month_str}: {e}") - - current_month += relativedelta(months=1) - - print(f"[Total] Collected {len(results)} monthly records\n") - return results - - -def fetch_history_hourly(plant_info, start_date, end_date): - """ - Sun-WMS ๋ฐœ์ „์†Œ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - - Args: - plant_info: dict, ๋ฐœ์ „์†Œ ์ •๋ณด - start_date: str, ์‹œ์ž‘์ผ (YYYY-MM-DD) - end_date: str, ์ข…๋ฃŒ์ผ (YYYY-MM-DD) - - Returns: - list: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ๋ ˆ์ฝ”๋“œ - """ - from datetime import datetime, timedelta - from .base import safe_float - import time - - results = [] - - # ์„ค์ • ์ถ”์ถœ - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[Sun-WMS History] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - # ๋กœ๊ทธ์ธ - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'http://tb6.sun-wms.com/public/main/login.php', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = { - 'act': 'loginChk', - 'user_id': payload_id, - 'user_pass': payload_pw - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code != 200: - print(f" โœ— Login failed") - return results - - print(f" โœ“ Login successful") - - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - # ๋‚ ์งœ ๋ฒ”์œ„ ๋ฐ˜๋ณต - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - print(f"\n[Processing Date] {date_str}") - - # ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - hourly_url = f"{base_url}/public/chart/getHourlyData.php?date={date_str}" - - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{hourly_url}&time={timestamp}", headers=headers, timeout=10) - res.encoding = 'euc-kr' - - if res.status_code == 200: - data = res.json() - hourly_data = data if isinstance(data, list) else data.get('hourly', []) - - if hourly_data and len(hourly_data) > 0: - print(f" โœ“ Found {len(hourly_data)} hourly records") - - for item in hourly_data: - hour = str(item.get('hour', item.get('time', '00'))).zfill(2) - generation_kwh = safe_float(item.get('power', item.get('kwh', 0))) - current_kw = safe_float(item.get('kw', 0)) - - timestamp = f"{date_str} {hour}:00:00" - - results.append({ - 'plant_id': plant_id, - 'timestamp': timestamp, - 'generation_kwh': generation_kwh, - 'current_kw': current_kw - }) - else: - print(f" โš  No hourly data for {date_str}") - else: - print(f" โœ— HTTP {res.status_code}") - - except Exception as e: - print(f" โœ— Error: {e}") - - # ๋‹ค์Œ ๋‚ ์งœ๋กœ - current_date += timedelta(days=1) - - print(f"\n{'='*60}") - print(f"[Total] Collected {len(results)} hourly records") - print(f"{'='*60}\n") - - return results diff --git a/crawler/crawlers/sun_wms_old.py b/crawler/crawlers/sun_wms_old.py deleted file mode 100644 index ad76f41..0000000 --- a/crawler/crawlers/sun_wms_old.py +++ /dev/null @@ -1,343 +0,0 @@ -# ========================================== -# crawlers/sun_wms.py - Sun-WMS ํฌ๋กค๋Ÿฌ (6ํ˜ธ๊ธฐ) -# ========================================== - -import requests -import re -import time -from .base import create_session - -def fetch_data(plant_info): - """ - Sun-WMS ๋ฐœ์ „์†Œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - company_name = plant_info.get('company_name', 'ํƒœ์–‘๊ณผ๋ฐ”๋žŒ') - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - data_url = system.get('data_url', '') - - session = create_session() - - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'http://tb6.sun-wms.com/public/main/login.php', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - # 1. ๋กœ๊ทธ์ธ - login_data = { - 'act': 'loginChk', - 'user_id': payload_id, - 'user_pass': payload_pw - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code != 200: - return [] - except Exception as e: - print(f"โŒ {plant_name} ์ ‘์† ์—๋Ÿฌ: {e}") - return [] - - # 2. ๋ฐ์ดํ„ฐ ์š”์ฒญ - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{data_url}?time={timestamp}", headers=headers) - res.encoding = 'euc-kr' - - content = res.text - - match_kw = re.search(r"id=['\"]cur_power['\"].*?value=['\"]([^'\"]+)['\"]", content) - curr_kw = float(match_kw.group(1)) if match_kw else 0.0 - - match_today = re.search(r"id=['\"]today_power['\"].*?value=['\"]([^'\"]+)['\"]", content) - today_kwh = float(match_today.group(1)) if match_today else 0.0 - - status = "๐ŸŸข ์ •์ƒ" if curr_kw > 0 else "๐Ÿ’ค ๋Œ€๊ธฐ" - - return [{ - 'id': plant_id, - 'name': f'{company_name} {plant_name}', - 'kw': curr_kw, - 'today': today_kwh, - 'status': status - }] - - except Exception as e: - print(f"โŒ {plant_name} ์—๋Ÿฌ: {e}") - return [] - - -def fetch_history_daily(plant_info, start_date, end_date): - """ - Sun-WMS ๋ฐœ์ „์†Œ์˜ ์ผ๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - from datetime import datetime, timedelta - from .base import safe_float - import time - - results = [] - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[Sun-WMS Daily] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw} - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - - # ์ผ๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - daily_url = f"{base_url}/public/chart/getDailyData.php?date={date_str}" - - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{daily_url}&time={timestamp}", headers=headers, timeout=10) - res.encoding = 'euc-kr' - - if res.status_code == 200: - data = res.json() - daily_kwh = safe_float(data.get('daily', data.get('today', 0))) - - results.append({ - 'plant_id': plant_id, - 'date': date_str, - 'generation_kwh': daily_kwh - }) - print(f" โœ“ {date_str}: {daily_kwh}kWh") - - except Exception as e: - print(f" โœ— {date_str}: {e}") - - current_date += timedelta(days=1) - - print(f"[Total] Collected {len(results)} daily records\n") - return results - - -def fetch_history_monthly(plant_info, start_month, end_month): - """ - Sun-WMS ๋ฐœ์ „์†Œ์˜ ์›”๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - """ - from datetime import datetime - from dateutil.relativedelta import relativedelta - from .base import safe_float - import time - - results = [] - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[Sun-WMS Monthly] {plant_name} ({start_month} ~ {end_month})") - print(f"{'='*60}") - - headers = { - 'User-Agent': 'Mozilla/5.0', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = {'act': 'loginChk', 'user_id': payload_id, 'user_pass': payload_pw} - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code == 200: - print(" โœ“ Login successful") - else: - print(" โœ— Login failed") - return results - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - current_month = datetime.strptime(start_month, '%Y-%m') - end_month_dt = datetime.strptime(end_month, '%Y-%m') - - while current_month <= end_month_dt: - month_str = current_month.strftime('%Y-%m') - - # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - monthly_url = f"{base_url}/public/chart/getMonthlyData.php?month={month_str}" - - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{monthly_url}&time={timestamp}", headers=headers, timeout=10) - res.encoding = 'euc-kr' - - if res.status_code == 200: - data = res.json() - monthly_kwh = safe_float(data.get('monthly', data.get('month', 0))) - - results.append({ - 'plant_id': plant_id, - 'month': month_str, - 'generation_kwh': monthly_kwh - }) - print(f" โœ“ {month_str}: {monthly_kwh}kWh") - - except Exception as e: - print(f" โœ— {month_str}: {e}") - - current_month += relativedelta(months=1) - - print(f"[Total] Collected {len(results)} monthly records\n") - return results - - -def fetch_history_hourly(plant_info, start_date, end_date): - """ - Sun-WMS ๋ฐœ์ „์†Œ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ - - Args: - plant_info: dict, ๋ฐœ์ „์†Œ ์ •๋ณด - start_date: str, ์‹œ์ž‘์ผ (YYYY-MM-DD) - end_date: str, ์ข…๋ฃŒ์ผ (YYYY-MM-DD) - - Returns: - list: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ๋ ˆ์ฝ”๋“œ - """ - from datetime import datetime, timedelta - from .base import safe_float - import time - - results = [] - - # ์„ค์ • ์ถ”์ถœ - plant_id = plant_info.get('id', 'sunwms-06') - auth = plant_info.get('auth', {}) - system = plant_info.get('system', {}) - plant_name = plant_info.get('name', '6ํ˜ธ๊ธฐ') - - payload_id = auth.get('payload_id', '') - payload_pw = auth.get('payload_pw', '') - login_url = system.get('login_url', '') - base_url = system.get('base_url', '') - - session = create_session() - - print(f"\n{'='*60}") - print(f"[Sun-WMS History] {plant_name} ({start_date} ~ {end_date})") - print(f"{'='*60}") - - # ๋กœ๊ทธ์ธ - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'http://tb6.sun-wms.com/public/main/login.php', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - } - - login_data = { - 'act': 'loginChk', - 'user_id': payload_id, - 'user_pass': payload_pw - } - - try: - res = session.post(login_url, data=login_data, headers=headers) - if res.status_code != 200: - print(f" โœ— Login failed") - return results - - print(f" โœ“ Login successful") - - except Exception as e: - print(f" โœ— Login error: {e}") - return results - - # ๋‚ ์งœ ๋ฒ”์œ„ ๋ฐ˜๋ณต - current_date = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d') - - while current_date <= end_dt: - date_str = current_date.strftime('%Y-%m-%d') - print(f"\n[Processing Date] {date_str}") - - # ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ (์ถ”์ •) - hourly_url = f"{base_url}/public/chart/getHourlyData.php?date={date_str}" - - try: - timestamp = int(time.time() * 1000) - res = session.get(f"{hourly_url}&time={timestamp}", headers=headers, timeout=10) - res.encoding = 'euc-kr' - - if res.status_code == 200: - data = res.json() - hourly_data = data if isinstance(data, list) else data.get('hourly', []) - - if hourly_data and len(hourly_data) > 0: - print(f" โœ“ Found {len(hourly_data)} hourly records") - - for item in hourly_data: - hour = str(item.get('hour', item.get('time', '00'))).zfill(2) - generation_kwh = safe_float(item.get('power', item.get('kwh', 0))) - current_kw = safe_float(item.get('kw', 0)) - - timestamp = f"{date_str} {hour}:00:00" - - results.append({ - 'plant_id': plant_id, - 'timestamp': timestamp, - 'generation_kwh': generation_kwh, - 'current_kw': current_kw - }) - else: - print(f" โš  No hourly data for {date_str}") - else: - print(f" โœ— HTTP {res.status_code}") - - except Exception as e: - print(f" โœ— Error: {e}") - - # ๋‹ค์Œ ๋‚ ์งœ๋กœ - current_date += timedelta(days=1) - - print(f"\n{'='*60}") - print(f"[Total] Collected {len(results)} hourly records") - print(f"{'='*60}\n") - - return results diff --git a/crawler/scripts_archive/README.md b/crawler/scripts_archive/README.md index fdd53cb..4516d3c 100644 --- a/crawler/scripts_archive/README.md +++ b/crawler/scripts_archive/README.md @@ -112,11 +112,13 @@ ## ๐Ÿ”ง ์‚ฌ์šฉ ๋ฐฉ๋ฒ• -๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ๋Š” crawler ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ ์‹คํ–‰: +๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ๋Š” `docs/development_and_testing.md`์— ๋”ฐ๋ผ ์ƒˆ๋กœ ๋งŒ๋“  crawler ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ ์‹คํ–‰: ```powershell cd d:\dev\etc\SolorPower\crawler -.\venv_win\Scripts\Activate.ps1 +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install -r requirements.txt # ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์˜ˆ์‹œ python scripts_archive/fetch_february.py diff --git a/crawler/venv_win/Scripts/Activate.ps1 b/crawler/venv_win/Scripts/Activate.ps1 deleted file mode 100644 index 7175cf1..0000000 --- a/crawler/venv_win/Scripts/Activate.ps1 +++ /dev/null @@ -1,502 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove VIRTUAL_ENV_PROMPT altogether. - if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { - Remove-Item -Path env:VIRTUAL_ENV_PROMPT - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } - $env:VIRTUAL_ENV_PROMPT = $Prompt -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" - -# SIG # Begin signature block -# MIIvJAYJKoZIhvcNAQcCoIIvFTCCLxECAQExDzANBglghkgBZQMEAgEFADB5Bgor -# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG -# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBnL745ElCYk8vk -# dBtMuQhLeWJ3ZGfzKW4DHCYzAn+QB6CCE8MwggWQMIIDeKADAgECAhAFmxtXno4h -# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK -# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV -# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z -# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ -# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 -# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z -# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ -# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s -# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL -# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb -# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 -# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c -# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx -# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 -# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL -# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud -# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf -# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk -# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS -# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK -# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB -# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp -# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg -# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri -# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 -# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 -# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 -# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H -# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G -# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ -# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 -# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla -# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE -# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz -# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C -# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce -# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da -# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T -# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA -# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh -# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM -# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z -# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 -# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY -# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP -# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T -# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD -# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG -# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY -# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj -# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV -# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU -# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN -# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry -# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL -# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf -# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh -# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh -# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV -# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j -# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH -# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC -# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l -# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW -# eE4wggd3MIIFX6ADAgECAhAHHxQbizANJfMU6yMM0NHdMA0GCSqGSIb3DQEBCwUA -# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE -# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz -# ODQgMjAyMSBDQTEwHhcNMjIwMTE3MDAwMDAwWhcNMjUwMTE1MjM1OTU5WjB8MQsw -# CQYDVQQGEwJVUzEPMA0GA1UECBMGT3JlZ29uMRIwEAYDVQQHEwlCZWF2ZXJ0b24x -# IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMSMwIQYDVQQDExpQ -# eXRob24gU29mdHdhcmUgRm91bmRhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -# ADCCAgoCggIBAKgc0BTT+iKbtK6f2mr9pNMUTcAJxKdsuOiSYgDFfwhjQy89koM7 -# uP+QV/gwx8MzEt3c9tLJvDccVWQ8H7mVsk/K+X+IufBLCgUi0GGAZUegEAeRlSXx -# xhYScr818ma8EvGIZdiSOhqjYc4KnfgfIS4RLtZSrDFG2tN16yS8skFa3IHyvWdb -# D9PvZ4iYNAS4pjYDRjT/9uzPZ4Pan+53xZIcDgjiTwOh8VGuppxcia6a7xCyKoOA -# GjvCyQsj5223v1/Ig7Dp9mGI+nh1E3IwmyTIIuVHyK6Lqu352diDY+iCMpk9Zanm -# SjmB+GMVs+H/gOiofjjtf6oz0ki3rb7sQ8fTnonIL9dyGTJ0ZFYKeb6BLA66d2GA -# LwxZhLe5WH4Np9HcyXHACkppsE6ynYjTOd7+jN1PRJahN1oERzTzEiV6nCO1M3U1 -# HbPTGyq52IMFSBM2/07WTJSbOeXjvYR7aUxK9/ZkJiacl2iZI7IWe7JKhHohqKuc -# eQNyOzxTakLcRkzynvIrk33R9YVqtB4L6wtFxhUjvDnQg16xot2KVPdfyPAWd81w -# tZADmrUtsZ9qG79x1hBdyOl4vUtVPECuyhCxaw+faVjumapPUnwo8ygflJJ74J+B -# Yxf6UuD7m8yzsfXWkdv52DjL74TxzuFTLHPyARWCSCAbzn3ZIly+qIqDAgMBAAGj -# ggIGMIICAjAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5hewiIZfROQjAdBgNVHQ4E -# FgQUt/1Teh2XDuUj2WW3siYWJgkZHA8wDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM -# MAoGCCsGAQUFBwMDMIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6Ly9jcmwzLmRp -# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI -# QTM4NDIwMjFDQTEuY3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdpY2VydC5jb20v -# RGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0Ex -# LmNybDA+BgNVHSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIBFhtodHRwOi8v -# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUF -# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6 -# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu -# aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZI -# hvcNAQELBQADggIBABxv4AeV/5ltkELHSC63fXAFYS5tadcWTiNc2rskrNLrfH1N -# s0vgSZFoQxYBFKI159E8oQQ1SKbTEubZ/B9kmHPhprHya08+VVzxC88pOEvz68nA -# 82oEM09584aILqYmj8Pj7h/kmZNzuEL7WiwFa/U1hX+XiWfLIJQsAHBla0i7QRF2 -# de8/VSF0XXFa2kBQ6aiTsiLyKPNbaNtbcucaUdn6vVUS5izWOXM95BSkFSKdE45O -# q3FForNJXjBvSCpwcP36WklaHL+aHu1upIhCTUkzTHMh8b86WmjRUqbrnvdyR2yd -# I5l1OqcMBjkpPpIV6wcc+KY/RH2xvVuuoHjlUjwq2bHiNoX+W1scCpnA8YTs2d50 -# jDHUgwUo+ciwpffH0Riq132NFmrH3r67VaN3TuBxjI8SIZM58WEDkbeoriDk3hxU -# 8ZWV7b8AW6oyVBGfM06UgkfMb58h+tJPrFx8VI/WLq1dTqMfZOm5cuclMnUHs2uq -# rRNtnV8UfidPBL4ZHkTcClQbCoz0UbLhkiDvIS00Dn+BBcxw/TKqVL4Oaz3bkMSs -# M46LciTeucHY9ExRVt3zy7i149sd+F4QozPqn7FrSVHXmem3r7bjyHTxOgqxRCVa -# 18Vtx7P/8bYSBeS+WHCKcliFCecspusCDSlnRUjZwyPdP0VHxaZg2unjHY3rMYIa -# tzCCGrMCAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu -# Yy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJT -# QTQwOTYgU0hBMzg0IDIwMjEgQ0ExAhAHHxQbizANJfMU6yMM0NHdMA0GCWCGSAFl -# AwQCAQUAoIHIMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC -# AQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCBnAZ6P7YvTwq0fbF62 -# o7E75R0LxsW5OtyYiFESQckLhjBcBgorBgEEAYI3AgEMMU4wTKBGgEQAQgB1AGkA -# bAB0ADoAIABSAGUAbABlAGEAcwBlAF8AdgAzAC4AMQAxAC4ANQBfADIAMAAyADMA -# MAA4ADIANAAuADAAMaECgAAwDQYJKoZIhvcNAQEBBQAEggIAhs4bX7EeJZ6oSTC7 -# 5QH/9Qx1Cllidnzj94PqDIL0MiS5adMxYTBkEtP3XNQvYkCtBFc6+Rz7bdN+zWWo -# ZYr+sDmEQmRerr3RYyqt+EpgOXpN4BGsHyD7r1Dat1wblGSva8zlOHiIAfzRj2JB -# 0+fRJPSBRj9RYwZb5h+I2AFLmHf3yItUdgs8GV9NZsAs+p79dRmoqhgNC6qm8I0j -# PkwGr5ATZLyCk2U1+VGeK8iwAdTB4HAlVVM146D/34j/QPnoqe9ICE6Foo6IArVV -# CbqWRShWHffvpKaR7ACoTy9LoIQf93orWoc+amQsyaUmlV/zQaCnyjc2UoFCDHcH -# 87Yg+frSB8xe2azuKUTVlUDx9Y5wOtEgK+o8wg4ufwPZP0JnsVzN06aCNBz2Bnfb -# Mb96Mp0PoCnjp8eAKttmRTXWE0DYIv/XAr2xwwJLFEUdoG6bj0bpNF7Wz0/c3mi0 -# NKZsd9xNLKBKjizQgCZ7SGCMuSjEnd6P0AI7M8jRx+NROKcJI6gjH0oKXm9JLvI8 -# oKB2COIlxKEUI/R/kBOeKp53zUSsPFRiJrDEkiCFocAFdUTE326b9/acGbQPJJJ+ -# nMXLrbTrMMohlj7qRshvO0ZVvpqBDoHlRQcJcfINEESgKNKx/bTpr5cuM3WIS5Ft -# 1GirQp9sABVeBom9Y0NDFXsCSkqhghdAMIIXPAYKKwYBBAGCNwMDATGCFywwghco -# BgkqhkiG9w0BBwKgghcZMIIXFQIBAzEPMA0GCWCGSAFlAwQCAQUAMHgGCyqGSIb3 -# DQEJEAEEoGkEZzBlAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgDHAE -# wrb/OjfkdGEAR/N6/5LxwnpqnhSdUI5gfWTSXKECEQDkKzKdiKykh3cqy0kBK32H -# GA8yMDIzMDgyNDE0NTcyOFqgghMJMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/ -# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln -# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 -# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx -# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu -# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN -# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX -# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj -# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7 -# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB -# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu -# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg -# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG -# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc -# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh -# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2 -# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD -# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW -# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg -# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O -# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6 -# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy -# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF -# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6 -# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT -# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g -# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s -# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q -# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4 -# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w -# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z -# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn -# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza -# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy -# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA -# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl -# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIGrjCCBJagAwIBAgIQBzY3tyRU -# fNhHrP0oZipeWzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UE -# ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYD -# VQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIwMzIzMDAwMDAwWhcN -# MzcwMzIyMjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs -# IEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEy -# NTYgVGltZVN0YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC -# AgEAxoY1BkmzwT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCwzIP5WvYRoUQVQl+k -# iPNo+n3znIkLf50fng8zH1ATCyZzlm34V6gCff1DtITaEfFzsbPuK4CEiiIY3+va -# PcQXf6sZKz5C3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ7Gnf2ZCHRgB720RB -# idx8ald68Dd5n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7QKxfst5Kfc71ORJn -# 7w6lY2zkpsUdzTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/tePc5OsLDnipUjW8LAx -# E6lXKZYnLvWHpo9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCYOjgRs/b2nuY7W+yB -# 3iIU2YIqx5K/oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9KoRxrOMUp88qqlnNC -# aJ+2RrOdOqPVA+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6dSgkQe1CvwWcZklS -# UPRR8zZJTYsg0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM1+mYSlg+0wOI/rOP -# 015LdhJRk8mMDDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbCdLI/Hgl27KtdRnXi -# YKNYCQEoAA6EVO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbECAwEAAaOCAV0wggFZ -# MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1NhS9zKXaaL3WMaiCP -# nshvMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQE -# AwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYB -# BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0 -# cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5j -# cnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp -# Z2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJ -# YIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7ZvmKlEIgF+ZtbYIULh -# sBguEE0TzzBTzr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI2AvlXFvXbYf6hCAl -# NDFnzbYSlm/EUExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/tydBTX/6tPiix6q4XN -# Q1/tYLaqT5Fmniye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVPulr3qRCyXen/KFSJ -# 8NWKcXZl2szwcqMj+sAngkSumScbqyQeJsG33irr9p6xeZmBo1aGqwpFyd/EjaDn -# mPv7pp1yr8THwcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc6UsCUqc3fpNTrDsd -# CEkPlM05et3/JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3cHXg65J6t5TRxktcm -# a+Q4c6umAU+9Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0dKNPH+ejxmF/7K9h+ -# 8kaddSweJywm228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZPJ/tgZxahZrrdVcA6 -# KYawmKAr7ZVBtzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLeMt8EifAAzV3C+dAj -# fwAL5HYCJtnwZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDyDivl1vupL0QVSucT -# Dh3bNzgaoSv27dZ8/DCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJ -# KoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu -# YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQg -# QXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1 -# OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE -# CxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBS -# b290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+Rd -# SjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20d -# q7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7f -# gvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRA -# X7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raR -# mECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzU -# vK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2 -# mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkr -# fsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaA -# sPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxf -# jT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEe -# xcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQF -# MAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaA -# FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcB -# AQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggr -# BgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz -# dXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5k -# aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQK -# MAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3 -# v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy -# 3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cn -# RNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3 -# WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2 -# zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGC -# A3YwggNyAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ -# bmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2 -# IFRpbWVTdGFtcGluZyBDQQIQBUSv85SdCDmmv9s/X+VhFjANBglghkgBZQMEAgEF -# AKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8X -# DTIzMDgyNDE0NTcyOFowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQUZvArMsLCyQ+C -# Xc6qisnGTxmcz0AwLwYJKoZIhvcNAQkEMSIEICXoYBY9n9CHBlb2ZPmyIOzhh93Z -# zUXRoskCDmMHyq3cMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEINL25G3tdCLM0dRA -# V2hBNm+CitpVmq4zFq9NGprUDHgoMA0GCSqGSIb3DQEBAQUABIICAFUzNs5f5wsA -# nHsLg2yauMwAyYAuQIL8+GKYnWW/AtSWnA/t+S4LbjIJaIpBzZaWTai8/I23tJJw -# W1CTYDV3hqPGG/8PEcs8RY12JQoYMRZHzHTkNvUJC9xMXfuZIxtCmoFP2xsQjLgP -# Pl45FYCo3NzWCwQ8A2SyR48lskuJ94Q7PADJHkTU7pEY0t/N6114Mo9aO+n6qSLJ -# huEu1DmWE7iarxtIKja66BQEHjdawlSbg82Fg8EfkfsAXDHLqH1pahvnWmOziFLp -# SOrFKfyUVdCoGR7k3bKkHO62AeWz/LbzN0HPkzV7xrh/PD+4rwzatBpSwzFUFgRN -# 8Zg+Kso4LgTktu3nW9rG3TkFUBM3WsP9atnUfvCGvAcDr4Qv5qSx1cyFhuTK4gRj -# FVEO93RAMu0S54vTXBQDjl/55MEgWkFinTMqbkM4DpqqMp2uBjp5sqHbns6cDOD+ -# o+HyXhE9XKxu3myklhq338QMSKE8bcdQ0XogmOwqgrVkpiG3jH+2R9CJlPwsyJLM -# TYwlsno3hd4+w2SPtySgNXwZGSj2KRNJFMaePyru8QeVS33pcmXNh+CKrdW5fxk0 -# C4wWetuCKJTCBDNCRzy5NjrGAkNH2F37JM1pi0n5x2esaTyLC4+gGzDfn5ki0BT4 -# 8w/WVhUIJnuoyQ33wBhxukpdRjwb71K4 -# SIG # End signature block diff --git a/crawler/venv_win/Scripts/activate b/crawler/venv_win/Scripts/activate deleted file mode 100644 index de2aa06..0000000 --- a/crawler/venv_win/Scripts/activate +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # This should detect bash and zsh, which have a hash command that must - # be called to get it to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null - fi - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV="D:\dev\etc\SolorPower\crawler\venv_win" -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/Scripts:$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1="(venv_win) ${PS1:-}" - export PS1 - VIRTUAL_ENV_PROMPT="(venv_win) " - export VIRTUAL_ENV_PROMPT -fi - -# This should detect bash and zsh, which have a hash command that must -# be called to get it to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null -fi diff --git a/crawler/venv_win/Scripts/activate.bat b/crawler/venv_win/Scripts/activate.bat deleted file mode 100644 index 2ed0bc1..0000000 --- a/crawler/venv_win/Scripts/activate.bat +++ /dev/null @@ -1,34 +0,0 @@ -@echo off - -rem This file is UTF-8 encoded, so we need to update the current code page while executing it -for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do ( - set _OLD_CODEPAGE=%%a -) -if defined _OLD_CODEPAGE ( - "%SystemRoot%\System32\chcp.com" 65001 > nul -) - -set VIRTUAL_ENV=D:\dev\etc\SolorPower\crawler\venv_win - -if not defined PROMPT set PROMPT=$P$G - -if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT% -if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME% - -set _OLD_VIRTUAL_PROMPT=%PROMPT% -set PROMPT=(venv_win) %PROMPT% - -if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME% -set PYTHONHOME= - -if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH% -if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH% - -set PATH=%VIRTUAL_ENV%\Scripts;%PATH% -set VIRTUAL_ENV_PROMPT=(venv_win) - -:END -if defined _OLD_CODEPAGE ( - "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul - set _OLD_CODEPAGE= -) diff --git a/crawler/venv_win/Scripts/deactivate.bat b/crawler/venv_win/Scripts/deactivate.bat deleted file mode 100644 index 62a39a7..0000000 --- a/crawler/venv_win/Scripts/deactivate.bat +++ /dev/null @@ -1,22 +0,0 @@ -@echo off - -if defined _OLD_VIRTUAL_PROMPT ( - set "PROMPT=%_OLD_VIRTUAL_PROMPT%" -) -set _OLD_VIRTUAL_PROMPT= - -if defined _OLD_VIRTUAL_PYTHONHOME ( - set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" - set _OLD_VIRTUAL_PYTHONHOME= -) - -if defined _OLD_VIRTUAL_PATH ( - set "PATH=%_OLD_VIRTUAL_PATH%" -) - -set _OLD_VIRTUAL_PATH= - -set VIRTUAL_ENV= -set VIRTUAL_ENV_PROMPT= - -:END diff --git a/crawler/venv_win/Scripts/dotenv.exe b/crawler/venv_win/Scripts/dotenv.exe deleted file mode 100644 index 5a3e015..0000000 Binary files a/crawler/venv_win/Scripts/dotenv.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/httpx.exe b/crawler/venv_win/Scripts/httpx.exe deleted file mode 100644 index 6735d52..0000000 Binary files a/crawler/venv_win/Scripts/httpx.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/markdown-it.exe b/crawler/venv_win/Scripts/markdown-it.exe deleted file mode 100644 index 38ee808..0000000 Binary files a/crawler/venv_win/Scripts/markdown-it.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/normalizer.exe b/crawler/venv_win/Scripts/normalizer.exe deleted file mode 100644 index c690909..0000000 Binary files a/crawler/venv_win/Scripts/normalizer.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/pip.exe b/crawler/venv_win/Scripts/pip.exe deleted file mode 100644 index 428ed33..0000000 Binary files a/crawler/venv_win/Scripts/pip.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/pip3.11.exe b/crawler/venv_win/Scripts/pip3.11.exe deleted file mode 100644 index 428ed33..0000000 Binary files a/crawler/venv_win/Scripts/pip3.11.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/pip3.exe b/crawler/venv_win/Scripts/pip3.exe deleted file mode 100644 index 428ed33..0000000 Binary files a/crawler/venv_win/Scripts/pip3.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/pygmentize.exe b/crawler/venv_win/Scripts/pygmentize.exe deleted file mode 100644 index 6935096..0000000 Binary files a/crawler/venv_win/Scripts/pygmentize.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/pyiceberg.exe b/crawler/venv_win/Scripts/pyiceberg.exe deleted file mode 100644 index 9ace56c..0000000 Binary files a/crawler/venv_win/Scripts/pyiceberg.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/python.exe b/crawler/venv_win/Scripts/python.exe deleted file mode 100644 index d30bc8e..0000000 Binary files a/crawler/venv_win/Scripts/python.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/pythonw.exe b/crawler/venv_win/Scripts/pythonw.exe deleted file mode 100644 index 2aae56f..0000000 Binary files a/crawler/venv_win/Scripts/pythonw.exe and /dev/null differ diff --git a/crawler/venv_win/Scripts/websockets.exe b/crawler/venv_win/Scripts/websockets.exe deleted file mode 100644 index 1385c8d..0000000 Binary files a/crawler/venv_win/Scripts/websockets.exe and /dev/null differ diff --git a/crawler/venv_win/pyvenv.cfg b/crawler/venv_win/pyvenv.cfg deleted file mode 100644 index 115c854..0000000 --- a/crawler/venv_win/pyvenv.cfg +++ /dev/null @@ -1,5 +0,0 @@ -home = C:\Users\haneu\AppData\Local\Programs\Python\Python311 -include-system-site-packages = false -version = 3.11.5 -executable = C:\Users\haneu\AppData\Local\Programs\Python\Python311\python.exe -command = C:\Users\haneu\AppData\Local\Programs\Python\Python311\python.exe -m venv D:\dev\etc\SolorPower\crawler\venv_win diff --git a/docs/development_and_testing.md b/docs/development_and_testing.md index 56a15ba..fbdeca0 100644 --- a/docs/development_and_testing.md +++ b/docs/development_and_testing.md @@ -44,6 +44,13 @@ $env:DEBUG = "false" `package-lock.json`์„ ์ €์žฅ์†Œ์— ํฌํ•จํ•˜๊ณ  CI์™€ ์ƒˆ ํ™˜๊ฒฝ์—์„œ๋Š” `npm ci`๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. +API ์ฃผ์†Œ์™€ ํ˜„์žฌ ์กฐํšŒ ์—…์ฒด๋Š” Expo ๊ณต๊ฐœ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์„ ํƒํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ์šด์˜ ํ™˜๊ฒฝ์„ ์œ ์ง€ํ•˜๋ฉฐ, ๋‹ค๋ฅธ ํ™˜๊ฒฝ์—์„œ๋Š” `app/.env.example`์„ `app/.env.local`๋กœ ๋ณต์‚ฌํ•ด ๊ฐ’์„ ๋ฐ”๊พผ๋‹ค. ์ด ๊ฐ’์€ ์•ฑ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜๋Š” ๊ณต๊ฐœ ์„ค์ •์ด๋ฏ€๋กœ ์ธ์ฆ ํ† ํฐ์ด๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค. + +| ํ™˜๊ฒฝ๋ณ€์ˆ˜ | ์šฉ๋„ | ๊ธฐ๋ณธ๊ฐ’ | +|---|---|---| +| `EXPO_PUBLIC_API_BASE_URL` | FastAPI/Nginx ๊ธฐ๋ณธ URL | `https://solorpower.dadot.net` | +| `EXPO_PUBLIC_COMPANY_ID` | ๋Œ€์‹œ๋ณด๋“œ์™€ ๋น„๊ต ํ™”๋ฉด์˜ ์—…์ฒด ID | `1` | + ```powershell cd app npm ci @@ -52,6 +59,8 @@ npm run test:ci ํ˜„์žฌ `test:ci`๋Š” Expo ์›น export๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ ์˜์กด์„ฑ, ๋ฒˆ๋“ค๋ง, ์ •์  ์‚ฐ์ถœ๋ฌผ ์ƒ์„ฑ์„ ํ•จ๊ป˜ ๊ฒ€์‚ฌํ•œ๋‹ค. ์ถœ๋ ฅ ๋””๋ ‰ํ„ฐ๋ฆฌ๋Š” `app/dist/`์ด๋ฉฐ Git์—์„œ ์ œ์™ธ๋œ๋‹ค. +์•ฑ์˜ ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ์€ `services/solarApi.js`, ํ™˜๊ฒฝ ์„ ํƒ์€ `config/appConfig.js`๋ฅผ ํ†ตํ•ด ์ˆ˜ํ–‰ํ•œ๋‹ค. ํ™”๋ฉด์— API URL์ด๋‚˜ ์—…์ฒด ID๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค. + ## DB migration ํ†ต๊ณ„ ์“ฐ๊ธฐ ์ผ๊ด€์„ฑ migration์€ ๋…๋ฆฝ PostgreSQL 15 DB์—์„œ ๋‹ค์Œ ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ๋กœ ํ™•์ธํ•œ๋‹ค. diff --git a/docs/repository_structure.md b/docs/repository_structure.md new file mode 100644 index 0000000..d17f10d --- /dev/null +++ b/docs/repository_structure.md @@ -0,0 +1,60 @@ +# ์ €์žฅ์†Œ ๋ฐ ์•ฑ ์ฝ”๋“œ ๊ตฌ์กฐ + +์ž‘์„ฑ์ผ: 2026-08-07 + +์ด ๋ฌธ์„œ๋Š” 8๋‹จ๊ณ„ ์ €์žฅ์†Œ ์ •๋ฆฌ ์ดํ›„ ์œ ์ง€ํ•ด์•ผ ํ•  ๋””๋ ‰ํ„ฐ๋ฆฌ ์ฑ…์ž„๊ณผ ์‚ฐ์ถœ๋ฌผ ๊ด€๋ฆฌ ๊ธฐ์ค€์„ ์ •์˜ํ•œ๋‹ค. + +## ์ตœ์ƒ์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ + +| ๊ฒฝ๋กœ | ์ฑ…์ž„ | +|---|---| +| `crawler/` | ์™ธ๋ถ€ ๋ฐœ์ „ ์‚ฌ์ดํŠธ ์ˆ˜์ง‘, ์•Œ๋ฆผ, ์ผยท์›” ํ†ต๊ณ„ ์ €์žฅ๊ณผ ๋ฐฑํ•„ | +| `api_server/` | ์•ฑ์šฉ FastAPI ์กฐํšŒยท์—…๋กœ๋“œ API | +| `app/` | Expo ๊ธฐ๋ฐ˜ ๋ชจ๋ฐ”์ผยท์›น ๋Œ€์‹œ๋ณด๋“œ | +| `supabase/` | PostgreSQL migration๊ณผ DB ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ | +| `deploy/` | Nginx ๋“ฑ ๋ฒ„์ „ ๊ด€๋ฆฌ ๋Œ€์ƒ ๋ฐฐํฌ ์„ค์ • | +| `docs/` | ์šด์˜, ๊ฐœ๋ฐœ, ๊ฐœ์„  ๋‹จ๊ณ„์™€ ๊ตฌ์กฐ ๋ฌธ์„œ | +| `sql/` | ์ดˆ๊ธฐ ์Šคํ‚ค๋งˆ ๋ฐ ๊ณผ๊ฑฐ SQL ์ฐธ๊ณ  ์ž๋ฃŒ | +| `tools/` | ํ”„๋กœ์ ํŠธ ์™ธ๋ถ€ ์ž…๋ ฅ์„ ๋ณ€ํ™˜ํ•˜๋Š” ๋…๋ฆฝ ๋„๊ตฌ | + +## ์•ฑ ๋‚ด๋ถ€ ๊ฒฝ๊ณ„ + +| ๊ฒฝ๋กœ | ์ฑ…์ž„ | +|---|---| +| `config/appConfig.js` | ํ™˜๊ฒฝ๋ณ„ API ๊ธฐ๋ณธ URL๊ณผ ํ˜„์žฌ ์—…์ฒด ์„ ํƒ | +| `services/solarApi.js` | HTTP ๊ฒฝ๋กœ, ์ฟผ๋ฆฌ ๊ตฌ์„ฑ, ์˜ค๋ฅ˜ ์‘๋‹ต ์ฒ˜๋ฆฌ | +| `hooks/usePlantStats.js` | ์ƒ์„ธ ํ†ต๊ณ„ ์กฐํšŒ ์ƒํƒœ์™€ ์ƒˆ๋กœ๊ณ ์นจ ์ˆ˜๋ช…์ฃผ๊ธฐ | +| `utils/statsChart.js` | API ํ†ต๊ณ„๋ฅผ ์ฐจํŠธ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ˆœ์ˆ˜ ๋กœ์ง | +| `components/` | ์—…๋กœ๋“œ ๋ชจ๋‹ฌ๊ณผ ๊ธฐ๊ฐ„ ์„ ํƒ ๋“ฑ ์žฌ์‚ฌ์šฉ UI | +| `screens/` | ํ™”๋ฉด ๋ฐฐ์น˜์™€ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์กฐํ•ฉ | + +ํ˜„์žฌ ์•ฑ์€ ํ•œ ์—…์ฒด๋ฅผ ์„ ํƒํ•ด ๋ณด์—ฌ ์ฃผ์ง€๋งŒ ์—…์ฒด ID๋ฅผ ํ™”๋ฉด์ด๋‚˜ URL์— ์ง์ ‘ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ฐฐํฌ ํ™˜๊ฒฝ์€ `EXPO_PUBLIC_COMPANY_ID`๋กœ ์—…์ฒด๋ฅผ ์„ ํƒํ•˜๊ณ , ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ๋ฐ›์€ ๊ฐ ๋ฐœ์ „์†Œ์˜ `company_id`๋ฅผ ๋ณ€๊ฒฝ API์— ๋‹ค์‹œ ์‚ฌ์šฉํ•œ๋‹ค. ๋น„๊ต API๋Š” ์„ ํƒ์ ์ธ `company_id` ์ฟผ๋ฆฌ๋ฅผ ๋ฐ›์•„ ๊ฐ™์€ ์—…์ฒด์˜ ๋ฐœ์ „์†Œ๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ํ–ฅํ›„ ์—…์ฒด ์„ ํƒ UI๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋Š” ์ด ์„ค์ •๊ฐ’์„ ์•ฑ ์ƒํƒœ๋กœ ์Šน๊ฒฉํ•˜๊ณ  ์„œ๋น„์Šค ํ•จ์ˆ˜์— ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ๊ณ„๋ฅผ ์œ ์ง€ํ•œ๋‹ค. + +## ์ƒ์„ฑ ํŒŒ์ผ ๊ด€๋ฆฌ + +๋‹ค์Œ ํŒŒ์ผ์€ Git์— ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๋Š”๋‹ค. + +- Python ๊ฐ€์ƒํ™˜๊ฒฝ: `.venv/`, `venv/`, `venv_win/` +- Node/Expo ์‚ฐ์ถœ๋ฌผ: `node_modules/`, `.expo/`, `dist/`, `build/` +- ๋Ÿฐํƒ€์ž„ ์ƒํƒœ์™€ ์ถœ๋ ฅ: `*.db`, `*.db-journal`, `*.log` +- ๋กœ์ปฌ ํ™˜๊ฒฝ๊ณผ ์ธ์ฆ์ •๋ณด: `.env`, `.env.*` (`.env.example` ์ œ์™ธ), `secrets.json` +- ์„œ๋ฒ„ ๋ฐฐํฌ ๋ฐฑ์—…๊ณผ staging: `deploy_backups/`, `deploy_staging/` +- ๋กœ์ปฌ Supabase CLI ๋ฐ”์ด๋„ˆ๋ฆฌ: `supabase.exe`, `supabase-go.exe` + +๊ฐ€์ƒํ™˜๊ฒฝ์€ ๊ฐ ํ•˜์œ„ ํ”„๋กœ์ ํŠธ์˜ ๊ณ ์ • `requirements.txt`๋กœ, ์•ฑ ์˜์กด์„ฑ์€ `package-lock.json`๊ณผ `npm ci`๋กœ ์žฌ์ƒ์„ฑํ•œ๋‹ค. + +## ๋ณด๊ด€ ์ •์ฑ… + +- ์‹คํ–‰ ์ฝ”๋“œ์™€ ๊ฐ™์€ ๋””๋ ‰ํ„ฐ๋ฆฌ์— `*_old.py`, `*.backup` ์‚ฌ๋ณธ์„ ๋‘์ง€ ์•Š๋Š”๋‹ค. ์ด์ „ ๋ฒ„์ „์€ Git ์ด๋ ฅ์—์„œ ๋ณต๊ตฌํ•œ๋‹ค. +- ์™„๋ฃŒ๋œ ์ผํšŒ์„ฑ ๋ฐ์ดํ„ฐ ๋ณด์ • ์Šคํฌ๋ฆฝํŠธ๋Š” ์‚ญ์ œ ๋ชฉ์ , ์˜ํ–ฅ ๋ฐ์ดํ„ฐ, ์‹คํ–‰ ์ฃผ์˜์‚ฌํ•ญ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ `crawler/scripts_archive/`์— ๋‘”๋‹ค. +- `scripts_archive/`์˜ ์ฝ”๋“œ๋Š” ์ •๊ธฐ ์‹คํ–‰ ์ง„์ž…์ ์ด ์•„๋‹ˆ๋ฉฐ, ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ํ˜„์žฌ DB ์Šคํ‚ค๋งˆ์™€ KST ์ฒ˜๋ฆฌ ์ •์ฑ…์„ ์žฌ๊ฒ€ํ† ํ•œ๋‹ค. +- ์šด์˜ ๋ฐฐํฌ ์ „ ๋ฐฑ์—…์€ ์„œ๋ฒ„์˜ `deploy_backups/`์— ๋ณด์กดํ•˜๋˜ ์ €์žฅ์†Œ์—๋Š” ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค. + +## 8๋‹จ๊ณ„์—์„œ ์ œ๊ฑฐํ•œ ์ถ”์  ํŒŒ์ผ + +- `crawler/venv_win/`: ๊ฐœ๋ฐœ PC์—์„œ ์ƒ์„ฑ๋œ Windows ๊ฐ€์ƒํ™˜๊ฒฝ๊ณผ ์‹คํ–‰ ํŒŒ์ผ +- `app/server.log`: Expo ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๋Ÿฐํƒ€์ž„ ๋กœ๊ทธ +- `crawler/crawlers/cmsolar_old.py`, `cmsolar_old2.py`: ํ˜„์žฌ ๋ผ์šฐํŒ…์—์„œ ์ฐธ์กฐ๋˜์ง€ ์•Š๋Š” ๊ตฌํ˜• ๊ตฌํ˜„ +- `crawler/crawlers/sun_wms_old.py`, `sun_wms.py.backup`: ํ˜„์žฌ ๊ตฌํ˜„๊ณผ ์ค‘๋ณต๋œ ์‚ฌ๋ณธ + +๋ชจ๋“  ์†Œ์Šค ์‚ฌ๋ณธ์€ ์‚ญ์ œ ์ „ Git ์ด๋ ฅ์— ์กด์žฌํ•˜๋ฏ€๋กœ ํ•„์š”ํ•˜๋ฉด ํ•ด๋‹น ์ปค๋ฐ‹์—์„œ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ๋‹ค.