| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 클라우드 보안 기반
- 모듈 프로젝트
- 루키즈
- DVWA#INSTALL#github#security#kali#linux
- Kali#Linux#Brute#Force#Attack#Test#DVWA#Hacking#Low#무차별#대입#공격#해킹
- 애플리케이션 보안 기술
- 클라우드기반 보안 시스템 구축/운영 실무
- 개인정보보호
- 기술 특강 및 OT
- Kali#Linux#KALI#LINUX#INSTALL#github#설치
- Case Study
- kisa #보안관제
- 인프라 활용을 위한 파이썬
- #루키즈
- 모의침투
- 보고서
- 29기
- VMWARE#INSTALL#설치
- 쉴더스
- AI #취업
- sk 쉴더스 루키즈
- Foxyproxy#install#setting#firefox
- 클라우드 기반
- 모듈프로젝트
- 클라우드 보안 기술
- 시스템-네트워크 보안 기술
- sk shieldus
- CERT
- rocky linux#siem#project#threat detection#soc#onpremise#ids#python#csv#pipeline#kali linux#DVWA#security monitoring
- DVWA#Brute#Force#Attack#Test#Kali#Linux#Medium#Level#sleep
- Today
- Total
이것저것
[SK shieldus Rookies 29기] 9일차 본문
- 오늘은 여태까지 배웠던 기술 스택을 활용하는 케이스 스터디를 진행하는 방식으로 수업을 진행하였다.
- 팀원들과 상의를 한 끝에 우리 조의 주제는 slack으로 4가지를 전송받기로 하였다. 여기서 4가지는 Google Calendar 일정, 날씨, 보안 뉴스, Github Push 알람 이벤트이다.
- 이 중에서 내가 맡은 파트는 날씨였다.
- 날씨에서 디벨롭된 방향은 기상청 API 기반 자동 날씨 수집·MongoDB캐싱·Slack 알림 시스템으로 결정하였다.
전체 흐름 구조
- 기상청 API에서 데이터를 가져옴
- MongoDB를 통해 캐시를 확인하거나 저장
- Slack으로 알림을 전송
weather_slack/ ├── slack_weather.py ├── models.py ├── config.py ├── requirements.txt ├── .env ├── .venv ├── __pycache__
먼저 코드를 제시하겠다.
slack_weather.py
import requests
from datetime import datetime, timedelta
import config # .env 로드
import models # MongoDB 캐싱
CITY_COORDS = {'seoul': {'nx': 60, 'ny': 127}} # 가이드 2. 참고자료 - 단기예보 지점 좌표(X,Y) 참조: 서울 nx=60, ny=127 엑셀 값.
def get_latest_base_time():
now = datetime.now()
base_times = ['0200', '0500', '0800', '1100', '1400', '1700', '2000', '2300'] # 가이드 2. 참고자료 - 예보 발표시각 ❍ 단기예보 발표시각 참조: 1일 8회 시간대.
current_hour = now.hour
for bt in reversed(base_times):
bt_hour = int(bt[:2])
if current_hour >= bt_hour + (1 if int(bt[2:]) > 0 else 0):
return bt
return '2300'
def fetch_current_weather(city):
coords = CITY_COORDS.get(city.lower())
if not coords:
return None
base_date = datetime.now().strftime('%Y%m%d') # 가이드 1.1 다. 상세기능내역 1) [초단기실황조회] b) 요청 메시지 명세 - base_date 필수 참조: YYYYMMDD 형식.
base_time = (datetime.now() - timedelta(minutes=10)).strftime('%H00') # 가이드 2. 참고자료 ❍ 초단기실황 발표시각 참조: 매시 10분 후 업데이트.
url = 'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst' # 가이드 1.1 가. API 서비스 개요 - 서비스 URL 참조: VilageFcstInfoService_2.0.
params = {
'serviceKey': config.SERVICE_KEY, # 가이드 1.1 다. 상세기능내역 1) b) 요청 메시지 명세 - serviceKey 필수 참조: URL Encode된 인증키.
'numOfRows': '100', # 가이드 1.1 다. 상세기능내역 1) b) numOfRows 참조: 한 페이지 결과 수 (Default:10).
'dataType': 'JSON', # 가이드 1.1 다. 상세기능내역 1) b) dataType 참조: JSON/XML (Default:XML).
'base_date': base_date,
'base_time': base_time, # 가이드 1.1 다. 상세기능내역 1) b) base_time 참조: HH00 형식, 매시 10분 후 호출.
'nx': coords['nx'], # 가이드 1.1 다. 상세기능내역 1) b) nx 참조: 예보지점 X 좌표 (엑셀 참조).
'ny': coords['ny'] # 가이드 1.1 다. 상세기능내역 1) b) ny 참조: 예보지점 Y 좌표.
}
response = requests.get(url, params=params)
if response.status_code != 200:
print('초단기실황 API 실패:', response.text) # 가이드 2. 참고자료 ※ Open API 에러 코드 정리 참조: 에러 코드(00=정상, 30=키 오류 등) 확인.
return None
items = response.json()['response']['body']['items']['item'] # 가이드 1.1 다. 상세기능내역 1) d) 응답 메시지 예제 참조: items 배열.
current = {'current_temp': None}
for item in items:
if item['category'] == 'T1H': # 가이드 1.1 다. 상세기능내역 1) c) 응답 메시지 명세 - category 참조: T1H=기온 실황 값.
current['current_temp'] = item['obsrValue'] # 가이드 1.1 다. 상세기능내역 1) c) obsrValue 참조: 실황 값 (실수/정수).
return current
def fetch_weather(city):
coords = CITY_COORDS.get(city.lower())
if not coords:
return None
base_date = datetime.now().strftime('%Y%m%d') # 가이드 1.1 다. 상세기능내역 3) [단기예보조회] b) 요청 메시지 명세 - base_date 필수 참조.
base_time = get_latest_base_time() # 가이드 1.1 다. 상세기능내역 3) b) base_time 참조: 0500 등 8회 시간대.
url = 'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst' # 가이드 1.1 다. 상세기능내역 3) a) Call Back URL 참조.
params = {
'serviceKey': config.SERVICE_KEY,
'numOfRows': '1000', # 가이드 1.1 다. 상세기능내역 3) b) numOfRows 참조: 충분히 가져옴 (Default:10).
'dataType': 'JSON',
'base_date': base_date,
'base_time': base_time,
'nx': coords['nx'],
'ny': coords['ny']
}
response = requests.get(url, params=params)
if response.status_code != 200:
print('단기예보 API 실패:', response.text) # 가이드 2. 참고자료 에러 코드 참조.
return None
items = response.json()['response']['body']['items']['item']
weather_data = {'max_temp': None, 'min_temp': None, 'precip_prob': None, 'precip_amt': None}
for item in items:
if item['fcstDate'] == base_date: # 가이드 1.1 다. 상세기능내역 3) c) fcstDate 참조: 오늘 예보 필터.
if item['category'] == 'TMX': # 가이드 1.1 다. 상세기능내역 3) c) category 참조: TMX=최고온도.
weather_data['max_temp'] = item['fcstValue'] # 가이드 1.1 다. 상세기능내역 3) c) fcstValue 참조: 실수/정수 값.
elif item['category'] == 'TMN': # TMN=최저온도.
weather_data['min_temp'] = item['fcstValue']
elif item['category'] == 'POP': # POP=강수확률.
if weather_data['precip_prob'] is None:
weather_data['precip_prob'] = item['fcstValue']
elif item['category'] == 'PCP': # PCP=1시간 강수량, 가이드 2. 참고자료 - 강수량(PCP) 범주 참조: mm 단위 표시.
if weather_data['precip_amt'] is None:
weather_data['precip_amt'] = item['fcstValue']
return weather_data
def send_to_slack(city, data):
webhook_url = config.SLACK_WEBHOOK_URL
message = {
'text': f"{city} 날씨: 현재 {data.get('current_temp', 'N/A')}도, 최고 {data.get('max_temp', 'N/A')}도, 최저 {data.get('min_temp', 'N/A')}도, 강수확률 {data.get('precip_prob', 'N/A')}%, 강수량 {data.get('precip_amt', 'N/A')}"
}
response = requests.post(webhook_url, json=message)
if response.status_code == 200:
print('Slack 메시지 전송 성공')
else:
print('Slack 실패:', response.text)
def main():
city = 'seoul'
cached = models.get_cached_data(city)
if cached:
send_to_slack(city, cached)
else:
current = fetch_current_weather(city) or {'current_temp': 'N/A'}
weather = fetch_weather(city) or {'max_temp': 'N/A', 'min_temp': 'N/A', 'precip_prob': 'N/A', 'precip_amt': 'N/A'}
data = {
'city': city,
'current_temp': current['current_temp'],
'max_temp': weather['max_temp'],
'min_temp': weather['min_temp'],
'precip_prob': weather['precip_prob'],
'precip_amt': weather['precip_amt']
}
models.save_data(city, data)
send_to_slack(city, data)
if __name__ == '__main__':
main()
models.py
from pymongo import MongoClient
from datetime import datetime, timedelta
import config # config.py 임포트
client = MongoClient('mongodb://localhost:27017')
db = client['weather_db']
collection = db['weather_data']
#검증
try:
client = MongoClient('mongodb://localhost:27017', serverSelectionTimeoutMS=3000)
# 서버에 연결 가능한지 강제 확인
client.server_info()
db = client['weather_db']
collection = db['weather_data']
print("MongoDB: 연결 성공", 'mongodb://localhost:27017')
except Exception as e:
print("MongoDB 연결 실패:", e)
collection = None
def get_cached_data(city):
# 1시간 내 캐싱된 데이터 찾기
one_hour_ago = datetime.now() - timedelta(hours=1)
data = collection.find_one({'city': city, 'timestamp': {'$gt': one_hour_ago}})
return data
def save_data(city, data):
# 데이터 저장 (timestamp 추가)
data['city'] = city
data['timestamp'] = datetime.now()
collection.insert_one(data)
config.py
import os
from dotenv import load_dotenv
load_dotenv()
SERVICE_KEY = os.getenv('SERVICE_KEY') # 가이드 1.1 가. API 서비스 개요 - serviceKey 필수 파라미터 참조: 공공데이터포털 발급 키 사용.
SLACK_WEBHOOK_URL = os.getenv('SLACK_WEBHOOK_URL') # Slack Incoming Webhook URL
공공데이터 포털 사이트 참조
# Python3 샘플 코드 #
import requests
url = 'http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst'
params ={'serviceKey' : '서비스키', 'pageNo' : '1', 'numOfRows' : '1000', 'dataType' : 'XML', 'base_date' : '20210628', 'base_time' : '0600', 'nx' : '55', 'ny' : '127' }
response = requests.get(url, params=params)
print(response.content)

.env example
SERVICE_KEY=your_service_key_here # 실제 기상청 키 입력
SLACK_WEBHOOK_URL = os.getenv('SLACK_WEBHOOK_URL')
서울 격자 위경도(기상청-단기예보 조회서비스-오픈API활용가이드)
| 구분 | 행정구역코드 | 1단계 | 2단계 | 3단계 | 격자 X | 격자 Y | 경도(시) | 경도(분) | 경도(초) | 위도(시) | 위도(분) | 위도(초) | 경도(초/100) | 위도(초/100) | 위치업데이트 | None |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kor | 1100000000 | 서울특별시 | 60 | 127 | 126 | 58 | 48.03 | 37 | 33 | 48.85 | 126.98 | 37.5636 |
문제점
seoul 날씨: 현재 5.1도, 최고 None도, 최저 None도, 강수확률 0%, 강수량 강수없음
- 왜 None값으로 반환이 되는가?
- 열심히 구글링을 돌린 결과 아래와 같은 결론이 나왔다.
TMX는 오늘 날짜로 잘 찾았으나...
TMN은 내일 fcstDate에 있어 필터링되어 버림.
TMX (최고 기온): 보통 오늘의 예상 최고 기온으로, 이미 지나간 시간대의 예보를 기반으로 확정되는 경우가 많습니다.
TMN (최저 기온): 보통 다음 날 새벽의 최저 기온을 의미하며, 이 데이터가 오늘 발표 예보에 포함될 때 fcstDate가 내일 날짜로 표기될 수 있습니다.
따라서
기상청 단기예보 특성상 오늘 최저기온은 대부분 “내일 새벽 기온”이라 fcstDate가 내일 날짜로 되어 있음 → 코드는 fcstDate == 오늘만 필터링하니까 TMN이 걸러져서 None 됨
이건 제 코드의 논리적 오류라기보다는 기상청 데이터 구조를 제가 아직 완벽히 반영하지 못한 부분입니다.
라고 결론을 냈습니다.
정리
① get_latest_base_time() 함수 로직 오류 - 현재 로직으로는 거의 항상 잘못된 base_time을 리턴함 - 예: 오전 9시 실행 → 0800을 써야 하는데 0200이나 2300을 리턴 - 결과적으로 오래된 예보를 가져오게 됨
② 초단기실황 base_time 계산 오류 - 지금 -10분만 빼면 최신 관측값을 못 가져올 때가 많음 - 실제로는 최소 40~50분 전 정시를 써야 안정적
③ 최고/최저 기온 필터링 문제 (★★★★ 가장 중요한 문제 ★★★★)
- 코드: if item['fcstDate'] == 오늘 날짜:
- TMX (오늘 최고기온): 대부분 오늘 fcstDate로 잘 나옴 → 정상 조회
- TMN (오늘 최저기온): 기상청 특성상 다음 날 새벽(내일) 최저기온을 의미하는 경우가 많음 → TMN의 fcstDate가 내일 날짜로 되어 있음 → 오늘 날짜로만 필터링하면 TMN이 걸러져서 항상 None 또는 N/A 됨
- 결과: 최고기온은 나오는데 최저기온은 항상 안 나오는 현상 발생
④ 강수확률/강수량도 첫 번째 값만 저장 - 하루 중 여러 시간대 값이 나오는데, 첫 번째 값만 저장 → 의미 없는 값일 수 있음 - 보통은 하루 중 최고 강수확률(max POP)을 보여주는 게 좋음
⑤ 새벽 00:00~02:10 사이에는 전날 2300 예보를 써야 하는데 base_date 처리 안 됨 버그 발생 이유: 기상청 데이터 특성 때문에 호출을 하는 시점에는 N/A로 나오는 것이다다다!!!
고심 끝에 이렇게 결론을 내렸다.
디버깅 쉽지 않다....
'SK Shieldus Rookies 29' 카테고리의 다른 글
| [SK shieldus Rookies 29기] 11일차 (0) | 2025.12.04 |
|---|---|
| [SK shieldus Rookies 29기] 10일차 (0) | 2025.12.04 |
| [SK shieldus Rookies 29기] 8일차 (0) | 2025.12.04 |
| [SK shieldus Rookies 29기] 7일차 (0) | 2025.12.04 |
| [SK shieldus Rookies 29기] 6일차 (0) | 2025.12.04 |