Skip to content

FEAT : 시설 정원 시계열 기반 입소 예측·지원금 발굴·거주지 비교 - #66

Merged
RosieOh merged 68 commits into
mainfrom
feat/growth-moat
Aug 10, 2026
Merged

FEAT : 시설 정원 시계열 기반 입소 예측·지원금 발굴·거주지 비교#66
RosieOh merged 68 commits into
mainfrom
feat/growth-moat

Conversation

@RosieOh

@RosieOhRosieOh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes#65
Closes#67

배경

상용화·투자 관점에서 방어 가능한 기능 세 가지를 넣었습니다. 우선순위는 "지금 시작하지 않으면 나중에 만회할 수 없는 것" 순입니다.

1. 시설 정원 시계열 — 가장 시급

동기화가 현원을 매주 덮어쓰기만 해서 관측 이력이 사라지고 있었습니다. 오늘 기준 스냅샷 하나뿐이라 "언제 자리가 나는가"에 답할 수 없습니다.

시계열은 돈으로 살 수 없습니다. 경쟁사가 내년에 같은 걸 하려 해도 그때부터 쌓아야 하고, 신학기 사이클을 최소 2번 관측해야 예측이 나옵니다. 적재를 시작하는 시점 자체가 경쟁 장벽입니다.

  • TBL_FACILITY_CAPACITY_SNAPSHOT — (시설, 관측일) 고유키라 같은 날 재동기화해도 행이 늘지 않습니다
  • 어린이집·유치원 동기화 양쪽에 연결
  • 스냅샷 기록 실패가 시설 저장을 롤백시키지 않도록 예외를 격리

2. 입소 가능 시점 예측

GET /facilities/{id}/admission-forecast?childAgeMonths=18&horizonMonths=6

관측 구간의 잔여석 비율을 기준선으로 잡고, 자리가 열린 발생률을 포아송(1 - e^(-λt))으로 얹은 뒤 3월 신학기를 보정합니다.

데이터가 부족할 때 그럴듯한 숫자를 만들어내지 않습니다. 관측 4회 미만이거나 60일 미만이면 확률 대신 부족한 이유를 반환합니다. 확률 상한은 95%이고, 근거 문장(reasons)과 신뢰도(LOW/MEDIUM/HIGH)를 함께 내려 과신을 막습니다.

지금 배포하면 전부 "관측 부족"으로 응답합니다. 정상 동작이며, 스냅샷이 쌓이면서 자연히 켜집니다.

3. 놓친 지원금 발굴

GET /policies/missed-benefits

기존 추천은 "지금 받을 수 있는 것"만 봐서 이미 지나간 기회를 못 잡았습니다. 연령 구간이 지난 정책 중 소급 신청이 가능한 것을 역추적합니다.

  • 수급 여부는 알 수 없으므로 "받았다/못 받았다"가 아니라 **"대상이었다"**로만 판정
  • 소급 가능 / 만료로 분류하고 소급 가능 건의 금액을 합산 → 첫 화면 "놓친 지원금 N원"
  • 정책에 소득 상한·자녀수 요건·소급 개월 추가 (V6)
  • 소득 미입력자를 탈락시키지 않고 보류 처리 — 탈락시키면 받을 수 있는 지원금이 통째로 사라집니다
  • 개인화 추천에도 동일 요건 반영
  • 프로필에 소득 비율·가구원 수 입력 경로 추가. 실제 금액이 아니라 기준중위소득 대비 비율만 받습니다(민감정보 최소 수집)

검증

./gradlew build 통과. 신규 테스트 19건 — AdmissionForecastServiceTest(9), MissedBenefitServiceTest(10).

부수 수정

.gitignoreCareCode_FE/ 추가 — 별도 저장소인데 임베디드 리포로 딸려 들어가고 있었습니다.

다음 단계

  • 공공데이터 서비스 키 발급 → 실동기화로 스냅샷 적재 개시
  • 관측 2개 분기 확보 후 예측 정확도 검증

@RosieOhRosieOh added type:feature 새 기능 추가 priority:P1 높은 우선순위 labels Aug 4, 2026
@RosieOhRosieOh added this to the Production Readiness milestone Aug 4, 2026
@RosieOhRosieOh added the domain:data 데이터/시드/마이그레이션 label Aug 4, 2026
@RosieOh

Copy link
Copy Markdown
ContributorAuthor

추가 작업 — 거주지별 지원금 비교 · 충원율 인기도 (#67)

#65 로 쌓기 시작한 정원 시계열 위에서만 가능해진 두 가지를 얹었습니다.

1. 거주지별 지원금 비교 — GET /policies/regional-comparison

같은 아이인데 사는 곳에 따라 받는 돈이 수백만 원씩 다릅니다. 226개 시군구를 비교해주는 곳이 없어 부모가 알 방법이 없던 정보입니다.

  • 전국 정책은 모든 지역에 공통 가산, 지역 정책만 차이로 계산
  • 현재 거주지 대비 차액과 금액 기여 상위 정책을 함께 노출
  • benefitType 표기가 월지급/월지원/월급여/일시지급/서비스제공 등 제각각이라 BenefitPaymentType 으로 판별

과대 계상 방지에 가장 신경 썼습니다. 이 기능은 틀리면 제품 신뢰가 한 번에 끝납니다.

  • 지급 방식이 불명확하면 1회 지급으로 계산 — 월 지급으로 오인하면 5년 기준 최대 60배 부풀려집니다
  • 무료검진·할인·주택공급은 금액에서 빼고 건수로만 표기
  • 응답에 dataQuality: ESTIMATED 와 면책 문구를 항상 포함

2. 충원율 기반 시설 인기도 — GET /facilities/{id}/popularity

평가인증은 대부분 최고등급이라 변별력이 없고 리뷰는 조작될 수 있지만, 충원율은 공공데이터가 원천이라 시설이 개입할 수 없습니다.

  • 평균 충원율 / 만원 유지 비율 / 추세(RISING·STABLE·FALLING)
  • 급락 시점 감지 — 운영 변화 신호. 3월 신학기 전환은 정상 변동이라 제외
  • 관측 4회 미만이면 판단하지 않고 부족 사유 반환

추가 데이터 수집 없이 #65 의 스냅샷 테이블만으로 나옵니다.

검증

테스트 31건 추가 — BenefitProjectionCalculatorTest(15), RegionalBenefitComparisonServiceTest(10), FacilityPopularityServiceTest(10). ./gradlew build 통과.

남은 일

지자체 정책 수집 정확도가 이 기능의 전부입니다. 상위 30개 지자체는 수기 검증 후 VERIFIED 로 구분하고 나머지는 추정치로 두는 편이 안전합니다.

@RosieOhRosieOh added the domain:search 검색/추천 label Aug 4, 2026
@RosieOhRosieOh changed the title FEAT : 시설 정원 시계열·입소 예측·놓친 지원금 발굴FEAT : 시설 정원 시계열 기반 입소 예측·지원금 발굴·거주지 비교Aug 4, 2026
@RosieOh

Copy link
Copy Markdown
ContributorAuthor

상용화·투자 실사 기반 (#68)

기능은 충분히 쌓였지만 상용화에 필요한 기반이 비어 있었습니다. 코드베이스 확인 결과 넷을 채웠습니다.

1. 사용자가 뭘 하는지 전혀 측정하지 않고 있었습니다

analytics·eventLog·trackEvent 검색 결과 0건. 가입 후 자녀 등록 전환율도, "놓친 지원금 → 신청" 전환율도 알 수 없었습니다.

  • TBL_USER_EVENT + EventLogger — 전용 스레드풀, 큐 폭주 시 이벤트를 버리고 요청 스레드는 붙잡지 않음
  • 퍼널: 가입 → 자녀 등록 → 놓친 지원금 확인 → 신청 링크 클릭
  • 앞 단계를 거친 사용자만 세어 전환율 산출. 단순 이벤트 수를 나누면 전환율이 아닙니다
  • 코호트 리텐션 D1/D7/D30 — 아직 오지 않은 날짜는 0%가 아니라 미집계로 구분(0%로 두면 리텐션이 폭락한 것처럼 왜곡됨)
  • GET /policies/{id}/apply 리다이렉트로 클릭 집계. 외부 URL 스킴을 검증해 javascript: 주입 차단
  • GET /api/admin/analytics/{funnel,retention,events}

오늘부터 쌓여야 3개월 뒤 보여줄 숫자가 생깁니다.

2. 건강정보가 일반 개인정보 동의에 묶여 있었습니다

이 서비스는 아동의 키·몸무게·접종이력·진료기록을 다룹니다. 개인정보보호법상 민감정보라 별도 동의가 필요한데PRIVACY_POLICY 안에 포함돼 있었고, 동의 여부를 확인하지도 않았습니다.

  • ConsentType.HEALTH_DATA 신설 + sensitive 플래그
  • ConsentGuard — 동의 없거나 철회 시 건강 기록 생성 차단. 받아두기만 하고 강제하지 않으면 받지 않은 것과 같습니다
  • 403 + CONSENT_REQUIRED + 필요한 동의 항목을 응답에 담아 클라이언트가 동의 화면을 띄울 수 있게

3. 금액 정확도 안전장치

"이사하면 3,360만원 더"가 틀리면 버그가 아니라 분쟁입니다.

  • verifiedAt/verifiedBy/sourceUrl 추가 (V9)
  • 지역별 VERIFIED/PARTIAL/ESTIMATED금액에 들어간 정책이 전부 검증돼야 확정 표기
  • 검증 API + 지역별 검증률 현황(미검증 많은 지역 순)

4. 새벽에 동기화가 깨져도 아무도 몰랐습니다

  • OperationalAlerter — 동기화 미완료·건별 실패·미처리 5xx 를 Slack 으로
  • 30분 쿨다운 — 같은 장애로 알림이 쏟아지면 아무도 안 보게 됩니다
  • 웹훅 미설정 시 로그만 남기고 조용히 비활성

검증

테스트 14건 추가. ./gradlew clean build 통과.

동의 게이트가 기존 HealthServiceTest 를 막았는데, 이는 의도한 동작이라 테스트에 ConsentGuard mock 을 추가해 맞췄습니다.

코드로 해결되지 않는 남은 것

  • data.go.kr 활용신청 4건 → 실동기화 (현재 최대 병목)
  • 개인정보처리방침 실물 문서
  • 상위 30개 지자체 금액 수기 검증

@RosieOh

Copy link
Copy Markdown
ContributorAuthor

회귀 방지 + 알림 루프 2종 추가

왜 오늘 문제들을 CI 가 못 잡았나 (#72)

통합 테스트 4개가 전부 ddl-auto=create-drop 이었습니다. Hibernate 가 엔티티로부터 스키마를 만들어 내니, Flyway 가 아무리 어긋나도 100% 통과합니다. 테이블 없는 정책 북마크가 그 상태로 계속 초록불이었습니다.

  • FlywaySchemaValidationTest — 운영과 같은 방식(Flyway 전체 + validate)으로 띄웁니다. 마이그레이션 15건 성공/0 실패, validate 통과를 실제 MariaDB 로 확인했습니다.
  • AccessControlContractTest — 규칙을 읽는 대신 실제 응답 코드를 봅니다. 23개 전부 통과, skip 0 (H2 라 Docker 없이도 돕니다). 오늘 고친 병원 401 문제를 이 테스트가 잡습니다.
  • Testcontainers 는 Docker 가 없으면 조용히 skip 되고 빌드는 초록불이 되므로, CI 에서 실행 여부까지 확인합니다.

대기 → 빈자리 알림 (#73)

정원 스냅샷은 자리가 난 걸 알고 있었고 대기 명단도 있었는데 둘이 이어져 있지 않았습니다. 입소 예측까지 만들어 놓고 정작 예측이 맞았을 때 알려주지 않았습니다.

실기동 검증: 대기자 1명 · 빈자리 0 → 3 변화 → 알림 1건 발송, 재실행 0건, 대기 기록에 발송일 기록됨.

공공데이터는 시설 전체 정원만 주므로 어느 반에 자리가 났는지는 알 수 없습니다. 반이 다르면 헛걸음이라 그 한계를 알림 문구에 그대로 밝혔습니다.

신청 마감 임박 알림 (#74)

지금까지는 MissedBenefitService이미 놓친 것을 사후에 알려줬습니다. 놓치기 전에 막는 편이 낫습니다.

실기동 검증: D-7 정책 2건(청주·제주) 중 지역이 맞는 1건만 발송, D-5 는 제외, 이후 15회 재실행 모두 0건.

⚠️ 중복 발송에서 실제 결함을 하나 잡았습니다. 처음엔 "남은 일수가 D-7 인 날에만 보내니 하루 한 번" 으로 설계했는데, 이 배포는 Blue/Green 이라 인스턴스가 잠깐 2대가 되면 모든 알림이 두 번씩 나갑니다. 발송 이력 테이블(V17)과 유니크 제약을 추가했습니다. 지원금 알림은 한 번 더 오는 순간 신뢰를 잃습니다.

또 스케줄러와 서비스가 같은 결과를 두 번 로그하고 있어 마치 두 번 실행된 것처럼 보였습니다(제가 실제로 그렇게 오독했습니다). 기존 4개 작업 모두 같은 문제라 함께 정리했습니다.

검증

./gradlew clean build 통과. prod 프로파일 실기동에서 마이그레이션 17건 적용, 공개 6개 200 / 보호 3개 401.

계층 구조와 배치 순서를 Mermaid 로 남기고 편집 가능한 draw.io 원본을 함께 둔다. 배치 순서에는 이유가 있다. 수집이 먼저고 발송이 나중이어야 그날 들어온 데이터로 알림이 나간다.
무엇을 만들었는지가 아니라 왜 그렇게 만들었는지를 남긴다. 대부분의 판단은 '공공데이터가 무엇을 주지 않는가' 에서 출발했다. 반별 정원을 주지 않아 빈자리 알림에 한계를 명시하고, 금액을 숫자로 주지 않아 제보와 검증을 만들었다.
테스트가 전부 통과하는데도 애플리케이션이 한 번도 기동한 적 없던 이유와, 통합 테스트가 create-drop 이라 스키마 어긋남을 구조적으로 못 잡던 문제를 남긴다.
SecurityConfig 는 앞선 규칙이 뒤를 덮고 클래스 레벨 @PreAuthorize 가 다시 덮는다. 규칙만 읽어서는 실제로 열렸는지 알 수 없어 의도를 문서로, 사실을 테스트로 나눠 남긴다.
@RosieOh

Copy link
Copy Markdown
ContributorAuthor

설계 문서 정리 (#75)

#65 ~ #74 의 기능과 판단이 커밋 메시지와 이슈 본문에만 흩어져 있어서, 새로 합류하는 사람이 "왜 이렇게 만들었는지" 를 알려면 51개 커밋을 역순으로 읽어야 했습니다. docs/ 에 기능별로 정리했습니다.

구조

분류문서
아키텍처시스템 개요, 데이터 흐름, draw.io 원본
기능공공데이터 · 지원금 · 시설 · 알림 · 지표 · 개인정보 · 운영
품질기동 안정화, 회귀 방지
레퍼런스마이그레이션 V1~V17, 접근제어 매트릭스

Mermaid 31개(기존 포함 42개)와 draw.io 2시트(시스템 구성 / 리텐션 루프)입니다.

무엇을 남겼나

기능 목록이 아니라 코드만 봐서는 알 수 없고 잘못 건드리면 되돌아오는 판단을 남겼습니다.

  • 빈자리 알림은 "있다" 가 아니라 "늘었다" 로 판단한다 — 계속 자리가 있는 시설을 매일 알리면 스팸
  • 소득 미입력을 탈락으로 처리하지 않는다 — 받을 수 있었던 지원금이 통째로 사라짐
  • 융자를 지원금 총액에 넣지 않는다 — 갚아야 하는 돈은 받는 돈이 아님
  • 표본이 부족하면 확률을 만들어내지 않는다 — 그럴듯한 숫자를 믿고 다른 시설을 포기한 부모에게는 피해
  • 빈자리 알림 문구에 "시설 전체 기준" 한계를 밝힌다 — 반이 다르면 헛걸음
  • 마감 알림에 유니크 제약이 필요하다 — Blue/Green 이라 인스턴스가 2대가 됨
  • 테이블명은 대문자 — Linux MariaDB 는 대소문자를 구분 (Windows 에서는 안 드러남)
  • 404·403 은 운영 알림을 보내지 않는다 — 진짜 장애가 소음에 묻힘

각 문서 끝에 미해결 항목을 남겨 두었습니다. 어린이집 운영키, 개인정보 처리방침 법률 검토, 배타 그룹 데이터 입력, 검색어 로그 등입니다.

검증

문서에 적은 엔드포인트 경로·설정 기본값·cron 표현식을 코드와 대조했습니다. 실제와 달랐던 5개 경로(waitlist/mywaitlist/me 등)를 코드 기준으로 교정했습니다. 내부 링크 깨짐 0건입니다.

설정 행이 없을 때 실제로 발송되는 채널에는 이메일이 없는데 기본 행만 이메일을 켜고 있었다. 사용자가 다른 채널 하나를 끄는 순간 기본 행이 만들어지면서 요청한 적 없는 이메일 알림이 켜졌다. 모두 끄기도 저장된 행만 꺼서, 설정을 건드린 적 없는 사용자는 눌러도 알림이 계속 왔다.
FCM 자격증명이 없으면 푸시가 비활성화되는데 설정 화면에서는 이유를 알 수 없어 사용자가 자기 문제인 줄 안다. 왜 못 쓰는지는 채널마다 달라 발송기 자신만 안다. 채널 이름은 설정 변경 API 가 받는 값과 같아야 해서 enum 이름 대신 별도 키를 둔다.
전체 교체만 있어서 금액 하나를 고치려 해도 모든 값을 담아야 하고, 빠뜨리면 데이터가 지워졌다. 값의 null 여부만 보면 비우기와 건드리지 않기를 구분할 수 없어 요청 JSON 에 키가 왔는지를 본다.
1:1 로 적혀 있었으나 실제로는 알림 유형별 한 행이라 1:N 이다. 누락됐던 인앱 활성화 컬럼도 채운다.
디바이스 토큰은 기기의 성질이지 알림 유형의 성질이 아닌데 설정 행마다 들고 있다. 등록은 SYSTEM 행에만 쓰므로 유형별 행에서 찾으면 정책·시설 알림은 토큰을 못 찾아 푸시가 누락된다. 유형과 무관하게 찾는다.
엔티티는 false 로 고쳤는데 DDL 은 TRUE 로 남아 있었다. JPA 는 값을 항상 명시해 쓰지만 시드 스크립트나 수기 SQL 은 기본값을 타므로 그 경로로 같은 버그가 재현된다.
문구는 바뀔 수 있어 클라이언트가 문자열로 판단하면 안 된다. 서버 설정 문제와 보낼 곳 없음은 사용자가 할 수 있는 일이 다르다. 둘 다 문제일 때 번호를 등록하라고 안내하면 등록하고도 알림을 못 받는다.
마이그레이션 번호를 문서 네 곳에 박아 두어 추가할 때마다 전부 손봐야 했다. 엔티티 기본값만 고치면 절반만 고친 것이라는 점도 함께 남긴다.
섭취를 기록하는 수단이 없는데 상수 85 를 돌려주어 모든 사용자가 자기 아이의 영양 상태를 85% 로 봤다. 근거 없는 숫자는 없는 것보다 나쁘다. 목표 문구는 남기고 달성률만 뺀다.
API 문서는 springdoc 이 런타임에 생성한다. .adoc 도 사용처도 없이 실행 jar 에 들어가 있었고 JRuby 까지 끌고 와서 191MB 를 156MB 로 줄였다. 취약점 스캔 표면도 함께 줄어든다.
traceId 를 @LogExecutionTime 안에서만 넣어서 인증 실패나 없는 경로처럼 컨트롤러 전에 끝나는 요청은 추적할 수 없었다. 보안 필터보다 먼저 돌려 401 에도 남기고, 응답 헤더와 오류 본문 양쪽에 담는다. 사용자는 화면을 캡처해 보내는데 헤더는 캡처에 안 나온다.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:data데이터/시드/마이그레이션domain:search검색/추천priority:P1높은 우선순위type:feature새 기능 추가

Projects

None yet

1 participant

@RosieOh