All case studies
CurrentBadabomKorea Institute of Marine Science & Technology Promotion (KIMST)Jan 2026 - Present

FRONTEND / COMPONENT

Replacing a Commercial Data Grid With an In-House Component

Every admin list screen hand-rolled its own render loop and paging math, and the rest were locked to a commercial grid licensed per server. I built an in-house grid that absorbs the existing response contracts as-is, then moved roughly 80 screens onto it without changing a single line of server code.

Scope
80+ screens

lists · editors · picker modals

Server changes
0

controllers · JSON untouched

Net code
-733 lines

measured on first 6 screens

Regressions
0

127 specs · 39-screen battery

Vanilla JS (ES5)jQuery 3SheetJSWeb WorkerSiteMesh decoratorKWCAG 2.2eGovFrame

Problem

The same code copied once per screen — and the rest tied to a license

Badabom's admin has dozens of list screens, and no single grid behind them. There were essentially two camps. One was hand-written tables: each screen built <tr> strings in a for loop, copied the pagination markup, computed the descending row-number offset by hand, and assembled a separate array for Excel export. The other was a commercial grid — functional, but licensed per server (IP) with a domain cap, and any environment on a lapsed license showed a trial watermark on screen.

The real cost was not line count but bug surface area. Paging offsets, XSS escaping, Excel-label drift — the exact code people get wrong existed as one copy per screen. A bug found in one place had to be hunted down in all the others. Add a column and you had to edit both the screen code and the Excel builder; someone editing only one of them did actually happen.

admTechInfoList.jsp — before
javascript
// Before — repeated on every screen. Each list carried the whole block.
function drawList(list, totalCnt) {
var html = '';
for (var i = 0; i < list.length; i++) {
var r = list[i];
var no = totalCnt - ((pageNo - 1) * PER_PAGE) - i; // descending row no — easy to get wrong
html += '<tr onclick="goDetail(\'' + r.techSn + '\')">'
+ '<td>' + no + '</td>'
+ '<td class="tit">' + esc(r.techNm) + '</td>' // forget esc() and it is an XSS hole
+ '<td>' + (r.regDt || '').substring(0, 10) + '</td>'
+ '<td>' + badge(r.aprvYn) + '</td>'
+ '</tr>';
}
$('#listBody').html(html || '<tr><td colspan="4">No results.</td></tr>');
drawPaging(totalCnt, pageNo); // pagination markup copied per screen too
$('#totCnt').text(totalCnt);
}
 
// + a separate array-of-arrays builder for Excel export (labels tracked apart from the
// on-screen headers, so adding a column meant editing both — and people edited one)
// + sorting/filtering mostly unimplemented; adding it meant another hand-written copy
  • Render logic duplicated once per screen

    Each list shipped a render loop, pagination markup, total-count update, and Excel assembly as a set. Fixing one bug meant fixing it as many times as there were screens.

  • XSS escaping depended on the developer remembering

    Building rows as strings means a missing esc() call is a live hole. That review item repeated on every screen.

  • Commercial grid: per-server license plus a domain cap

    Adding a server or a domain meant re-running the license math, and dev environments showed the trial watermark right in the UI.

  • Sorting, filtering, and export were inconsistent

    Some screens sorted, some did not, and users kept asking why. Adding it by hand only produced more copies — a dilemma the component was meant to end.

Approach

Designing around one hard constraint: do not touch the server

Buying another commercial grid or adopting an open-source one were both on the table. But both typically ask you to reshape your responses to fit the grid. On an iBATIS + JSP legacy stack, rewriting the response JSON of dozens of controllers is not a risk this system could absorb. So I fixed the first constraint up front: zero lines of server code changed.

I started by cataloguing the admin list ajax contracts to count how many response shapes were actually in use. Three. So the grid got a thin adapter layer — envelope presets — and screens just name their shape; anything unusual passes a function instead. That one layer means the server keeps returning exactly what it already returns.

Before building, I ran a gap analysis across 14 commercial and open-source grids to separate "features we actually use" from "features that only exist in the brochure." That became a P0–P2 roadmap: sorting and paging first, edit mode and virtual scrolling deferred. Trying to build everything up front would never have shipped.

Delivery matched the legacy stack too: no build tooling, one JS file and one CSS file, with the admin console decorator loading both globally. Improve the component once and it lands on every grid at the same time, with no WAR rebuild.

LOAD

ajax response

legacy JSON as-is

adapter

envelope

shape adapter

transform

derived-field hook

DERIVE → RENDER

filter

quickFilter · filterRow

sort

client all / server page

window

virtual scroll slice

render

keyed reconciliation

Envelope adapter

Grid absorbs 3 response shapes — controllers and SQL untouched

No-build deploy

Copy JS/CSS; the decorator loads it on every screen

Gap analysis first

14 grids surveyed, then P0–P2 priorities — not everything got built

Process

How it was built

  1. 01

    Collapsing screens into a column declaration

    What a screen actually does is one sentence: query this URL with this form, and draw these columns. So the module took over the render loop, pagination, empty/error/loading states, total-count updates, and Excel assembly — leaving only a columns declaration on the page. Every default renderer escapes, which moved XSS from "the developer remembered" to a component contract. Excel labels reuse the column titles, so there is nothing left to keep in sync.

    admTechInfoList.jsp — after
    javascript
    // After — same screen. It collapses into one declaration, and the endpoint is untouched.
    var grid = new BdbGrid('#techGrid', {
    url: '/adm/techTrade/techInfo/data.do', // existing endpoint, unchanged
    form: '#listForm', // serialize this form as the request
    envelope: 'listTotal', // just name the response shape
    paging: 'server',
    perPage: 10,
    caption: 'Technology listings', // accessibility caption (sr-only)
    rowKey: 'techSn',
    totalEl: '#totCnt',
    columns: [
    { title: 'No.', type: 'rownum', width: 64 },
    { title: 'Title', field: 'techNm', type: 'title', width: '32%', sortable: true,
    href: function (r) { return '/adm/techTrade/techInfo/edit/view.do?techSn=' + r.techSn; } },
    { title: 'Created', field: 'regDt', type: 'date', format: 'YYYY-MM-DD', width: 110 },
    { title: 'Approved', field: 'aprvYn', type: 'badge', width: 96,
    badge: { 'Y': 'ok', 'N': 'no' } }
    ],
    excel: { fileName: 'tech-info' } // Excel labels = column titles, nothing to sync
    });
     
    $('#searchBtn').on('click', function () { grid.reload(); });
  2. 02

    The envelope adapter — where the server stays untouched

    Surveying the admin list responses turned up three families: the standard {response:{list,totalCnt}}, the array form {response:[...]}, and a small tail. The first two became presets; the tail passes a function. I deliberately kept "interpreting the envelope" and "preprocessing the data" as separate hooks — merge them and a change in response shape breaks your derived-field logic along with it.

    bdbGrid.js
    javascript
    // Envelope presets — the one place that absorbs legacy response shapes, so the server never moves
    var ENVELOPES = {
    // {response:{list, totalCnt}} — the server-paging standard
    listTotal: function (res) {
    var r = (res && res.response) || {};
    var list = r.list || [];
    return {
    list: list,
    totalCnt: (r.totalCnt !== undefined && r.totalCnt !== null)
    ? Number(r.totalCnt) : list.length
    };
    },
    // {response:[...]} — response is the array itself (full-load screens)
    array: function (res) {
    var arr = (res && res.response) || [];
    if (!isArray(arr)) arr = [];
    return { list: arr, totalCnt: arr.length };
    }
    };
     
    BdbGrid.prototype._parse = function (res) {
    var env = this.opts.envelope;
    var fn = (typeof env === 'function') ? env : ENVELOPES[env]; // third shape: pass a function
    if (!fn) fn = ENVELOPES.listTotal;
    var out = fn(res) || {};
    return {
    list: out.list || [],
    totalCnt: (out.totalCnt != null) ? Number(out.totalCnt)
    : (out.list ? out.list.length : 0)
    };
    };
  3. 03

    Discarding stale responses, resetting edit and sort state

    What actually breaks in production is never the flashy feature. Click search twice quickly and the earlier request can arrive last, overwriting the screen with older data. Each request carries a sequence number, and anything that is no longer the newest gets dropped. In the same spirit, new data resets edit state and sort order — while client-side page switches, which never hit ajax, keep the edit state intact.

    bdbGrid.js
    javascript
    // Discarding stale responses — hit search twice quickly and the older reply can land last
    var reqId = ++this._req; // render only while this request is still the newest
     
    $.ajax(ajax).done(function (res) {
    if (reqId !== self._req) return; // a newer request went out — drop this reply
    var out = self._parse(res);
    if (o.transform) out.list = o.transform(out.list, self) || out.list; // derived-field hook
    if (o.tree) out.list = self._treeFlatten(out.list); // tree: DFS flatten
    self._resetEdit(); // new data means edit state resets
    if (o.paging === 'client') {
    self._all = out.list;
    self._resetSort(); // new data means a new original order
    self.total = out.totalCnt;
    self._clientPage(page);
    } else {
    self.total = out.totalCnt;
    self._render(out.list);
    }
    });
  4. 04

    From read-only to editing — the condition for retiring the commercial grid

    Replacing only the lists never retires a commercial grid; what is left is always the editors. So inline editors, C/U/D dirty tracking, derived bulk-save payloads, undo/redo, Excel paste, frozen columns, column show/hide, grouping and tree views all moved into the component — features that usually sit in a higher license tier on commercial products. Only after that could the editing screens migrate.

  5. 05

    Accessibility — tables for reading, widgets only for editing

    This is a public-sector site, so web accessibility is a gate, not a nice-to-have. Read-only lists deliberately stay static-table semantics — caption plus th scope — so screen readers keep their native table navigation, and only editing screens become role="grid" with roving tabindex. Sort state is announced via aria-sort, query and validation results via aria-live. Widgetizing everything looks more impressive, but for read-only lists it is a net loss.

  6. 06

    A regression net: spec runner plus click battery

    The decorator loads this component on every admin screen, which means one component bug is a site-wide outage. So I kept a standing spec runner accumulating assertions and ran a separate battery that walks real screens in a real browser and clicks through them. Each migration batch only proceeded after confirming zero battery regressions following a major version bump. Bulk migrations ran as parallel Claude Code agent batches, with adversarial code review assigned to separate agents as a cross-check.

Outcome

Results and takeaways

Net code reduction per migration batch (first 6 screens, measured)

lines

tech info · deals · showcase

-408

exhibition ×3 (video · event · cmrcl)

-325

total

-733 (995 removed / 262 added)

  • Front end swapped with zero server changes

    Controllers, endpoints, and response JSON stayed exactly as they were. With backend risk at zero I could push the migration in batches and roll back a single screen if something went wrong. Without that property, this scale of change on a live system would not have been attempted.

  • Bug surface went from screens × N down to one

    Paging offsets, XSS escaping, Excel-label sync — the error-prone code now lives in one component. That mattered more than the -733 lines: fix it once and every screen gets the fix.

  • License and watermark dependency removed

    Adding a server or a domain no longer triggers a license recalculation, and the trial watermark is gone from dev environments. It also bought control: neither an abandoned open-source grid nor a vendor license policy change can move this stack now.

  • The key was deciding what not to build

    What the 14-grid gap analysis produced was not a feature list but a list of features we do not use. Chasing the full brochure would still be unfinished today. P0 covered what was genuinely in use; the rest got added when something actually needed it.

  • What is still outstanding, honestly

    Hand-written tables remain on some screens, and a few shared picker modals still run the commercial grid. The UX is identical, so those are being converted opportunistically when the screen is next touched. Meanwhile the enhancers (tooltips, column resize) apply to unconverted tables too, which narrows the gap in the interim.

MORE

Explore other cases

Badabom

AUTH / SSO

Building an SSO Provider for Partner Sites

Implemented an SSO Provider so external partner sites (e.g., OTT) could sign in with Badabom accounts. Single-use UUID tokens stored in the database support multiple WAS nodes, and CI (Connecting Information) auto-maps accounts across both sides.

View detail

Badabom

DEVOPS / OBSERVABILITY

SSE + Cross-WAS Real-Time Log Viewer

The WAS lived in the Daejeon IDC, but network-segregation policy meant only Busan-office PCs could reach it — so pulling a log effectively meant flying to Busan. I built an SSE-based viewer inside the admin web and added a cross-WAS relay so logs from both WAS nodes stream into a single screen.

View detail

Badabom

LEGACY MIGRATION

Migrating the OTT Technology-Trade System into Badabom

Moved an Oracle + MyBatis technology-trade platform (OTT) onto PostgreSQL + iBATIS. Rewrote 87 URLs, 34 JSPs, 80+ SQL queries, and 14 tables.

View detail

Badabom

LEGACY UI / ARCHITECTURE

Wrapping 106 Admin Screens in a Console Shell Without Touching Them

The admin menu had to become a separate-window console, but rewriting ~100 /adm screens was off the table. I converted the existing SiteMesh decorator in place into a child decorator so every current mapping followed automatically, kept the topbar and sidebar resident in a parent shell, and swapped only the iframe body.

View detail

GAIS — Government Advertising Integrated Support System

CI/CD

Automating the Build and Deploy Pipeline

Replaced a fully manual build-and-deploy workflow with a Jenkins + GitLab Webhook pipeline, cutting deploy time from 15–20 min down to around 4 min.

View detail

GAIS — Government Advertising Integrated Support System

INFRA / SESSION

Redis-Backed Session Clustering

JEUS Standard doesn't support native session clustering, so I put Redis in front as an external session store. That unlocked rolling restarts across WAS nodes.

View detail

GAIS — Government Advertising Integrated Support System

SECURITY / NETWORK

Applying TLS 1.3 via an Nginx Reverse Proxy

Touching the shared WebtoB SSL felt risky, so I put Nginx in front and terminated TLS there instead. Existing services kept running untouched while TLS 1.3 was rolled out.

View detail

Freelance · Side Projects

CLIENT WORK / WEB

Pitched and Built a Postpartum Care Center Site Renewal

My wife had stayed at a postpartum care center whose website felt dated, so I mocked up a UI sample and pitched it myself. I built an Astro static site with a 192-frame scroll animation, Kakao Map, and SEO — then shipped it to their production domain.

View detail

Freelance · Side Projects

SIDE PROJECT / AI

Family-Driven Baby Naming with AI + Tournament-Style Voting

Existing naming services are designed for solo use, so I built a way for the whole family to join in. GPT-4o suggests names aligned with Saju (birth-chart) and Ohaeng (Five-Element) rules, and the family votes tournament-style to pick the final name.

View detail
In-House Data Grid Component | Case Study