2024.09.06 - [Java/공중화장실 찾기] - 공중화장실 찾기 - 1 <프로젝트 생성 및 네이버 지도 API 적용>
이전 시간에는 프로젝트를 생성하고 네이버 지도 API를 사용하기 위한 밑작업을 진행했었다.
이번에는 공공데이터를 가지고 네이버 지도 API를 통해 화면에 출력하는 작업을 진행해 보겠다.
✅ 공공데이터 가공
공중화장실 데이터를 가져오기 위하여 공공데이터를 활용한다.
https://www.data.go.kr/data/15012892/standard.do?recommendDataYn=Y
본인이 제대로 찾지 못한 건지는 모르겠지만 공중화장실의 경우 API를 제공하지는 않고 엑셀을 통해서만
데이터를 제공하는 것 같다. 따라서 우리는 엑셀 데이터를 csv 형식으로 변형하여 DB에 삽입하도록 한다.
1️⃣ 엑셀 파일 csv로 변환
엑셀을 csv 파일로 변환시키는 것은 매우 간단하다. 다운로드한 엑셀 파일을 "다른 이름으로 저장"하여
저장 방식을 "CSV UTF-8(쉼표로 분리)"로 저장해 주면 된다.
하지만 지도에 데이터를 표기하기 위해서 lat, lng 데이터가 존재해야 하는데 누락된 데이터가 너무 많아
python을 통해 csv 데이터를 약간 가공하였다. 데이터 가공에 대해서는 아래에 포스팅해두었다.
2024.08.29 - [Python] - python 주소값으로 경도, 위도 구하기
2️⃣ csv 파일 DB에 Insert 하기
csv 파일을 DB에 넣기 위해서 Spring Batch, JPA를 이용하였다. property 파일에 DB 연동하는 방법 같은 것은
다 알고 있다고 생각하고 따로 기입하지는 않겠다.
🟨 SimpleJobConfiguration
package toy.project.suddenPoo.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import toy.project.suddenPoo.csv.CsvDTO;
import toy.project.suddenPoo.csv.CsvReader;
import toy.project.suddenPoo.csv.CsvSchedulerWriter;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SimpleJobConfiguration {
private final CsvReader csvReader;
private final CsvSchedulerWriter csvSchedulerWriter;
@Bean
public Job toiletDataLoadJob(JobRepository jobRepository, Step toiletDataLoadStep) {
return new JobBuilder("toiletInformationLoadJob", jobRepository)
.start(toiletDataLoadStep)
.build();
}
@Bean
public Step toiletDataLoadStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager) {
return new StepBuilder("toiletDataLoadStep", jobRepository)
.<CsvDTO, CsvDTO>chunk(100, platformTransactionManager)
.reader(csvReader.csvScheduleReader())
.writer(csvSchedulerWriter)
.build();
}
}
🟨 CsvReader
package toy.project.suddenPoo.csv;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.separator.DefaultRecordSeparatorPolicy;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
@RequiredArgsConstructor
public class CsvReader {
@Value("${csv-path}")
private String csvPath;
@Bean
public FlatFileItemReader<CsvDTO> csvScheduleReader() {
// 파일 경로 지정 및 인코딩
FlatFileItemReader<CsvDTO> flatFileItemReader = new FlatFileItemReader<>();
flatFileItemReader.setResource(new ClassPathResource(csvPath));
flatFileItemReader.setEncoding("UTF-8");
// 데이터 내부에 개행이 있으면 꼭! 추가해주세요
flatFileItemReader.setRecordSeparatorPolicy(new DefaultRecordSeparatorPolicy());
// 읽어온 파일을 한 줄씩 읽기
DefaultLineMapper<CsvDTO> defaultLineMapper = new DefaultLineMapper<>();
// 따로 설정하지 않으면 기본값은 ","
DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer();
// "toiletName", "roadName", "latitude", "longitude" 필드 설정
delimitedLineTokenizer.setNames(CsvDTO.getFieldNames().toArray(String[]::new));
defaultLineMapper.setLineTokenizer(delimitedLineTokenizer);
// 매칭할 class 타입 지정(필드 지정)
BeanWrapperFieldSetMapper<CsvDTO> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
beanWrapperFieldSetMapper.setTargetType(CsvDTO.class);
defaultLineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);
flatFileItemReader.setLineMapper(defaultLineMapper);
return flatFileItemReader;
}
}
csvPath는 변환시킨 csv 파일의 경로를 적어주면 된다. application.properties / application.yml에 적어둔 값을 가져오자.
🟨 CsvSchedulerWriter
package toy.project.suddenPoo.csv;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.Transactional;
import toy.project.suddenPoo.entity.Csv;
import toy.project.suddenPoo.repository.ToiletRepository;
@Configuration
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CsvSchedulerWriter implements ItemWriter<CsvDTO> {
private final ToiletRepository toiletRepository;
@Override
@Transactional
public void write(Chunk<? extends CsvDTO> chunk) throws Exception {
Chunk<Csv> csvs = new Chunk<>();
chunk.forEach(csvDTO -> {
Csv csv = Csv.builder()
.csvDTO(csvDTO)
.build();
csvs.add(csv);
});
toiletRepository.saveAll(csvs);
}
}
🟨 CsvDTO
package toy.project.suddenPoo.csv;
import lombok.Data;
import toy.project.suddenPoo.entity.Csv;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
@Data
public class CsvDTO {
private String toiletName;
private String roadName;
private String latitude;
private String longitude;
public static List<String> getFieldNames() {
Field[] declaredFields = CsvDTO.class.getDeclaredFields();
List<String> result = new ArrayList<>();
for (Field declaredField : declaredFields) {
result.add(declaredField.getName());
}
return result;
}
}
🟨 ToiletRepository
package toy.project.suddenPoo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import toy.project.suddenPoo.entity.Csv;
public interface ToiletRepository extends JpaRepository<Csv, Long>, ToiletRepositoryCustom {
}
ToiletRepositoryCustom은 추후 지도에 필요한 기능을 넣기 위하여 추가한 것이니 지금은 신경 쓸 필요 없다.
🟨 Csv
package toy.project.suddenPoo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import toy.project.suddenPoo.csv.CsvDTO;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Csv {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String toiletName;
private String roadName;
private String latitude;
private String longitude;
@Builder
public Csv(CsvDTO csvDTO) {
this.toiletName = csvDTO.getToiletName();
this.roadName = csvDTO.getRoadName();
this.latitude = csvDTO.getLatitude();
this.longitude = csvDTO.getLongitude();
}
}
사실 Entity나 Repository의 경우 대부분 알고 있을 것이기 때문에 굳이 기입하지 않아도 되겠지만
혹시라도 헤매시는 분들이 있을 수 있으니 올린다.
이 상태로 application을 가동하면 연동해 둔 DB에 Batch 테이블과 CSV 테이블이 생성된다.
물론 프로퍼티 파일에 jpa.hibernate.ddl-auto=create로 해둔 뒤 한번 실행 후 다시 update로 변경해줘야 한다.
3️⃣ DB 확인
위 사진처럼 가공된 csv 파일의 데이터가 제대로 들어간 것을 확인할 수 있다.
✅ 마무리
원래 이번 포스팅에 네이버 지도 API 적용하는 것까지 작성하려고 했으나 코드를 올려두는 게 생각보다 길어서
다음 포스팅에 적어야겠다. csv 파일을 db에 insert 하는 부분이 어려운 분들은 github 코드를 참고하면 좋을 것 같다.
https://github.com/Kimmingki/suddenPoo
'Java > 공중화장실 찾기' 카테고리의 다른 글
공중화장실 찾기 - 4 <Spring Boot GGP 배포> (2) | 2024.09.11 |
---|---|
공중화장실 찾기 - 3 <네이버 지도 API 적용> (2) | 2024.09.09 |
공중화장실 찾기 - 1 <프로젝트 생성 및 네이버 지도 API 적용> (1) | 2024.09.06 |