Minhu Park

Looking for the next problem to solve

Complex problems,simple structures.

Minhu Park — Software Engineer. For the past two years I've built a clinic CRM across the stack — from a C#/WPF desktop app through a React web migration to Kotlin backend APIs. When I find a structural problem I propose a fix, and I automate whatever repeats.

About

I've been building a clinic CRM at SmartDoctor since 2024. I started out maintaining the desktop app (C#/WPF), took on the design and implementation of my modules in the web migration (React), and now build the backend APIs (Kotlin/Spring) as well. I believe development moves faster and stays more consistent when one person can handle everything from the frontend to the backend, and that's how I work today.

I prefer fixing structures over symptoms. I rebuilt a flow that re-fetched an entire screen whenever a single reservation changed into single-item updates, and isolated the 32-bit-only integration modules that blocked a 64-bit migration into a separate process, so the same problems don't come back.

I automate repetitive work. I moved release tagging, Jira version management, and reviewer assignment onto GitHub Actions, and brought Sentry and a load-time monitoring bot into an environment where errors used to be diagnosed from customer screenshots. I enjoy building tools that give the team its time back.

Skills

Languages
TypeScriptC#KotlinSQL
Frontend
ReactNext.jsZustandTailwind CSSFSD architecture
Backend
SpringJPA/HibernateKafkaWebSocketMSSQLAWS S3
Desktop
.NET · WPF · WinFormsWebView2 bridgeInter-process communication (IPC)
Tooling · Observability
GitHub ActionsSentryGA4Jira automation

Experience

  1. Aug 2024 — Present

    SmartDoctor

    Software Engineer — Clinic CRM

    • Started with maintenance and feature work on the desktop CRM (C#/WPF), designed and built my modules in the web migration (React), and now develop the backend APIs (Kotlin/Spring) directly, expanding my scope across the stack
    • Owned the monthly reservation calendar, the clinical records screen, and a treatment history tool in the web migration, and split month-wide fetches into parallel weekly requests that can reuse the server’s date-range cache
    • Rebuilt the desktop refresh flow that re-fetched everything on a single reservation change into single-item updates, and proposed and led replacing the embedded browser (CefSharp → WebView2)
    • Ported three desktop-only screens with an 'add API → standalone web app → webview embed' pattern — cross-stack development across the web, API, and desktop codebases
    • Consolidated call-center option lookups into a batch API: 12 initial requests became 1 in a replay using the category count observed in production. A separate replay with 1,000 matched phone numbers eliminated 1,000 follow-up customer searches
    • Built mobile operations screens for Cashdoc’s clinic CMS and connected consultation notifications across four repositories. Staged deployment as schema → API → frontend → ingestion, so reverting the final ingestion step preserves the previous behavior
    • Replaced screenshot-based error reports with automated collection and alerting by introducing Sentry and building a GA4-based load-time monitoring bot
    • Built the release and review automation: automatic rc/hotfix tagging, Jira release creation, cherry-pick chaining across rc branches, and an AI code-review bot

    TypeScript / React / C# / .NET / Kotlin / Spring / Kafka / MSSQL / GitHub Actions / Sentry

Projects

At work

Desktop-to-Web CRM Migration

2025 — Present

SmartDoctor · Module design & implementation, plus the runtime foundation

A long-running migration of the desktop CRM's reservation and clinical screens to the web. I designed and built the reservation calendar and clinical records screen, alongside authentication, the native bridge, and WebView2 runtime deployment. I then established an 'add API → standalone web app → webview embed' pattern and ported three desktop-only screens.

Cross-stack development spanning the web, API, and desktop codebases

React / TypeScript / Zustand / Kotlin / C# / WebView2

Desktop-to-Web CRM Migration

SmartDoctor · Module design & implementation, plus the runtime foundation · 2025 — Present

A long-running migration of the desktop CRM's core screens to the web, starting with the reservation calendar and ending with a repeatable pattern applied to three more screens.

Desktop-to-Web CRM MigrationMy scopeembedsbridgeRESTDesktop CRMC# · WPFWebView2 hostWeb appReactAPIKotlin · SpringMSSQL
A WebView2 host inside the desktop CRM serves the web app, which reaches the database through a newly built API. The web app and the API are my scope.

Background

The desktop CRM is a C#/WPF application used every day by clinic front-desk and consultation staff. Migrating its core screens to the web has been running since 2025 and is still underway, starting as a desktop-and-web effort before growing to include the backend API codebase as well.

The approach was to embed a WebView2 host inside the desktop CRM and move screens to the web one at a time, rather than rewriting the whole desktop app at once. Each screen could go live as soon as its port was done, instead of waiting on one big release.

What I built

I started with the monthly reservation calendar — the "month view" — designing and building its layout, cell grid, department infinite-scroll, and column-width logic. From there I kept owning whatever screen came next in the migration, design through implementation.

The month view originally fetched an entire month's reservations in one request. I split it into parallel weekly requests that can reuse the server’s date-range cache. Replaying the original and changed code confirmed identical reservation results when a 35-day range was split into five seven-day requests.

Because the web app runs inside a desktop webview, I also built the runtime foundation alongside the screens: refresh-token authentication, a bridge for events like settings changes between desktop and web, and pinned WebView2 runtime deployment after recurring runtime errors on certain hospital PCs.

Representative module: clinical records and data integrity

I built the clinical records screen, the largest module I owned in the web migration, connecting diagnosis and prescription entry, fee calculation, treatment-pass usage, and saving. A duplicate-save guard and a form refactor that established a single source of truth resolved repeated-save and popup-state issues.

External integration and a verification tool

I integrated HIRA drug-utilization review (DUR) into the prescription-save flow, building both the web review popup and the backend broker integration. The workflow issues a confirmation number, runs the checks, handles the results, and proceeds to saving.

Since results exchanged with an external server were hard to verify visually, I built a separate dur-conformance CLI. It replays the same call path as the integration and automatically compares responses from YAML-defined cases against expected results. It was run manually after changes to outbound messages or review logic, rather than connected to CI.

Establishing a pattern

Later in the migration I settled on a repeatable pattern — add the API, build a standalone web app, embed it in the desktop webview — and applied it to three more desktop-only screens: assignment inquiry, the clinic status board, and the reception/wait-status board. Building the API first and the web app in isolation before wiring it into the desktop webview kept all three ports fast and consistent.

Polling during persistent failures

Added longer polling intervals after consecutive failures. Screens with a 60-second base interval switch to 300 seconds after three consecutive failures. In the sustained-failure state, scheduled polling frequency falls from 60 to 12 times per hour (80%). This is calculated from the configured intervals, not measured production traffic.

React / TypeScript / Zustand / Kotlin / C# / WebView2

Call Center Consultation Management

2026

SmartDoctor · Frontend and backend API together

A new web module for collecting, assigning, and tracking consultation leads at clinic call centers. It talks to the telephony middleware over WebSocket to create leads from incoming calls automatically, and provides three intake channels — bulk Excel upload, individual registration, and incoming-call auto-creation — plus server-side filtering. Fixed field-reported issues in short cycles, including a race condition that duplicated leads on a single call and reconnection instability.

Replay with 12 categories observed in production: initial option requests 12 → 1, down 91.7%

React / TypeScript / Kotlin / WebSocket / MSSQL

Call Center Consultation Management

SmartDoctor · Frontend and backend API together · 2026

Built a new web module for managing call-center leads and inbound consultations from the ground up, owning telephony integration, three intake channels, and consultation history across both the UI and its API.

Call Center Consultation ManagementMy scopeinbound callCTI middlewaretelephonyWebSocketbackoff reconnectConsole UIReactConsole APIKotlinBulk uploadManual entryMSSQL
Leads arrive through three channels: inbound calls, bulk spreadsheet upload, and manual entry. Telephony runs over a WebSocket link to CTI middleware; I built both the console UI and its API.

Background

A new web module for managing marketing leads and inbound-call consultations at a clinic's call center. Field feedback from a large plastic-surgery clinic running its own call center kept arriving as tickets, and the module went through short cycles of intake, build, and on-site verification. Early on it was live at that one clinic, handling roughly 480 leads and calls a day.

Telephony runs over a local WebSocket to the 'MediCall' CTI middleware, with exponential backoff on reconnect. For this module I also built the backend API myself, not just the frontend.

Three intake channels

Leads arrive through three channels: a bulk Excel upload, manual single-lead registration, and automatic creation from inbound calls. The bulk upload became a wizard — upload, customer matching, then data correction — with a modal calling out failed rows, and matched rows classified straight off the server's matchType. I had the validate response embed candidate customers directly, which removed the follow-up customer search for each distinct phone number requiring a match.

The manual registration modal auto-matches contact info and requires both lead-source fields to be filled together. On an inbound call, the module auto-assigns the logged-in agent and creates a lead automatically; if the number isn't registered yet, it opens the registration modal automatically.

The lead list got header filters across eight columns, a two-level tree filter for lead source, and filter state persisted to localStorage. I later moved column filtering entirely server-side so the list, tabs, and Excel export all agreed on the same result set. I also added an API for a customer's full consultation history, plus edit and delete on individual records. Permissions took eleven CCMS-specific codes, added across three repositories: the desktop permission tree, the API's enum, and the web's permission checks.

What broke in the field

A race condition let a single inbound call spawn two leads, and consultation drafts in progress would disappear if the call dropped mid-conversation. I fixed both as part of stabilizing the inbound-call path.

Later, in July 2026, I removed an N+1 pattern that queried options once per lead-source category, replacing it with a single batch endpoint for the whole list.

Measured results

Replaying the original and changed customer-matching code with 1,000 distinct auto-matched phone numbers reduced follow-up searches from 1,000 to zero. Results also matched for repeated numbers, new customers, and ambiguous matches. The initial file validation and final registration requests were outside this measurement.

Twenty-six recent production log responses showed 12 lead-source categories at one clinic. Replaying both implementations with that category count reduced initial option requests from 12 to 1 (91.7%). Including the category lookup, related requests fell from 13 to 2 (84.6%). An immediate remount with a fresh options cache made no additional option requests in either version.

For Excel export, I removed redundant validation of merged-cell ranges. A local benchmark with 300 synthetic leads, 600 rows, and 4,500 merged ranges reduced median file-generation time from 1.77 seconds to 19 ms (98.9%). Both implementations ran five measured trials with Apache POI 5.4.1, and every output was checked for identical cell values and merged ranges. This measures file generation, excluding database queries and download time.

React / TypeScript / Kotlin / WebSocket / MSSQL

Toss Payment Terminal Integration

2026

SmartDoctor · Across desktop, web, and backend

Integrated Toss payment terminals into the CRM checkout flow. Built a WebSocket client with automatic reconnection for the desktop (.NET) and a payment-session state machine for the web, and solved the multi-pod failure — where the payment plugin and the CRM connected to different pods and couldn't find each other — by replacing the in-memory session registry with a Kafka fan-out relay.

Proposed and designed the session routing for multi-pod environments

Kafka / WebSocket / C# / React / Zustand

Toss Payment Terminal Integration

SmartDoctor · Across desktop, web, and backend · 2026

Integrated a Toss payment terminal into the CRM's checkout flow, owning both the desktop and web client work plus backend stabilization. When sessions landed on different pods and lost track of each other, I fixed it with a Kafka relay.

Toss Payment Terminal IntegrationMultiple podsWS sessionWS sessionCRM checkoutToss pluginterminalAPI pod AAPI pod BKafkasession relay
When the CRM and the payment plugin land on different pods, an in-memory registry cannot find the terminal. Relaying sessions across pods through Kafka keeps any pairing connected.

Background

This was an epic to wire a Toss payment terminal (their Front Plugin) into the CRM's checkout flow. The backend payment-session infrastructure already existed; I owned both the desktop CRM (.NET 6, WPF) and web client integrations, plus backend stabilization. The integration came about because Toss reached out about a partnership: pairing the terminal with MediCash, the company's own point-based payment service, to supply more Toss terminals to hospitals.

Afterward I kept fixing balance-consistency issues that surfaced whenever MediCash points and Toss payments mixed, and reworked the pairing screens and settings after the integration moved from leader mode to client mode. As of August 2026 it's still pending production rollout.

What I built

On the desktop CRM I built a WebSocket client with automatic reconnect and a single connection per app, implemented the Toss session and refund services, and wired them into the checkout screen.

On the web side I built the payment slice itself: a Zustand session state machine, a retry queue, and the WebSocket hooks around them.

On the backend I stabilized the failure and cancellation paths: sending session.abort to the plugin on cancel or failure so the terminal screen wouldn't get stuck, hardening the watchdogs that catch a dropped connection, and re-establishing the security context on the WebSocket handler.

Later I worked through a chain of MediCash consistency bugs in order: the quick-checkout discount summary not updating when points were applied mid-session, balances left sitting in pending payment by the point amount even after payment completed, stale balances reappearing when a prescription reloaded, and usage not showing up on screen for records created without a reservation. When the integration moved from leader mode to client mode, I built a new device pairing screen — issuing codes, listing devices, deregistering them — and removed the old terminal settings UI from preferences.

The multi-pod problem

When the payment plugin's WebSocket and the CRM's WebSocket landed on different pods, we'd hit a DEVICE_OFFLINE error. Sessions lived in an in-memory registry, so whenever the two connections landed on different pods, neither pod had any way to find the other terminal.

I proposed this structure myself and built it: a Kafka fan-out relay that carries session state across pods, so no matter which pod pairing the two connections land on, the session reaches the other side. Payment sessions now stay connected regardless of how the pods get assigned.

Kafka / WebSocket / C# / React / Zustand

64-bit Migration of the Desktop CRM

2025

SmartDoctor · Proposed and led

What blocked the CRM's 64-bit migration was the set of 32-bit-only DLLs for carrier telephony and payment terminals. I designed a structure that isolates them in a separate 32-bit process communicating with the main app over IPC, complete with lifecycle management, automatic restarts, and error logging. I also collected the OS bitness distribution of real installations via Sentry to ground the migration decision in data.

Cleared the path to 64-bit while keeping legacy integrations compatible

C# / .NET / WPF / IPC / Sentry

64-bit Migration of the Desktop CRM

SmartDoctor · Proposed and led · 2025

Migrated the CRM host to 64-bit by isolating vendor DLLs that only shipped as 32-bit into their own process, talking to the host over IPC. I proposed and led the migration.

64-bit Migration of the Desktop CRM32-bit-only vendor DLLsCRM host64-bitIPCcross-processBridge process32-bitTelephony DLLTerminal DLL
Vendor DLLs shipped only as 32-bit blocked the migration. Confining them to a separate 32-bit process that talks to the host over IPC let the host itself move to 64-bit.

Background

Porting the CRM host to 64-bit ran into vendor DLLs that only shipped as 32-bit builds: the carrier-specific Smart Call APIs for KT, LG, and SK, and the card payment-terminal (VAN) module. Smart Call is the telephony system wired into the CRM, and the terminal module handles card payments in the checkout flow.

A 64-bit host can't load those DLLs as they stand, so the migration needed a way to keep both integrations working. The work ran from late July through September 2025.

What I built

I split those DLLs into a dedicated 32-bit process (Dll32Server) that talks to the host over IPC, and moved the KT/LG/SK Smart Call integration and payment-terminal logic into it.

I reworked how the process's lifecycle was managed, moved the host itself to multithreading, added automatic restarts with error logging, and changed startup to initialize only the services actually needed.

I cleaned up event delivery as well, wiring a global event sender through injection to stop events from getting dropped, and running the process on an STA thread to accommodate the KT DLL's habit of raising its own UI.

I supported regression testing across every carrier and the full payment-terminal surface, and fixed the items QA sent back as failed.

The data behind it

I added OS 32-bit/64-bit ratio tracking to our Sentry collection, which gave us the field's actual bitness distribution. It rode along with the Sentry observability work I did that same quarter.

C# / .NET / WPF / IPC / Sentry

Cloud Code Signing Migration & Shared CI for Windows Apps

2026.08–09

SmartDoctor · Shared signing CI for internal Windows apps

Migrated Windows code signing to DigiCert KeyLocker in the cloud to address the management overhead and CI/CD constraints of physical USB authentication. Built a shared GitHub action for signing and verification that internal Windows apps can reuse in their existing build pipelines, and shared usage guidance with the team.

Reusable cloud code-signing CI across apps without physical USB dependence

GitHub Actions / PowerShell / DigiCert KeyLocker

Cloud Code Signing Migration & Shared CI for Windows Apps

SmartDoctor · Shared signing CI for internal Windows apps · 2026.08–09

Migrated to cloud code signing to address the management overhead and CI/CD constraints of physical USB authentication. Built a shared DigiCert KeyLocker CI process so internal Windows apps can use the same signing and verification procedure.

Background and role

The existing code-signing process depended on a physical USB authentication device. Managing the device and connecting it to the signing environment created operational overhead and constrained automated build-and-sign flows in CI/CD. I migrated signing to DigiCert KeyLocker in the cloud to address those constraints.

Built a shared GitHub action that Windows apps built with .NET, Electron, or Tauri can reuse for signing and verification. Each repository can integrate it into its existing build CI by adding an action call and specifying the files to sign.

Standardized signing and verification for EXE, DLL, and MSI files and provided a manual signing tool so local files and folders can use the same cloud signing procedure. Shared usage instructions and guidance for avoiding unnecessary signing calls with the team.

GitHub Actions / PowerShell / DigiCert KeyLocker

Deploy Notification Relay

2026

SmartDoctor · Internal tool wired into the deploy pipeline

An internal tool built to cut down on everyone individually checking whether a production hotfix actually shipped. It listens for deploy webhooks, diffs the commit range against the previous deploy, and posts a completion reply on the Slack thread linked to whatever Jira issue keys show up in those commits. I judged that notifying on regular releases too would likely become noise, so it picks out hotfix-shaped deploys only, and when it doesn't have enough information to tell, it stays silent and logs a warning rather than risk a false report.

Built without being asked, and now a fixture of how the team operates

TypeScript / Vercel Functions / Slack API / Jira API

Deploy Notification Relay

SmartDoctor · Internal tool wired into the deploy pipeline · 2026

A tool I built on my own, unasked, to remove the need for everyone to individually check whether a production deploy actually went out. It pulls the commit range from a deploy webhook, finds the Slack thread tied to each issue, and replies there — narrowed down to hotfixes only, and still in active use today.

Deploy Notification RelayLookups and deliverydeploy eventDeploy platformwebhookNotifierhotfix filterCommit rangeIssue thread linkSlack threadreply
On a deploy webhook it extracts issue keys from the commit range since the previous deploy, finds the Slack thread linked to each issue, and replies there. When the inputs needed to decide are missing, it stays silent instead of sending a wrong notification.

Background

Checking whether a production deploy had actually landed was left to each person who needed to know. The core behavior is simple: pull issue keys out of the commits in a deploy's range, find the Slack thread linked to each issue, and reply there once the deploy completes.

What I built

I built the Vercel webhook entry point with signature verification and async processing, a client that looks up the previous production deploy and pulls the commit range through the GitHub compare API (handling pagination and response truncation), a Jira thread lookup paired with a Slack archive-link parser, and the step that checks for an existing reply before posting one. Webhook in, commit range, thread lookup, reply out — the four steps run as one pass.

Redeploys of the same SHA are skipped, and commit file lookups run in parallel. I covered edge cases with tests: staying at 200 on a malformed webhook body, returning 401 when the secret isn't configured.

On July 15, 2026 I widened the target projects to five, including the call-center console and the wait-status board, rotated the webhook secret, and cut over. I also wrote design-spec docs, a script that replays real payloads for verification, and a DRY_RUN mode.

Keeping alerts narrow

It started out notifying on every production deploy. I judged that notifying on routine releases too would likely become noise, so I narrowed it to hotfix-style deploys only: same branch as the previous production deploy, or same service prefix and major.minor with only the patch bumped, and only when the first commit line matches hotfix formatting.

When it doesn't have enough information to decide, it stays silent and just logs a warning instead of sending anything. I chose silence over a wrong notification.

TypeScript / Vercel Functions / Slack API / Jira API

Personal · Research · Collaboration

LRAGE — Legal-Domain RAG Evaluation Toolkit

2024 — 2025

Open-source research · First author

An open-source toolkit for evaluating LLMs on legal tasks in RAG settings. It extends lm-evaluation-harness with retriever and reranker abstractions and LLM-as-a-judge evaluation, ships pre-built indexes for Pile-of-law, and provides a GUI. Validated on Korean (KBL), English (LegalBench), and Chinese (LawBench) legal benchmarks.

First-author demo paper on arXiv; live demo on Hugging Face Spaces

Python / lm-evaluation-harness / Pyserini / Hugging Face

Woodshed — A Community for Guitar Licks

2026

Personal project · From idea to deployment

A community where guitarists write down and share licks as TAB. I built a graphic TAB editor — click frets to input notes, with hammer-ons, bends, and other articulations rendered as real sheet music — plus everything a running service needs: accounts, visibility controls, an explore feed, likes, comments, collections, and a moderation queue.

Next.js / TypeScript / Drizzle / Turso / Auth.js / Vitest

Korean Architecture: A Prediction Model

2026

Joint entry with an architecture teammate · Problem framing & argument development

A collaborative work imagining a future where behavior in spaces shaped by AI predictions reinforces those predictions. I helped frame the question of how lives never chosen can disappear from the data, structure the feedback-loop argument, and review the narrative and presentation.

2026 젊은 건축가포럼 건축상 · Selected Entry

Problem Framing / Feedback-Loop Argument / Narrative & Presentation Review

Korean Architecture: A Prediction Model

Joint entry with an architecture teammate · Problem framing & argument development · 2026

A joint competition entry exploring how AI predictions affect human choice and space. From a CS/AI perspective, I helped frame the question, structure the feedback-loop argument, and review the narrative and presentation. My teammate primarily connected the work to architectural discourse and history and developed its visualizations.

The question

A thought experiment taking a future of fully predictable human behavior to its extreme. It asks not only whether AI predictions are accurate, but how environments designed around those predictions influence human choice.

A self-reinforcing prediction loop

AI predicts behavior, spaces are designed around those predictions, and people live within them. Their behavior becomes data that reinforces the original predictions: prediction → spatial design → behavior → data → prediction.

Lives that were never chosen leave no trace in that data. The project questions whether high prediction accuracy describes the range of possible lives, or reflects choices narrowed by an environment built around the predictions.

My role and collaboration

I helped clarify the core idea and problem framing and structure the feedback-loop argument connecting prediction, space, behavior, and data. I reviewed how that argument carried through the narrative and presentation and provided feedback.

My teammate primarily connected the question to architectural discourse and history and developed the visualizations. Bringing our perspectives together, we extended technical thinking about data and system feedback into architectural questions.

Competition result

Our joint entry was selected in the 2026 젊은 건축가포럼 건축상 (입선 / Selected Entry).

Problem Framing / Feedback-Loop Argument / Narrative & Presentation Review

View project book ↗

Education

  1. Mar 2022 — Present

    University of Seoul

    B.S. in Computer Science

    On leave of absence

Contact

Coffee chats, job opportunities, and collaboration proposals are all welcome.

alsgn2003@naver.com