마시는 물 안전 정보

2026-09

마시는 물 안전 정보
TypeScriptExpoReact NativeNext.jsNestJSSQLiteDrizzle ORMTanStack QueryViteRefineTurborepo

흩어진 생수 공개 데이터를 제품과 취수원 기준으로 연결하다

마시는 물 안전 정보는 먹는샘물 제품명으로 검사 결과를 찾아보는 데서 출발해, 그 제품을 만든 원수지의 검사 이력과 업체 위반 이력까지 따라갈 수 있게 한 데이터 서비스입니다. 공공데이터포털의 제품·원수지 수질검사 자료와 환경부의 먹는물영업자 위반 공고를 수집해 모바일 앱과 웹으로 제공합니다.

이 프로젝트의 핵심은 데이터를 가져오는 것보다 서로 다른 원천 레코드가 가리키는 제품·업체를 일관되게 연결하는 것입니다. 제품명·업체명 표기가 출처마다 다르고, 한 제품명이 여러 업체의 검사 기록에 등장합니다. 그래서 원천 레코드를 그대로 덮어쓰지 않고 보존하면서, 사람이 검토하고 조정할 수 있는 정규 제품·정규 원수지 계층을 별도로 두었습니다.

사용자가 확인할 수 있는 정보

앱과 웹의 첫 화면은 제품 목록입니다. 제품명으로 검색하거나 이력 건수 조건을 적용해 살펴볼 대상을 좁힙니다. 목록은 이름, 제품 안전도, 연결 원수지 안전도와 검사·위반 요약을 함께 보여주며, 웹은 화면 아래에 도달하면 다음 페이지를 이어 불러옵니다. 별도 제조업체 화면에서는 원수지 이름으로 검색하고 원수지별 상태를 비교할 수 있습니다.

제품 상세는 위반 이력, 제품 검사, 제조업체 검사 탭으로 나뉩니다. 제품 검사에서는 분기별 검사 기록의 검사일과 항목별 측정·적합 여부를 보고, 부적합 항목만 모아볼 수 있습니다. 제조업체 검사 탭에서는 제품과 연결된 원수지마다 등급과 호정별 검사 기록을 확인합니다. 원수지 상세는 반기별 검사를 호정 단위로 보여주고, 연결 상품과 위반 이력을 함께 제공합니다. 목록 및 상세에서 등급의 산정 시각을 확인하고, 등급 안내와 데이터 출처 설명을 바로 볼 수 있도록 했습니다.

등급은 단일 점수가 아니라 최근 기록의 최악 조건을 반영합니다

서버는 오늘을 기준으로 최근 3년의 **실제 검사(check_at=Y)**와 처분일 기준 위반 이력을 평가합니다. 위반 하나 또는 건강유해 항목의 부적합 하나가 있으면 위험, 심미 항목의 부적합이 있으면 주의입니다. 해당 기간에 실제 검사를 받았고 부적합이 없다면 안전, 실제 검사와 위반 기록이 모두 없다면 정보 없음입니다. check_at=N인 미검사 기록은 이력에서는 확인할 수 있지만 안전 등급의 근거로 사용하지 않습니다.

제품 하나에 여러 원수지가 연결되면 제품 화면의 대표 원수지 등급은 연결된 원수지 등급 중 가장 나쁜 값으로 계산합니다. 사용자는 제품 상세의 원수지 목록에서 각각의 상태를 확인할 수 있습니다. 해외 제조 제품처럼 국내 원수지가 연결되지 않은 경우에도 제품 검사 기록은 조회할 수 있습니다. 등급과 공개 데이터는 참고 정보이며 정부의 공식 판정이 아닙니다. 현재 동기화 코드는 수질검사 자료를 2026년부터 수집하도록 제한합니다. 따라서 그 이전 기록은 이 서비스의 평가에 포함되지 않습니다.

데이터 모델과 자동 연결의 경계

저장 구조는 원천 업체별 product·raw_water 레코드, 정규 제품·정규 원수지, 검사와 항목별 측정값, 위반 공고, 그리고 제품-원수지·위반 연결을 분리합니다. 검사 원본은 원문 업체명과 제품명/호정명, 검사 연도·회차, 채수/수거일, 검사일, 검사기관 등을 담고, 측정 항목은 별도 테이블에서 관리합니다. 정규 대상을 병합해도 검사 수천 건을 다시 쓰지 않고 원천 레코드의 외래 키를 통해 새 묶음에 도달합니다.

자동 정규화는 유니코드 NFC 처리, 법인격 표기 제거, 공백 정리처럼 제한된 이름 규칙만 씁니다. 유사도 추정으로 별개의 대상을 임의 병합하지 않습니다. 이미 만들어진 원천 레코드는 기존 정규 대상 연결을 유지해, 어드민에서 한 번 수정한 결과가 다음 동기화 때 이름 규칙으로 되돌아가지 않게 합니다. 제품의 원수지 자동 연결은 원수지 소유 업체명 매칭에 더해 어드민이 등록한 제품-원수지 규칙을 우선 적용합니다.

위반 공고는 업체명만으로 제품이 확정되지 않는 경우가 있어 기존 정규 대상 중 이름이 일치하는 경우에 한해 자동 연결하고, 해당 연결을 자동으로 표시합니다. 원본이 없는 업체의 정규 대상을 새로 만들지는 않습니다. 어드민에서는 자동 연결을 검토·해제할 수 있고, 자동으로 연결되지 않은 건은 후보를 확인해 수동으로 연결할 수 있습니다. 공표 기간이 지나 환경부 게시판에서 내려간 공고도 DB에서 삭제하지 않고 최초·최근 확인 시각과 함께 보존합니다. 이로써 짧은 공표 기간에만 볼 수 있는 처분 이력을 서비스에서 추적할 수 있습니다.

운영 어드민과 재계산 흐름

어드민에는 정규 제품, 정규 원수지, 위반 공고 각각의 목록·상세 화면이 있습니다. 운영자는 대표 이름 변경, 숨김 처리, 병합·분리, 제품 이미지 지정, 제품에 속한 원천 레코드의 원수지 일괄 지정, 제품-원수지 연결 추가/해제, 위반-제품·원수지 연결 후보 검토 및 수동 연결을 수행할 수 있습니다. 자동 링크와 수동 링크를 구분해 확인할 수 있고, 분리 작업은 원천 레코드를 선택해 새 정규 대상으로 옮깁니다.

안전도는 매일 수질검사 동기화 후 다시 계산합니다. 매일 오전 4시에 수질검사를, 4시 30분에 위반 공고를 수집하고, 어드민의 연결·병합 등 데이터 수정도 안전도 재계산을 트리거합니다. 최근 3년 창이 매일 이동하므로 신규 데이터가 없어도 만료된 기록을 평가에서 제외합니다. 계산 결과와 계산 시각은 정규 제품·원수지에 저장되어 목록과 상세에서 같은 값을 재사용합니다.

시스템 구성

프런트엔드는 Turborepo 모노레포로 관리합니다. Expo Router와 React Native 모바일 앱, Next.js 웹 앱, Vite·React·Refine 운영 어드민이 공통 UI·라이브러리 패키지를 사용합니다. 웹은 TanStack Query로 페이지 데이터와 무한 목록을 관리합니다. NestJS 서버는 서비스 조회 API와 어드민 API를 분리하고 SQLite·Drizzle ORM으로 저장소를 다룹니다. 공공 Open API와 환경부 게시판 수집은 페이지 단위 트랜잭션과 natural key 기반 upsert를 사용합니다. 동일 자료를 다시 받아도 중복 생성하지 않고, 검사 한 건의 항목 집합은 교체해 원천의 최신 값을 반영합니다.

구현에서 해결한 문제

  1. 중복 이름과 실제 정체성의 차이 — 표시 이름만 고치는 것으로는 검사 원본과 관계가 안정되지 않습니다. 원천 레코드와 정규 대상을 나누고, 자동 매칭은 보수적으로 제한하며, 수정 결과는 외래 키와 명시적 연결 규칙에 남겼습니다.
  2. 공개 데이터의 불완전한 관계 정보 — 위반 공고는 기존 정규 대상에 한해서만 이름 기반 자동 연결하고, 자동 연결의 출처를 기록해 어드민에서 검토·수정할 수 있게 했습니다. 제품의 원수지 관계는 규칙 우선, 원천 업체명 매칭 순서로 정리합니다.
  3. 시간에 따라 달라지는 등급 — 최근 3년이라는 이동 창, 검사 미실시, 항목별 위험군, 위반 우선순위를 코드로 명시하고 매일 재계산합니다. 목록에서 빠르게 쓸 수 있도록 결과를 저장하되, 어드민 변경 후에도 갱신합니다.
  4. 게시 종료 뒤에도 필요한 이력 — 위반 공고가 원 게시판에서 사라지는 것을 삭제 신호로 보지 않고, 수집한 이력과 최초/최근 확인 정보를 누적 보존합니다.

⚠️ Warning

공개 자료의 누락이나 게시 시점 차이, 수집 시작 연도 이전의 공백은 남아 있을 수 있습니다. 정보 없음은 안전이 확인됐다는 뜻이 아닙니다. 중요한 판단에는 원 출처의 최신 정보를 함께 확인하세요.