← Back to Projects

CPE

A clinic operations program that systematised consultation and payment around how aesthetic clinics actually work.

CompanyGaia Labs
RoleFrontend
TeamFrontend 2 Backend 3 Design 1 QA 5
PeriodFeb 2025 - Present
StatusTheia clinics In production

A marketing and operations program for aesthetic dermatology clinics with the traits of both a CRM and an EMR. Clinicians and back-office staff—30 to 50 people per clinic—use it at the same time, and it supports the clinic's core work from check-in through consultation, payment and treatment. I was one of two frontend engineers; building on problems found while running the single-site V1, I redesigned it as the multi-site V2. V1 operation and V2 development ran in parallel, then the switch was made without service interruption.

Problems and solutions

Problem

Customer details and past treatment purchases, which have to be checked together during a consultation, lived in different tabs and several layers down, so consultants kept switching screens mid-session.

Judgement

Before adding features I visited the clinic and interviewed the consultants. The conclusion was that information was not missing; the information that existed was simply not laid out to match the consultation flow.

I decided to gather the related information onto one screen in the order consultants actually check it.

Solution

Rebuilt the consultation screen so customer details and treatment history are visible inside it, and treatments can be purchased and discounts applied on the same screen.

Result

Consultants can now go from checking information to purchase and discount without leaving the screen, and the structure was carried over into V2.

Problem

An operations tool used only after login was running on the Next.js App Router, which assumes SSR. Most components carried 'use client', so the framework's constraints applied while none of the server-component benefits did.

Judgement

When the new V2 repository was starting, I proposed switching to React + Vite.

I wrote up the trade-offs of changing frameworks and what the existing structure gained and lost, and got the team's agreement first.

Solution

Adopted FSD for the folder structure so domain boundaries are explicit in the file system.

With two frontend engineers in one repository, I drew a clear line between shared code and domain features and migrated in stages: design system → shared components → domain features.

Result

Ran V1 operations and V2 development in parallel for three months and switched over with zero downtime.

Problem

It never showed on a single item, but when several items were bundled, as in a treatment package, rounding errors accumulated and the amount shown differed from screen to screen.

Judgement

Made the final displayed amount the single basis for calculation and derived the other values backwards from it.

Truncation rules and the order of discounts and VAT cannot be decided unilaterally on the frontend, so I agreed the calculation rules explicitly with the backend.

Solution

Moved rounding from individual items to the total, back-calculated the net amount from the displayed amount, and derived VAT as the remainder.

However many items are bundled, net amount plus VAT now always equals the displayed amount.

Result

Removed the path that let amounts drift in V1.

Since calculation logic left on the frontend could reproduce the problem on another screen, V2 moved the single source of truth for amounts to the backend and left the frontend responsible for display only.

↗ What I learned handling money (blog, Korean)

Problem

When a purchased item was withdrawn or refunded, some paths were missing from the transaction and receipt responses. The screen looked fine, but the actual amount data was left behind.

Judgement

Rather than simply asking for a fix, I first established which data was wrong and how far it reached.

Refund data feeds several places in the settlement process, so I also had to check whether fixing one API would affect other screens.

Solution

Compared the responses of three endpoints to identify the reference data, and wrote a reproduction case including patient, transaction, invoice and receipt IDs.

Grouped the backend requests into seven categories, such as defects, new APIs, added fields and response optimisation, and marked each with a blocking status.

Result

For issues that blocked frontend development, I introduced temporary workarounds so development could continue before the API changes were available.

Problem

In V1 there was no clear design system and everyone worked page by page, so components with the same role were rebuilt per page and almost nothing was reused.

Starting the new V2 repository, how the initial structure was set up would shape development speed and consistency from then on.

Judgement

Decided to establish the design system and shared UI before initial feature development.

Even if screen development was slower for the first two weeks, the groundwork meant the same UI would not be rebuilt afterwards and existing components could be extended instead.

Solution

Built around 20 primitives and Storybook first, then had the whole team build screens on top of them.

Result

The team now builds by extending existing components instead of creating new ones each time. More than 90% of the current components are reused.

Tech Stack

V1Next.js 14 (App Router)V2React 19Vitereact-router v7TypeScript 5.9ZustandTanStack QueryTailwind 4StorybookVitestFSD