Skip to content

FEAT : 공공데이터 연동 확대 및 검색·개인화·관측 고도화 - #62

Merged
RosieOh merged 24 commits into
feat/RosieOhfrom
feat/public-data-integration
Aug 4, 2026
Merged

FEAT : 공공데이터 연동 확대 및 검색·개인화·관측 고도화#62
RosieOh merged 24 commits into
feat/RosieOhfrom
feat/public-data-integration

Conversation

@RosieOh

@RosieOhRosieOh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

배경

공공데이터 연동을 점검한 결과 서울시 하나만 실제로 동작하고 나머지는 죽은 코드였습니다. 연동을 넓히고 나니 늘어난 데이터를 조회·추천에 쓰지 못하는 한계가 이어서 드러나, 검색·개인화·관측까지 함께 정리했습니다.

Closes#61
Closes#63

변경 사항

1. 공공데이터 연동 확대 (#61)

항목내용
공급자 추상화PublicDataProvider 로 서울 열린데이터광장(경로 파라미터)과 data.go.kr(serviceKey 쿼리) 규격 차이 캡슐화
전국 어린이집서울 외 지역 시설 데이터가 아예 없던 문제 해소
정부지원서비스손으로 입력하던 정책을 육아 키워드로 걸러 자동 적재
소아청소년과 병원비어 있던 Hospital 테이블을 채움
전국 유치원FacilityType.KINDERGARTEN 이 정의만 돼 있고 채우는 경로가 없어 유치원 검색이 항상 0건이던 문제 해소
주기 동기화수동 호출을 스케줄러로 이관, 동기화 이력(성공/실패/건수)을 남겨 조용한 실패 제거

부가 조치 — XML 응답 파서(XXE 차단), serviceKey 이중 인코딩 방지, 데이터셋 경로 프로퍼티 외부화(경로 개편 시 재배포 불필요), 절대 URL 리소스 지원(표준데이터는 호스트가 다름).

수집 절차 공통화 — 3개 서비스에 복붙돼 있던 페이징·응답 언랩·에러 처리를 PagedSyncTemplate 으로 모으면서 두 가지를 고쳤습니다.

  • 응답 필드명이 개편되면 200페이지를 돌며 전건 실패 로그만 쌓이던 것 → 첫 페이지 전멸 시 즉시 중단 + 사유 기록
  • 필터에 걸린 행이 저장 실패와 섞이던 것 → skipped 분리

2. 검색·개인화·관측 (#63)

항목내용
위치 검색findWithinRadius존재하지 않는 테이블(care_facilities)을 참조해 동작하지 않던 버그 수정. 바운딩 박스로 후보를 좁힌 뒤 거리 계산, 위경도 복합 인덱스 추가
전문 검색MATCH ... AGAINST 도입, 짧은 키워드·연산자 문자·미지원 DB 는 LIKE 로 폴백
챗봇 RAG단일 → 다중 키워드(길이 내림차순 3개), 한국어 조사 제거
개인화 추천자녀 월령 × 대상 연령, 거주지 × 대상 지역, 마감 임박도 점수화. GET /policies/recommendations, 추천 근거 동봉
연령 단위 정규화동기화 정책이 연령 필드를 채우지 않아 개인화에서 전부 제외되던 문제. 지원대상 자유 텍스트를 개월로 환산(AgeRangeParser)
N+1 관측요청별 쿼리 수 집계 → 임계치 초과 시 경고. 기본 비활성, dev 만 on

부수 수정/policies/recommendations·/policies/bookmarks/policies/* permitAll 에 걸려 인증이 강제되지 않던 인가 구멍을 막았습니다.

검증

./gradlew build 통과 (H2 기반 컨텍스트 로딩 테스트 포함).

신규 테스트 — PagedSyncTemplateTest(9), KindergartenUpsertServiceTest(6), AgeRangeParserTest(10), CareKnowledgeRetrieverTest(5), BoundingBoxTest(3), FullTextSearchSupportTest(4), DataGoKrProviderTest 절대 URL(1).

남은 확인 사항

데이터셋 오퍼레이션 경로와 응답 필드명은 문서 기준으로 넣었고 실제 호출 검증은 하지 못했습니다(서비스 키 없음). 어긋나면 첫 페이지에서 중단되고 DEBUG 로그에 실제 필드명이 찍히므로, public.data.resource.* 프로퍼티만 고쳐 맞출 수 있습니다.

이 PR 의 base 는 feat/RosieOh 라 CI 워크플로(pull_request: branches: [main])가 걸리지 않습니다. 중간 PR 도 검증하려면 트리거를 [main, 'feat/**'] 로 넓혀야 합니다.

@RosieOhRosieOh added this to the Production Readiness milestone Aug 4, 2026
@RosieOh

Copy link
Copy Markdown
ContributorAuthor

추가: 심평원 병원정보 연동 (커밋 5개)

Hospital 엔티티가 데이터를 넣을 경로 없이 비어 있던 문제를 해결했습니다. 공급자 추상화 덕분에 DataGoKrProvider를 그대로 재사용했고, 동기화 서비스만 추가했습니다.

구현

  • XmlResponseParser — 심평원 데이터셋은 XML로 응답합니다. jackson-dataformat-xml을 추가하면 Spring MVC가 XML 메시지 컨버터를 자동 등록해 기존 API의 응답 협상 동작까지 바뀌므로, 의존성 없이 JDK 내장 DOM 파서로 처리했습니다. XXE 차단(DOCTYPE 금지, 외부 엔티티 비활성)을 넣고 테스트로 고정했습니다.
  • Hospital.externalCode — 심평원 ykiho(암호화 요양기호)를 외부 식별자로 추가했습니다. 복호화 수단이 제공되지 않아 값 자체를 키로 씁니다. 기존 수기 등록 병원은 NULL로 남고 UNIQUE 제약이 NULL을 중복으로 보지 않습니다. (V3__hospital_external_code.sql)
  • PediatricHospitalSyncService — 진료과목 코드로 소아청소년과만 필터링. 코드는 개정될 수 있어 설정(public.data.hospital.pediatric-subject-code)으로 뺐습니다.
  • 스케줄러 매주 화요일 03:00 (시설 동기화와 같은 날 돌리면 일일 트래픽 한도를 함께 소진하므로 하루 띄웠습니다)
  • 수동 트리거: POST /api/admin/public-data/hospitals/sync

주의해서 본 부분

심평원 좌표는 XPos=경도, YPos=위도 순서입니다. 통상적인 (lat, lng) 순서와 반대라 그대로 매핑하면 지도에서 엉뚱한 위치가 나옵니다. 주석으로 남겼습니다.

DataGoKrProvider의 응답 포맷 파라미터 중복을 고쳤습니다. 기존에는 type=json을 무조건 붙여서, 호출부가 _type을 지정하면 파라미터가 둘 다 들어갔습니다.

검증

BUILD SUCCESSFUL
XmlResponseParser 5건 / DataGoKrProvider 4건 / 시도 코드 4건 통과

여전히 확인 필요

진료과목 코드 10(소아청소년과)과 오퍼레이션 경로 B551182/hospInfoServicev2/getHospBasisList공개 문서에서 확정하지 못했습니다. 심평원은 코드표를 보건의료빅데이터개방시스템 > 용어설명 > 코드조회에서만 제공합니다. API 키 발급 후 실제 응답으로 검증이 필요하며, 둘 다 설정/상수로 빼두어 코드 수정 없이 교정할 수 있습니다.

@RosieOh

Copy link
Copy Markdown
ContributorAuthor

추가 작업 (#63)

공공데이터 연동으로 늘어난 데이터를 실제로 쓸 수 있게 검색·추천·관측을 보강했습니다.

항목내용
A. 위치 검색findWithinRadius 가 존재하지 않는 테이블 care_facilities 를 참조해 아예 동작하지 않던 버그 수정. 바운딩 박스로 후보를 좁힌 뒤 Haversine 계산, 위경도 복합 인덱스 추가
B. 전문 검색MATCH ... AGAINST 기반 시설 검색. 짧은 키워드·연산자 문자·미지원 DB 는 LIKE 로 폴백
C. 챗봇 RAG단일 → 다중 키워드(길이 내림차순 3개), 한국어 조사 제거
D. 개인화 추천자녀 월령 × 대상 연령, 거주지 × 대상 지역, 마감 임박도 점수화. GET /policies/recommendations, 추천 근거 동봉
E. N+1 관측요청별 SQL 수 집계 → 임계치 초과 시 경고. 기본 비활성, dev 만 on

부수 수정

  • /policies/recommendations, /policies/bookmarks/policies/* permitAll 에 걸려 있던 문제 — 와일드카드보다 앞에 authenticated() 선언
  • 여러 줄 주석을 한 줄로 정리, @Operation description 축약

테스트: BoundingBoxTest, FullTextSearchSupportTest, CareKnowledgeRetrieverTest 추가. ./gradlew build 통과 (컨텍스트 로딩 테스트 포함).

@RosieOh

Copy link
Copy Markdown
ContributorAuthor

연령 단위 정규화 (#63)

개인화 추천을 붙이면서 Policy.targetAgeMin/Max 의 단위가 스키마에 명시돼 있지 않은 문제가 드러났습니다. 시드 데이터는 개월 기준("부모급여(1세)" → 1223, "누리과정 만 35세" → 36~71)인데, 공공데이터 동기화는 이 필드를 아예 채우지 않아 동기화된 정책이 개인화에서 전부 "조건 없음"으로 처리되고 있었습니다.

  • AgeRangeParser 추가 — 지원대상·선정기준·목적요약 자유 텍스트에서 연령 조건을 개월로 환산
    • 생후 12~23개월 → 12~23
    • 만 3~5세 → 36~71 (5세 생일부터 6세 생일 전날까지 포함)
    • 만 7세 미만 → 083, 만 8세 이하 → 0107, 만 6세 이상 → 72~무제한
    • 연령으로 보기 어려운 값(2024~2025세, 500개월)과 역순 범위는 버림 — 추정하지 않음
  • PolicyUpsertService 에서 적재 시 적용. 못 찾으면 기존 값을 덮어쓰지 않습니다.
  • Policy 엔티티 필드에 단위(개월) 명시, /policies/age·/policies/child-age 파라미터 설명을 월령으로 수정

테스트 10건 추가 (AgeRangeParserTest), ./gradlew build 통과.

@RosieOh

Copy link
Copy Markdown
ContributorAuthor

공공데이터 4번째 소스 + 수집 공통화 (#63)

데이터 공백FacilityType.KINDERGARTEN 이 정의돼 있는데 이 값을 채우는 경로가 하나도 없었습니다. 어린이집만 동기화되고 유치원 검색은 항상 0건이었습니다.

작업내용
전국 유치원 표준데이터KindergartenSyncService + KindergartenUpsertService. 국공립 여부, 정원·현원, 위경도, 시도·시군구 적재
고유 코드 없는 데이터셋 대응표준데이터는 식별자를 안 주는 경우가 많아 이름+주소 SHA-256 앞 12바이트를 코드로 사용 → 재동기화 시 중복 생성되지 않음
절대 URL 리소스표준데이터는 apis.data.go.kr 이 아닌 다른 호스트라 DataGoKrProvider 가 절대 URL 을 그대로 호출하도록 확장
데이터셋 경로 외부화4개 경로를 프로퍼티로. 공공데이터 오퍼레이션은 개편되므로 재배포 없이 교체 가능

수집 절차 공통화 — 3개 서비스에 복붙돼 있던 페이징·응답 언랩·에러 처리를 PagedSyncTemplate 으로 모으고, 그 김에 취약점 두 가지를 고쳤습니다.

  • 매핑 불일치 조기 중단 — 응답 필드명이 바뀌면 기존에는 200페이지를 돌면서 전건 실패 로그만 쌓였습니다. 이제 첫 페이지가 전멸이면 즉시 중단하고 사유를 남깁니다.
  • 제외/실패 구분 — 필터에 걸린 행(보조금24 비육아 서비스)이 실패와 섞이지 않게 skipped 를 분리했습니다. 필터가 과도한지 판단할 수 있습니다.
  • 첫 행의 실제 응답 필드명을 DEBUG 로 남겨 매핑이 어긋났을 때 바로 진단됩니다.
  • XML 폴백을 템플릿으로 올려 모든 데이터셋에 적용됩니다(기존엔 병원만).

엔드포인트: POST /api/admin/public-data/kindergartens/sync, 스케줄 월요일 04:00.
테스트 16건 추가(PagedSyncTemplateTest 9, KindergartenUpsertServiceTest 6, 절대 URL 1). ./gradlew build 통과.

데이터셋 오퍼레이션 경로와 필드명은 문서 기준으로 넣었고 실호출 검증은 못 했습니다. 서비스 키를 넣고 수동 동기화를 한 번 돌려 보시면, 어긋난 경우 첫 페이지에서 바로 중단되고 DEBUG 로그에 실제 필드명이 찍히므로 프로퍼티만 고쳐 맞출 수 있습니다.

@RosieOhRosieOh added type:feature 새 기능 추가 type:perf 성능 개선 labels Aug 4, 2026
@RosieOhRosieOh added priority:P2 일반 우선순위 domain:data 데이터/시드/마이그레이션 domain:search 검색/추천 labels Aug 4, 2026
@RosieOhRosieOh changed the title FEAT : 공공데이터 공급자 추상화 및 전국 어린이집·정책 연동FEAT : 공공데이터 연동 확대 및 검색·개인화·관측 고도화Aug 4, 2026
@RosieOh
RosieOh merged commit 1746f85 into feat/RosieOhAug 4, 2026
RosieOh added a commit that referenced this pull request Aug 4, 2026
FEAT : 공공데이터 연동 확대 및 검색·개인화·관측 고도화
@RosieOh
RosieOh deleted the feat/public-data-integration branch August 4, 2026 05:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:data데이터/시드/마이그레이션domain:search검색/추천priority:P2일반 우선순위type:feature새 기능 추가type:perf성능 개선

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@RosieOh