← 주요 프로젝트← Key projects

개인 프로젝트

PERSONAL PROJECT

DEMOCRACY

DEMOCRACY

주소로 지역구 의원을 찾고 법안, 표결과 출결을 확인하는 Flutter 앱입니다. 국회와 선관위 데이터를 수집해 출처와 함께 보여줍니다.

A Flutter app for finding district lawmakers by address and checking bills, votes and attendance with National Assembly and election sources.

FLUTTERRIVERPOD 3SUPABASEDENOPOSTGRESQLGITHUB ACTIONS
담당 작업Work involved

Flutter 앱, Supabase 서버, 공공데이터 수집과 CI

Flutter app, Supabase backend, public-data collection and CI

현재 상태Current status

Supabase 서버 배포 · 스토어 출시 전

Supabase backend deployed; app store release pending

01

프로젝트 개요

DEMOCRACY의 문제 정의와 앱·서버 범위

DEMOCRACY는 주소나 현재 위치로 지역구 의원을 찾는 Flutter 앱이다. 열린국회정보와 중앙선거관리위원회 데이터를 수집해 의원의 법안, 표결, 본회의 출결과 역대 선거 결과를 출처와 함께 보여준다.

핵심 기능

  • 주소·현재 위치로 국회의원 선거구 판정
  • 의원별 대표 발의 법안, 표결, 본회의 출석률과 출처 표시
  • 20~22대 당선인과 22대 개표 결과, 선거구 역사
  • 공직선거법 공표 제한 기간에 맞춘 결과 표시 제한
  • Apple·카카오·Google·이메일 로그인, 계정 삭제와 데이터 내보내기

담당 영역

1인 개발로 Flutter 앱, Supabase 스키마와 Edge Function, 공공데이터 수집 스크립트, CI를 모두 맡았다.

현재 상태: Supabase 서울 리전에 배포되어 실데이터(지역구 254개, 의원 299명)를 제공한다. 스토어 출시 전이며 실시간 개표와 후보 성향 분석은 준비 중이다.

01

Overview

Problem and app-server scope of DEMOCRACY

DEMOCRACY is a Flutter app for finding district lawmakers from an address or current location. It collects National Assembly and National Election Commission data and shows bills, votes, plenary attendance and past election results with sources.

Core capabilities

  • District resolution from an address or current location
  • Bills, votes and plenary attendance per lawmaker, each with its source
  • Winners of the 20th–22nd elections, 22nd count results and district history
  • Result display limited by the Public Official Election Act publication windows
  • Apple, Kakao, Google and email sign-in, account deletion and data export

Ownership

As the only developer, I built the Flutter app, Supabase schema and Edge Functions, public-data ingestion scripts and CI.

Current status: Deployed in the Supabase Seoul region with live data (254 districts, 299 lawmakers). Not yet released to stores; live counting and candidate analysis are in progress.

02

아키텍처

공공 API, Supabase BFF와 Flutter 앱의 경계

1. 앱·서버 경계

앱은 공공 API를 직접 호출하지 않는다. 모든 데이터는 Supabase Edge Function bff를 거치며, 앱의 fixture JSON이 곧 BFF 응답 계약이다. 서버 contract.ts가 Dart 파서를 그대로 따라 두 쪽 모양이 어긋나지 않게 한다.

2. 데이터 수집

ingest-assembly와 ingest-nec 함수가 열린국회정보 4개 서비스와 선관위 6개 오퍼레이션을 pg_cron 9개 잡으로 수집한다. Open API가 없는 본회의 출결은 회기별 xlsx를 받아 합계 열로 검산한 뒤 적재한다.

3. 주소 → 선거구 매핑

선거구 법령 표는 2024.04.10 기준 행정동으로 쓰여 있다. build_district_areas.ts가 현재 행정동 코드를 코드 일치, 같은 시군구 안 동 이름, 법정동 겹침 순으로 매칭해 3,628개를 모두 배치하고, 애매한 경우는 추측하지 않고 멈춘다.

4. 공표 제한을 타입으로

Restricted<T>는 sealed 타입이고 값을 꺼내는 getter가 없다. 공개 불가 분기를 처리하지 않으면 컴파일되지 않는다. 시각은 오프셋이 있는 KstInstant로만 다루고, 투표 마감 시각은 서버 값에서 받는다.

5. 앱 구조

기능마다 data·domain·application·presentation 4계층을 두고 Riverpod 3와 go_router 셸로 조립한다. 재시도는 연결 끊김에만 적용해 404 화면이 로딩에 갇히지 않게 했다.

6. 신뢰 경계와 보안

지역구는 서버가 juso·V-World로 직접 판정하고 클라이언트가 보낸 값은 무시한다. 토큰은 SHA-256만 저장하고 주소·좌표는 저장하지 않는다. 모든 테이블에 RLS를 켜고 정책을 두지 않아 service role만 접근한다.

7. 캐시와 오프라인

BFF는 경로별로 프로필 300초, 커뮤니티 15초, 계정 no-store 캐시를 쓴다. 앱은 네트워크가 끊기면 마지막 응답을 보여준다.

8. 남은 과제

실시간 개표 스트림, 서버 측 공표 차단, 후보 성향 분석은 미구현이다. 공약 데이터는 API가 없어 일부 지역구만 수동 입력되어 있다.

02

Architecture

Boundaries between public APIs, the Supabase BFF and the Flutter app

1. App-server boundary

The app never calls public APIs directly. All data goes through the bff Edge Function, and the app’s fixture JSON is the BFF response contract. The server’s contract.ts mirrors the Dart parsers so the two sides cannot drift.

2. Data ingestion

ingest-assembly and ingest-nec collect four National Assembly services and six NEC operations through nine pg_cron jobs. Plenary attendance has no open API, so per-session xlsx files are downloaded, checked against their total columns and loaded.

3. Address → district mapping

The district table in law is written against administrative dongs as of 2024-04-10. build_district_areas.ts matches today’s dong codes by code, then dong name within the same district, then legal-dong overlap, placing all 3,628 and stopping instead of guessing on ambiguity.

4. Publication limits as types

Restricted<T> is a sealed type without a value getter, so code that skips the withheld branch does not compile. Time is handled only as offset-aware KstInstant, and poll closing time comes from the server.

5. App structure

Each feature has data, domain, application and presentation layers composed with Riverpod 3 and a go_router shell. Retries apply only to dropped connections so 404 screens do not hang in loading.

6. Trust boundary and security

The server resolves districts itself through juso and V-World and ignores client-sent values. Only SHA-256 hashes of tokens are stored, never addresses or coordinates. Every table has RLS enabled with no policies, so only the service role can read it.

7. Caching and offline

The BFF caches per route: profiles 300 s, community 15 s, account no-store. When offline, the app shows the last response.

8. Open work

Live count streaming, server-side publication blocking and candidate analysis are not implemented. Pledge data has no API and is entered manually for a few districts.

구조와 데이터 흐름

Structure and data flow

도식을 누르면 크게 볼 수 있습니다. 구현 여부와 참고한 코드도 표시했습니다.

Select a diagram to enlarge it. Labels show implementation status and source files.

01
시스템 컨텍스트 · 신뢰 경계System context · trust boundaries유권자, Flutter 앱, 공공 API와 서버 신뢰 경계를 나눕니다.Separates voters, the Flutter app, public APIs and the server trust boundary.
선택하면 전체 화면에서 세부 구조와 근거 번호를 볼 수 있습니다.Select to inspect the structure and evidence references full screen.
02
런타임 · 모듈 · 상태 소유권Runtime · modules · state ownership4계층 앱 구조, Riverpod 상태와 BFF 계약을 분해합니다.Decomposes the four-layer app, Riverpod state and BFF contract.
선택하면 전체 화면에서 세부 구조와 근거 번호를 볼 수 있습니다.Select to inspect the structure and evidence references full screen.
03
핵심 사용자 흐름 · 요청 시퀀스Core user flow · request sequence주소 입력이 서버 판정 지역구와 의원 활동 화면으로 이어지는 흐름입니다.Flow from an address to a server-derived district and lawmaker activity.
선택하면 전체 화면에서 세부 구조와 근거 번호를 볼 수 있습니다.Select to inspect the structure and evidence references full screen.
04
데이터 · 배포 · 보안 · 복구Data · delivery · security · recoverypg_cron 수집, RLS, CI 3개 잡과 실패 시 멈춤 규칙을 표시합니다.Shows pg_cron ingestion, RLS, three CI jobs and stop-on-ambiguity rules.
선택하면 전체 화면에서 세부 구조와 근거 번호를 볼 수 있습니다.Select to inspect the structure and evidence references full screen.
03

기술적 결정

BFF, 타입 강제와 서버 판정에 관한 선택

앱이 공공 API를 직접 부르지 않음

API 키를 앱에 넣지 않고, 기관마다 다른 응답 형식을 서버 한 곳에서 정규화하기 위해 BFF를 두었다. 대신 서버 배포와 계약 테스트를 함께 관리해야 한다.

법 규칙은 위젯 조건문이 아니라 타입으로

공표 제한을 화면마다 if로 막으면 새 화면에서 빠뜨리기 쉽다. sealed 타입으로 만들어 컴파일러가 누락을 잡게 했다.

애매하면 멈추는 데이터 적재

선거구 매핑과 출결 적재는 확신이 없는 경우 추측해서 채우지 않고 중단한다. 적재가 멈출 수 있지만 틀린 정치 정보를 내보내는 것보다 낫다고 판단했다.

지역구는 서버가 판정

클라이언트가 보낸 지역구를 믿으면 다른 지역구 커뮤니티에 글을 쓸 수 있다. 서버가 주소를 직접 판정하고 주소 자체는 저장하지 않는다.

03

Technical decisions

Choices around the BFF, type enforcement and server-side resolution

The app does not call public APIs

A BFF keeps API keys out of the app and normalizes each agency’s response format in one place. The cost is managing server deployment and contract tests together.

Guarding publication limits with an if on each screen is easy to miss on a new screen. A sealed type lets the compiler catch the omission.

Stop on ambiguous data

District mapping and attendance loading stop instead of guessing when unsure. Ingestion can halt, but that is better than publishing wrong political data.

Districts are resolved by the server

Trusting a client-sent district would let users post in another district’s community. The server resolves the address itself and never stores it.

04

검증

테스트, CI와 실데이터 확인

자동 검증

  • Dart 테스트 468개, Deno 테스트 115개, 390dp 골든 이미지 38장
  • CI 3개 잡: format·analyze·test, macOS 골든, deno task ci
  • DateTime.now() 직접 사용을 막는 테스트, 응답에 API 키가 섞이지 않는지 확인하는 계약 테스트

실데이터 확인

  • 첫 적재에서 선거구에 매칭되지 않은 지역구 의원 20명을 찾아 세종 표기와 통합특별시 규칙을 고친 뒤 0명으로 만들었다.
  • 도로명 주소 API의 행정동 이름 형식이 fixture와 달라 빠지던 주소를 실응답 기준으로 수정했다.

한계

사용자 대상 검증과 운영 지표는 아직 없다.

04

Validation

Tests, CI and live-data checks

Automated checks

  • 468 Dart tests, 115 Deno tests and 38 golden images at 390dp
  • Three CI jobs: format, analyze and test; macOS goldens; deno task ci
  • A test blocking direct DateTime.now() use and a contract test making sure API keys never leak into responses

Live-data checks

  • The first load left 20 district lawmakers unmatched; fixing Sejong naming and integrated-city rules brought it to zero.
  • The address API returned dong names in a different form than the fixtures, dropping addresses; parsing now follows the live response.

Limits

There is no user testing or production metric yet.

05

로드맵

DEMOCRACY의 완료 기능과 다음 단계

완료

  • 주소·위치 기반 선거구 판정과 지역구 홈
  • 의원별 법안·표결·출결과 출처 표시
  • 20~22대 당선인, 22대 개표 결과, 선거구 역사
  • 로그인 4종, 계정 삭제와 데이터 내보내기
  • Supabase 서울 리전 배포와 CI

다음 단계

  • 실시간 개표와 서버 측 공표 차단
  • 공약 데이터 수집 범위 확대
  • 스토어 출시
05

Roadmap

Completed features and next steps for DEMOCRACY

Done

  • District resolution from address or location, and a district home
  • Bills, votes and attendance per lawmaker with sources
  • 20th–22nd election winners, 22nd count results and district history
  • Four sign-in methods, account deletion and data export
  • Deployment in the Supabase Seoul region with CI

Next

  • Live counting and server-side publication blocking
  • Wider pledge data coverage
  • Store release
확대 보기Expanded view

도식을 좌우로 이동하거나 확대해 세부 흐름을 확인할 수 있습니다.

Pan or zoom the diagram to inspect the detailed flow.