SiteWeatherInfoService.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IdentityModel.Tokens.Jwt;
  4. using System.Linq;
  5. using System.Security.Claims;
  6. using System.Text;
  7. using FMSAdmin.Data;
  8. using FMSAdmin.Helpers;
  9. using FMSAdmin.Entities;
  10. using Microsoft.EntityFrameworkCore;
  11. using Microsoft.Extensions.Logging;
  12. using Microsoft.Extensions.Options;
  13. using Microsoft.IdentityModel.Tokens;
  14. using FMSAdmin.Models;
  15. using System.ComponentModel.DataAnnotations;
  16. using System.Threading.Tasks;
  17. using System.Net.Http;
  18. using System.Xml.Linq;
  19. using System.Net;
  20. using System.Data;
  21. using System.IO;
  22. namespace FMSAdmin.Services {
  23. public class SiteWeatherInfoService {
  24. private readonly ILogger<SiteWeatherInfoService> _logger;
  25. private readonly AppSettings _appSettings;
  26. private readonly FMSContext _context;
  27. public SiteWeatherInfoService(
  28. ILogger<SiteWeatherInfoService> logger,
  29. IOptions<AppSettings> appSettings,
  30. FMSContext context,
  31. StorageHelper storage
  32. ) {
  33. _logger = logger;
  34. _appSettings = appSettings.Value;
  35. _context = context;
  36. }
  37. public DataTable WeatherStat(int dayAgo = 0) {
  38. var query = $@"
  39. SELECT Sido AS [시도], Sigun AS [구군], [00],[01],[02],[03],[04],[05],[06],[07],[08],[09],[10],[11],[12],[13],[14],[15],[16],[17],[18],[19],[21],[22],[23] FROM
  40. (
  41. SELECT weather.Sido, weather.Sigun, weather.CurrentTemp, DATEPART(HOUR, BaseTime) as Hour FROM SiteWeatherInfo weather
  42. LEFT JOIN CmSite site ON site.apiSido = weather.Sido AND site.ApiSigun = weather.Sigun
  43. WHERE CAST(weather.BaseTime AS DATE) = CAST(DATEADD(DAY, {dayAgo}, GETDATE()) AS DATE)
  44. ) T
  45. PIVOT (
  46. MAX(CurrentTemp) FOR Hour in ([00],[01],[02],[03],[04],[05],[06],[07],[08],[09],[10],[11],[12],[13],[14],[15],[16],[17],[18],[19],[21],[22],[23])
  47. ) AS PV
  48. ";
  49. var conn = _context.Database.GetDbConnection();
  50. conn.Open();
  51. var command = conn.CreateCommand();
  52. command.CommandText = query;
  53. var reader = command.ExecuteReader();
  54. DataTable dt = new DataTable();
  55. dt.Load(reader);
  56. return dt;
  57. }
  58. public string ReplaceSido(string sido) {
  59. string result = sido.ToString();
  60. result = result.Replace("특별시", "시");
  61. result = result.Replace("광역시", "시");
  62. result = result.Replace("특별자치도", "도");
  63. result = result.Replace("특별자치시", "시");
  64. return result;
  65. }
  66. public SiteWeatherInfo GetInfo(int siteId, string sido = null, string sigun = null, bool weather = true, bool dust = true) {
  67. SiteWeatherInfo weatherInfo = null;
  68. var site = _context.CmSite.Where(x => x.SiteId == siteId).FirstOrDefault();
  69. if (site != null) {
  70. if (string.IsNullOrEmpty(sido)) {
  71. sido = site.ApiSido;
  72. }
  73. if (string.IsNullOrEmpty(sigun)) {
  74. sigun = site.ApiSigun;
  75. }
  76. }
  77. if (string.IsNullOrEmpty(sido)) {
  78. sido = "서울시";
  79. }
  80. if (string.IsNullOrEmpty(sigun)) {
  81. sigun = "용산구";
  82. }
  83. sido = this.ReplaceSido(sido);
  84. var basetime = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 0, 0);
  85. var query = from x in _context.SiteWeatherInfo
  86. where x.Sido == sido && x.Sigun == sigun && x.BaseTime == basetime
  87. orderby x.BaseTime descending
  88. select x;
  89. if (query.Count() > 0) {
  90. weatherInfo = query.FirstOrDefault();
  91. if (weatherInfo.CurrentTemp != null && weatherInfo.Pm10 != null) {
  92. return weatherInfo;
  93. }
  94. }
  95. var isNew = false;
  96. if (weatherInfo == null) {
  97. weatherInfo = new SiteWeatherInfo {
  98. Sido = sido,
  99. Sigun = sigun,
  100. BaseTime = basetime,
  101. };
  102. isNew = true;
  103. }
  104. try {
  105. if (weatherInfo.CurrentTemp == null && weather) {
  106. var shortTermNow = _apiShortTermNow(sido, sigun);
  107. weatherInfo.ShortTermNowTime = shortTermNow.BaseTime;
  108. if (shortTermNow.T1H != null) {
  109. weatherInfo.CurrentTemp = Util.ToDouble(shortTermNow.T1H);
  110. }
  111. if (shortTermNow.RN1 != null) {
  112. weatherInfo.Sky = Util.ToInt(shortTermNow.RN1);
  113. }
  114. if (shortTermNow.REH != null) {
  115. weatherInfo.Humidity = Util.ToDouble(shortTermNow.REH);
  116. }
  117. if (shortTermNow.PTY != null) {
  118. weatherInfo.RainType = Util.ToInt(shortTermNow.PTY);
  119. }
  120. if (shortTermNow.RN1 != null) {
  121. weatherInfo.Rain = Util.ToDouble(shortTermNow.RN1);
  122. }
  123. if (shortTermNow.LGT != null) {
  124. weatherInfo.Thunder = Util.ToInt(shortTermNow.LGT);
  125. }
  126. if (shortTermNow.VEC != null) {
  127. weatherInfo.WindAngle = Util.ToInt(shortTermNow.VEC);
  128. }
  129. if (shortTermNow.WSD != null) {
  130. weatherInfo.WindSpeed = Util.ToDouble(shortTermNow.WSD);
  131. }
  132. }
  133. if (weatherInfo.HighTemp == null && weather) {
  134. var forecast = _apiForecast(sido, sigun);
  135. weatherInfo.ForecastTime = forecast.BaseTime;
  136. if (forecast.TMX != null) {
  137. weatherInfo.HighTemp = Util.ToDouble(forecast.TMX);
  138. }
  139. if (forecast.TMN != null) {
  140. weatherInfo.LowTemp = Util.ToDouble(forecast.TMN);
  141. }
  142. }
  143. } catch (ApiException ex) {
  144. _logger.LogWarning(ex.Message);
  145. } catch (Exception ex) {
  146. _logger.LogError(ex, "");
  147. }
  148. try {
  149. if (weatherInfo.Pm10 == null && dust) {
  150. var dustHourly = _apiDustHourly(sido, sigun);
  151. if (dustHourly != null) {
  152. if (dustHourly.DataTime != null) {
  153. weatherInfo.DustTime = _parseDate(dustHourly.DataTime);
  154. }
  155. if (dustHourly.Pm10Value != null) {
  156. weatherInfo.Pm10 = Util.ToDouble(dustHourly.Pm10Value);
  157. }
  158. if (dustHourly.Pm25Value != null) {
  159. weatherInfo.Pm25 = Util.ToDouble(dustHourly.Pm25Value);
  160. }
  161. }
  162. }
  163. if (weatherInfo.CurrentTemp < -200 || weatherInfo.CurrentTemp > 200) {
  164. weatherInfo.CurrentTemp = null;
  165. //throw new Exception("잘못된 데이터");
  166. }
  167. weatherInfo.IsNew = true;
  168. if (isNew) {
  169. _context.SiteWeatherInfo.Add(weatherInfo);
  170. }
  171. _context.SaveChanges();
  172. } catch (ApiException ex) {
  173. _logger.LogWarning(ex.Message);
  174. } catch (Exception ex) {
  175. _logger.LogError(ex, "");
  176. }
  177. if (weatherInfo.CurrentTemp == null && weatherInfo.Pm10 == null) {
  178. var query2 = from x in _context.SiteWeatherInfo
  179. where x.Sido == sido && x.Sigun == sigun && x.BaseTime <= basetime
  180. orderby x.BaseTime descending
  181. select x;
  182. if (query2.Count() > 0) {
  183. weatherInfo = query.FirstOrDefault();
  184. return weatherInfo;
  185. }
  186. }
  187. return weatherInfo;
  188. }
  189. private DateTime? _parseDate(string datestr) {
  190. var items = datestr.Split(" ");
  191. DateTime? date = null;
  192. try {
  193. if (items.Length >= 2 && items[1].StartsWith("24")) {
  194. date = DateTime.Parse(items[0]);
  195. date.Value.AddDays(1);
  196. } else {
  197. date = DateTime.Parse(datestr);
  198. }
  199. } catch (Exception ex) {
  200. _logger.LogError(ex, "");
  201. }
  202. return date;
  203. }
  204. private DustApiItem _apiDustHourly(string sido, string sigun) {
  205. if (sido == null) sido = "";
  206. if (sido == "경상북도") {
  207. sido = "경북";
  208. } else if (sido == "경상남도") {
  209. sido = "경남";
  210. } else if (sido == "전라북도") {
  211. sido = "전북";
  212. } else if (sido == "전라남도") {
  213. sido = "전남";
  214. } else if (sido == "충청북도") {
  215. sido = "충북";
  216. } else if (sido == "충청남도") {
  217. sido = "충남";
  218. }
  219. if (sido[sido.Length - 1] == '시') {
  220. sido = sido.Substring(0, sido.Length - 1);
  221. }
  222. if (sido[sido.Length - 1] == '도') {
  223. sido = sido.Substring(0, sido.Length - 1);
  224. }
  225. var sidoName = WebUtility.UrlDecode(sido);
  226. //var forecast = $"http://openapi.airkorea.or.kr/openapi/services/rest/ArpltnInforInqireSvc/getCtprvnMesureSidoLIst?serviceKey={_appSettings.DustApiKey}&numOfRows=100&pageNo=1&sidoName={sidoName}&searchCondition=HOUR";
  227. var forecast = $"http://apis.data.go.kr/B552584/ArpltnStatsSvc/getCtprvnMesureSidoLIst?serviceKey={_appSettings.DustApiKey}&numOfRows=100&pageNo=1&sidoName={sidoName}&searchCondition=HOUR";
  228. string content = null;
  229. content = pullUrl(forecast);
  230. // _logger.LogInformation(content);
  231. XElement xml = XElement.Parse(content);
  232. var resultCode = xml.Descendants("resultCode").FirstOrDefault()?.Value;
  233. var resultMsg = xml.Descendants("resultMsg").FirstOrDefault()?.Value;
  234. if (resultCode != "00") {
  235. throw new ApiException($"apiDustHourly {resultCode}:{resultMsg}");
  236. }
  237. var items = from item in xml.Descendants("item")
  238. select new DustApiItem {
  239. DataTime = item.Element("dataTime")?.Value,
  240. CityName = item.Element("cityName")?.Value,
  241. So2Value = item.Element("so2Value")?.Value,
  242. CoValue = item.Element("coValue")?.Value,
  243. No2Value = item.Element("no2Value")?.Value,
  244. Pm10Value = item.Element("pm10Value")?.Value,
  245. Pm25Value = item.Element("pm25Value")?.Value,
  246. };
  247. var result = items.Where(x => x.CityName == sigun).FirstOrDefault();
  248. if (result == null) {
  249. result = items.FirstOrDefault();
  250. }
  251. return result;
  252. }
  253. private WeatherForecast _apiForecast(string sido, string sigun, string gu = null) {
  254. var pos = _GetRegionPosition(sido, sigun, gu);
  255. var nx = pos.Nx;
  256. var ny = pos.Ny;
  257. var forenow = DateTime.Now.AddHours(-4);
  258. var forehour = forenow.Hour;
  259. if (forehour >= 2 && forehour < 5) {
  260. forehour = 2;
  261. }
  262. if (forehour < 2) {
  263. forenow = forenow.AddDays(-1);
  264. forehour = 23;
  265. } else {
  266. // 2시부터 23시까지 3시간 간격으로 발표
  267. forehour = (int)Math.Floor((float)(forehour - 2) / 3) * 3 + 2;
  268. }
  269. forenow = new DateTime(forenow.Year, forenow.Month, forenow.Day, forehour, 0, 0);
  270. var targetnow = forenow.AddHours(4);
  271. var foredate = forenow.ToString("yyyyMMdd");
  272. var foretime = forenow.ToString("HH00");
  273. var targettime = targetnow.ToString("HH00");
  274. //_logger.LogInformation("foredate:" + foredate);
  275. //_logger.LogInformation("foretime:" + foretime);
  276. //_logger.LogInformation("targettime:" + targettime);
  277. var forecast = $"http://apis.data.go.kr/1360000/VilageFcstInfoService/getVilageFcst?serviceKey={_appSettings.WeatherApiKey}&pageNo=1&numOfRows=100&dataType=XML&base_date={foredate}&base_time={foretime}&nx={nx}&ny={ny}";
  278. //_logger.LogInformation(forecast);
  279. string content = null;
  280. content = pullUrl(forecast);
  281. //_logger.LogInformation(content);
  282. XElement xml = XElement.Parse(content);
  283. var resultCode = xml.Descendants("resultCode").FirstOrDefault()?.Value;
  284. var resultMsg = xml.Descendants("resultMsg").FirstOrDefault()?.Value;
  285. if (resultCode != "00") {
  286. throw new ApiException($"apiForecast {resultCode}:{resultMsg}");
  287. }
  288. var items = from item in xml.Descendants("item")
  289. select new WeatherApiItem {
  290. BaseDate = item.Element("baseDate")?.Value,
  291. BaseTime = item.Element("baseTime")?.Value,
  292. Category = item.Element("category")?.Value,
  293. FcstValue = item.Element("fcstValue")?.Value,
  294. FcstTime = item.Element("fcstTime")?.Value,
  295. };
  296. var list = items.ToList();
  297. //_logger.LogInformation(list.Dump());
  298. var result = new WeatherForecast();
  299. result.BaseTime = targetnow;
  300. result.ForecastTime = forenow;
  301. result.POP = list.Where(x => x.Category == "POP").Select(x => x.FcstValue).FirstOrDefault();
  302. result.PTY = list.Where(x => x.Category == "PTY").Select(x => x.FcstValue).FirstOrDefault();
  303. result.R06 = list.Where(x => x.Category == "R06").Select(x => x.FcstValue).FirstOrDefault();
  304. result.REH = list.Where(x => x.Category == "REH").Select(x => x.FcstValue).FirstOrDefault();
  305. result.S06 = list.Where(x => x.Category == "S06").Select(x => x.FcstValue).FirstOrDefault();
  306. result.SKY = list.Where(x => x.Category == "SKY").Select(x => x.FcstValue).FirstOrDefault();
  307. result.T3H = list.Where(x => x.Category == "T3H").Select(x => x.FcstValue).FirstOrDefault();
  308. result.TMN = list.Where(x => x.Category == "TMN").Select(x => x.FcstValue).FirstOrDefault();
  309. result.TMX = list.Where(x => x.Category == "TMX").Select(x => x.FcstValue).FirstOrDefault();
  310. result.UUU = list.Where(x => x.Category == "UUU").Select(x => x.FcstValue).FirstOrDefault();
  311. result.VVV = list.Where(x => x.Category == "VVV").Select(x => x.FcstValue).FirstOrDefault();
  312. result.VEC = list.Where(x => x.Category == "VEC").Select(x => x.FcstValue).FirstOrDefault();
  313. result.WSD = list.Where(x => x.Category == "WSD").Select(x => x.FcstValue).FirstOrDefault();
  314. return result;
  315. }
  316. private WeatherShortTermNow _apiShortTermNow(string sido, string sigun, string gu = null) {
  317. var pos = _GetRegionPosition(sido, sigun, gu);
  318. var nx = pos.Nx;
  319. var ny = pos.Ny;
  320. var nowminute = DateTime.Now.Minute;
  321. var shortnow = DateTime.Now;
  322. if (nowminute <= 40) {
  323. shortnow = shortnow.AddHours(-1);
  324. }
  325. shortnow = new DateTime(shortnow.Year, shortnow.Month, shortnow.Day, shortnow.Hour, 0, 0);
  326. var nowdate = shortnow.ToString("yyyyMMdd");
  327. var nowtime = shortnow.ToString("HH00");
  328. var shortNow = $"http://apis.data.go.kr/1360000/VilageFcstInfoService/getUltraSrtNcst?serviceKey={_appSettings.WeatherApiKey}&pageNo=1&numOfRows=100&dataType=XML&base_date={nowdate}&base_time={nowtime}&nx={nx}&ny={ny}";
  329. //_logger.LogInformation(shortNow);
  330. string content = null;
  331. content = pullUrl(shortNow);
  332. //_logger.LogInformation(content);
  333. XElement xml = XElement.Parse(content);
  334. var resultCode = xml.Descendants("resultCode").FirstOrDefault()?.Value;
  335. var resultMsg = xml.Descendants("resultMsg").FirstOrDefault()?.Value;
  336. if (resultCode != "00") {
  337. throw new ApiException($"apiShortTermNow {resultCode}:{resultMsg}");
  338. }
  339. var items = from item in xml.Descendants("item")
  340. select new WeatherApiItem {
  341. BaseDate = item.Element("baseDate")?.Value,
  342. BaseTime = item.Element("baseTime")?.Value,
  343. Category = item.Element("category")?.Value,
  344. ObsrValue = item.Element("obsrValue")?.Value,
  345. };
  346. var list = items.ToList();
  347. //_logger.LogInformation(list.Dump());
  348. var result = new WeatherShortTermNow();
  349. result.BaseTime = shortnow;
  350. result.T1H = list.Where(x => x.Category == "T1H").Select(x => x.ObsrValue).FirstOrDefault();
  351. result.RN1 = list.Where(x => x.Category == "RN1").Select(x => x.ObsrValue).FirstOrDefault();
  352. result.SKY = list.Where(x => x.Category == "SKY").Select(x => x.ObsrValue).FirstOrDefault();
  353. result.UUU = list.Where(x => x.Category == "UUU").Select(x => x.ObsrValue).FirstOrDefault();
  354. result.VVV = list.Where(x => x.Category == "VVV").Select(x => x.ObsrValue).FirstOrDefault();
  355. result.REH = list.Where(x => x.Category == "REH").Select(x => x.ObsrValue).FirstOrDefault();
  356. result.PTY = list.Where(x => x.Category == "PTY").Select(x => x.ObsrValue).FirstOrDefault();
  357. result.LGT = list.Where(x => x.Category == "LGT").Select(x => x.ObsrValue).FirstOrDefault();
  358. result.VEC = list.Where(x => x.Category == "VEC").Select(x => x.ObsrValue).FirstOrDefault();
  359. result.WSD = list.Where(x => x.Category == "WSD").Select(x => x.ObsrValue).FirstOrDefault();
  360. return result;
  361. }
  362. private WeatherRegionNxNy _GetRegionPosition(string sido, string sigun, string gu = null) {
  363. sido = sido.Trim();
  364. if (sido == "서울시") {
  365. sido = "서울특별시";
  366. } else if (sido == "제주도") {
  367. sido = "제주특별자치도";
  368. } else if (sido == "인천시") {
  369. sido = "인천광역시";
  370. } else if (sido == "광주시") {
  371. sido = "광주광역시";
  372. } else if (sido == "대구시") {
  373. sido = "대구광역시";
  374. } else if (sido == "대전시") {
  375. sido = "대전광역시";
  376. } else if (sido == "부산시") {
  377. sido = "부산광역시";
  378. } else if (sido == "울산시") {
  379. sido = "울산광역시";
  380. } else if (sido == "세종시") {
  381. sido = "세종특별자치시";
  382. }
  383. var query = _context.WeatherRegionNxNy.Where(x => x.Sido == sido && x.Sigun == sigun);
  384. WeatherRegionNxNy find = null;
  385. if (!string.IsNullOrEmpty(gu)) {
  386. if (query.Where(x => x.Gu == gu).Count() > 0) {
  387. find = query.Where(x => x.Gu == gu).FirstOrDefault();
  388. } else {
  389. find = query.FirstOrDefault();
  390. }
  391. } else {
  392. find = query.FirstOrDefault();
  393. }
  394. return find;
  395. }
  396. private string pullUrl(string url) {
  397. // Create a request for the URL.
  398. HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
  399. request.Timeout = 5000;
  400. // If required by the server, set the credentials.
  401. request.Credentials = CredentialCache.DefaultCredentials;
  402. // Get the response.
  403. HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  404. // Display the status.
  405. // Get the stream containing content returned by the server.
  406. Stream dataStream = response.GetResponseStream();
  407. // Open the stream using a StreamReader for easy access.
  408. StreamReader reader = new StreamReader(dataStream);
  409. // Read the content.
  410. string responseFromServer = reader.ReadToEnd();
  411. // Display the content.
  412. reader.Close();
  413. dataStream.Close();
  414. response.Close();
  415. return responseFromServer;
  416. }
  417. }
  418. public class DustApiItem {
  419. public string DataTime { get; set; }
  420. public string CityName { get; set; }
  421. public string So2Value { get; set; }
  422. public string CoValue { get; set; }
  423. public string O3Value { get; set; }
  424. public string No2Value { get; set; }
  425. public string Pm10Value { get; set; }
  426. public string Pm25Value { get; set; }
  427. }
  428. public class WeatherApiItem {
  429. public string BaseDate { get; set; }
  430. public string BaseTime { get; set; }
  431. public string Category { get; set; }
  432. public string ObsrValue { get; set; }
  433. public string FcstValue { get; set; }
  434. public string FcstTime { get; set; }
  435. }
  436. public class WeatherForecast {
  437. public WeatherForecast() {
  438. }
  439. public DateTime BaseTime;
  440. public DateTime ForecastTime;
  441. [Display(Name = "강수확률")]
  442. public string POP { get; set; }
  443. [Display(Name = "강수형태")]
  444. public string PTY { get; set; }
  445. [Display(Name = "6시간 강수량")]
  446. public string R06 { get; set; }
  447. [Display(Name = "습도")]
  448. public string REH { get; set; }
  449. [Display(Name = "6시간 신적설")]
  450. public string S06 { get; set; }
  451. [Display(Name = "하늘상태")]
  452. public string SKY { get; set; }
  453. [Display(Name = "3시간 기온")]
  454. public string T3H { get; set; }
  455. [Display(Name = "아침 최저기온")]
  456. public string TMN { get; set; }
  457. [Display(Name = "낮 최고기온")]
  458. public string TMX { get; set; }
  459. [Display(Name = "풍속(동서)")]
  460. public string UUU { get; set; }
  461. [Display(Name = "풍속(남북)")]
  462. public string VVV { get; set; }
  463. [Display(Name = "풍향")]
  464. public string VEC { get; set; }
  465. [Display(Name = "풍속")]
  466. public string WSD { get; set; }
  467. }
  468. public class WeatherShortTermNow {
  469. public WeatherShortTermNow() {
  470. }
  471. public DateTime BaseTime;
  472. [Display(Name = "기온")]
  473. public string T1H { get; set; }
  474. [Display(Name = "1시간 강수량")]
  475. public string RN1 { get; set; }
  476. [Display(Name = "하늘상태")]
  477. public string SKY { get; set; }
  478. [Display(Name = "동서바람성분")]
  479. public string UUU { get; set; }
  480. [Display(Name = "남북바람성분")]
  481. public string VVV { get; set; }
  482. [Display(Name = "습도")]
  483. public string REH { get; set; }
  484. [Display(Name = "강수형태")]
  485. public string PTY { get; set; }
  486. [Display(Name = "낙뢰")]
  487. public string LGT { get; set; }
  488. [Display(Name = "풍향")]
  489. public string VEC { get; set; }
  490. [Display(Name = "풍속")]
  491. public string WSD { get; set; }
  492. }
  493. }