Selected cases across payments, apps, roaming, pricing, membership, promotions, DX, and personal products

Project Portfolio

This page highlights projects such as multi-vendor payments, app/WebView bridge work, roaming reliability, pricing APIs, membership migration, point-wallet design, DX automation, and Commit Map as a personal product built from a real planning problem.

Projects

11

Primary Stack

Kotlin Spring Boot MySQL

Focus

Architecture, state transitions, operational resilience

01

Backend development experience centered on Kotlin and Spring Boot

02

Experience integrating MSAs with OAuth2, PGs, gateways, and external certificate services

03

Hybrid app and app-validation tooling experience with Flutter/WebView

04

Operational stabilization of public roaming integrations using events, retries, and monthly resync

05

Operational stabilization with Pub/Sub DLQ, Athena batches, monthly partitions, and circuit-breaker patterns

06

DX improvements using run-gemini-cli, Docusaurus, Firebase App Distribution, capture/replay extensions, and Jenkins/TestFlight

Project

01

LG Uplus VoltUp / Oct 2024 - Present

Multi-Vendor Payment System and Arrears Recovery

Owned the service from initial design to implementation

Owned the payment service from initial design through implementation, expanding a single payment flow into a modular structure that supports multiple payment vendors and building DLQ-based unpaid-event processing with automatic recovery. Later hardened operational paths such as DLQ retry stuck prevention, PG not-found repayment continuity, and cancellation-message branching.

Kotlin Spring Boot Spring Batch GCP Pub/Sub Cloud SQL

Design Context

The service needed to move beyond a single-payment structure into one that can absorb multiple payment vendors through the same extension point, while also handling unpaid-processing flows.

Key Point

A project for explaining both PG expansion and unpaid-event retry strategy.

Core Implementation

  • Built a PG integration architecture with an abstract-class-based vendor strategy pattern.
  • Implemented a GCP Pub/Sub-based DLQ pattern that isolates failed events and keeps arrears-processing targets traceable.
  • Implemented retries, DLQ, and NACK-based recovery paths so unpaid events could flow into follow-up payment recovery.
  • Hardened FAILOVER transitions and retry timestamp recording to prevent dead-letter retries from getting stuck, while keeping unpaid-payment recovery flows intact on PG not-found responses.
  • Separated AlimTalk contexts for full, partial, and roaming payment cancellations and normalized amount formatting to improve user-facing communication accuracy.

Engineering Lens

  • Centered request orchestration, point hold/confirm, and success/failure transitions around PaymentProcessor so state changes remain traceable in one place.
  • Protected point holds from concurrent payment attempts by keeping both hold and payment-READY creation inside a user-scoped lock.
  • Split PG-network uncertainty into repair, consumer retries, and failover batches so immediate recovery and operator intervention remain clearly separated.
  • Treated retries themselves as observable operations, leaving latestRetriedAt and state transitions behind so operators can tell where a failed event stopped.

Architecture Snapshot

Mermaid View

Multi-vendor orchestration: lock, hold, and state transitions

Keeps request data, lock key, point hold, payment READY creation, internal `PG Vendor` approval steps, and success/failure transitions in one integrated diagram.

flowchart TD
  Req["pay / rePay<br/>order=ORD-240915-001<br/>user=421 method=17 point=2000"] --> Lock["distributed lock<br/>payment-user-process:421"]
  Lock --> Hold["PointUpdater.hold()<br/>wallet -> HOLD 2000P"]
  Hold --> Ready["createWithReady()<br/>payment READY"]
  Ready --> Sub["resolve subscription<br/>methodId=17 or primary"]
  Sub --> Vendor["PG Vendor Router<br/>supports: KakaoPay / TossPayments / KakaoT<br/>selected: KakaoT"]
  subgraph PGV["PG Vendor Layer"]
    direction TD
    Vendor --> Keys["read vendor keys<br/>pgPayKey + token"]
    Keys --> Api["vendor client.pay(...)"]
    Api --> Tx["save pgTransactionId<br/>paymentId / tid / paymentKey"]
  end
  Tx --> Result{"approval result"}
  Result -->|success| Done["updateSuccess<br/>payment PAID<br/>point HOLD->CONFIRM"]
  Result -->|fail| Fail["updateFailed<br/>releaseHold(order)"]
  Fail --> Recovery["repair / retry / failover"]
  classDef vendor fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  class Vendor,Keys,Api,Tx vendor;
  style PGV fill:#eef7fb,stroke:#0f4c81,stroke-width:2px,color:#0f172a;

Mermaid View

Multi-vendor system: mandatory contracts and optional extensions

Places `VendorChecker.select()` on top of the `VendorType` extension point, separates contracts required for every vendor from features needed by only some vendors, and uses `@RequiredVendor` plus `VendorRequirementsValidator` to catch missing mandatory implementations at startup.

flowchart TD
  Vendors["VendorType<br/>KakaoPay / TossPayments / KakaoT"] --> Select["VendorChecker.select(vendorType)"]
  Select --> Required["Required on all vendors<br/>VendorPaymentProcessor<br/>VendorMethodProcessor"]
  Select --> Partial["Required on some vendors<br/>VendorPaymentOnceProcessor<br/>(KakaoPay only)"]
  Select --> Optional["Optional extensions<br/>RepairService / vendor hooks"]
  Required --> Validate["@RequiredVendor<br/>+ VendorRequirementsValidator"]
  Partial --> Validate
  Validate --> Boot{"startup validation"}
  Boot -->|missing| Error["application start fail"]
  Boot -->|ok| Route["route to concrete impl"]
  classDef core fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef optional fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  classDef error fill:#fff1f2,stroke:#be123c,stroke-width:2px,color:#0f172a;
  class Vendors,Select,Required,Partial,Validate,Route core;
  class Optional optional;
  class Error error;

Operational Outcomes

  • Built a payment architecture that integrates multiple PG vendors behind one interface and remains extensible as new vendors are added.
  • Applied DLQ-based unpaid-event processing and automatic recovery paths in operation.
  • Improved trust in unpaid-payment recovery and user-facing cancellation messages by hardening external PG exception handling and cancellation-message branching.
Project

02

LG Uplus VoltUp / Jul 2025 - Present

KakaoT Account Linking and Payment Method Registration

Unified member identity, linked AuthMethod records, and designed card-registration state around t_partner_user_token

Designed the flow that links KakaoT external accounts to existing VoltUp members through encrypted CI and carries payment-method registration from the FEAPP one-step API through billing READY-state and ACTIVE-state transitions. Later promoted `t_partner_user_token` as the key between external payment methods and internal user context, stabilizing lookup, unlink validation, and app-callback activate flows.

Resume Link Points

User-side Backend

The resume entry for User-side Backend maps here as the broader area around KakaoT integration, vehicle/PnC (Plug & Charge) flows, and user-facing stability work.

Kotlin Spring Boot OAuth2 Flyway T Partner API

Reference Views

Preview

Linked auth methods: connecting KakaoT under the same member

A screen showing how multiple auth methods were unified under one member, then connected into the KakaoT payment-registration flow.

VoltUp linked auth-method screen

Linked auth methods: connecting KakaoT under the same member

A screen showing how multiple auth methods were unified under one member, then connected into the KakaoT payment-registration flow.

Payment registration: KakaoT, KakaoPay, and card options

A bottom-sheet entry that exposes KakaoT, KakaoPay, and normal card registration in one place to simplify the user choice flow.

VoltUp payment-method registration bottom sheet

Payment registration: KakaoT, KakaoPay, and card options

A bottom-sheet entry that exposes KakaoT, KakaoPay, and normal card registration in one place to simplify the user choice flow.

Design Context

The system had to merge existing VoltUp members with KakaoT users without creating duplicate identities, then keep card-registration sessions and final payment-method state under the same user context.

Key Point

A strong project for explaining both identity unification and payment-method state transitions.

Core Implementation

  • Designed the linking flow that resolves the existing VoltUp member from KakaoT OAuth account data and encrypted CI, then adds `AuthMethod(KAKAO_T)`.
  • Built the one-step FEAPP flow that uses the current user encrypted CI to create the KakaoT payment-link session.
  • In billing, implemented the state transition that stores the `session_key` in `pg_payloads` and finalizes `pgPayKey` plus `t_partner_user_token` during confirm.
  • Added `t_partner_user_token` lookup filters plus a composite index, and hardened unlink validation so KakaoT teardown checks whether the T_PAYMENTS record belongs to the current user.
  • Separated the app-callback-specific activate API and added DTO aliases, `@JsonProperty`, and search logs to absorb external schema drift and improve operational traceability.

Engineering Lens

  • Split responsibilities so auth owns external account acquisition, user-service owns identity resolution and AuthMethod ownership, and billing owns payment-method state.
  • Prevented identity mismatches by not starting card registration until account linking is complete, then creating sessions and activate calls only afterward.
  • Grouped the `session_key`, `partner_user_token`, and `pgPayKey` around the same subscription row during the `READY -> ACTIVE` transition so later approve/cancel calls can reuse them.
  • Treated `t_partner_user_token` not as a response field but as an operational key for user-to-external-payment consistency, so lookup and unlink validation share the same basis.

Architecture Snapshot

Mermaid View

Card registration after VoltUp-to-KakaoT account linking

Summarizes identity linking, encrypted-CI based account matching, link-session creation, and the READY-state to ACTIVE-state transition in one pass.

flowchart TD
  User["VoltUp member<br/>encrypted CI"] --> OAuth["KakaoT OAuth<br/>external account + encrypted CI"]
  OAuth --> Link["user-service<br/>AuthMethod(KAKAO_T) link"]
  Link --> Session["FEAPP link session<br/>ACCOUNT + PAYMENT"]
  Session --> Ready["billing init<br/>READY transition"]
  Ready --> Active["confirm success<br/>ACTIVE transition"]

Operational Outcomes

  • Established the user flow that links existing VoltUp members to KakaoT accounts before continuing into card registration.
  • Made subscriptions stay in the ACTIVE state after registration so approve, cancel, and lookup operations can reuse the same identity context.
  • Kept the current user and payment method matching basis stable across mixed app-callback, web-login, and unlink-validation flows.
Project

03

LG Uplus VoltUp / Jul 2025 - Present

Vehicle Registration, Vehicle-Key Auto-Mapping, and PnC (Plug & Charge) Authorization

Designed vehicle verification, bidirectional auto-mapping, and PnC (Plug & Charge) authorization flows

Designed a flow where plate-number vehicle info and Plug & Charge vehicle entities arrive independently, auto-link only when exactly one unmatched pair exists, and then feed the same vehicle context into PnC (Plug & Charge) authorization.

Kotlin Spring Boot JPA Redis

Reference Views

Preview

Vehicle management: registered car and Plug & Charge entry

A user-facing screen that leads from vehicle registration into Plug & Charge, showing how vehicle context and charging authorization meet in the product flow.

VoltUp vehicle registration and Plug & Charge screen

Vehicle management: registered car and Plug & Charge entry

A user-facing screen that leads from vehicle registration into Plug & Charge, showing how vehicle context and charging authorization meet in the product flow.

Design Context

Because vehicle info (`plateNumber`) and PnC (Plug & Charge) identifiers (`evccId`) are registered at different times, the system needed an auto-linking rule that avoids wrong matches without forcing users into manual selection every time.

Key Point

A good project for explaining when plate numbers and vehicle keys should auto-link and when the flow must fall back to manual choice.

Core Implementation

  • Applied a bidirectional auto-mapping rule that checks whether exactly one unmatched counterpart exists from both the vehicle-info and PnC (Plug & Charge) registration sides.
  • Stored vehicle info around `plateNumber` and PnC (Plug & Charge) vehicles around `evccId`, then filled `userVehicleInfo.userVehicleId` only at link time to keep state transitions explicit.
  • Made `authorizeByEvccId` resolve through the mapped vehicle so charger authorization can reuse the same plate-number and user context consistently.

Engineering Lens

  • Auto-linking is a convenience feature with high downside risk, so it was designed conservatively: link only when there is exactly one unmatched counterpart.
  • Handled different registration timing naturally by storing vehicle info and PnC (Plug & Charge) entities separately and joining them through an explicit link instead of forcing them into one record early.
  • After linking, `evccId -> userId/plateNumber` resolution becomes possible, making the real charging authorization path directly explainable from the data model.

Architecture Snapshot

Mermaid View

Safe auto-mapping flow between vehicle info and vehicle keys

Embeds the vehicle and vehicle-key example values directly into the nodes so the auto-link condition and final authorization result read as one flow.

flowchart TD
  Vehicle["vehicleInfo 501<br/>plate=12GA3456"] --> Match{"unlinked pair<br/>count == 1 ?"}
  Pnc["userVehicle 88<br/>evccId=EVCC-A1B2"] --> Match
  Match -->|yes| Link["linkPncVehicle(501,88)"]
  Match -->|no| Manual["manual select"]
  Link --> Auth["authorizeByEvccId<br/>user=421 plate=12GA3456"]
  Auth --> Charge["start charge"]

Operational Outcomes

  • Enabled auto-mapping from either direction so the system can link correctly whether vehicle info or PnC (Plug & Charge) registration happens first.
  • Established an operational flow where PnC (Plug & Charge) authorization identifies the user by `evccId` and resolves the linked plate number together.
Project

04

LG Uplus VoltUp / Jul 2025 - Present

Unified Promotion Platform for Coupons and Points Buildout and Enhancement

Extended coupon-service policy, payment-vendor restrictions, and point-wallet structure

In VoltUp `coupon-service`, handled code issuance, code registration, direct user assignment without codes, and expiry reminder batches based on coupon-pack registration and usage windows, while applying partner-coupon payment-vendor restrictions across issuance, lookup, usage, and Admin creation flows. Separately designed point wallets and redemption order for per-accrual expiration.

Kotlin Spring Boot Spring Batch MySQL JPA QueryDSL JDBC Distributed Lock

Design Context

The project had to support partner-specific promotion requirements while handling both code-based coupon issuance and direct coupon assignment without codes, and some coupon packs needed different allowed payment vendors such as KakaoT, normal cards, or KakaoPay. The point model also had to handle per-accrual expiration reliably.

Key Point

A good project for explaining coupon-service issuance, direct assignment, expiry reminders, and point-wallet redemption structure together.

Core Implementation

  • Managed coupons with separate `registerStartAt/registerEndAt` and `usableStartAt/usableEndAt` windows on `couponPack`, separating registration timing from usage timing.
  • Issued code-based coupons through `batchIssue`, converting Snowflake IDs into 10-character base36 codes after SHA-256 hashing, bulk-saving them first and falling back to individual saves on conflicts.
  • For direct assignment without coupon codes, created coupon rows through `mapping(code=null)` or `batchMapping(userIds)`, while code registration linked the user inside a `coupon-mapping:{code}` lock.
  • Added `allowedPaymentVendors` as a coupon-pack policy, treating empty values as allowing all vendors to preserve compatibility while applying the same restriction across issuance, lookup, and usage.
  • Added an allowed-payment-vendor multiselect and Encoded ID exposure to the Admin coupon-pack creation flow so operators can verify the policy from creation time.
  • In `addBulk`, created a new `PointWallet` whenever `expiredAt` exists, while merging into the same `type + chargeType` wallet when it does not, so accrual and expiration units stay aligned.
  • When points are used, paged through active wallets and sorted them by `FREE -> earliest expiredAt`, holding across multiple wallets sequentially and restoring only the wallets that participated in the hold on failure.
  • Handled coupon expiry through `couponExpiryReminderJob`, which reads a `usableEndAt` window and publishes `EXPIRED` events to `coupon.event`, while point expiration is reflected by the next-month history batch.

Engineering Lens

  • Split coupon responsibilities so `couponPack` owns timing and discount policy while each `coupon` owns user mapping and usage state, allowing both code-based and code-less issuance within the same model.
  • Protected code registration with `coupon-mapping:{code}` locks, usage with `coupon-process:{userId}` locks, and reinforced both with `unique(code)` plus `unique(userId,couponPackId)` constraints to prevent duplicate registration and duplicate issuance.
  • Handled expiry without adding another online coupon state: the batch reads coupon packs by `usableEndAt` and emits `EXPIRED` events only for unused assigned coupons.
  • Promoted payment-vendor restrictions from a UI condition into a coupon-pack domain policy, so policies created in Admin carry the same meaning through user issuance, lookup, and usage.
  • Split points into `PointWallets` entities instead of one balance, creating new wallets for expiring accruals and merging non-expiring ones into the same `type + chargeType` wallet so expiration rules are visible in the data model.
  • During usage, distributed holds across active wallets in `FREE -> expiredAt asc` order, and made `releaseHold(order)` restore only the actually held `pointWalletId`s so deduction and recovery follow the same sequence.

Architecture Snapshot

Mermaid View

coupon-service: code issuance, direct assignment, and expiry reminders

Shows the actual `coupon-service` flow from coupon-pack based code issuance and code registration to direct assignment without codes and `usableEndAt`-based expiry reminder batches.

flowchart TD
  Pack["couponPack 71<br/>register 10/01~10/31<br/>usable 10/01~11/30"] --> Code["batchIssue(size=1000)<br/>Snowflake -> SHA-256/base36<br/>code=37PRPT85WA"]
  Pack --> Direct["mapping(code=null) / batchMapping<br/>user=421 or [421,422]"]
  Pack --> VendorPolicy["allowedPaymentVendors<br/>KAKAO_T / CARD / KAKAO_PAY"]
  Code --> Claim["mapping(user=421, code=37PRPT85WA)<br/>lock coupon-mapping:37PRPT85WA"]
  Claim --> Guard["DB unique guard<br/>code / (userId,couponPackId)"]
  Direct --> Guard
  Guard --> Ready["coupon row<br/>userId=421 status=READY"]
  Ready --> Process["process(price=32000, user=421)<br/>lock coupon-process:421<br/>READY -> PROCESSING"]
  VendorPolicy --> Process
  Process --> Finish["complete -> COMPLETE<br/>rollback -> READY"]
  Pack --> Expire["couponExpiryReminderJob<br/>usableEndAt D+3 window"]
  Expire --> Scan["getAllByCouponPackId<br/>completeAt is null"]
  Scan --> Event["publish coupon.event<br/>eventType=EXPIRED"]

Mermaid View

Point wallets: partner accrual and expiration-ordered deduction

Separates point handling from coupon flow and shows, with concrete example data, how partner points split into wallets and move through active-wallet scan, ordering, hold, and confirm/release.

flowchart TD
  Grant["addBulk / partner accrual<br/>BASE 1200P exp 10-18<br/>TOYOTA 3000P exp 10-20<br/>BLUEMEMBERS 800P exp 10-22<br/>NEXEN 5000P exp 10-31<br/>EVENT 700P exp 11-15<br/>BASE 900P exp null"] --> Rule["wallet rule<br/>new wallet if expiredAt exists<br/>merge by same type+chargeType if null"]
  Rule --> Wallets["wallet #11 BASE/FREE 1200 exp 10-18<br/>wallet #12 TOYOTA/FREE 3000 exp 10-20<br/>wallet #13 BLUEMEMBERS/FREE 800 exp 10-22<br/>wallet #14 NEXEN/CHARGE 5000 exp 10-31<br/>wallet #15 EVENT/FREE 700 exp 11-15<br/>wallet #16 BASE/FREE 900 exp null"]
  Wallets --> Active["active wallet scan<br/>expiredAt > now only<br/>page query by createdAt asc"]
  Active --> Order["deduction order<br/>FREE first<br/>then expiredAt asc"]
  Order --> Hold["hold 4500P<br/>#11 -1200<br/>#12 -3000<br/>#13 -300"]
  Hold --> Usage["point_usage rows<br/>order=ORD-240915-001<br/>walletId=11,12,13<br/>status=HOLD"]
  Usage --> Result{"payment result"}
  Result -->|success| Confirm["confirm<br/>HOLD -> CONFIRM<br/>wallet amount final"]
  Result -->|fail| Release["releaseHold(order)<br/>wallet 11 +1200<br/>wallet 12 +3000<br/>wallet 13 +300<br/>usage -> FAILED"]
  Wallets --> Expire["expiry handling<br/>expired wallet excluded from active<br/>expiringSoon queried separately"]
  classDef wallet fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef state fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Wallets,Active,Order,Hold,Usage wallet;
  class Confirm,Release,Expire state;

Operational Outcomes

  • Organized code issuance, code registration, direct assignment without codes, and expiry reminder batches under the same `coupon-service` model.
  • Absorbed partner-promotion payment-vendor requirements into coupon-pack policy, reducing the chance that discount policy drifts from payment and settlement conditions.
  • Kept point wallets, expiration-order redemption, and next-month history batching in the same backend so coupons and points can be explained together as one promotion platform.
Project

05

LG Uplus VoltUp / Dec 2024 - Present

VoltUp Hybrid App: WebView Bridge and Native Features

Flutter hybrid launch, JSBridge, and QR/permission/push/forced-update flows

Built the Flutter-based Android/iOS hybrid app for the VoltUp 2.0 launch and designed JSBridge plus core app flows so WebView surfaces can call native capabilities reliably. Continued improving production quality through QR scanning, camera permission, FCM, forced-update handling, and Crashlytics-driven stabilization.

Flutter Dart Kotlin Swift WebView JSBridge ML Kit FCM Crashlytics

Design Context

The app had to ship quickly on Android and iOS while keeping service screens flexible through WebView. At the same time, app-only capabilities such as QR scanning, camera permission, new-window handling, push notifications, and forced updates needed reliable native support.

Key Point

A strong project for explaining rapid user-app delivery together with WebView-native bridge design and app-specific flows such as QR, permissions, push, and updates.

Core Implementation

  • Defined the Flutter hybrid structure and designed the boundary between WebView screens and native capability calls for a two-month Android/iOS launch.
  • Implemented the JSBridge contract that lets the frontend call native features such as new-window handling, external URLs, QR scanning, camera permission, app messages, and forced updates.
  • Built a custom ML Kit-based QR scanner page with responsive scan UI to control the QR recognition experience and reduce dependency on the previous scanner package.
  • Connected Crashlytics for Dart/native error collection and hardened null-safe handling, camera lifecycle exceptions, and FCM token upload throttling.

Engineering Lens

  • In the hybrid app, WebView owns fast surface iteration while the native layer owns OS permissions and hardware capabilities. I treated JSBridge as the product contract between them and organized callable frontend features into explicit message flows.
  • Because QR scanning and camera permission are key entry points for starting a charge, I focused on controlling device layout, lifecycle, and permission states inside the app UX rather than just wrapping a scanner package.
  • Production stability improvements were driven by Crashlytics signals. I grouped small crash causes such as NPE candidates, camera pause exceptions, and FCM-token upload throttling to improve reliability around user entry flows.

Architecture Snapshot

Mermaid View

App bridge between WebView surfaces and native features

Shows how WebView surfaces call native features such as new-window handling, QR scanning, camera permission, FCM, and forced updates through JSBridge, then feed stability improvements through Crashlytics.

flowchart TD
  Web["VoltUp WebView surface"] --> Bridge["JSBridge contract<br/>frontend -> native"]
  Bridge --> Window["new-window / external URL handling"]
  Bridge --> QR["QR scanning<br/>ML Kit custom scanner"]
  Bridge --> Camera["camera permission / lifecycle"]
  Bridge --> Push["FCM push token"]
  Bridge --> Version["forced update / version branch"]
  QR --> Native["Android / iOS native layer"]
  Camera --> Native
  Push --> Native
  Version --> Native
  Native --> Observe["Crashlytics<br/>Dart + native error tracking"]
  Observe --> Fix["NPE / camera / token throttle stabilization"]
  classDef web fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef native fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef ops fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Web,Bridge web;
  class Window,QR,Camera,Push,Version,Native native;
  class Observe,Fix ops;

Operational Outcomes

  • Launched the 2.0 app quickly across Android and iOS and established an operational contract for WebView surfaces to call native features.
  • Stabilized core app-owned capabilities such as QR scanning, camera permission, push notifications, and forced-update handling in the native layer.
  • Used Crashlytics to track and fix production crashes, continuously improving stability around core app entry points.
Project

06

LG Uplus VoltUp / May 2026 - Present

VoltUp App Validation and Ops Correction Extension

App-free validation, API capture/replay, and Admin-unsupported ops corrections

Reduced validation time by recreating app-dependent flows such as new-window handling, QR scanning, camera permission, and forced-update version branches inside a browser extension instead of requiring `voltup-app` attachment every time. The same capture/replay structure was then extended to single-API operational corrections not directly supported by the Admin UI.

TypeScript Chrome Extension Vitest GitHub Actions API Replay WebView Debugging

Design Context

App feature validation had high setup cost. Even simple API flows or WebView-app bridge behavior required attaching the app, while operations sometimes had cases such as charge-zone correction where the Admin UI lacked a feature but the issue could be corrected through a single API.

Key Point

A tooling project for speeding up app development and operations response rather than an app feature itself. It is useful for showing a working style of spotting bottlenecks and turning them into small internal tools.

Core Implementation

  • Built Chrome Extension flows that can adjust or recreate app-provided behaviors such as new windows, QR scanning, camera permission, and forced-update version conditions.
  • Implemented API capture plus row-based replay so repeated QA and API-flow checks can be performed quickly without attaching the app.
  • Added variable templates, row parsing, and an executor so single-API correction work beyond the Admin UI can run through Bulk Replay.
  • Separated popup modes by host and added confirmation, 401/403 early-stop guards, batch skip notifications, Vitest-based parser/executor tests, and CI.

Engineering Lens

  • This tool did not start as ops automation alone. It first targeted app-attachment validation delays, then expanded once the same capture/replay structure proved useful for operational corrections.
  • After handling a charge-zone creation issue with a hand-written JS `fetch` script, I turned that pattern into a row-based execution tool the team can reuse instead of writing one-off scripts every time.
  • Because app/admin hosts coexist, I separated host-specific UI to avoid exposing the wrong operation in the wrong context, and designed permission expiration or access errors to stop before bulk replay proceeds.

Architecture Snapshot

Mermaid View

From app-validation bottlenecks to ops-correction replay

Shows how the extension recreates app-dependent flows without app attachment, then turns captured API requests into row-based replay for both development QA and Admin-unsupported operational corrections.

flowchart TD
  Pain["app-attachment bottleneck<br/>new window / QR / camera / version"] --> Extension["Chrome Extension<br/>app-like controls"]
  Extension --> Sim["recreate app-dependent flows in browser"]
  Extension --> Capture["API request capture"]
  Capture --> Template["row parser<br/>variable template"]
  Template --> Replay["Bulk Replay executor"]
  Replay --> Guard["confirm / 401·403 early stop<br/>batch skip notification"]
  Guard --> QA["shorter repeated dev QA"]
  Replay --> Ops["single-API ops correction<br/>beyond Admin UI"]
  Ops --> Share["one-off JS fetch -> team tool"]
  classDef pain fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef tool fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef result fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Pain pain;
  class Extension,Sim,Capture,Template,Replay,Guard tool;
  class QA,Ops,Share result;

Operational Outcomes

  • Reduced waiting and repeated interactions by making app-dependent flows quickly verifiable from the browser without the app.
  • Turned Admin-unsupported single-API correction work from one-off scripts into a repeatable internal tooling procedure.
  • Became a concrete example of spotting bottlenecks and sharing small tools that improve real app development and operations workflows.
Project

07

LG Uplus VoltUp / Feb 2026 - Present

Roaming Reliability: Public-Integration Resync and Retry

MCEE roaming card-state redesign, public API retries, and monthly full resync

Redesigned the Ministry of Climate, Energy and Environment public roaming integration so member-card state does not drift long-term from the external system, moving card-state updates from payment responses to billing-arrears events. Added public API retry handling and a monthly full-resync scheduler so baseline data can recover after missed events or transient failures.

Kotlin Spring Boot Spring Batch GCP Pub/Sub Scheduler

Design Context

Public roaming data needs to stay aligned with the external system, but online events alone cannot reliably recover after missed events or transient failures. Treating baseline-like member-card data and more tolerably lossy charger-status data with the same priority could also waste retry capacity.

Key Point

An operational reliability project that combines online events, retries, and full resync to keep internal state aligned with an external public system over time.

Core Implementation

  • Moved card-state updates from payment-response-driven logic to billing-arrears-event-driven logic, updating state selectively only for arrears cases.
  • Simplified the roaming card-state processing path and consolidated billing lookups to reduce transformation and lookup overhead.
  • For public API retries, prioritized baseline-like member-card data and separated more tolerably lossy charger-status data into a lower-priority path.
  • Added a monthly full-resync scheduler and task seed for MCEE member cards so differences missed by online events can be periodically restored.

Engineering Lens

  • Keeping state updates tied to payment responses could make normal payment flows a cause of roaming-state changes, so I narrowed the trigger to arrears events where correction is actually needed.
  • I treated retries as an operational design problem rather than “try everything again.” Member cards recover first because they are baseline data, while charger status is lower priority to control retry cost.
  • The monthly full resync complements online event handling. Instead of trying to eliminate every missed event, it creates a recovery path that prevents long-term drift from accumulating.

Architecture Snapshot

Mermaid View

Event handling, retries, and monthly resync for public roaming data

Shows how billing-arrears-based updates, priority-based public API retries, and monthly full resync work together to reduce long-term drift from the external system.

flowchart TD
  Source["MCEE roaming API<br/>member cards / charger status"] --> Online["online event handling<br/>card state update"]
  Online --> Arrears["billing-arrears events<br/>update only required cases"]
  Source --> Retry["public API error retry<br/>priority by importance"]
  Retry --> Member["member-card retry first<br/>recover baseline data"]
  Retry --> Charger["charger status later<br/>separate tolerable loss"]
  Source --> Monthly["monthly full resync<br/>dynamic scheduler + seed"]
  Monthly --> Baseline["prevent long-term external drift"]
  Arrears --> Stable["operational data accuracy"]
  Member --> Stable
  Baseline --> Stable
  classDef external fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef process fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef result fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Source external;
  class Online,Arrears,Retry,Member,Charger,Monthly process;
  class Baseline,Stable result;

Operational Outcomes

  • Reduced unnecessary state-change risk and improved data accuracy by redesigning the card-state update basis.
  • Established an operational retry path where important data recovers first after public API errors.
  • Added a monthly full-resync safety net so member-card baseline data realigns with the external system after missed events or transient failures.
Project

08

LG Uplus VoltUp / Oct 2024 - Present

Developer Productivity Automation and DevOps Improvements

AI review, Vault-to-local sync, internal API standards, and mobile CI/CD security hardening

Turned recurring PR review, local environment setup, internal API integration, and service/app delivery work in Voltup into shared workflows. In particular, I built Gradle logic that syncs Vault values directly into `application-local.yaml`, then extended the standardization into Admin internal API conventions and Workload Identity-based mobile delivery.

Vault CLI Gradle Kotlin DSL GitHub Actions Jenkins ArgoCD Workload Identity Firebase CLI Gemini API GitHub Copilot Claude Code gcloud CLI

Design Context

As the number of services grew, review rules, microservice-level conventions, recurring task patterns, local secret delivery, internal API invocation, and delivery steps were drifting per person. Mobile delivery also needed to reduce operational risks such as Service Account JSON key storage, external CDN rate limits, and skip-condition misfires.

Key Point

A DX/DevOps project that uses Vault-to-local sync for non-public local secrets and standardizes repeatedly fragile boundaries such as internal API calls and mobile delivery authentication.

Core Implementation

  • Built a reusable comment-triggered GitHub Actions workflow in `voltup-workflow` around `/gemini-review`, using the organization-level `GEMINI_API_KEY` plus per-repo `project-context`, `review-template`, and docs to provide consistent first-pass reviews.
  • Added `.agent/workflows`, `.github/skills`, `.github/prompts`, and `copilot-instructions.md` to the MSA workspace so microservice conventions, recurring task shapes, API-first development flow, and security rules become reusable skills that can be consumed across generative LLM tools.
  • Added root `build.gradle.kts` logic that replaces base-yaml placeholders from Vault and generates `application-local.yaml`, checking both project-specific paths and `secret/SHARED/voltup/dev`. It also verifies Vault CLI login, handles non-interactive environments, and fills `IAM_DB_USER_NAME` from the current `gcloud` account so local environments stay automatically aligned across developers even when new keys are added.
  • Added an `updateCodefCertificatesToVault` task in `feapp-domain-service` that Base64-encodes certificates and `conf.json` fields into Vault, so developers can refresh sensitive assets themselves without sharing files through the repo or chat.
  • Documented and applied an `admin-internal-*` client pattern plus `X-Internal-Caller` header convention so growing Admin internal API integrations keep a consistent caller identity and trust boundary.
  • Standardized delivery on top of the `devops-cicd` Jenkins shared library so each service `Jenkinsfile` routes API/BATCH/CONSUMER/APP targets by job name and continues into Docker build/push plus ArgoCD deploy. The Android app pipeline follows the same pattern with cache restore/save, release track selection, and notifications.
  • Moved Android delivery away from fastlane and Service Account JSON keys to Workload Identity with Gradle/Play Store REST API plus `firebase-tools`, while improving Jenkins logging, Slack notifications, and token exposure guards.
  • Fixed iOS delivery skip checks so commit bodies do not accidentally suppress workflows, and added netrc authentication to avoid CocoaPods CDN raw.githubusercontent.com 429 failures.

Engineering Lens

  • Treated AI adoption as a workflow-and-context design problem rather than just attaching a model, tying repo-local knowledge directly to review quality.
  • Approached local setup as “sync Vault directly into local environments so authenticated developers receive the same baseline automatically” rather than “who hands secrets out.” By automating yaml generation and certificate refresh behind Vault CLI authentication, the workflow keeps developer environments aligned even as new keys are added.
  • Converged delivery onto a shared-library model with job-name-based target routing so operational steps stay standardized while app/backend differences appear only at the target layer.
  • Moved mobile delivery authentication toward removing long-lived JSON keys via Workload Identity, and made deployment failures easier to trace from Slack messages and logs.
  • When repetitive work started becoming an operational risk, I did not stop at documentation alone; I turned it into Gradle tasks, reusable workflows, and Jenkins pipelines the team could use immediately.

Architecture Snapshot

Mermaid View

AI review, Vault-to-local sync, and deployment standardization

Summarizes how repeated work was turned into three tracks: AI review workflows, Vault-to-local sync based yaml generation, and Jenkins/ArgoCD deployment standardization.

flowchart TD
  Pain["Repeated work<br/>PR review / local env / deploy / internal APIs"] --> Review["voltup-workflow<br/>/gemini-review<br/>org reusable workflow"]
  Review --> Context["project-context + prompts + skills<br/>repo-specific rules injected"]
  Pain --> Local["Gradle generateYamlAction<br/>application-local.yaml"]
  Local --> Vault["Vault CLI login<br/>project path + SHARED path<br/>no secret commits"]
  Local --> IAM["gcloud account -><br/>IAM_DB_USER_NAME"]
  Pain --> Internal["admin-internal-* client<br/>X-Internal-Caller"]
  Pain --> Deploy["Jenkins shared library<br/>job name -> target routing"]
  Deploy --> Build["docker/app build<br/>cache / track / notifications"]
  Deploy --> Android["Android Workload Identity<br/>Play REST API + firebase-tools"]
  Deploy --> IOS["iOS workflow hardening<br/>skip conditions / CocoaPods CDN"]
  Build --> Argo["deployArgoCD"]
  Android --> Argo
  IOS --> Argo
  classDef ai fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef sec fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  classDef ops fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  class Review,Context ai;
  class Local,Vault,IAM,Internal sec;
  class Deploy,Build,Android,IOS,Argo ops;

Operational Outcomes

  • Established an organization-wide AI review workflow plus a skill system for microservice conventions and recurring task shapes, so new repos and workstreams can be onboarded under the same standards much faster.
  • Kept sensitive values out of the repository while syncing Vault directly into local environments, so developer configs stay aligned automatically even as new keys are introduced.
  • Simplified service and app delivery with Jenkins shared-library plus ArgoCD deployment patterns, reducing manual branching in release operations.
  • Standardized Admin internal APIs and mobile delivery authentication, reducing recurring security and operational risks around operator-tool expansion and app releases.
Project

09

Kakao Style / Dec 2023 - Sep 2024

Pricing Platform: Product Management System and Promotion Service

Designed the customer-facing best-price flow across PIM and Promotion

Split the boundary so PIM owns internal/external product matching, dynamic pricing, and shopping-catalog Engine Pages while Promotion owns membership and Final Pricing, allowing the user-facing best price to be chosen by comparing promotion-calculated benefit prices against external market prices.

Resume Link Points

Internal/External Identical Product Matching

This covers product matching that stabilizes comparable groups through image similarity, same-shop exact matches, and winner scores.

Resume Link Points

Final Pricing (Unified API for Service-Level Pricing Logic)

This covers the Final Pricing API that standardizes membership, coupon, promotion, and shipping-adjusted benefit prices.

Resume Link Points

Shopping Catalog Engine Page & Lowest-Price Updates (Naver Shopping / YouTube Shopping)

This covers shopping integration that generates Engine Page outputs, feed CSVs, and sync datasets from changed items only.

Kotlin Spring Boot DGS Framework(GraphQL) AWS Athena

Design Context

Price drivers such as external market prices, internal optimization signals, and membership or coupon benefits were spread across multiple services, while operating policies kept changing. The system needed a structure that separated PIM from Promotion yet still produced a consistent and rational best price for users.

Key Point

A strong project for explaining the boundary where PIM combines external product values with Promotion Final Pricing to expose a rational best price to users.

Core Implementation

  • Used versioned product-matching caches in PIM to resolve `productId -> matchingId`, then grouped exact same-shop matches with winner scores as the basis for pricing comparison.
  • Ran price-optimization batches that read Athena-applied targets, rebuilt internal/external comparison sets, and upserted price scores with rules such as `SUPERIOR / EQUAL = 100` and `UNKNOWN = 50`.
  • Built a shared shopping-catalog path that consumes product and price update events, filters only changed items, and generates Engine Pages, feed CSVs, and sync datasets.
  • Separated membership eligibility and `product / item / order final price` APIs in Promotion, then composed shipping fees through a `MappedBatchLoader` into the final benefit price.

Engineering Lens

  • Split the boundary so PIM owns internal/external matching, dynamic pricing, and shopping catalogs while Promotion owns membership and Final Pricing, allowing PIM to determine the user-facing best price by comparing promotion-calculated benefit prices against external product values.
  • Stabilized comparable product groups first through versioned caches and same-shop exact matching, while exposing winner-score context for operational decisions.
  • Replaced the flow that re-read the entire catalog on every run with a shared path that consumes product and price update events, filters only changed items, and generates the Engine Page plus the Naver Shopping feed CSV and sync dataset, meeting the CPS 2-hour refresh interval and making it quick to extend the same structure to Google Engine Page (YouTube Shopping).
  • Separated final pricing by `product / item / order` boundary and combined membership, coupons, promotions, and shipping in one response while controlling shipping-query cost through DataLoader.

Architecture Snapshot

Mermaid View

Pricing flow from the product-management system to the promotion service

Shows in one diagram how PIM owns internal/external matching, dynamic pricing, and shopping catalogs while Promotion owns membership and Final Pricing, then how PIM combines promotion-calculated benefit prices with external product values to expose the best user-facing price.

flowchart TD
  Req["request<br/>product=421 user=3001 site=KR"] --> Match
  Req --> Member
  subgraph PIMSYS["Product Management System (PIM)"]
    direction TD
    Match["Internal/external product matching<br/>matchingId / same-shop / winner score"]
    Optimize["Dynamic pricing<br/>price score / compare set"]
    Catalog["Shopping catalog Engine Page<br/>Naver / YouTube feed sync"]
    External["External product values<br/>lowest price / sync dataset"]
    Match --> Optimize --> Catalog --> External
  end
  subgraph PROMO["Promotion Service"]
    direction TD
    Member["Membership<br/>grade / eligibility"]
    Final["Final Pricing API<br/>coupon / promotion / shipping"]
    Member --> Final
  end
  External --> Expose
  Final --> Expose
  Expose["PIM user-facing price<br/>promotion final + external price"] --> Resp["response<br/>show best reasonable price"]
  classDef pim fill:#fff4db,stroke:#9a6700,stroke-width:2px,color:#0f172a;
  classDef promo fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
  classDef expose fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
  class Match,Optimize,Catalog,External pim;
  class Member,Final promo;
  class Expose,Resp expose;

Operational Outcomes

  • Structured the system so PIM can combine Promotion Final Pricing values with external product prices to calculate the user-facing best price.
  • Instead of depending on a full-catalog refresh that took about 6 hours, added an event-driven path for changed items so the Naver Shopping feed CSV and sync dataset can be generated within an hour.
  • Commonized the Engine Page and lowest-price update structure built for Naver Shopping so Google Engine Page (YouTube Shopping) could be added quickly on top of the same foundation.
  • Standardized final-pricing responses across product, item, and order boundaries so membership, coupon, and promotion-adjusted prices can be reused under one contract across surfaces and operational batches.
  • Made it possible to change policies inside the product-management system while keeping the user-facing response contract in the promotion service stable.
Project

10

Kakao Style / Apr 2023 - Jun 2023

Membership & Mileage Migration

Migrated the legacy service to Spring Boot through API-response parity verification

Redesigned membership tiers for retention, migrated the legacy membership service (cormo.js-based) to Spring Boot through a 1:1 DB migration with zero downtime, and did the cutover by collecting real request/response sets from the legacy membership API, turning them into test cases, replaying them through the Spring implementation, and comparing output before gradually switching the gateway. The monthly tier calculation was also rebuilt around a partitioned Athena source.

Kotlin Spring Boot Spring Batch DGS Framework(GraphQL) JPA MySQL Kafka AWS Athena

Reference Views

Preview

Zigzag membership: tier-benefit screen

A reference view showing how the expanded membership-tier system and tier benefits were exposed in the actual user-facing UI.

Zigzag membership benefit screen

Zigzag membership: tier-benefit screen

A reference view showing how the expanded membership-tier system and tier benefits were exposed in the actual user-facing UI.

Design Context

The project required a 1:1 DB migration from the cormo.js legacy service while keeping real user-facing responses unchanged, while also preventing monthly tier batches from widening their scan scope as both users and months accumulated.

Key Point

A strong project for explaining zero-downtime migration through legacy-response parity checks together with Athena-based membership-batch optimization.

Core Implementation

  • Collected real request/response sets from the legacy membership API, organized them into test cases across query, body, and edge conditions, and replayed the same inputs through the Spring Boot implementation to compare output diffs.
  • Expanded the membership tier calculation period from 3 to 6 months.
  • After passing response-parity verification, gradually switched the gateway so the Spring Boot service could be opened without breaking user responses.
  • Queried Athena `vip_confirmed_paid_partitioned` by `stamp_date`, paged results with `queryExecutionId + nextToken`, and converted them into `UserConfirmedPaid(cashAmountConfirmed, cashAmountPredicted)` as the monthly batch input.
  • Used `cashAmountConfirmed` as the six-month confirmed cumulative amount for level calculation, batch-upserted the result through `saveMembershipBatchInsert` / `updateMembershipBatchUpdate`, and bounded reads to recent months with `getConfirmedAmountUpdateDateMonthYmSet(3/2)` plus `date_applied_ym` range partitioning on `membership_logs`.

Engineering Lens

  • Treated zero-downtime migration as a response-parity problem first, turning real legacy API request/response sets into reusable test assets that were repeatedly replayed against the Spring implementation.
  • Built the monthly batch to read only the required `stamp_date` from a partitioned Athena source and page through results with `queryExecutionId + nextToken`, avoiding a full in-memory load for large target sets.
  • Stored both six-month confirmed totals and current-month-inclusive totals in `UserConfirmedPaid`, then persisted them as `confirmedAmount`, `confirmedAmountNow`, and `confirmed5MonthAmount` so tier logic and later reads could rely on the model directly.
  • Grouped writes into JDBC batch insert/update, limited reads to recent month sets with `getConfirmedAmountUpdateDateMonthYmSet`, and managed `membership_logs` with `date_applied_ym` range partitions so only the needed months are touched as data grows.

Architecture Snapshot

Mermaid View

Zero-downtime migration through legacy API response-parity checks

Shows the zero-downtime migration flow that collects request/response sets from the legacy membership API, replays the same inputs through the Spring Boot implementation, compares output diffs, and then gradually switches the gateway.

flowchart TD
  Legacy["legacy membership API<br/>request / response set capture"] --> Cases["test case conversion<br/>query / body / edge case"]
  Cases --> Replay["replay the same input<br/>into Spring Boot logic"]
  Replay --> Compare{"same as legacy response?"}
  Compare -->|yes| Ready["ready for rollout"]
  Compare -->|no| Fix["fix logic / serializer diff"]
  Fix --> Replay
  Ready --> Switch["gradual gateway switch"]
  Switch --> Open["zero-downtime release"]

Mermaid View

Membership batch with monthly cumulative sums and monthly partitioning

Shows the flow from Athena `vip_confirmed_paid_partitioned` by `stamp_date`, through `UserConfirmedPaid` and membership batch upserts, into recent-month-scoped lookups and `membership_logs` partitioning.

flowchart TD
  Source["Athena source<br/>vip_confirmed_paid_partitioned<br/>stamp_date=2023-10-08"] --> Reader["reader paging<br/>queryExecutionId + nextToken"]
  Reader --> Paid["UserConfirmedPaid<br/>user=421 confirmed=330000<br/>predicted=350000"]
  Paid --> Calc["level calc<br/>latest 6-month cumulative sum"]
  Calc --> Upsert["membership upsert<br/>dateAppliedYm=202310"]
  Upsert --> Current["memberships<br/>batch insert / update"]
  Upsert --> Archive["membership_logs<br/>RANGE(date_applied_ym)"]
  Current --> Query["recent Ym lookup<br/>202310, 202309, 202308"]
  Archive --> Query

Operational Outcomes

  • Reduced DB load from a 70% threshold to under 30% through monthly cumulative sums and monthly partitioning.
  • Achieved zero-downtime deployment by passing request/response-set-based parity tests between the legacy API and the Spring implementation, then gradually switching the gateway.
  • Combined a partitioned Athena source, paged reader, JDBC batch upsert, and recent-month-scoped lookups to keep monthly tier-calculation performance stable as customer data accumulated.
Project

11

Personal Project / Ongoing

Commit Map

Designed an authoring flow that turns natural-language travel routes into structured map content

A map-based travel planning service built to share personal itineraries with friends. Natural-language destinations and routes are turned into a rough itinerary draft through an AI workflow, then refined manually in Markdown.

Astro React Leaflet TypeScript Markdown GitHub Pages Antigravity

Reference Views

Preview

Home screen: world map and trip cards

A reference view showing the country filters, world map, and trip-planning cards in one continuous screen.

Reference image of the Commit Map home screen

Home screen: world map and trip cards

Open service

A reference view showing the country filters, world map, and trip-planning cards in one continuous screen.

Design Context

Travel plans tend to scatter across chat messages, map links, and notes, making them hard to share and revise, while manually structuring locations and coordinates from scratch also costs too much effort.

Key Point

Although it started as a hobby project, it became a real product example of natural-language input flowing into a structured draft that a human refines further.

Core Implementation

  • Built a static web service with Astro, React, and Leaflet that combines travel cards, detailed maps, and timelines, visualizing routes by place type, order, and visit date.
  • Managed trip posts through Markdown frontmatter and a location schema so dates, coordinates, notes, and links remain structured and easy to refine manually later.
  • Added an AI workflow so natural-language destinations and routes can quickly produce a draft post, candidate places, and a starter itinerary, after which I refine the plan and content manually.
  • Deployed it statically on GitHub Pages so travel plans can be shared instantly, with each trip post addressable by its own URL.

Engineering Lens

  • Treated AI not as the final planner but as a draft generator for the first route pass, while keeping the source of truth in Markdown data.
  • Viewed travel content as more valuable when maps, timelines, and location data are shown together rather than as text-only blog posts, so visualization and data structure were designed as one flow.
  • Kept operating cost low with static deployment and file-based content so the personal project stays lightweight and can grow only where needed.

Operational Outcomes

  • Operates as a personal service where itineraries can be shared through a single link instead of scattered messages.
  • Built an authoring flow where just listing destinations and routes is enough for the AI workflow to produce a starter plan that can later be refined in detail.
  • Unified maps, timelines, and location metadata under one content model so both past trips and future plans can be operated in the same way.