All work

Bdsillok

A real estate service with 66 tax, loan and auction calculators, a transaction price map and AI summaries for each complex

Period
Since Mar 2026
Role
Side project, the whole service solo
Operated by
EVERYTICK
Commits
391

Stack Django 5.2, DRF, Celery, PostGIS, Redis, Next.js 16, Kakao Maps API, FastAPI, GitHub Actions

Bdsillok map screen. Round clusters showing complex counts are spread over a map of central Seoul.
Transaction price map. It groups the 1,815 complexes in view into clusters

Problem

Korean real estate taxes are calculated differently for acquisition, holding, transfer, gifts and inheritance, and the rates change. Every calculator had to follow the law exactly.

For people choosing a home, calculators alone weren't enough. But public data like transaction prices was hard to trust as is, because of reporting delays, canceled deals and direct-deal outliers.

Goals

  • Offer more than 50 calculators, and update them automatically when tax rates change.
  • Render on the server so pages show up well in search, and support mobile and dark mode.
  • Collect transaction prices, addresses and building data, but cross-check them for reliability before showing them.
  • Give people a reason to come back with a community and articles.

How I got there

  1. Mar 2026

    A shared calculation base first, then tax by tax

    Instead of building each calculator separately, I built a shared calculation base first. On top of it I planned the order: brokerage fees and acquisition tax, holding and transfer tax, gift and inheritance tax, loan limits, units and dates, then finance and auction calculators, with the community and real estate data after that.

  2. May 2026

    Cross-checking public data

    Comparing the transaction, address and geocoding APIs showed that no single source covered everything: geocoders have no lasting building ID, and the transaction data has no normalized address key. So I enriched records with road-name addresses and building registers, and linked transactions to addresses only when the building management numbers matched exactly.

  3. Jul 2026

    Reviewing the roadmap for gaps

    Reviewing the feature roadmap, I confirmed there was still no layer that handles data quality. Rules for filtering out reporting delays, canceled deals and direct-deal outliers are still the next task.

How it works now

Public data is collected and checked, then shown through the calculators and the map.

  1. 1

    It collects MOLIT real transaction prices and enriches them with road-name addresses and building registers. It also imports building footprints from the Ministry of the Interior and Safety.

  2. 2

    Every collection run leaves a record, and transaction prices and road-name addresses are checked against each other.

  3. 3

    Housing subscription notices are merged from 5 sources by priority, with duplicates removed.

  4. 4

    A calculation engine that follows the tax law computes with Decimal. There are 66 calculators.

  5. 5

    AI writes the complex summaries, which are cached for 7 days by a hash of the inputs.

  6. 6

    An LLM drafts the articles, and only those that clear a score threshold are published automatically.

Acquisition tax result. For a 500,000,000 KRW purchase price, it shows acquisition tax of 5,000,000 KRW, local education tax of 500,000 KRW and 5,500,000 KRW total due.
Acquisition tax calculator. It shows the amount for each tax item along with the rate applied
Real estate map QA screen. A notice says the 153,962 complexes in the current map area are shown as 150 neighborhood clusters.
Across a wide area, 150,000-plus complexes are grouped into 150 neighborhood clusters, and individual markers are drawn only when zoomed in

Key decisions

Auto-publish only drafts that pass a score threshold

Why
The LLM only writes drafts. Deterministic code handles publishing, retries and duplicate prevention. A draft is auto-published only if both its intro score and content score are 7 or higher and there are no review warnings. Drafts that fall short are held.
Alternatives
A person approves every draft. One setting can still switch to this mode today.
Trade-off
The scores are the LLM grading itself, so they can be biased.

Cache AI summaries, and fall back to rules on failure

Why
Calling the LLM on every page view is slow, and if the AI server goes down, the page goes blank. With the same inputs, the same summary is reused for 7 days, and if the AI server doesn't respond, a rule-based summary is shown.
Alternatives
Call the LLM directly on every page view
Trade-off
A summary up to 7 days old can show, and the fallback summary is less rich.

Clusters when zoomed out, markers when zoomed in

Why
Drawing every complex in the country at once is more than the browser can handle. The total count of complexes in view is kept in the clusters, and individual markers are drawn only when zoomed in.
Alternatives
Draw markers for every complex at once
Trade-off
When zoomed out, you can't tap an individual complex directly.

Hard problems

This includes problems I caught before release.

A year-over-year holding tax change that was always 0 KRW

At the time, an audit that checked 58 calculation engines against the law, one by one, found 4 P0 bugs. One was in the combined holding tax calculator: its year-over-year change was always 0 KRW, and it had no tax burden cap. It also found that the inheritance tax disability deduction estimated life expectancy as 80 years, and that the rental income tax expense rate came from a single example.

Why it was hard

0 KRW is a plausible value, so it didn't look like an error. And since the previous year's tax was fixed at 0, the result on screen was the same with or without the bug. It was the kind of bug you can only find by checking the formula line by line with the law beside you.

How I traced it

  1. 1. Does the change field actually compare anything?

    It was subtracting the previous year's tax from itself.

  2. 2. Where does the previous year's value come from?

    It was fixed at 0, and there was no way to input it at all.

  3. 3. Checked against the law, what's missing?

    The tax burden cap in Article 122 of the Local Tax Act wasn't implemented.

  4. 4. Do other calculators have the same kind of problem?

    The same method found more P0s in inheritance tax, rental income tax and capital gains tax. In capital gains tax, 8 inputs taken on the screen weren't used in the calculation.

Root cause

The holding tax engine didn't take the previous year's tax as input. It fixed it at 0 and computed the change as 'previous year's tax minus previous year's tax'.

Fix

It now takes the previous year's tax as input and applies a tax burden cap of 105% for a one-home household and 130% otherwise. The change is this year's tax minus the previous year's. In the same audit, I fixed inheritance tax to use Statistics Korea's life expectancy table with linear interpolation, and rental income tax to separate the simple and standard expense rates and compute deemed rent.

Before

previous_year_total = 0
"year_over_year_change": previous_year_total - previous_year_total,

After

previous_year_total = self._to_int(self.inputs.get("previous_year_tax", 0))
tax_burden_cap_rate = 1.05 if is_one_household_one_home else 1.30
if previous_year_total > 0:
    tax_burden_cap_amount = self.floor_to_won(previous_year_total * tax_burden_cap_rate)
    if total_holding_tax > tax_burden_cap_amount:
        tax_burden_cap_applied = True
        total_holding_tax = tax_burden_cap_amount
"year_over_year_change": total_holding_tax - previous_year_total,
The pre-fix code is the lines the audit found

Verification

  • 3 holding tax tests (cap applies when the previous year's tax is given, API alias input, change equals the total when the previous year isn't given)
  • Added 6 inheritance tax, 5 rental income tax and 6 capital gains tax tests
  • All 302 audit tests pass

Prevention

  • Every tax rate that was hardcoded now has a named constant and a legal source.
  • Items outside this scope are logged separately as P3 so the next audit can pick them up.
  • Splitting the housing tax burden cap by official-price bracket is left for the next audit.

Takeaway

A plausible value like 0 KRW doesn't look like an error, so tax calculations get checked against the law line by line.

Open tabs getting stuck on an error screen after a deploy

In a tab opened before a deploy, going to the calculators, community, real estate or My Page sections got stuck on an error screen, and 'Try again' didn't bring it back. It reproduced on every calculator right after a deploy.

Why it was hard

It didn't reproduce in a freshly opened tab. The error screen hid the cause behind a generic message, and the on-screen 'Try again' couldn't fetch files that were already gone.

How I traced it

  1. 1. Is it the API?

    It happened in 4 groups of screens at once. I suspected the browser's file loading before the server.

  2. 2. What went missing?

    When the deploy replaced the container, all the lazy-loaded chunk files from the previous build disappeared.

  3. 3. How can it recover?

    The only way for an open tab to get the new build was a full reload.

  4. 4. How do I confirm it's fixed?

    I used a headless browser to inject chunk 404s into a local production build on purpose. Before the fix, it got stuck on the error screen. After the fix, it reloaded once and then loaded normally.

Root cause

The 4 error boundaries (error.tsx) only logged the error and didn't check whether it was a chunk loading error.

Open tab (previous build)Server
  1. 1Open, holding the previous build's file listPrevious container
  2. 2Deploy, new container, previous chunks deleted
  3. 3Navigates, requests a previous chunk404
  4. 4Chunk loading error, stuck on the error screen
  5. 5(After the fix) Reloads once and gets the new buildServes the new chunks

Fix

On a chunk loading error, it reloads once. It keeps the last reload time in session storage and won't reload again within 30 seconds. If storage isn't available, there's no way to prevent a loop, so it doesn't auto-reload at all.

Before

useEffect(() => {
  console.error("Calculator error:", error);
}, [error]);

After

useEffect(() => {
  if (reloadOnChunkLoadError(error)) return;
  console.error("Calculator error:", error);
}, [error]);

// core of chunk-reload.ts
try { lastReloadAt = Number(sessionStorage.getItem(KEY)) || 0; }
catch { return false; }   // without storage we can't prevent a loop, so don't reload
if (Date.now() - lastReloadAt < 30_000) return false;
sessionStorage.setItem(KEY, String(Date.now()));
window.location.reload();
return true;
A shortened version of the actual code

Verification

  • 8 unit tests (detection by name and message, ignoring unrelated errors, reloading only once, before and after the cooldown, no storage)
  • Injected chunk 404s with a headless browser and compared before and after the fix
  • 1,332 frontend tests, type check, lint and build pass

Prevention

  • A 30-second cooldown keeps reloads from repeating.
  • Where session storage isn't available, it doesn't auto-reload.

Results

Calculators
66
Calculator audit
4 P0 bugs fixed, 302 tests pass
Complex coordinates
173,602 of 190,857 obtained (91%)
Backend tests
1,027 test functions
  • Calculators, the transaction price map, housing subscription notices, a real estate wiki and a community all live on one site.

Next case

Cafe Operations System

There was no way to manage inventory, and the owner was filing taxes without an accountant.