# SolarPower 개선 작업 계획 작성일: 2026-08-06 ## 1. 목적 현재 운영 중인 SolarPower 시스템을 중단 없이 개선하기 위한 순차 작업 계획이다. 데이터 유실 또는 잘못된 집계를 일으킬 수 있는 문제를 먼저 처리하고, 이후 앱과 API의 기능 일관성, 시간대, 유지보수성과 테스트 체계를 개선한다. 인증 토큰 교체, 외부 서비스 계정의 환경변수 이전, Git 이력 정리 등 비밀정보 관련 작업은 현재 저장소를 단독으로 사용하고 있다는 전제하에 후순위로 보류한다. API 인증과 Supabase 접근 정책 강화도 별도 보안 단계에서 함께 검토한다. ## 현재 진행 현황 최종 갱신: 2026-08-07 14:42 KST | 단계 | 작업 | 상태 | 현재 결과 | |---:|---|---|---| | 0 | 운영 서버 연결 및 배포 기준선 점검 | 완료 | 외부 사이트, NAS 프록시, 크롤러, Supabase, FastAPI, Nginx와 웹 연결 경로 확인 | | 1 | 역방향 백필 데이터 무결성 | 완료·운영 반영 | 실패 시 커서 유지, 실제 0·데이터 없음·조회 오류 분리, 기존 정상값 보호 | | 2 | 발전소 알림 설정 연동 | 완료·운영 반영 | `alerts_enabled` 연동, 3회 연속 0kW 판정, 수집 오류 오탐 제외 및 실제 설정 ON/OFF 검증 | | 3 | Excel 업로드 화면과 API 계약 | 완료·운영 반영 | 일간·월간 업로드 통합, 입력 검증과 4xx 응답 정리, Web/API 배포 완료 | | 4 | KST 시간대 처리 통일 | 완료·운영 반영 | 크롤러·API의 KST 판단 통일, UTC 반개구간 조회와 자정·월말·연말 검증 완료 | | 5 | API 정확성과 응답 규약 | 완료·운영 반영 | 문자열 ID, 입력 오류, 실제 DB health, 응답 모델, 최신 로그 단일 조회와 동기 호출 처리 완료 | | 6 | 통계 생성 경로 일관성 | 완료·운영 반영 | 공통 일간 저장 정책, 월간 자동 집계, 출처·갱신 시각과 오타 컬럼 마이그레이션 완료 | | 7 | 테스트와 재현 가능한 개발 환경 | 완료·운영 반영 | Python/npm 의존성 고정, 5개 사이트 응답 fixture, 4개 CI job과 새 환경 설치 검증 완료 | | 8 | 저장소 및 코드 구조 정리 | 대기 | 기능·데이터 안정화 후 진행 | 현재 운영 상태: - 2026-08-05 NREMS 3·4·9호기 알림은 발전소 장애가 아니라 NAS 프록시 타임아웃을 오류성 0kW로 처리한 오탐으로 확인했고, 오류 데이터를 알림 판정에서 제외하도록 수정했다. - 운영 API와 Nginx는 active 상태이며 공개 health는 실제 Supabase 조회를 포함한 JSON을 반환하고 전체 비교 통계와 시간별 통계도 HTTP 200을 반환한다. - 2026-08-06 18:00 KST 정기 크롤링에서 9개 발전소 연결이 모두 성공했고 수집 오류나 텔레그램 오탐은 발생하지 않았다. - 2026-08-07 10:50 KST 정기 크롤링에서 새 통계 저장 함수로 9개 발전소의 실시간·일간 저장이 모두 성공했고, 일·월 통계 72쌍이 전부 일치했다. - 2026-08-07 12:00 KST 정기 크롤링은 SQLite 연결 종료 수정 반영 후 9개 발전소가 모두 정상 상태였고, 변경된 8개 발전소의 로그·일간 통계를 오류 없이 저장했다. - 크롤러 시간대·알림·백필·통계·응답 fixture 회귀 테스트 36개가 Windows, Linux 컨테이너와 운영 서버에서 통과했고 API 계약·시간대·업로드 회귀 테스트 31개도 새 Python 환경에서 통과했다. - 1~6단계 배포 전 운영 파일은 단계별 `deploy_backups/20260806_*`, `deploy_backups/20260807_*` 디렉터리에 보존했다. - 완료된 백필 cron은 현재도 매시간 무작업 실행 중이며, 비활성화 여부는 별도 운영 결정으로 남겨 두었다. - 인증 토큰 교체, 환경변수 정리, API 인증과 Supabase 접근 정책 강화는 사용자 결정에 따라 후순위로 보류 중이다. - 1~7단계 변경은 커밋 `a716dbe`로 원격 `main`에 push했고 운영 서버의 `HEAD`와 `origin/main`도 같은 커밋으로 정렬했다. 서버에는 복구용 `deploy_backups/`, `deploy_staging/`만 미추적 상태로 보존한다. 다음 진행 기준: - 다음 구현 단계는 8단계 `저장소 및 코드 구조 정리`이다. - 단계별 세부 원인, 수정 내용, 테스트와 배포 기록은 이 문서의 `6. 운영 서버 기준선 점검 결과`와 `7. 변경 기록`에 보존한다. ## 2. 작업 원칙 - 각 단계는 원인 재현, 수정, 로컬 검증, 운영 반영 여부 확인 순서로 진행한다. - 데이터 변경 로직은 실패 시 진행 상태를 갱신하지 않는 것을 기본 원칙으로 한다. - 모든 날짜와 시간 판단은 KST 기준을 명시하고, DB 조회 시 UTC로 변환한다. - 현재 작업 트리의 미커밋 변경을 보존하고 관련 파일을 수정할 때 먼저 diff를 확인한다. - 한 단계가 검증되기 전에는 다음 단계와 한 커밋에 섞지 않는다. - 운영 DB를 사용하는 검증은 dry-run 또는 조회 전용 확인을 먼저 수행한다. ## 3. 작업 순서 ### 0단계: 운영 서버 연결 및 배포 기준선 점검 상태: 완료 대상: - Oracle Cloud 운영 서버 - Nginx 및 FastAPI systemd 서비스 - 실시간 크롤러 및 역방향 백필 cron - Tailscale 및 NAS 프록시 경로 - FastAPI/크롤러의 Supabase 연결 - 운영 웹 배포 디렉터리 검토 항목: - SSH 접속, 서버 시간대, 호스트 정보와 기본 자원 상태를 확인한다. - 운영 배포 경로, Git 브랜치, 마지막 커밋과 미커밋 변경을 확인한다. - `solar-api`, Nginx 서비스 상태와 실제 리슨 포트를 확인한다. - 로컬 및 공개 주소의 health/API 응답을 각각 확인한다. - API 데이터 조회를 통해 FastAPI에서 Supabase까지의 실제 연결을 간접 검증한다. - Nginx의 정적 웹 루트, API 프록시 경로와 TLS 인증서 상태를 확인한다. - 실시간 크롤러, 일일 집계, 백필 cron 등록 내용과 최근 로그를 확인한다. - Tailscale 연결 상태와 NAS 프록시 도달 가능 여부를 확인한다. - 로컬 저장소와 운영 배포본의 커밋 및 미커밋 차이를 기록한다. - 점검 과정에서 비밀 환경변수의 실제 값은 출력하지 않는다. 완료 기준: - 앱에서 외부 모니터링 사이트까지 이어지는 각 연결 구간의 성공 또는 실패 상태가 기록된다. - 현재 운영 중인 코드 버전과 자동 실행 스케줄을 식별한다. - 서비스 장애 또는 배포 차이를 발견하면 코드 수정 전에 영향도와 복구 방법을 정리한다. - 운영 상태를 변경하는 명령 없이 읽기 전용 점검을 완료한다. ### 1단계: 역방향 백필 데이터 무결성 상태: 완료 대상: - `crawler/backward_backfill.py` - `crawler/database.py` - `crawler/crawler_manager.py` - 백필 관련 테스트 및 운영 문서 검토 및 수정 항목: - 초기 기준일을 첫 수집 전에 하루 차감하여 어제 데이터가 누락되는 커서 의미를 수정한다. - 원본의 실제 0kWh, 원본 데이터 없음, 네트워크/파싱 오류를 서로 다른 결과로 구분한다. - 네트워크 오류를 연속 무발전 일수에 포함하지 않는다. - Supabase 저장 성공을 확인한 경우에만 SQLite 진행 날짜를 갱신한다. - 기존 양수 데이터를 0 또는 더 작은 값으로 덮어쓰지 않도록 백필 저장 정책을 정의한다. - 발전소 한 곳의 오류가 다른 발전소의 백필 진행을 막지 않도록 유지한다. - 재시작, 부분 실패, 가동개시일 도달, 30일 연속 실제 무발전 조건을 테스트한다. 완료 기준: - 지정한 시작일이 첫 번째 수집 대상이 된다. - 요청 또는 DB 저장 실패 후 재실행하면 같은 날짜부터 재시도한다. - 30일 연속 종료 조건은 성공적으로 조회된 실제 0값에만 적용된다. - 기존 정상 통계가 백필의 오류성 0값으로 덮어써지지 않는다. ### 2단계: 발전소 알림 설정 연동 상태: 완료 대상: - `crawler/config.py` - `crawler/sync_plants.py` - `crawler/alert_manager.py` - `crawler/main.py` 검토 및 수정 항목: - 크롤러의 문자열 업체 키(`sunwind`)와 DB의 숫자형 `company_id`를 분리한다. - 발전소 ID가 DB 기본키이므로 알림 활성화 조회에 업체 조건이 실제로 필요한지 검토한다. - 앱에서 `alerts_enabled=false`로 변경했을 때 크롤러가 알림을 보내지 않는지 검증한다. - 현재 미커밋 상태인 3회 연속 0kW 및 누적 발전량 증가 기반 오탐 방지 로직을 상태 전이 테스트로 검증한다. - Supabase 조회 실패 시 알림을 계속 진행할지 중단할지 명시적인 장애 정책을 정한다. 완료 기준: - 앱에서 알림을 끈 발전소는 실제 텔레그램 알림 대상에서 제외된다. - 0kW 단발값은 알림을 발생시키지 않는다. - 정상 복구 후 다음 장애를 다시 감지할 수 있다. ### 3단계: Excel 업로드 화면과 API 계약 정리 상태: 완료 대상: - `app/screens/PlantDetailScreen.js` - `app/components/UploadModal.js` - `api_server/app/routers/upload.py` 검토 및 수정 항목: - 상세 화면에서 항상 월간 업로드 API를 호출하는 문제를 수정한다. - 기존 `UploadModal`을 실제 화면에 연결하거나, 중복 구현을 제거하고 하나의 업로드 흐름으로 통합한다. - 일간 형식(`date`, `generation`)과 월간 형식(`year`, `month`, `kwh`)을 UI에서 명확히 구분한다. - Web과 Native의 `FormData` 처리 방식을 각각 검증한다. - 파일명 누락, 잘못된 확장자, 빈 파일, 필수 열 누락, 잘못된 날짜를 4xx 오류로 반환한다. - 업로드 파일 크기 및 행 수 제한을 정한다. - `fillna(method='ffill')` 등 향후 제거 예정인 pandas 사용법을 정리한다. 완료 기준: - 일간 및 월간 샘플 파일이 각각 올바른 API로 전송된다. - 화면 안내와 실제 필수 열이 일치한다. - 잘못된 입력이 서버 500 오류가 아닌 설명 가능한 4xx 응답으로 처리된다. ### 4단계: KST 시간대 처리 통일 상태: 완료 대상: - `crawler/crawler_manager.py` - `crawler/alert_manager.py` - `crawler/main.py` - `crawler/daily_summary.py` - `crawler/backward_backfill.py` - `api_server/app/routers/stats.py` 검토 및 수정 항목: - 공통 KST 시간대 유틸리티를 만들고 naive `datetime.now()` 사용을 제거한다. - 야간 크롤링 차단, 알림 시간, 일일 마감, 기본 조회 날짜, 백필의 어제 계산을 모두 KST 기준으로 통일한다. - Supabase `timestamptz` 조회 범위는 KST 경계를 UTC ISO 문자열로 변환한다. - KST 자정 전후와 월말/연말 경계를 테스트한다. 완료 기준: - 서버 OS 시간대가 UTC여도 동일한 수집 및 통계 결과를 낸다. - KST 날짜의 00:00~23:59 로그만 해당 일 통계에 포함된다. ### 5단계: API 정확성과 응답 규약 상태: 완료 대상: - `api_server/app/routers/plants.py` - `api_server/app/routers/stats.py` - `api_server/app/routers/upload.py` - `api_server/app/main.py` - `api_server/app/schemas/` 검토 및 수정 항목: - 문자열 발전소 ID를 받는 상세 API의 `plant_id: int` 선언을 수정한다. - 잘못된 날짜와 범위 파라미터를 조용히 오늘로 대체하지 않고 400 또는 422로 반환한다. - `HTTPException`이 일반 예외 처리에 잡혀 500으로 바뀌는 경로를 정리한다. - 단순 URL 존재 여부가 아닌 실제 DB 연결 상태를 health check에 반영한다. - 응답 모델과 오류 형식을 가능한 범위에서 통일한다. - 발전소별 최신 로그 N+1 조회를 일괄 조회로 바꿀 수 있는지 검토한다. - 동기 Supabase 호출이 `async` 요청 처리에 미치는 영향을 측정한 뒤 스레드풀 또는 동기 핸들러 적용을 결정한다. 완료 기준: - 주요 API에 정상, 데이터 없음, 잘못된 입력, DB 오류 테스트가 존재한다. - 공개된 OpenAPI 스키마와 실제 응답이 일치한다. ### 6단계: 통계 생성 경로 일관성 상태: 완료·운영 반영 (2026-08-07) 대상: - `crawler/database.py` - `crawler/daily_summary.py` - `crawler/fetch_history.py` - `api_server/app/routers/upload.py` - DB 스키마 및 마이그레이션 검토 및 수정 항목: - 실시간 수집, 일일 마감, 과거 백필, Excel 업로드가 동일한 덮어쓰기 정책을 사용하도록 정리한다. - 실시간 경로에만 적용된 MAX 보호가 다른 저장 경로에도 필요한지 결정한다. - 원본 사이트 보정값과 로그 기반 집계값 중 어떤 값을 우선할지 기록 가능한 정책으로 만든다. - `daily_stats` 변경 후 `monthly_stats` 갱신 책임을 한 곳으로 모은다. - `created_at`, `updated_at`, `peak_kw`, `generation_hours` 갱신 규칙을 통일한다. - `currnet_last_date` 오타 컬럼의 사용 여부와 제거 또는 마이그레이션 필요성을 확인한다. 완료 기준: - 동일 날짜를 어떤 경로로 다시 처리해도 더 신뢰도 높은 기존 데이터가 손상되지 않는다. - 일간 데이터 수정 후 월간 합계가 일관되게 갱신된다. 적용 결과: - 실시간 수집, 일일 마감, 과거 백필과 일간 Excel 업로드가 `upsert_daily_stats` DB 함수를 공통으로 사용한다. - 자동 수집 경로는 기존보다 작은 일 발전량으로 덮어쓰지 않고, 사용자가 명시적으로 수행하는 일간 Excel 보정만 하향 수정을 허용한다. - 일일 마감에서 원본 사이트 값이 로그 최댓값보다 큰 경우만 상향 보정하며, 작은 원본 값은 로그 집계값을 유지한다. - `daily_stats` 변경 트리거가 영향받은 월의 `monthly_stats`를 즉시 재집계한다. Excel·원본 월간값은 권위 있는 월간 입력으로 표시하여 자동 일간 집계가 덮어쓰지 않는다. - `daily_stats`에 `updated_at`, `source`를 추가하고 발전시간은 선택된 발전량과 발전소 용량으로 계산한다. 최고 출력은 저장 경로 간 최댓값을 유지한다. - `monthly_stats.currnet_last_date`를 `last_date date`로 바로잡고 `source`를 추가했다. 기존 과거 월간 총액은 보존하고 현재 월 누락 행만 생성했다. - 운영 적용 전 2026년 통계를 진단한 결과 1~7월 63쌍은 모두 일치했고 8월 9개 발전소의 월간 행만 누락되어 있었다. 적용 후 1~8월 72쌍의 불일치와 누락은 모두 0건이다. ### 7단계: 테스트와 재현 가능한 개발 환경 상태: 대기 대상: - 크롤러 및 API 테스트 디렉터리 - `crawler/requirements.txt` 또는 공통 Python 의존성 정의 - `app/package-lock.json` - CI 설정 검토 및 수정 항목: - 크롤러별 HTTP 응답 fixture를 사용한 파싱 테스트를 만든다. - SQLite 임시 DB와 가짜 Supabase 클라이언트를 사용해 스케줄러, 알림, 백필 상태 전이를 테스트한다. - FastAPI 주요 엔드포인트 테스트를 추가한다. - 프론트엔드 lockfile을 추적하여 설치 버전을 고정한다. - 크롤러 실행에 필요한 최소 Python 의존성을 별도로 정의한다. - 테스트와 기본 정적 검사를 수행하는 CI를 추가한다. 완료 기준: - 외부 사이트와 운영 DB에 접속하지 않고 핵심 로직을 검증할 수 있다. - 새 환경에서 문서화된 명령으로 의존성을 재현할 수 있다. ### 8단계: 저장소 및 코드 구조 정리 상태: 대기 대상: - 추적 중인 `crawler/venv_win/` - `app/server.log` - 이전 크롤러 및 백업 파일 - 대형 React Native 화면 컴포넌트 검토 및 수정 항목: - Git에 추적된 가상환경 실행 파일과 로그 파일을 제거하고 ignore 규칙을 확인한다. - `*_old.py`, `*.backup`, 일회성 스크립트는 보존 필요성을 확인한 후 archive 또는 Git 이력으로 정리한다. - API 기본 URL을 환경별 설정으로 이동한다. - 상세 화면의 API 호출, 업로드, 차트 포맷팅 로직을 hooks와 컴포넌트로 분리한다. - 현재 단일 업체 ID `1` 하드코딩을 제거하고 향후 다중 업체 확장 경계를 정한다. 완료 기준: - 저장소에 실행 환경 산출물과 런타임 로그가 추적되지 않는다. - 화면 컴포넌트와 데이터 접근 로직이 독립적으로 수정 및 테스트 가능하다. ### 9단계: 보안 개선 — 후순위 보류 상태: 보류 후속 검토 항목: - 텔레그램 토큰과 외부 모니터링 계정 비밀번호 교체 - 모든 비밀정보의 환경변수 또는 비밀 저장소 이전 - 과거 Git 이력에 포함된 비밀정보 처리 - Excel 업로드와 알림 설정 변경 API 인증 - Supabase anon 쓰기 정책 제거 및 최소 권한 재설계 - 운영 CORS 허용 출처 제한 - 요청 크기 제한, rate limiting, 감사 로그 ## 4. 권장 커밋 단위 각 단계는 가능하면 다음 단위로 나눈다. 1. 실패를 재현하는 테스트 또는 검증 스크립트 2. 최소 기능 수정 3. 회귀 테스트 및 운영 문서 갱신 긴급 수정이 필요한 경우에도 데이터 로직, UI 변경, 문서 변경은 가능한 한 별도 커밋으로 유지한다. ## 5. 진행 기록 | 단계 | 상태 | 시작일 | 완료일 | 비고 | |---|---|---|---|---| | 0. 운영 서버 연결 및 배포 기준선 | 완료 | 2026-08-06 | 2026-08-06 | 아래 기준선 점검 결과 참조 | | 1. 역방향 백필 무결성 | 완료 | 2026-08-06 | 2026-08-06 | 상태 전이·저장 보호·원본 시작일·운영 dry-run 검증 완료 | | 2. 알림 설정 연동 | 완료 | 2026-08-06 | 2026-08-06 | 코드·단위 테스트·정기 실행·실제 OFF/ON 토글 검증 완료 | | 3. Excel 업로드 계약 | 완료 | 2026-08-06 | 2026-08-06 | 공통 모달 연결, 입력 제한, API 테스트, 웹·API 배포 완료 | | 4. KST 시간대 통일 | 완료 | 2026-08-06 | 2026-08-06 | 공통 KST 유틸리티, UTC 반개구간과 경계 테스트, 운영 cron 검증 완료 | | 5. API 정확성과 응답 | 완료 | 2026-08-07 | 2026-08-07 | 문자열 ID, 응답 모델, DB health, 단일 최신 로그 조회와 운영 배포 완료 | | 6. 통계 생성 경로 일관성 | 완료 | 2026-08-07 | 2026-08-07 | 공통 DB 저장 함수·월간 트리거·출처 필드·오타 마이그레이션과 운영 검증 완료 | | 7. 테스트 및 개발 환경 | 다음 작업 | | | | | 8. 저장소 및 코드 구조 | 대기 | | | | | 9. 보안 개선 | 보류 | | | 단독 저장소 사용 중이므로 후순위 | ## 6. 운영 서버 기준선 점검 결과 점검 시각: 2026-08-06 16:40~17:00 KST 점검 방식: Oracle Cloud 운영 서버에 SSH로 접속하여 상태를 변경하지 않는 읽기 전용 명령만 실행했다. 환경변수와 인증정보의 실제 값은 출력하지 않았다. ### 6.1 서버 및 배포 상태 - 호스트: `holdem-server` - 서버 시간대: `Asia/Seoul` - NTP 동기화: 정상 - 가동 시간: 약 197일 - 루트 디스크: 45GB 중 13GB 사용, 사용률 28% - 운영 저장소: `/home/ubuntu/solorpower` - 브랜치: `main` - 운영 커밋: `b834cc4` (`docs: add historical backfill operation and verification guide`) - 로컬 저장소와 운영 서버의 HEAD 커밋은 동일하다. - 운영 서버에는 `crawler/alert_manager.py`, `crawler/config.py`, `crawler/main.py` 미커밋 변경이 존재한다. - 서버의 diff는 줄바꿈 차이까지 포함되어 크게 표시되므로 배포 또는 커밋 전에 실제 의미 변경과 CRLF/LF 변경을 분리해야 한다. ### 6.2 API, Nginx 및 TLS - `solar-api`: active/enabled, 재시작 횟수 0 - `solar-api` 실행 경로: `/home/ubuntu/solorpower/api_server/venv/bin/uvicorn` - `solar-api` 작업 경로: `/home/ubuntu/solorpower/api_server` - API 리슨 주소: `127.0.0.1:8000` - Nginx: active/enabled, `0.0.0.0:80` 및 `0.0.0.0:443` 리슨 - 로컬 API `/health`: HTTP 200 및 JSON 응답 - 공개 `/plants/1`: HTTP 200 및 JSON 응답. FastAPI에서 Supabase까지의 실제 조회 경로가 정상임을 확인했다. - 공개 `/health`: HTTP 200이지만 API JSON이 아닌 SPA `index.html`을 반환한다. 현재 Nginx 프록시 정규식에 `health`가 포함되지 않았기 때문이다. - TLS 인증서: Let's Encrypt, `solorpower.dadot.net`, 2026-10-18 만료 - 최근 24시간 `solar-api` warning 이상 journal 항목은 없었다. 후속 조치: - 5단계 API 작업에서 Nginx 프록시 대상에 `/health`와 필요 시 `/redoc`을 추가한다. - health check가 실제 Supabase 쿼리를 수행하도록 개선한 뒤 외부 모니터링 경로로 사용한다. ### 6.3 웹 배포 상태 - 웹 루트: `/var/www/html/dist` - 공개 루트: HTTP 200, `text/html` - 운영 `index.html` 수정 시각: 2026-02-12 10:53 KST - 운영 번들에는 전체 발전소 비교와 월간 업로드 기능이 포함되어 있다. - 운영 번들에는 현재 로컬 `App.js`의 `alerts_enabled` 기능이 포함되어 있지 않다. 후속 조치: - 2단계 알림 설정 연동을 수정하고 검증한 뒤 앱을 새로 빌드하여 운영 웹 배포본을 동기화한다. ### 6.4 자동 실행 및 최근 로그 운영 cron: ```cron */10 * * * * cd ~/solorpower/crawler && /usr/bin/python3 main.py >> crawler.log 2>&1 10 0 * * * cd ~/solorpower/crawler && /usr/bin/python3 daily_summary.py >> summary.log 2>&1 30 * * * * cd ~/solorpower/crawler && export PYTHONUTF8=1 && /usr/bin/python3 backward_backfill.py --days 5 --delay 2.0 >> backfill_cron.log 2>&1 ``` - 실시간 크롤러 로그는 점검 시각까지 10분마다 갱신되고 있었다. - 최근 실행마다 9개 발전소의 `solar_logs` 저장 성공을 확인했다. - 일일 집계 로그에서 최근 대상일의 9개 발전소 저장 성공을 확인했다. - Supabase URL과 키 환경변수의 설정 여부를 확인했으며 둘 다 설정되어 있다. - `USE_PROXY=true`가 서버 `.env`에 설정되어 있다. - 로그에 `company_id="sunwind"`를 bigint와 비교하는 오류가 발전소마다 반복되고 있다. 앱의 알림 비활성화 설정이 크롤러에 반영되지 않는 문제가 운영 환경에서도 재현되었다. 후속 조치: - 2단계 알림 설정 연동을 데이터 백필 다음 우선순위로 유지한다. - 로그에서 Supabase URL prefix를 출력하는 디버그 문구를 제거한다. ### 6.5 Tailscale, NAS 프록시 및 외부 사이트 - Oracle 서버 Tailscale 주소: `100.116.0.32` - NAS 프록시 노드 `100.83.7.81`이 Tailscale peer로 확인된다. - NAS 프록시 TCP 3128 포트 연결에 성공했다. - 서버의 `USE_PROXY=true` 설정과 코드의 프록시 주소가 일치한다. - NAS 프록시를 경유한 비인증 기본 URL 점검 결과 NREMS, KREMC, Sun-WMS, Hyundai, CMSolar 모두 HTTP 200을 반환했다. - 최근 실시간 크롤러가 9개 결과를 저장했으므로 인증을 포함한 실제 크롤러 경로도 현재 동작 중인 것으로 판단한다. ### 6.6 백필 상태와 운영 데이터 범위 - SQLite `backfill_state`의 9개 발전소가 모두 `COMPLETED` 상태다. - 완료 이후에도 백필 cron이 매시간 실행되어 `실행 중인 작업이 없습니다`만 기록하고 있다. - 일부 사이트는 연속 30일 0값 조건으로 완료되었으나 Supabase에는 더 오래된 데이터가 이미 존재하여 SQLite 상태만으로 전체 데이터 완전성을 판단할 수 없다. 운영 `daily_stats` 범위: | 발전소 | 건수 | 최초 날짜 | 최종 날짜 | |---|---:|---|---| | nrems-01 | 4,512 | 2014-03-31 | 2026-08-06 | | nrems-02 | 4,512 | 2014-03-31 | 2026-08-06 | | nrems-03 | 3,881 | 2015-12-22 | 2026-08-06 | | nrems-04 | 3,495 | 2017-01-11 | 2026-08-06 | | kremc-05 | 2,791 | 2018-10-09 | 2026-08-06 | | sunwms-06 | 2,412 | 2019-12-30 | 2026-08-06 | | hyundai-08 | 2,374 | 2020-02-06 | 2026-08-06 | | nrems-09 | 2,109 | 2020-10-28 | 2026-08-06 | | cmsolar-10 | 2,115 | 2020-09-22 | 2026-08-06 | 확인 결과: - `kremc-05`는 원본에서 2018-06-28과 2018-10-08 데이터가 없고, 2018-10-09의 첫 데이터 86kWh가 확인됐다. DB 최초 날짜와 일치하므로 백필 누락이 아니다. - `cmsolar-10`은 원본에서 2020-08-31과 2020-09-21 데이터가 없고, 2020-09-22의 첫 데이터 50kWh가 확인됐다. DB 최초 날짜와 일치하므로 백필 누락이 아니다. - 같은 원본 조회 도구로 2026-08-05의 KREMC 160kWh와 CMSolar 127kWh도 재확인했다. - 백필 작업이 모두 완료되었으므로 데이터 검증 후 cron을 비활성화할지 결정한다. 변경은 별도 승인 후 수행한다. ### 6.7 2026-08-05 NREMS 3·4·9호기 오탐 분석 사용자 확인에 따르면 실제 NREMS 사이트의 발전 상태에는 문제가 없었지만 3·4·9호기 장애 알림이 전송되었다. 운영 로그와 당일 집계 데이터를 대조한 결과 발전소 장애가 아니라 NAS 프록시 연결 장애를 발전소 0kW로 잘못 분류한 오탐으로 확인되었다. 발생 타임라인: | KST 시각 | 관측 내용 | |---|---| | 14:30 | 3호기 71.10kW, 4호기 50.70kW, 9호기 70.72kW로 정상 수집 | | 14:40 | NAS 프록시 `100.83.7.81:3128` 연결 타임아웃, 각 호기 0kW 의심 1회 | | 14:50 | 동일 프록시 타임아웃, 0kW 의심 2회 | | 15:00 | 동일 프록시 타임아웃, 0kW 3회 연속으로 텔레그램 알림 전송 | | 15:10 | 프록시 타임아웃 지속 | | 15:20 | 연결 복구. 3호기 61.30kW, 4호기 43.37kW, 9호기 62.76kW 정상 수집 | 같은 시간 NREMS 1·2호기뿐 아니라 KREMC, Sun-WMS, CMSolar도 동일 NAS 프록시 타임아웃을 기록했다. 따라서 특정 발전소 또는 NREMS 원본 사이트 장애가 아닌 공통 프록시 경로 장애다. 3·4·9호기에만 텔레그램 알림이 전송된 이유: - NREMS 비분할 발전소의 예외 처리 경로는 수집 실패 시 `kw=0`, `today=0`, `status='🔴 오류'`인 결과를 생성한다. - `main.py`는 결과의 오류 상태를 확인하지 않고 `AlertManager.check_and_alert()`에 0값을 전달한다. - `AlertManager`는 오류/미수집 상태와 실제 정상 응답의 0kW를 구분하지 않아 세 번의 프록시 오류를 발전소 정지로 판단한다. - NREMS 1·2호기 분할 경로와 다른 사이트 크롤러는 같은 예외에서 결과를 반환하지 않아 알림 검사 자체가 호출되지 않았다. 이로 인해 크롤러별 실패 처리도 일관되지 않다. 데이터 영향: - 14:40의 3·4·9호기 오류 레코드는 장애 이력 추적 목적으로 `solar_logs`에 한 번 저장되었다. - `database.py`의 오류 상태 보호 로직으로 해당 0값은 `daily_stats`를 덮어쓰지 않았다. - 동일 오류값이 반복된 14:50~15:10에는 중복 저장 방지 로직이 적용되었다. - 15:20 정상값 수집 후 상태가 복구되었다. - 8월 6일 00:10 일일 마감에서 로그 최댓값과 원본 사이트 일 통계를 대조했으며 9개 발전소 모두 저장에 성공했다. 2026-08-05 최종 일 발전량: | 발전소 | 최종 발전량(kWh) | 원본 보정 결과 | |---|---:|---| | nrems-01 | 188.0 | 일치 | | nrems-02 | 186.0 | 일치 | | nrems-03 | 432.0 | 일치 | | nrems-04 | 339.0 | 일치 | | kremc-05 | 160.0 | 일치 | | sunwms-06 | 287.7 | 일치 | | hyundai-08 | 389.5 | 일치 | | nrems-09 | 533.2 | 일치 | | cmsolar-10 | 127.0 | 일치 | 수정 방향: 1. 크롤러 결과에 `data_valid` 또는 명확한 수집 상태를 추가하여 `오류/미수집`, `정상 0kW`, `정상 발전`을 구분한다. 2. 수집 오류 상태는 발전소 0kW 카운터를 증가시키지 않는다. 3. 공통 프록시 또는 여러 사이트의 동시 실패는 발전소 장애가 아닌 수집 인프라 장애로 한 번만 알린다. 4. 공통 HTTP 세션에 연결 오류 재시도와 backoff를 추가한다. 5. 모든 크롤러가 동일한 실패 결과 규약을 사용하도록 통일한다. 6. 오류 레코드의 `solar_logs` 보존 여부와 별도 수집 장애 로그 테이블 도입 여부를 검토한다. ### 6.8 기준선 결론 현재 연결 구조인 `외부 사이트 → Tailscale/NAS 프록시 → 크롤러 → Supabase → FastAPI → Nginx → 웹 앱`은 동작 중이다. 즉시 서비스 장애는 발견되지 않았다. 코드 수정 전에 해결해야 할 운영상 주요 문제는 다음과 같다. 1. 백필 완료 상태와 실제 데이터 완전성의 불일치 가능성 2. 운영 중 반복되는 알림 설정 `company_id` 타입 오류 3. 공개 `/health`의 Nginx 오라우팅 4. 로컬 소스보다 오래된 운영 웹 번들 5. 완료된 백필 cron의 불필요한 반복 실행 ## 7. 변경 기록 ### 7.1 알림 오탐 선조치 작업일: 2026-08-06 변경 내용: - 크롤러 결과의 `data_valid` 값과 오류 상태를 이용해 수집 오류를 실제 발전소 0kW와 분리했다. - 수집 오류가 발생하면 진행 중인 0kW 의심 카운트를 초기화하고 알림 판정에서 제외한다. - NREMS 비분할 발전소의 예외 결과에 `data_valid=False`를 명시했다. - `alerts_enabled` 조회에서 문자열 크롤러 업체 키를 제거하고 전역 고유키인 `plants.id`만 사용한다. - 알림 설정 조회가 일시적으로 실패하면 마지막 성공 값을 사용하는 메모리 캐시를 추가했다. - 테스트를 위해 현재 시각 주입이 가능하도록 `AlertManager` 생성자를 확장했다. 검증 결과: - 알림 상태 전이 단위 테스트 6개 통과 - 변경 Python 파일 5개 AST 구문 검증 통과 - 운영 Supabase에서 9개 발전소 모두 `plants.id` 단독 조건으로 `alerts_enabled` 조회 성공 - 점검 시점의 9개 발전소 알림 설정은 모두 활성 상태 - 운영 서버의 기존 미커밋 변경과 대조하여 이번 변경이 기존 3회 연속 0kW 로직을 보존함을 확인했다. - 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260806_alert_data_valid/`에 백업했다. - `alert_manager.py`, `main.py`, `crawlers/base.py`, `crawlers/nrems.py`와 알림 테스트를 운영 서버에 배포했다. - 운영 서버에서 Python 구문 검사와 알림 상태 전이 단위 테스트 6개가 모두 통과했다. - 로컬과 운영 서버의 배포 대상 5개 파일 SHA-256 해시가 모두 일치했다. - 2026-08-06 17:10 KST 정기 실행에서 외부 사이트 연결과 Supabase 저장이 정상 완료됐다. 6건을 저장하고 변경 없는 3건은 정상적으로 건너뛰었다. - 배포 후 실행 구간에서 기존 `company_id` bigint 변환 오류와 텔레그램 알림 발송은 발생하지 않았다. - 운영 PATCH API로 8호기의 `alerts_enabled`를 잠시 `false`로 변경했을 때 크롤러가 실제로 `false`를 읽는 것을 확인했다. - 같은 검증 흐름에서 8호기 설정을 즉시 `true`로 복구했고, 크롤러에서 최종 `true` 상태를 재확인했다. - 운영 알림 설정 확인용 읽기 전용 도구 `tests/check_alert_setting.py`를 추가했다. - 서비스 재시작과 cron 변경은 필요하지 않아 수행하지 않았다. 남은 관찰 항목: - 배포 직전인 17:00 KST에 NAS 프록시 연결 실패가 재발했고 17:10에는 복구됐다. 다음 실제 수집 오류에서 `수집 실패 데이터는 0kW 판정에서 제외` 로그가 남는지 확인한다. ### 7.2 역방향 백필 무결성 작업일: 2026-08-06 변경 내용: - 최초 상태 커서를 첫 수집 대상의 다음 날로 저장하여 지정한 시작일을 건너뛰지 않도록 수정했다. - 일별 원본 조회 계약을 `list=정상 응답`, 빈 `list=정상 응답이지만 해당 날짜 없음`, `None=요청·로그인·파싱 실패`로 구분했다. - 네트워크·로그인·파싱 실패 시 무발전 일수를 늘리지 않고 커서를 유지하여 다음 실행이 같은 날짜부터 재시도하게 했다. - Supabase 저장 성공이 확인된 경우에만 SQLite 진행 날짜를 갱신하도록 수정했다. - 원본에 날짜가 없는 경우는 실제 0kWh와 분리하고 연속 무발전 횟수를 초기화한다. - 성공적으로 조회된 실제 0kWh만 30일 연속 종료 조건에 포함한다. - `daily_stats`의 기존 값보다 작거나 같은 백필 값은 upsert하지 않으며, 기존값 조회 실패 시에도 저장하지 않는다. - 음수 발전량은 잘못된 과거 데이터로 거부한다. - 일간 저장 후 월 집계 실패가 발생해도 같은 날짜 재시도에서 월간 합계를 다시 계산하도록 했다. - 운영 원본 값을 DB에 쓰지 않고 확인하는 `tests/check_history_source.py`를 추가했다. 검증 결과: - 알림 회귀 테스트 6개, 백필 상태 전이 7개, DB 저장 보호 4개, 사이트 조회 계약 6개 등 총 23개 테스트가 로컬과 운영 서버에서 모두 통과했다. - 운영 서버 대상 Python 파일 구문 검사가 통과했다. - 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260806_backfill_integrity/`에 백업했다. - 원본과 운영 DB의 KREMC·CMSolar 최초 날짜가 일치하며 2026-08-05 값도 일치함을 확인했다. - 운영 dry-run에서 9개 발전소 모두 완료 상태이며 실행 중인 백필이 없음을 확인했다. - 백필 cron은 변경하지 않았으며 현재 매시간 무작업 실행만 반복한다. ### 7.3 Excel 업로드 화면과 API 계약 작업일: 2026-08-06 변경 내용: - 상세 화면의 월간 전용 즉시 업로드 구현을 제거하고 기존 `UploadModal`을 연결했다. - 사용자가 일간 또는 월간 형식을 선택한 후 파일을 업로드하도록 흐름을 하나로 통합했다. - Web에서는 DocumentPicker의 실제 `File`을, Native에서는 URI 객체를 `FormData`에 추가한다. - `multipart/form-data`의 boundary를 런타임이 만들도록 수동 `Content-Type` 헤더를 제거했다. - API에 `.xlsx`·`.xls` 확장자, 빈 파일, 5MB 파일 크기, 5,000행 제한을 추가했다. - 일간 업로드의 필수 열, 날짜, 빈 발전량, 음수 발전량을 검증한다. - 월간 업로드의 필수 열, 월 범위, 빈·비숫자·음수 발전량을 검증한다. - 존재하지 않는 발전소가 내부 오류 500이 아닌 404를 반환하도록 조회 방식을 정리했다. - pandas의 `fillna(method='ffill')`를 `ffill()`로 변경했다. 검증 및 배포 결과: - Expo Web production export가 성공했다. - 정상 일간·월간 샘플과 잘못된 확장자, 빈 파일, 필수 열 누락, 잘못된 날짜, 음수, 미등록 발전소, 파일 크기, 행 수를 다루는 API 테스트 10개가 운영과 동일한 Python 환경에서 통과했다. - API 수정 전 파일과 기존 웹 배포본을 `/home/ubuntu/solorpower/deploy_backups/20260806_upload_contract/`에 백업했다. - 이전 웹 디렉터리는 `/var/www/html/dist.pre_20260806_upload_contract`에도 보존했다. - `solar-api` 재시작 후 내부 `/health`가 정상이고 Nginx와 API 서비스가 모두 active 상태임을 확인했다. - 공개 웹과 `/plants/1`은 HTTP 200, 잘못된 확장자의 공개 업로드 요청은 설명 가능한 HTTP 400을 반환했다. - 운영 웹 번들은 `AppEntry-80396685aa3d044c4ab36bbf91a02696.js`로 갱신됐고 일간·월간 업로드 경로가 포함됐다. ### 7.4 KST 시간대 처리 통일 작업일: 2026-08-06 변경 내용: - 크롤러와 API의 독립 배포 구조에 맞춰 각각 공통 KST 시간 유틸리티를 추가했다. - 서버 OS 시간대에 의존하던 크롤러 시작 로그, 야간 실행 차단, 저장 heartbeat, 알림 허용 시간, 일일 마감, 백필 기본 날짜를 KST 기준으로 통일했다. - 기존 SQLite에 저장된 naive ISO 시각은 과거 기록 방식과의 호환을 위해 KST로 간주하고, 신규 기록은 `+09:00` 오프셋을 포함한다. - NREMS 1·2호기 인버터 조회의 오늘 날짜와 월도 KST 기준으로 계산한다. - 실시간 및 과거 이력 저장의 생성·갱신 시각을 공통 KST 함수로 통일했다. - API의 오늘 비교 통계, 발전소 일별 통계, 시간별 통계와 크롤러 일일 마감 조회 범위를 KST 하루의 UTC 반개구간으로 변경했다. - 조회 조건은 `KST 00:00 이상, 다음 날 KST 00:00 미만`으로 적용하여 다음 날 자정 데이터가 이전 날짜에 포함되지 않도록 했다. - 시간별 통계의 잘못된 날짜 형식에서 발생한 `HTTPException`이 일반 예외 처리에 잡혀 500으로 바뀌지 않도록 유지했다. 검증 및 배포 결과: - UTC 시각 주입 시 KST 04:59에는 크롤링이 차단되고 KST 05:00에는 허용되는 것을 테스트했다. - KST 자정, 8월 말일, 12월 31일·1월 1일 경계가 UTC 전날 15:00부터 다음 15:00 미만으로 변환되는 것을 테스트했다. - 크롤러 시간대 및 기존 알림·백필 회귀 테스트 30개가 Linux 컨테이너와 운영 서버에서 통과했다. - API 시간대 테스트 4개와 Excel 업로드 회귀 테스트 10개 등 운영 서버 API 테스트 14개가 통과했다. - 운영 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260806_kst_time/`에 백업했다. - `solar-api` 재시작 후 서비스와 8000 포트 리슨을 확인했고 내부 `/health`가 HTTP 200을 반환했다. - 공개 `/health`, 오늘 전체 비교 통계, 2026-08-05 시간별 통계가 모두 HTTP 200을 반환했다. - API 재시작 직후 약 4초의 기동 구간에는 Nginx가 일시적으로 502를 반환했으며 애플리케이션 시작 완료 후 정상 복구됐다. - 2026-08-06 18:00 KST 정기 cron 실행에서 9개 발전소 연결이 모두 성공했고, 값이 변경된 8개 로그를 저장했으며 변경 없는 현대 8호기는 정상적으로 건너뛰었다. - 같은 정기 실행에서 시작 시각과 마감 스킵 판단이 모두 KST 18:00으로 기록됐고 수집 오류나 텔레그램 알림은 발생하지 않았다. ### 7.5 API 정확성과 응답 규약 작업일: 2026-08-07 변경 내용: - 상세 API의 `plant_id`를 정수형에서 실제 DB 키와 같은 문자열형으로 수정하여 `nrems-03` 같은 요청이 422가 되던 문제를 해결했다. - 상세 발전소 조회에서 `.single()`이 미등록 데이터를 내부 오류로 바꾸지 않도록 `.limit(1)` 조회와 명시적 404 처리를 적용했다. - 비교·발전소·시간별 통계에 응답 모델을 추가하고 발전소 상세, 알림 변경, health, Excel 업로드 응답도 Pydantic 모델로 OpenAPI에 명시했다. - 비교 통계와 시간별 통계 날짜는 `YYYY-MM-DD`만 허용하고 잘못된 형식이나 존재하지 않는 날짜를 400으로 반환한다. - 통계의 연도는 2000~2100, 월은 1~12로 제한하여 범위를 벗어나면 FastAPI 검증 응답 422를 반환한다. - 미등록 발전소의 일별·시간별 통계가 0으로 채운 성공 응답을 반환하지 않고 404를 반환하도록 수정했다. - `HTTPException`을 일반 예외 처리보다 먼저 다시 전달하여 의도한 4xx 상태가 500으로 바뀌지 않게 했다. - 선택한 연도의 통계 조회 종료 월이 현재 연도로 고정되던 문제를 수정하고 윤년의 연간 발전시간 계산에 366일을 적용했다. - 발전소 목록의 최신 로그 조회를 발전소 1회와 로그 9회의 N+1 요청에서 관계 중첩 단일 요청으로 변경했다. - 동기 Supabase 호출을 사용하는 조회 API는 FastAPI 동기 핸들러로 전환하여 threadpool에서 실행되게 했다. - 비동기 파일 업로드에서는 Excel 파싱과 Supabase 조회·저장을 `run_in_threadpool`로 분리하여 이벤트 루프 차단을 줄였다. - `/health`가 환경변수 존재 여부가 아니라 실제 `plants` 읽기 요청으로 Supabase 연결을 확인하며 실패 시 503을 반환하도록 변경했다. - Nginx에 `/health`와 `/redoc`을 포함한 명시적 API 프록시 규칙을 추가하고 재현 가능한 설정을 `deploy/nginx/solorpower.conf`에 보존했다. 검증 및 배포 결과: - 정상 업로드, 시간대 경계, 문자열 ID, 빈 목록, 날짜·범위 오류, 미등록 발전소, DB 조회 실패, DB health 실패, OpenAPI 모델과 단일 관계 조회를 다루는 API 테스트 31개가 스테이징과 운영 서버에서 모두 통과했다. - 운영 Supabase 읽기 전용 스테이징 점검에서 health, 발전소 목록·상세, 비교 통계와 시간별 통계가 모두 JSON HTTP 200을 반환했다. - 공개 발전소 목록 응답 시간은 변경 전 약 2.46초에서 배포 후 약 0.23초로 확인됐다. - 운영 API 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260807_api_contract/`에 백업했다. - 기존 Nginx 설정은 `/etc/nginx/sites-available/solorpower.pre_20260807_api_contract`에 백업했다. - Nginx 설정 검사가 성공한 경우에만 reload했으며 `solar-api`와 Nginx 모두 active 상태를 유지했다. - 공개 `/health`는 `application/json`과 `supabase_connected=true`를 반환하고, 문자열 발전소 상세와 OpenAPI는 200, 잘못된 날짜는 400, 잘못된 월은 422를 반환했다. ### 7.6 통계 생성 경로 일관성 작업일: 2026-08-07 변경 전 확인: - 운영 Supabase에는 최초 원격 스키마 마이그레이션 1건만 적용되어 있었다. - `daily_stats`에는 값 변경 시각과 입력 출처 컬럼이 없었고, `monthly_stats`에는 사용되지 않는 `currnet_last_date text` 오타 컬럼이 존재했다. 운영 937개 월간 행에서 이 오타 컬럼의 실제 값은 모두 NULL이었다. - 실시간 저장만 기존 최댓값을 보호했고 조회 실패 시 빈 기존값으로 간주하여 보호가 우회될 수 있었다. - 과거 백필과 일일 마감은 각각 별도 일간 upsert를 사용했고, 백필과 월말 마감이 별도로 월간 합계를 갱신했다. 일간 Excel 업로드는 월간 합계를 갱신하지 않았다. - 변경 전 2026-01~2026-08 일·월 통계를 읽기 전용으로 대조한 결과 1~7월 63쌍의 합계 차이는 0건이었으나 8월 9개 발전소의 월간 행이 모두 누락되어 있었다. 저장 정책: - `realtime`, `daily_summary`, `history`는 자동 경로이며 같은 날짜의 기존 발전량보다 큰 값만 반영한다. - `excel_daily`는 사용자의 명시적 보정 경로이므로 발전량의 상향·하향 수정을 모두 허용한다. - 자동 경로가 더 작은 값을 제출하더라도 기존 총발전량과 출처를 유지한다. 최고 출력은 경로와 관계없이 기존값과 신규값 중 큰 값을 유지한다. - 발전시간은 최종 선택된 총발전량을 `plants.capacity`로 나눠 DB에서 계산한다. `created_at`은 최초 생성 시각을 유지하고, 총발전량 또는 최고 출력의 실제 변경 시에만 `updated_at`을 갱신한다. - 일일 마감의 원본 사이트 값은 로그 기반 당일 최댓값보다 큰 경우에만 상향 보정한다. 원본 0 또는 더 작은 값은 로그 집계를 낮추지 않는다. - `derived_daily` 월간 값은 일간 변경 트리거가 합계와 마지막 포함일을 갱신한다. `excel_monthly`, `history_monthly`는 권위 있는 월간 입력으로 보호한다. - 마이그레이션 당시의 기존 월간 값은 `legacy`로 표시하고 총액을 일괄 재계산하지 않았다. 현재 KST 월만 생성·갱신하여 과거 수동 입력 손상 가능성을 차단했다. 코드 및 스키마 변경: - `crawler/database.py`와 `api_server/app/core/stats_storage.py`에 공통 RPC 호출 래퍼를 추가했다. - `crawler/database.py`의 실시간·백필 일간 저장과 `crawler/daily_summary.py`의 마감 저장을 공통 DB 함수로 전환했다. - 백필과 월말 마감에 중복되어 있던 애플리케이션 월간 재집계 코드를 제거했다. - 두 일간 Excel API는 `excel_daily`, 월간 Excel API는 `excel_monthly` 출처를 기록한다. 월간 업로드는 해당 월의 말일을 `last_date`로 저장한다. - `supabase/migrations/20260807000001_stats_write_consistency.sql`에 컬럼·제약조건, `upsert_daily_stats`, `refresh_monthly_stat`과 `daily_stats_sync_monthly` 트리거를 추가했다. - `currnet_last_date`는 `last_date date`로 이름과 자료형을 바로잡았다. - `crawler/tests/check_stats_consistency.py`를 추가하여 PostgREST 1,000행 제한을 페이지 처리하면서 일·월 합계, 누락과 스키마 버전을 읽기 전용으로 점검할 수 있게 했다. 검증 및 배포 결과: - 격리된 Supabase PostgreSQL 15 컨테이너에서 실제 마이그레이션을 적용하고 자동 하향 방지, 자동 상향, Excel 하향 정정, 최고 출력 보호, 발전시간 재계산, 월간 트리거와 권위 월간 보호를 SQL로 검증했다. - 크롤러 전체 회귀 테스트 31개가 Linux 컨테이너에서 통과했다. Windows 직접 실행에서 보인 15개 오류는 SQLite 연결이 열린 상태의 임시 파일 정리 잠금 문제였으며 Linux 실행에서는 모두 통과했다. - API 전체 회귀 테스트 31개가 운영과 동일한 서버 Python 환경의 스테이징 및 배포본에서 모두 통과했다. - Supabase 마이그레이션 적용 후 2026-01~2026-08 통계는 `daily_rows=1970`, `monthly_rows=72`, `mismatches=0`, `missing_monthly=0`으로 확인됐다. - 운영 배포 전 API·크롤러 파일은 `/home/ubuntu/solorpower/deploy_backups/20260807_stats_consistency/`에 백업했다. - `solar-api` 재시작 후 active 상태이며 공개 `/health`는 실제 Supabase 연결을 포함한 JSON 200을 반환한다. 공개 `/plants/stats/comparison`의 2026년 8월 월간 및 2026-08-05 일간 요청도 JSON 200을 반환한다. - 2026-08-07 10:50 KST 실제 cron이 새 저장 함수를 사용하여 9개 발전소의 `solar_logs`와 `daily_stats`를 모두 저장했고 수집·통계 오류 없이 종료했다. ### 7.7 테스트와 재현 가능한 개발 환경 작업일: 2026-08-07 의존성과 실행 기준: - crawler의 직접 의존성과 Python 3.10 호환 NumPy 제약을 `crawler/requirements.in`에 기록하고, 해석된 전이 의존성 56개 전체를 `crawler/requirements.txt`에 고정했다. - API의 직접 의존성 기준을 `api_server/requirements.in`에 분리하고 기존 전체 고정 목록인 `requirements.txt`를 배포·CI 기준으로 유지했다. - Linux 전용 `uvloop`에 `sys_platform != "win32"` 조건을 추가하여 같은 고정 목록이 Windows 개발 환경에서도 설치되도록 수정했다. - 앱의 `package-lock.json`을 Git 관리 대상으로 전환하고 중국 npm mirror 주소와 버전이 비어 있던 Supabase CLI 선택 패키지 항목을 제거한 공식 npm registry 기반 lockfile로 재생성했다. - 깨끗한 설치에서 드러난 누락 직접 의존성 `expo-asset ~11.0.5`를 추가했다. - `npm run build:web`과 CI용 `npm run test:ci` 명령을 추가했다. 테스트 보강: - NREMS, KREMC, 현대, Sun-WMS, CMSolar의 성공 응답을 개인정보 없는 최소 JSON/HTML fixture로 저장했다. - 외부 네트워크와 실제 계정 없이 로그인·월간 요청·날짜와 발전량 파싱을 검증하는 성공 경로 테스트 5개를 추가했다. - Windows 임시 SQLite 파일 잠금의 원인이었던 연결 미종료를 `contextlib.closing`으로 수정했다. 운영 코드의 알림 상태, scheduler, 백필 DB와 관련 테스트 헬퍼가 연결을 명시적으로 닫는다. CI 및 문서: - `.github/workflows/ci.yml`에 crawler Python 3.10/3.11, API Python 3.11, PostgreSQL 15 migration 계약, Node 20 Expo 웹 build의 4개 독립 job을 추가했다. - CI는 테스트용 더미 Supabase 설정만 사용하며 실제 외부 사이트, 운영 DB, Telegram 비밀값을 요구하지 않는다. - 설치와 실행 명령, 지원 버전, 테스트 범위를 `docs/development_and_testing.md`에 정리했다. - 워크플로는 커밋 `a716dbe`에 포함되어 원격 Gitea `main`에 push됐다. 실제 job 실행 여부는 Gitea Actions runner/UI에서 확인한다. 검증 및 배포 결과: - 새 Python 3.11 가상환경에 crawler와 API 고정 의존성을 함께 설치했다. Windows에서 Linux 전용 `uvloop`가 정상 제외됐고 `pip install`이 성공했다. - 같은 새 환경에서 crawler 36개와 API 31개 테스트가 모두 통과했다. - crawler 36개 테스트는 기존 Linux 컨테이너와 운영 서버 `/usr/bin/python3`에서도 모두 통과했다. - 공식 registry lockfile만 있는 깨끗한 npm 환경에서 931개 패키지를 `npm ci`로 설치하고 Expo 웹 export를 완료했다. - 격리된 PostgreSQL 15 컨테이너에서 `stats_write_consistency_test.sql`을 다시 실행하여 migration 계약이 통과했다. 테스트용 컨테이너는 종료 후 자동 제거했다. - `npm ci` 감사 결과는 17건(중간 10, 높음 6, 치명적 1)을 보고했다. 대부분 현재 Expo 52 의존성 트리의 전이 패키지이며 호환성 검토가 필요한 버전 업그레이드는 8단계 이후 별도 작업으로 남긴다. - 운영 반영 전 SQLite 관련 파일 3개는 `/home/ubuntu/solorpower/deploy_backups/20260807_test_environment/`에 백업했다. - 2026-08-07 12:00 KST 실제 cron에서 3호기는 직전 값과 동일하여 정상적으로 저장을 건너뛰었고 나머지 8개 발전소는 저장에 성공했다. SQLite 잠금, 수집 실패, 통계 저장 실패와 Telegram 오탐은 발생하지 않았다. - 직접 배포된 서버 파일과 원격 커밋을 전체 인덱스로 대조하고, 내용이 달랐던 앱 소스 2개와 백필 문서는 `/home/ubuntu/solorpower/deploy_backups/20260807_git_alignment/`에 백업했다. 이후 커밋이 변경한 63개 경로만 `origin/main`과 일치시켰다. - 정렬 후 로컬·원격·운영 서버가 모두 `a716dbe`를 가리키며 crawler 36개와 API 31개 테스트, 내부·공개 health HTTP 200을 다시 확인했다. 14:40 KST 정기 crawler도 9건의 실시간·일간 통계를 정상 저장했다.