날씨 데이터에 이어서 미세먼지 데이터를 출력하는 작업을 시작했다
https://www.data.go.kr/iim/api/selectAPIAcountView.do
해당 api 중 "시도별 실시간 측정정보 조회 상세기능명세"를 사용했다.
기본적인 데이터 출력 상태
{"so2Grade":"1","coFlag":null,"khaiValue":"98","so2Value":"0.003","coValue":"0.4","pm25Flag":null,"pm10Flag":null, "o3Grade":"2","pm10Value":"30","khaiGrade":"2","pm25Value":"12","sidoName":"경기", "no2Flag":null,"no2Grade":"1","o3Flag":null,"pm25Grade":"2","so2Flag":null,"dataTime":"2024-05-12 19:00", "coGrade":"1","no2Value":"0.009","stationName":"소사본동","pm10Grade":"2","o3Value":"0.037"}
- 깔끔하게 출력하기 위해 공백을 넣었지만 실제 데이터에서는 공백이 들어가지 않는다.
- 이중 필요한 데이터는 다음과 같다
sidoName: 시도 명
dataTime: 측정 시간
stationName: 측정소 명
pm25Grade: 초미세먼지 상태
pm25Flag: 초미세먼지 출력 에러 요인
pm25Value: 초미세먼지 농도
pm10Grade: 미세먼지 상태
pm10Flag: 미세먼지 출력 에러 요인
pm10Value: 미세먼지 농도
sidoName | 시도 명 | 10 | 1 | 서울 | 시도 이름 (전국, 서울, 부산, 대구, 인천, 광주, 대전, 울산, 경기, 강원, 충북, 충남, 전북, 전남, 경북, 경남, 제주, 세종) |
pm10Value | 미세먼지(PM10) 농도 | 10 | 1 | 68 | 미세먼지(PM10) 농도 (단위 : ㎍/㎥) |
pm25Value | 미세먼지(PM2.5) 농도 | 10 | 1 | 39 | 미세먼지(PM2.5) 농도 (단위 : ㎍/㎥) |
pm10Flag | 미세먼지(PM10) 플래그 | 10 | 점검및교정 | 측정자료 상태정보(점검및교정,장비점검,자료이상,통신장애) | |
pm25Flag | 미세먼지(PM2.5) 플래그 | 10 | 1 | 점검및교정 | 측정자료 상태정보(점검및교정,장비점검,자료이상,통신장애) |
등급 | 좋음 | 보통 | 나쁨 | 매우나쁨 |
Grade 값 | 1 | 2 | 3 | 4 |
- 해당 표들은 공식문서에서 발췌했다
- 측정소는 엄청나게 많아서 한번에 수십 곳씩 데이터가 출력된다
예시 sidoName이 경기인 경우의 stationName
신풍동, 인계동, 광교동, 영통동, 천천동, 경수대로(동수원), 고색동, 호매실, 대왕판교로(백현동), 단대동, 정자동, 수내동, 성남대로(모란역), 복정동, 운중동, 경안동, 오포1동, 곤지암, 김량장동, 수지, 기흥, 중부대로(구갈동), 모현읍, 이동읍, 백암면, 설성면, 창전동, 장호원읍, 부발읍, 관인면, 선단동, 일동면, 고읍, 보산동, 봉산동, 공도읍, 죽산면, 중앙동(경기), 대신면, 가남읍, 연천, 전곡, 가평, 설악면, 용문면, 양평읍, 연천(DMZ), 사우동, 고촌읍, 월곶면, 한강신도시, 한강로, 당동, 산본동, 오산동, 금암로(신장동), 신장동, 미사, 남양읍, 향남읍, 동탄, 우정읍, 청계동, 새솔동, 봉담읍, 서신면, 백석읍, 상대원동, 의정부동, 의정부1동, 송산3동, 안양8동, 부림동, 호계3동, 안양2동, 철산동, 소하동, 고잔동, 원시동, 본오동, 원곡동, 부곡동1, 대부동, 호수동, 중앙대로(고잔동), 별양동, 과천동, 교문동, 동구동, 부곡3동, 고천동, 정왕동, 시화산단, 대야동, 목감동, 장현동, 서해안로, 배곧동, 금곡동, 오남읍
이 데이터를 기존에 날씨 데이터와 연계하기 위해 area와 stationName이 가까운 곳끼리 매칭시켰다
chat gpt를 사용했기 때문에 오류가 있을 수 있지만, 어쩔 수 없다...
area | stationName |
수원시장안구 | 경수대로(동수원) |
수원시권선구 | 신장동 |
수원시팔달구 | 영통동 |
수원시영통구 | 영통동 |
성남시수정구 | 성남대로(모란역) |
성남시중원구 | 성남대로(모란역) |
성남시분당구 | 새솔동 |
의정부시 | 의정부동 |
안양시만안구 | 안양8동 |
안양시동안구 | 안양2동 |
부천시원미구 | 오산동 |
부천시소사구 | 부곡동1 |
부천시오정구 | 대야동 |
광명시 | 경안동 |
평택시 | 고덕면 |
동두천시 | 별내면 |
안산시상록구 | 고잔동 |
안산시단원구 | 고잔동 |
고양시덕양구 | 산본동 |
고양시일산동구 | 산본동 |
고양시일산서구 | 백석읍 |
과천시 | 과천동 |
구리시 | 별내면 |
남양주시 | 다산동 |
오산시 | 오산동 |
시흥시 | 금오동 |
군포시 | 산본동 |
의왕시 | 오전동 |
하남시 | 고잔동 |
용인시처인구 | 동탄 |
용인시기흥구 | 기흥 |
용인시수지구 | 수지 |
파주시 | 부곡동1 |
이천시 | 대월면 |
안성시 | 고잔동 |
김포시 | 고촌읍 |
화성시 | 동탄 |
광주시 | 경안동 |
양주시 | 은현면 |
포천시 | 중앙동(경기) |
여주시 | 가남읍 |
연천군 | 연천 |
가평군 | 가평 |
양평군 | 원덕읍 |
이 데이터에 Country까지 더해서 DB에 저장했다.
create table place(
id serial primary key,
country varchar(50) not null,
area varchar(50) unique,
stationName varchar(50) not null
);
INSERT INTO place (country, area, stationName) VALUES
('경기', '수원시장안구', '경수대로(동수원)'),
('경기', '수원시권선구', '신장동'),
('경기', '수원시팔달구', '영통동'),
('경기', '수원시영통구', '영통동'),
('경기', '성남시수정구', '성남대로(모란역)'),
('경기', '성남시중원구', '성남대로(모란역)'),
('경기', '성남시분당구', '새솔동'),
('경기', '의정부시', '의정부동'),
('경기', '안양시만안구', '안양8동'),
('경기', '안양시동안구', '안양2동'),
('경기', '부천시원미구', '오산동'),
('경기', '부천시소사구', '부곡동1'),
('경기', '부천시오정구', '대야동'),
('경기', '광명시', '경안동'),
('경기', '평택시', '고덕면'),
('경기', '동두천시', '별내면'),
('경기', '안산시상록구', '고잔동'),
('경기', '안산시단원구', '고잔동'),
('경기', '고양시덕양구', '산본동'),
('경기', '고양시일산동구', '산본동'),
('경기', '고양시일산서구', '백석읍'),
('경기', '과천시', '과천동'),
('경기', '구리시', '별내면'),
('경기', '남양주시', '다산동'),
('경기', '오산시', '오산동'),
('경기', '시흥시', '금오동'),
('경기', '군포시', '산본동'),
('경기', '의왕시', '오전동'),
('경기', '하남시', '고잔동'),
('경기', '용인시처인구', '동탄'),
('경기', '용인시기흥구', '기흥'),
('경기', '용인시수지구', '수지'),
('경기', '파주시', '부곡동1'),
('경기', '이천시', '대월면'),
('경기', '안성시', '고잔동'),
('경기', '김포시', '고촌읍'),
('경기', '화성시', '동탄'),
('경기', '광주시', '경안동'),
('경기', '양주시', '은현면'),
('경기', '포천시', '중앙동(경기)'),
('경기', '여주시', '가남읍'),
('경기', '연천군', '연천'),
('경기', '가평군', '가평'),
('경기', '양평군', '원덕읍');
이와 동시에 country 데이터를 맞추기 위해 weatherAreaVO 테이블을 변경했다
CREATE TABLE weatherAreavo (
id SERIAL PRIMARY KEY,
country VARCHAR(50) NOT NULL,
area VARCHAR(50) NOT NULL,
nx INTEGER NOT NULL,
ny INTEGER NOT NULL
);
INSERT INTO weatherAreavo (country, area, nx, ny)
VALUES
('서울', '종로구', 60, 127),
('서울', '중구', 60, 127),
('서울', '용산구', 60, 126),
('서울', '성동구', 61, 127),
('서울', '광진구', 62, 126),
('서울', '동대문구', 61, 127),
('서울', '중랑구', 62, 128),
('서울', '성북구', 61, 127),
('서울', '강북구', 61, 128),
('서울', '도봉구', 61, 129),
('서울', '노원구', 61, 129),
('서울', '은평구', 59, 127),
('서울', '서대문구', 59, 127),
('서울', '마포구', 59, 127),
('서울', '양천구', 58, 126),
('서울', '강서구', 58, 126),
('서울', '구로구', 58, 125),
('서울', '금천구', 59, 124),
('서울', '영등포구', 58, 126),
('서울', '동작구', 59, 125),
('서울', '관악구', 59, 125),
('서울', '서초구', 61, 125),
('서울', '강남구', 61, 126),
('서울', '송파구', 62, 126),
('서울', '강동구', 62, 126),
('부산', '중구', 97, 74),
('부산', '서구', 97, 74),
('부산', '동구', 98, 75),
('부산', '영도구', 98, 74),
('부산', '부산진구', 97, 75),
('부산', '동래구', 98, 76),
('부산', '남구', 98, 75),
('부산', '북구', 96, 76),
('부산', '해운대구', 99, 75),
('부산', '사하구', 96, 74),
('부산', '금정구', 98, 77),
('부산', '강서구', 96, 76),
('부산', '연제구', 98, 76),
('부산', '수영구', 99, 75),
('부산', '사상구', 96, 75),
('부산', '기장군', 100, 77),
('대구', '중구', 89, 90),
('대구', '동구', 90, 91),
('대구', '서구', 88, 90),
('대구', '남구', 89, 90),
('대구', '북구', 89, 91),
('대구', '수성구', 89, 90),
('대구', '달서구', 88, 90),
('대구', '달성군', 86, 88),
('인천', '중구', 54, 125),
('인천', '동구', 54, 125),
('인천', '미추홀구', 54, 124),
('인천', '연수구', 55, 123),
('인천', '남동구', 56, 124),
('인천', '부평구', 55, 125),
('인천', '계양구', 56, 126),
('인천', '서구', 55, 126),
('인천', '강화군', 51, 130),
('인천', '옹진군', 54, 124),
('광주', '동구', 60, 74),
('광주', '서구', 59, 74),
('광주', '남구', 59, 73),
('광주', '북구', 59, 75),
('광주', '광산구', 57, 74),
('대전', '동구', 68, 100),
('대전', '중구', 68, 100),
('대전', '서구', 67, 100),
('대전', '유성구', 67, 101),
('대전', '대덕구', 68, 100),
('울산', '중구', 102, 84),
('울산', '남구', 102, 84),
('울산', '동구', 104, 83),
('울산', '북구', 103, 85),
('울산', '울주군', 101, 84),
('세종', '세종', 66, 103),
('경기', '수원시장안구', 60, 121),
('경기', '수원시권선구', 60, 120),
('경기', '수원시팔달구', 61, 121),
('경기', '수원시영통구', 61, 120),
('경기', '성남시수정구', 63, 124),
('경기', '성남시중원구', 63, 124),
('경기', '성남시분당구', 62, 123),
('경기', '의정부시', 61, 130),
('경기', '안양시만안구', 59, 123),
('경기', '안양시동안구', 59, 123),
('경기', '부천시원미구', 57, 125),
('경기', '부천시소사구', 57, 125),
('경기', '부천시오정구', 57, 126),
('경기', '광명시', 58, 125),
('경기', '평택시', 62, 114),
('경기', '동두천시', 61, 134),
('경기', '안산시상록구', 58, 121),
('경기', '안산시단원구', 57, 121),
('경기', '고양시덕양구', 57, 128),
('경기', '고양시일산동구', 56, 129),
('경기', '고양시일산서구', 56, 129),
('경기', '과천시', 60, 124),
('경기', '구리시', 62, 127),
('경기', '남양주시', 64, 128),
('경기', '오산시', 62, 118),
('경기', '시흥시', 57, 123),
('경기', '군포시', 59, 122),
('경기', '의왕시', 60, 122),
('경기', '하남시', 64, 126),
('경기', '용인시처인구', 64, 119),
('경기', '용인시기흥구', 62, 120),
('경기', '용인시수지구', 62, 121),
('경기', '파주시', 56, 131),
('경기', '이천시', 68, 121),
('경기', '안성시', 65, 115),
('경기', '김포시', 55, 128),
('경기', '화성시', 57, 119),
('경기', '광주시', 65, 123),
('경기', '양주시', 61, 131),
('경기', '포천시', 64, 134),
('경기', '여주시', 71, 121),
('경기', '연천군', 61, 138),
('경기', '가평군', 69, 133),
('경기', '양평군', 69, 125),
('충북', '청주시상당구', 69, 106),
('충북', '청주시서원구', 69, 107),
('충북', '청주시흥덕구', 67, 106),
('충북', '청주시청원구', 69, 107),
('충북', '충주시', 76, 114),
('충북', '제천시', 81, 118),
('충북', '보은군', 73, 103),
('충북', '옥천군', 71, 99),
('충북', '영동군', 74, 97),
('충북', '증평군', 71, 110),
('충북', '진천군', 68, 111),
('충북', '괴산군', 74, 111),
('충북', '음성군', 72, 113),
('충북', '단양군', 84, 115),
('충남', '천안시동남구', 63, 110),
('충남', '천안시서북구', 63, 112),
('충남', '공주시', 63, 102),
('충남', '보령시', 54, 100),
('충남', '아산시', 60, 110),
('충남', '서산시', 51, 110),
('충남', '논산시', 62, 97),
('충남', '계룡시', 65, 99),
('충남', '당진시', 54, 112),
('충남', '금산군', 69, 95),
('충남', '부여군', 59, 99),
('충남', '서천군', 55, 94),
('충남', '청양군', 57, 103),
('충남', '홍성군', 55, 106),
('충남', '예산군', 58, 107),
('충남', '태안군', 48, 109),
('전북', '전주시완산구', 63, 89),
('전북', '전주시덕진구', 63, 89),
('전북', '군산시', 56, 92),
('전북', '익산시', 60, 91),
('전북', '정읍시', 58, 83),
('전북', '남원시', 68, 80),
('전북', '김제시', 59, 88),
('전북', '완주군', 63, 89),
('전북', '진안군', 68, 88),
('전북', '무주군', 72, 93),
('전북', '장수군', 70, 85),
('전북', '임실군', 66, 84),
('전북', '순창군', 63, 79),
('전북', '고창군', 56, 80),
('전북', '부안군', 56, 87),
('전남', '목포시', 50, 67),
('전남', '여수시', 73, 66),
('전남', '순천시', 70, 70),
('전남', '나주시', 56, 71),
('전남', '광양시', 73, 70),
('전남', '담양군', 61, 78),
('전남', '곡성군', 66, 77),
('전남', '구례군', 63, 77),
('전남', '고흥군', 65, 66),
('전남', '보성군', 65, 63),
('전남', '화순군', 68, 74),
('전남', '장흥군', 70, 63),
('전남', '강진군', 73, 63),
('전남', '해남군', 66, 60),
('전남', '영암군', 69, 62),
('전남', '무안군', 68, 70),
('전남', '함평군', 62, 69),
('전남', '영광군', 57, 67),
('전남', '장성군', 62, 74),
('전남', '완도군', 76, 67),
('전남', '진도군', 70, 66),
('전남', '신안군', 54, 61),
('경북', '포항시남구', 98, 75),
('경북', '포항시북구', 98, 76),
('경북', '경주시', 102, 81),
('경북', '김천시', 93, 86),
('경북', '안동시', 88, 92),
('경북', '구미시', 93, 90),
('경북', '영주시', 84, 89),
('경북', '영천시', 98, 79),
('경북', '상주시', 93, 96),
('경북', '문경시', 89, 94),
('경북', '경산시', 95, 83),
('경북', '군위군', 82, 85),
('경북', '의성군', 88, 84),
('경북', '청송군', 99, 73),
('경북', '영양군', 91, 70),
('경북', '영덕군', 97, 73),
('경북', '청도군', 92, 87),
('경북', '고령군', 94, 85),
('경북', '성주군', 90, 92),
('경북', '칠곡군', 97, 87),
('경북', '예천군', 88, 91),
('경북', '봉화군', 97, 79),
('경북', '울진군', 102, 72),
('경북', '울릉군', 107, 85),
('경남', '창원시의창구', 91, 77),
('경남', '창원시성산구', 91, 77),
('경남', '창원시마산합포구', 94, 74),
('경남', '창원시마산회원구', 94, 75),
('경남', '창원시진해구', 94, 79),
('경남', '진주시', 85, 76),
('경남', '통영시', 82, 73),
('경남', '고성군', 92, 65),
('경남', '사천시', 81, 69),
('경남', '김해시', 94, 76),
('경남', '밀양시', 88, 74),
('경남', '거제시', 88, 68),
('경남', '의령군', 89, 70),
('경남', '함안군', 87, 80),
('경남', '창녕군', 86, 83),
('경남', '양산시', 96, 82),
('경남', '하동군', 85, 71),
('경남', '남해군', 81, 78),
('경남', '함양군', 82, 85),
('경남', '산청군', 85, 87),
('경남', '거창군', 84, 93),
('경남', '합천군', 86, 96),
('제주', '제주시', 52, 38),
('제주', '서귀포시', 48, 38),
('강원', '춘천시', 73, 134),
('강원', '원주시', 76, 122),
('강원', '강릉시', 92, 131),
('강원', '동해시', 97, 127),
('강원', '태백시', 95, 119),
('강원', '속초시', 87, 141),
('강원', '삼척시', 98, 125),
('강원', '홍천군', 75, 130),
('강원', '횡성군', 77, 125),
('강원', '영월군', 86, 119),
('강원', '평창군', 84, 123),
('강원', '정선군', 89, 123),
('강원', '철원군', 65, 139),
('강원', '화천군', 72, 139),
('강원', '양구군', 77, 139),
('강원', '인제군', 80, 138),
('강원', '고성군', 85, 145),
('강원', '양양군', 88, 138);
다만 Foreign Key에 대한 고민이 생겼다.
Foreign Key를 사용하는 것이 좋을까?
아니면 사용하지 않는 것이 좋을까?
위와 같은 DB 변경 상황에서 데이터 정합성을 위해서는 FK를 쓰는 것이 타당하나, 반대로 이렇게 수정해야 할 때는 FK가 없는 것이 훨씬 작업이 간단하고 빠를 것이다...
시간은 영특하게도 알아서 최신 시간의 데이터를 가져와 준다.
이제 얼추 데이터 수집이 되었으니 작업을 시작하자
전체 코드
DB
create table paticulateMattervo(
id SERIAL primary key,
sidoName varchar(50) not null,
dataTime varchar(50) not null,
stationName varchar(50) not null,
pm25Grade varchar(50) not null,
pm25Flag varchar(50),
pm25Value varchar(50),
pm10Grade varchar(50) not null,
pm10Flag varchar(50),
pm10Value varchar(50)
);
create table place(
id serial primary key,
country varchar(50) not null,
area varchar(50) unique,
stationName varchar(50) not null
);
INSERT INTO place (country, area, stationName) VALUES
('경기', '수원시장안구', '경수대로(동수원)'),
('경기', '수원시권선구', '신장동'),
('경기', '수원시팔달구', '영통동'),
('경기', '수원시영통구', '영통동'),
('경기', '성남시수정구', '성남대로(모란역)'),
('경기', '성남시중원구', '성남대로(모란역)'),
('경기', '성남시분당구', '새솔동'),
('경기', '의정부시', '의정부동'),
('경기', '안양시만안구', '안양8동'),
('경기', '안양시동안구', '안양2동'),
('경기', '부천시원미구', '오산동'),
('경기', '부천시소사구', '부곡동1'),
('경기', '부천시오정구', '대야동'),
('경기', '광명시', '경안동'),
('경기', '평택시', '고덕면'),
('경기', '동두천시', '별내면'),
('경기', '안산시상록구', '고잔동'),
('경기', '안산시단원구', '고잔동'),
('경기', '고양시덕양구', '산본동'),
('경기', '고양시일산동구', '산본동'),
('경기', '고양시일산서구', '백석읍'),
('경기', '과천시', '과천동'),
('경기', '구리시', '별내면'),
('경기', '남양주시', '다산동'),
('경기', '오산시', '오산동'),
('경기', '시흥시', '금오동'),
('경기', '군포시', '산본동'),
('경기', '의왕시', '오전동'),
('경기', '하남시', '고잔동'),
('경기', '용인시처인구', '동탄'),
('경기', '용인시기흥구', '기흥'),
('경기', '용인시수지구', '수지'),
('경기', '파주시', '부곡동1'),
('경기', '이천시', '대월면'),
('경기', '안성시', '고잔동'),
('경기', '김포시', '고촌읍'),
('경기', '화성시', '동탄'),
('경기', '광주시', '경안동'),
('경기', '양주시', '은현면'),
('경기', '포천시', '중앙동(경기)'),
('경기', '여주시', '가남읍'),
('경기', '연천군', '연천'),
('경기', '가평군', '가평'),
('경기', '양평군', '원덕읍');
VO
package org.weather;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@Entity
@Table(name = "paticulatemattervo")
public class Paticulatemattervo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ColumnDefault("nextval('paticulatemattervo_id_seq'")
@Column(name = "id", nullable = false)
private Integer id;
@Size(max = 50)
@NotNull
@Column(name = "sidoname", nullable = false, length = 50)
private String sidoname;
@Size(max = 50)
@NotNull
@Column(name = "datatime", nullable = false, length = 50)
private String datatime;
@Size(max = 50)
@NotNull
@Column(name = "stationname", nullable = false, length = 50)
private String stationname;
@Size(max = 50)
@Column(name = "pm25grade", nullable = false, length = 50)
private String pm25grade;
@Size(max = 50)
@Column(name = "pm25flag", length = 50)
private String pm25flag;
@Size(max = 50)
@Column(name = "pm25value", length = 50)
private String pm25value;
@Size(max = 50)
@Column(name = "pm10grade", nullable = false, length = 50)
private String pm10grade;
@Size(max = 50)
@Column(name = "pm10flag", length = 50)
private String pm10flag;
@Size(max = 50)
@Column(name = "pm10value", length = 50)
private String pm10value;
}
voDTO
package org.weather;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.Value;
import java.io.Serializable;
/**
* DTO for {@link Paticulatemattervo}
*/
@Data
public class PaticulatemattervoDto implements Serializable {
@NotNull
@Size(max = 50)
String sidoname;
@NotNull
@Size(max = 50)
String datatime;
@NotNull
@Size(max = 50)
String stationname;
@Size(max = 50)
String pm25grade;
@Size(max = 50)
String pm25flag;
@Size(max = 50)
String pm25value;
@Size(max = 50)
String pm10grade;
@Size(max = 50)
String pm10flag;
@Size(max = 50)
String pm10value;
}
DataDTO
package org.weather;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.Value;
import java.io.Serializable;
/**
* DTO for {@link Paticulatemattervo}
*/
@Data
public class PaticulatemattervoDto implements Serializable {
@NotNull
@Size(max = 50)
String sidoname;
@NotNull
@Size(max = 50)
String datatime;
@NotNull
@Size(max = 50)
String stationname;
@Size(max = 50)
String pm25grade;
@Size(max = 50)
String pm25flag;
@Size(max = 50)
String pm25value;
@Size(max = 50)
String pm10grade;
@Size(max = 50)
String pm10flag;
@Size(max = 50)
String pm10value;
public void setPm25grade(String pm25grade) {
this.pm25grade = switch (pm25grade){
case "1" -> "좋음";
case "2" -> "보통";
case "3" -> "나쁨";
case "4" -> "매우나쁨";
default -> "점검 중";
};
}
public void setPm25value(String pm25value) {
this.pm25value = pm25value + "㎍/㎥";
}
public void setPm10grade(String pm10grade) {
this.pm10grade = switch (pm10grade){
case "1" -> "좋음";
case "2" -> "보통";
case "3" -> "나쁨";
case "4" -> "매우나쁨";
default -> "점검 중";
};
}
public void setPm10value(String pm10value) {
this.pm10value = pm10value + "㎍/㎥";
}
}
Place VO
package org.weather;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@Entity
@Table(name = "place")
public class Place {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ColumnDefault("nextval('place_id_seq'")
@Column(name = "id", nullable = false)
private Integer id;
@Size(max = 50)
@NotNull
@Column(name = "country", nullable = false, length = 50)
private String country;
@Size(max = 50)
@Column(name = "area", length = 50)
private String area;
@Size(max = 50)
@NotNull
@Column(name = "stationname", nullable = false, length = 50)
private String stationName;
}
PlaceDTO
package org.weather;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Value;
import java.io.Serializable;
/**
* DTO for {@link Place}
*/
@Value
public class PlaceDto implements Serializable {
@NotNull
@Size(max = 50)
String country;
@Size(max = 50)
String area;
@NotNull
@Size(max = 50)
String stationName;
}
Dust 컨트롤러
package org.weather.dustApi;
import ch.qos.logback.core.model.Model;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
import org.weather.PaticulatemattervoDto;
import org.weather.place.PlaceService;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Controller
public class DustApiController {
private final DustApiService dustApiService;
private final PlaceService placeService;
private DustApiController(DustApiService dustApiService, PlaceService placeService){
this.dustApiService = dustApiService;
this.placeService = placeService;
}
@Value("${dust.api.key}")
private String apiKey;
@GetMapping("/dustMain")
public ModelAndView dustMain(){
String country = "경기";
String area = "용인시수지구";
String Stationname = placeService.findStationnameByCountryAndArea(country, area);
PaticulatemattervoDto paticulatemattervoDto = dustApiService.findByStationname(Stationname);
ModelAndView mav = new ModelAndView("DustMain");
mav.addObject("paticulatemattervoDto", paticulatemattervoDto);
mav.addObject("area", area);
return mav;
}
@GetMapping("/dustRequest")
public String dustRequest() throws IOException, JSONException {
String country = "%EA%B2%BD%EA%B8%B0"; // 한글이 아스키코드로 변형되어야 함
//Url 생성
String stringUrl =
"http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty" +
"?sidoName=" + country +
"&pageNo=" + "1" +
"&numOfRows=" + "100" +
"&returnType=" + "json" +
"&serviceKey=" + apiKey +
"&ver=" + "1.0";
//HTTP Request
URL url = new URL(stringUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
//응답코드 확인
//log.info("Response Code: {}", connection.getResponseCode());
//응답 처리
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String response = String.join("/n", bufferedReader.lines().toList());
bufferedReader.close();
connection.disconnect();
//JSON 데이터 출력
//log.info("Response: {}", response);
//JSON 데이터에서 필요한 데이터 추출
JSONObject jsonObject = new JSONObject(response);
JSONObject items = jsonObject.getJSONObject("response").getJSONObject("body");
JSONArray itemsArray = items.getJSONArray("items");
List<PaticulatemattervoDto> paticulateMatterList = new ArrayList<>();
for (int i = 0; i < itemsArray.length(); i++) {
JSONObject item = itemsArray.getJSONObject(i);
PaticulatemattervoDto paticulatemattervoDto = new PaticulatemattervoDto();
paticulatemattervoDto.setSidoname(item.getString("sidoName"));
paticulatemattervoDto.setDatatime(item.getString("dataTime"));
paticulatemattervoDto.setStationname(item.getString("stationName"));
paticulatemattervoDto.setPm25grade(item.getString("pm25Grade"));
paticulatemattervoDto.setPm25flag(item.getString("pm25Flag"));
paticulatemattervoDto.setPm25value(item.getString("pm25Value"));
paticulatemattervoDto.setPm10grade(item.getString("pm10Grade"));
paticulatemattervoDto.setPm10flag(item.getString("pm10Flag"));
paticulatemattervoDto.setPm10value(item.getString("pm10Value"));
paticulateMatterList.add(paticulatemattervoDto);
}
//List에 들어간 데이터 확인
//log.info("particulateMatterList: {}", paticulateMatterList.toString());
dustApiService.dustRequest(paticulateMatterList);
return "redirect:/dustMain";
}
}
Dust 서비스
package org.weather.dustApi;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.weather.Paticulatemattervo;
import org.weather.PaticulatemattervoDto;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class DustApiService {
private final DustApiRepository apiRepository;
public DustApiService(DustApiRepository apiRepository) {
this.apiRepository = apiRepository;
}
public void dustRequest(List<PaticulatemattervoDto> paticulateMatterList) {
for(PaticulatemattervoDto dto : paticulateMatterList) {
Paticulatemattervo paticulatemattervo = new Paticulatemattervo();
BeanUtils.copyProperties(dto, paticulatemattervo);
if(paticulatemattervo.getPm10flag() == null || paticulatemattervo.getPm25flag() == null) {
// apiRepository.updateSidonameAndDatatimeAndStationnameAndPm25gradeAndPm25flagAndPm25valueAndPm10gradeAndPm10flagAndPm10valueBy(
// paticulatemattervo.getSidoname(), paticulatemattervo.getDatatime(),paticulatemattervo.getStationname(),
// paticulatemattervo.getPm25grade(),paticulatemattervo.getPm25flag(),paticulatemattervo.getPm25value(),
// paticulatemattervo.getPm10grade(),paticulatemattervo.getPm10flag(),paticulatemattervo.getPm10value());
apiRepository.deleteByStationname(paticulatemattervo.getStationname());
apiRepository.save(paticulatemattervo);
}
}
}
public PaticulatemattervoDto findByStationname(String stationname) {
Paticulatemattervo paticulatemattervo = apiRepository.findByStationname(stationname);
PaticulatemattervoDto paticulatemattervoDto = new PaticulatemattervoDto();
BeanUtils.copyProperties(paticulatemattervo, paticulatemattervoDto);
return paticulatemattervoDto;
}
}
Dust 리포지토리
package org.weather.dustApi;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.weather.Paticulatemattervo;
@Repository
public interface DustApiRepository extends JpaRepository<Paticulatemattervo, Integer> {
@Transactional
void deleteByStationname(String stationname);
Paticulatemattervo findByStationname(String stationname);
// @Transactional
// @Modifying
// @Query("update Paticulatemattervo p set p.sidoname = ?1, p.datatime = ?2, p.stationname = ?3, p.pm25grade = ?4, p.pm25flag = ?5, p.pm25value = ?6, p.pm10grade = ?7, p.pm10flag = ?8, p.pm10value = ?9")
// int updateSidonameAndDatatimeAndStationnameAndPm25gradeAndPm25flagAndPm25valueAndPm10gradeAndPm10flagAndPm10valueBy(@NonNull String sidoname, @NonNull String datatime, @NonNull String stationname, @Nullable String pm25grade, @Nullable String pm25flag, @Nullable String pm25value, @Nullable String pm10grade, @Nullable String pm10flag, @Nullable String pm10value);
}
Place 서비스
package org.weather.place;
import org.springframework.stereotype.Service;
import org.weather.Place;
@Service
public class PlaceService {
private final PlaceRepository placeRepository;
public PlaceService(PlaceRepository placeRepository) {
this.placeRepository = placeRepository;
}
public String findStationnameByCountryAndArea(String country, String area) {
Place place = placeRepository.findStationnameByCountryAndArea(country, area);
return place.getStationName();
}
}
Place 리포지토리
package org.weather.place;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.weather.Place;
@Repository
public interface PlaceRepository extends JpaRepository<Place, Integer> {
Place findStationnameByCountryAndArea(String country, String area);
}
JSP
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script type="text/javascript" src="<c:url value='/resources/js/DustMain.js'/>"></script>
</head>
<body>
<h1>Dust Main Page</h1>
<p>
<H2><c:out value="${area}"/></H2>
<table>
<thead>
<tr>
<th>측정 시간</th>
<th>초미세먼지 상태</th>
<th>초미세먼지 수치</th>
<th>미세먼지 상태</th>
<th>미세먼지 수치</th>
</tr>
</thead>
<tbody>
<tr>
<td><c:out value="${paticulatemattervoDto.datatime}"/></td>
<td><c:out value="${paticulatemattervoDto.pm25grade}"/></td>
<td><c:out value="${paticulatemattervoDto.pm25value}"/></td>
<td><c:out value="${paticulatemattervoDto.pm10grade}"/></td>
<td><c:out value="${paticulatemattervoDto.pm10value}"/></td>
</tr>
</tbody>
</table>
</p>
<p>
<form id="DustRequest" action="<c:url value='/dustRequest'/>" method="get">
<input type="submit" value="확인">
</form>
</p>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"></script>
</body>
</html>
날씨 api를 사용했을 때와 겹치는 부분이 많다.
그 부분은 생략하고 살펴보겠다.
Dust 컨트롤러
String country = "%EA%B2%BD%EA%B8%B0"; // 한글이 아스키코드로 변환되어야 함
- 지역 명은 서울 / 경기 이런 식으로 기입되도록 되어 있었는데 백엔드에서 넘길 때는 아스키코드로 변환되지 않으면 데이터가 제대로 불러와지지 않았다.
- UrlEncoder나 직접 인코딩도 해보았지만 잘 되지 않아 현재는 방치 상태...
- 일단 지역 항목만 아스키코드로 바꾸면 되기 때문에 많지 않아서 현재는 별 문제가 없긴 하다
Dust 서비스
BeanUtils.copyProperties(dto, paticulatemattervo);
- dto의 값을 vo로 복사할 수 있는 메서드다. 굉장히 유용할 것 같기 때문에 앞으로도 많이 쓸 것 같다.
if(paticulatemattervo.getPm10flag() == null || paticulatemattervo.getPm25flag() == null) {
- flag컬럼은 각 측정소에서 데이터를 못 가져올 경우, 못 가져오는 원인이 입력되는 컬럼이다.
- 따라서 해당 값이 null이 아닐 경우에는 데이터가 갱신되지 않도록 막기 위해 설정했다.
apiRepository.updateSidonameAndDatatimeAndStationnameAndPm25gradeAndPm25flagAndPm25valueAndPm10gradeAndPm10flagAndPm10valueBy(
paticulatemattervo.getSidoname(), paticulatemattervo.getDatatime(),paticulatemattervo.getStationname(),
paticulatemattervo.getPm25grade(),paticulatemattervo.getPm25flag(),paticulatemattervo.getPm25value(),
paticulatemattervo.getPm10grade(),paticulatemattervo.getPm10flag(),paticulatemattervo.getPm10value());
apiRepository.deleteByStationname(paticulatemattervo.getStationname());
apiRepository.save(paticulatemattervo);
- 이 부분은 3가지 방법을 고민했다
1. saveAll을 사용하여 List를 그대로 DB에 넣는 방법
- 그러나 JPA 특성 상 ID가 필요하기 때문에 같은 장소의 데이터가 입력되는 문제가 발생했다.
- serial로 지정한 탓에 데이터를 입력할 때마다 Insert 처리가 되었던 것
- 그리고 flag컬럼이 null이 아닌 경우를 상정해야 했다.
2. update 쿼리
- 직접 쿼리를 작성하는 방법을 통해 업데이트를 구현해보았다.
- 그러나 효율성이 떨어지는 것 같아 현재 보류 상태
3. flag컬럼이 null이 아닌 데이터를 삭제 후 재입력
- 현재 사용중인 방법
- 그러나 save는 데이터가 많아질 수록 saveAll에 비해 성능 차이가 많이 난다
1) save → 1건 마다 save() 함수 호출.
2) saveAll → 1건 마다 인스턴스 내부의 save() 함수 호출.
@Transactional은 AOP 프록시 기반으로, 외부 Bean 객체가 있고, 이 객체의 함수를 호출해야 Intercept가 되어 트랜잭션으로 묶이게 된다.
그렇기 때문에 Bean 객체 내부에서 내부함수 호출하게 되는 경우 @Transactional이 적용이 되지 않는다.
save() 호출 시 상위에 Transaction이 존재하는 경우, 해당 Transaction에 참여하나, 존재하지 않는 경우 새로 Transaction을 생성하고, save 후 commit한다.
즉, 기존 Transaction에 참여하게 되더라도 외부 Bean(repository) 객체의 save 함수를 호출하는 것이기 때문에, 위 과정이 생겨 비용이 발생한다. 당연히 개수가 많아질수록 비용은 점점 커질 것이고, 그만큼 시간도 더 오래 걸리게 된다.
반면, saveAll() 같은 경우는 Bean 객체의 내부함수를 호출하기 때문에 save() 호출마다 트랜잭션이 생성되거나 참여하는 프록시 로직을 전혀 타지 않고, 단순한 메소드 호출만 하기 때문에 위와 같은 비용이 발생하지 않는다.
--> 지금 당장은 delete & save를 사용하고 있지만 saveAll로 변환할 예정이다. saveAll에 사용되는 List는 flag컬럼이 null인 경우에만 포함되도록 변경할 것이다. 또한 DB의 Table의 Id 역시 바꿔야 할 것이다. 숫자로 하던가, 아니면 다른 명칭을 사용하던가
public ModelAndView dustMain(){
String country = "경기";
String area = "용인시수지구";
String Stationname = placeService.findStationnameByCountryAndArea(country, area);
PaticulatemattervoDto paticulatemattervoDto = dustApiService.findByStationname(Stationname);
ModelAndView mav = new ModelAndView("DustMain");
mav.addObject("paticulatemattervoDto", paticulatemattervoDto);
mav.addObject("area", area);
return mav;
}
- 과정을 이러하다
1. jsp에서 보내준 country와 area를 변수로 받는다. (현재는 임시 값 저장)
2. country와 area를 통해 DB의 place Table에서 stationname(측정소 명)을 찾는다
3. 측정소 명을 통해 DB에서 해당 area의 미세먼지 데이터를 가져온다. (dustRequest에서 저장됨)
4. 저장된 값을 jsp로 출력한다.
DTO
public void setPm25grade(String pm25grade) {
this.pm25grade = switch (pm25grade){
case "1" -> "좋음";
case "2" -> "보통";
case "3" -> "나쁨";
case "4" -> "매우나쁨";
default -> "점검 중";
};
}
public void setPm25value(String pm25value) {
this.pm25value = pm25value + "㎍/㎥";
}
- grade는 상태 메세지로 변경했고, value는 단위를 추가했다.
출력화면
개선점
1. 지역 명을 아스키 코드로 자동 변환하도록 설
2. delete & save --> saveAll
'개인프로젝트 > 기능프로그램_오늘뭐입지' 카테고리의 다른 글
20240514_미세먼지 파트 조정 (0) | 2024.05.14 |
---|---|
20240513_ChatGpt Api 사용하기 (0) | 2024.05.13 |
오늘 뭐입지? 프로그램 관련 각종 문서 (0) | 2024.05.12 |
20240511_날씨 데이터 출력 (0) | 2024.05.12 |
20240510_옷 출력/추가/삭제 기능 (0) | 2024.05.11 |