Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit 9d0551c

Browse files
✨ Feat: 공공데이터 API 파싱 개발
* 📦Chore: API 응답 파싱을 위한 의존성 라이브러리, 환경변수 추가 * ✨Feat: API 요청용 객체 추가 * 📦 Chore: 주석 추가 * ✨ Feat: 의약품 상세정보 API 호출 개발 * ✨ Feat: 샘플 검색 데이터 추출 개발 * ✨ Feat: 공공데이터 API 파싱 개발 * ♻️ Refactor: 파싱을 위한 Wrappeer 클래스 정리 (인터페이스 추가) * ♻️ Refactor: 불필요한 생성자 삭제 * ♻️ Refactor: aricle parsing 가독성 개선 * 🐛 Fix: SectionWrapper parseElement 오타 수정 * ♻️ Refactor: 로직 수정 및 가독성 개선 * ♻️ Refactor: Paragraph 파싱 수정 및 헥사고날 아키텍처 적용 * ♻️ Refactor: 도메인 메소드 접근자 변경 * 📦 gitignore 수정 및 재 커밋 * ♻️ Refactor: 공공데이터 개요정보 API 파싱 수정 * ✨ Feat: API 전체 데이터 저장 * ✨ Feat: api 전체 데이터 저장 * 🐛 Fix: Xml 파서에 null 체크 로직 추가 * ✨ Feat: 공공데이터 전체 저장 기능 개발 * ♻️ Refactor: 전체 데이터 저장 함수 리팩토링 --------- Co-authored-by: HaechangLee <112938092+HaechangLee@users.noreply.github.com> * ♻️ Refactor: 패키지구조 개선 * 🐛 Fix: import에러 수정 --------- Co-authored-by: thelightway <dev@thelightway.kr>
1 parent 6894f6e commit 9d0551c

22 files changed

Lines changed: 935 additions & 3 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ build/
1919
*.iml
2020
*.iws
2121
*.ipr
22-
out/
22+
/out/
2323
.idea/**/
2424
.idea_modules/
2525
.idea/httpRequests

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies {
3939
// RDB
4040
runtimeOnly 'com.mysql:mysql-connector-j'
4141
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
42+
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
4243

4344
// Elastic search
4445
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.likelion.backendplus4.yakplus.application.port.in;
2+
3+
public interface DrugApprovalDetailScraperUseCase {
4+
void requestUpdateRawData();
5+
6+
void requestUpdateAllRawData();
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.likelion.backendplus4.yakplus.application.port.out;
2+
3+
import java.util.List;
4+
5+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity;
6+
7+
public interface DrugDetailRepositoryPort {
8+
9+
List<GovDrugDetailEntity> findAll(String code);
10+
void saveAllAndFlush(List<GovDrugDetailEntity> entities);
11+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package com.likelion.backendplus4.yakplus.application.service;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
// import com.fasterxml.jackson.dataformat.xml.XmlMapper;
8+
import com.likelion.backendplus4.yakplus.support.api.ApiResponseMapper;
9+
import com.likelion.backendplus4.yakplus.support.api.ApiUriCompBuilder;
10+
import com.likelion.backendplus4.yakplus.support.parser.MaterialParser;
11+
import com.likelion.backendplus4.yakplus.support.parser.XMLParser;
12+
import com.likelion.backendplus4.yakplus.application.port.in.DrugApprovalDetailScraperUseCase;
13+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity;
14+
import com.likelion.backendplus4.yakplus.application.port.out.DrugDetailRepositoryPort;
15+
16+
import jakarta.transaction.Transactional;
17+
import lombok.RequiredArgsConstructor;
18+
import lombok.extern.slf4j.Slf4j;
19+
20+
import org.springframework.stereotype.Component;
21+
import org.springframework.web.client.RestTemplate;
22+
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Set;
26+
27+
@Slf4j
28+
@Component
29+
@RequiredArgsConstructor
30+
public class DrugApprovalDetailScraper implements DrugApprovalDetailScraperUseCase {
31+
private final ObjectMapper objectMapper;
32+
private final RestTemplate restTemplate;
33+
private final ApiUriCompBuilder apiUriCompBuilder;
34+
private final DrugDetailRepositoryPort drugDetailRepositoryPort;
35+
36+
@Transactional
37+
@Override
38+
public void requestUpdateRawData() {
39+
log.info("API 데이터 요청");
40+
String response = restTemplate.getForObject(apiUriCompBuilder.getUriForDetailApi(1), String.class);
41+
log.debug("API Response: {}", response);
42+
43+
JsonNode items = ApiResponseMapper.getItemsFromResponse(response);
44+
List<GovDrugDetailEntity> drugs = toListFromJson(items);
45+
for (GovDrugDetailEntity drug : drugs) {
46+
System.out.println(drug);
47+
}
48+
drugDetailRepositoryPort.saveAllAndFlush(drugs);
49+
}
50+
51+
52+
@Override
53+
public void requestUpdateAllRawData() {
54+
int pageNo = 1;
55+
int receivedCount = 0;
56+
int savedCountWithoutDuplicates = 0;
57+
58+
String response = fetchPage(pageNo);
59+
int totalCount = ApiResponseMapper.getTotalCountFromResponse(response);
60+
61+
while (hasMoreData(receivedCount, totalCount)) {
62+
JsonNode items = ApiResponseMapper.getItemsFromResponse(response);
63+
List<GovDrugDetailEntity> drugs = toListFromJson(items);
64+
receivedCount += drugs.size();
65+
66+
// item_seq 기준 중복 제거된 약품 개수 유지 (실제 db에 저장된 데이터와 같은 지 비교용)
67+
int uniqueItems = deduplicateByItemSeq(drugs);
68+
savedCountWithoutDuplicates += uniqueItems;
69+
70+
drugDetailRepositoryPort.saveAllAndFlush(drugs);
71+
72+
log.info("Page {}, received: {}, saved (unique): {}, totalReceived: {}, totalUniqueSaved: {}",
73+
pageNo, drugs.size(), uniqueItems, receivedCount, savedCountWithoutDuplicates);
74+
75+
response = fetchPage(++pageNo);
76+
}
77+
78+
}
79+
80+
private List<GovDrugDetailEntity> toListFromJson(JsonNode items) {
81+
82+
log.info("items 약품 객체로 맵핑");
83+
try {
84+
List<GovDrugDetailEntity> apiDataDrugDetails = toApiDetails(items);
85+
for (int i = 0; i < apiDataDrugDetails.size(); i++) {
86+
GovDrugDetailEntity drugDetail = apiDataDrugDetails.get(i);
87+
JsonNode item = items.get(i);
88+
log.debug("item seq: " + item.get("ITEM_SEQ").asText());
89+
90+
String materialRawData = item.get("MATERIAL_NAME").asText();
91+
String materialInfo = MaterialParser.parseMaterial(materialRawData);
92+
drugDetail.changeMaterialInfo(materialInfo);
93+
94+
String efficacyXmlText = item.get("EE_DOC_DATA").asText();
95+
String efficacy = XMLParser.toJson(efficacyXmlText);
96+
drugDetail.changeEfficacy(efficacy);
97+
98+
String usageXmlText = items.get(i).get("UD_DOC_DATA").asText();
99+
String usages = XMLParser.toJson(usageXmlText);
100+
drugDetail.changeUsage(usages);
101+
102+
String precautionxmlText = items.get(i).get("NB_DOC_DATA").asText();
103+
String precautions = XMLParser.toJson(precautionxmlText);
104+
drugDetail.changePrecaution(precautions);
105+
}
106+
return apiDataDrugDetails;
107+
} catch (Exception e) {
108+
throw new RuntimeException(e);
109+
}
110+
}
111+
112+
private List<GovDrugDetailEntity> toApiDetails(JsonNode items) {
113+
try {
114+
return objectMapper.readValue(items.toString(),
115+
new TypeReference<List<GovDrugDetailEntity>>() {
116+
});
117+
} catch (JsonProcessingException e) {
118+
throw new RuntimeException(e);
119+
}
120+
}
121+
122+
// private JsonNode toJsonFromXml(String usageXmlText) throws JsonProcessingException {
123+
// XmlMapper xmlMapper = new XmlMapper();
124+
//
125+
// JsonNode jsonNode = xmlMapper.readTree(usageXmlText)
126+
// .path("SECTION")
127+
// .path("ARTICLE");
128+
// return jsonNode;
129+
// }
130+
131+
// TODO: 추후 삭제 예정
132+
// private String replaceText(String text){
133+
// return text.replace("&#x119e; ", "&")
134+
// .replace("&#x2022; ","")
135+
// .replace("&#x301c; ", "~");
136+
// }
137+
138+
private int deduplicateByItemSeq(List<GovDrugDetailEntity> drugs) {
139+
// itemseq 기준으로 set에 저장 --> set은 중복 허용하지 않으므로 item seq 다 넣으면 알아서 중복 없이 저장됨
140+
Set<Long> uniqueItems = new HashSet<>();
141+
142+
for (GovDrugDetailEntity drug : drugs) {
143+
uniqueItems.add(drug.getDrugId());
144+
}
145+
return uniqueItems.size();
146+
}
147+
148+
private String fetchPage(int pageNo) {
149+
return restTemplate.getForObject(apiUriCompBuilder.getUriForDetailApi(pageNo), String.class);
150+
}
151+
152+
private boolean hasMoreData(int receivedCount, int totalCount) {
153+
return receivedCount < totalCount;
154+
}
155+
156+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.likelion.backendplus4.yakplus.application.service;
2+
3+
import java.net.URI;
4+
import java.util.List;
5+
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.web.client.RestTemplate;
8+
9+
import com.fasterxml.jackson.core.JsonProcessingException;
10+
import com.fasterxml.jackson.core.type.TypeReference;
11+
import com.fasterxml.jackson.databind.JsonNode;
12+
import com.fasterxml.jackson.databind.ObjectMapper;
13+
import com.likelion.backendplus4.yakplus.support.api.ApiResponseMapper;
14+
import com.likelion.backendplus4.yakplus.support.api.ApiUriCompBuilder;
15+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.ApiDataDrugImgRepo;
16+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity;
17+
18+
import jakarta.transaction.Transactional;
19+
import lombok.RequiredArgsConstructor;
20+
import lombok.extern.slf4j.Slf4j;
21+
22+
@Service
23+
@Slf4j
24+
@RequiredArgsConstructor
25+
public class DrugImageGovScraper {
26+
private final ApiUriCompBuilder uriCompBuilder;
27+
private final RestTemplate restTemplate;
28+
private final ApiDataDrugImgRepo imgRepo;
29+
private final ObjectMapper objectMapper;
30+
31+
@Transactional
32+
public void getApiData(){
33+
log.info("의약품 개요 정보 API 호출 시작");
34+
35+
URI uriForImgApi = uriCompBuilder.getUriForImgApi(1);
36+
37+
String response = restTemplate.getForObject(uriForImgApi, String.class);
38+
JsonNode items = ApiResponseMapper.getItemsFromResponse(response);
39+
List<ApiDataDrugImgEntity> imgDatas = null;
40+
try {
41+
imgDatas = objectMapper.readValue(items.toString(),
42+
new TypeReference<List<ApiDataDrugImgEntity>>() {});
43+
} catch (JsonProcessingException e) {
44+
throw new RuntimeException(e);
45+
}
46+
imgRepo.saveAllAndFlush(imgDatas);
47+
}
48+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.likelion.backendplus4.yakplus.domain.model;
2+
3+
import java.time.LocalDate;
4+
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
9+
import lombok.Builder;
10+
11+
@Builder
12+
public class GovDrugDetail {
13+
private Long drugId;
14+
private String drugName;
15+
private String company;
16+
private LocalDate permitDate;
17+
private boolean isGeneral;
18+
private String materialInfo;
19+
private String storeMethod;
20+
private String validTerm;
21+
private String efficacy;
22+
private String usage;
23+
private String precaution;
24+
25+
public JsonNode toJson(String json) {
26+
try {
27+
return new ObjectMapper().readValue(json, JsonNode.class);
28+
} catch (JsonProcessingException e) {
29+
//TODO 에러 로그 처리 필요합니다.
30+
throw new RuntimeException(e);
31+
}
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.out;
2+
3+
import java.util.List;
4+
5+
import org.springframework.stereotype.Component;
6+
7+
import com.likelion.backendplus4.yakplus.application.port.out.DrugDetailRepositoryPort;
8+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity;
9+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.GovDrungDetailJpaRepository;
10+
import com.likelion.backendplus4.yakplus.domain.model.GovDrugDetail;
11+
12+
import jakarta.transaction.Transactional;
13+
import lombok.RequiredArgsConstructor;
14+
15+
@Component
16+
@RequiredArgsConstructor
17+
public class GovDrugDetailRepositoryAdapter implements DrugDetailRepositoryPort {
18+
private final GovDrungDetailJpaRepository govDrungDetailJpaRepository;
19+
20+
@Override
21+
public List<GovDrugDetailEntity> findAll(String code) {
22+
return govDrungDetailJpaRepository.findAll();
23+
}
24+
25+
@Override
26+
@Transactional
27+
public void saveAllAndFlush(List<GovDrugDetailEntity> entities) {
28+
govDrungDetailJpaRepository.saveAllAndFlush(entities);
29+
}
30+
31+
public GovDrugDetail toDomainFromEntity(GovDrugDetailEntity detail){
32+
return GovDrugDetail.builder()
33+
.drugId(detail.getDrugId())
34+
.drugName(detail.getDrugName())
35+
.company(detail.getCompany())
36+
.permitDate(detail.getPermitDate())
37+
.isGeneral(detail.isGeneral())
38+
.materialInfo(detail.getMaterialInfo())
39+
.storeMethod(detail.getStoreMethod())
40+
.validTerm(detail.getValidTerm())
41+
.efficacy(detail.getEfficacy())
42+
.usage(detail.getUsage())
43+
.precaution(detail.getPrecaution())
44+
.build();
45+
}
46+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.stereotype.Repository;
5+
6+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity;
7+
8+
@Repository
9+
public interface ApiDataDrugImgRepo extends JpaRepository<ApiDataDrugImgEntity, Long> {
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
5+
import com.likelion.backendplus4.yakplus.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity;
6+
7+
public interface GovDrungDetailJpaRepository extends JpaRepository<GovDrugDetailEntity,Long> {
8+
}

0 commit comments

Comments
 (0)