[portfolio](https://geonwoo.kim/en/portfolio) › SSAFY

# jansorry · holiday nagging receipts

https://geonwoo.kim/en/portfolio/jansorry

Period: 2024.01 — 2024.02

The project started from the stress young people feel from holiday nagging and the gap between generations. Users share nagging and how they handled it, then turn those stories into receipts and statistics. A preliminary survey with 300+ participants collected holiday stress data, and the project won first place among SSAFY common projects and the Project Excellence Award. I handled more than half of the backend work and set up the servers and CI/CD alone.

About 200 sign-ups over Lunar New Year

- [github](https://github.com/jansorry/jansorry-be)

Team: 6 people · Backend 3, frontend 3

Role: Team lead · Backend lead · Infrastructure and tech owner · Nagging, like, follow and receipt API · Batch

Java 17 · Spring Boot 3.2.1 · Spring Data JPA · Spring Security · Spring Batch · Querydsl · MySQL 8.0.35 · Redis 7.2 · AWS EC2 · GitLab Runner · Docker

- Feed of nagging and responses
- Likes and follows
- Receipt printing and sharing
- Account page
- Statistics visualization
- Deployment to EC2 through GitLab Runner and Docker Hub
- Next.js, Spring Boot, MySQL and Redis containers behind Nginx

## Two-way follow relations in Redis

Problem: Follow relationships had to live in a non-relational store. Unlike one-way likes, follows are many-to-many, so storing both directions uses more space while storing one direction slows lookups.

Solution: I decided wildcard scans should be avoided wherever possible and stored follows in both directions. Keys carry following and follower prefixes, so each user's following and follower lists are read directly. Follower and following lists became much faster to look up.

## Full scans in the batch sync

Problem: The daily batch moving Redis data to MySQL scanned the entire dataset. I needed a way to move only the data that had changed.

Solution: Changed keys go into a Redis sorted set, scored by their update time. The batch writes only changes since the previous run to MySQL, then clears the set so it holds only new changes. Rows left empty after likes or follows were canceled were also removed from the table to free space.

## Top 5 receipts by price

Problem: The final statistics needed the 5 highest-priced receipts. Searching every receipt of every user at once was costly.

Solution: Reused the sorted-set approach from the batch sync. Saving a receipt adds it to a top-5 set scored by price, and the receipt pushed to sixth place is removed right away. This spreads the search load across saves instead of one moment, so no query scans all receipts at once.

## Deploying frontend and backend from one repository

Problem: The project could use only one repository, so the frontend and backend code shared it and deployment had to be automated from there. Jenkins had a high setup cost and took up a lot of server resources.

Solution: Chose GitLab Runner, which has fewer plugins but sets up a pipeline with little overhead. The pipeline has 4 stages: create the environment file, build, build the Docker image and push it to Docker Hub, then run it on the server. Secrets such as database credentials live in GitLab CI/CD variables and become a .env file at deploy time.

## Architecture

jansorry deployment diagram: GitLab Runner, Docker Hub, and Nginx, Next.js, Spring Boot, MySQL and Redis on EC2

Images built by GitLab Runner and deployed as Docker containers on EC2

Angled smartphone mockup showing the jansorry start screen

Smartphone mockup showing the jansorry start screen

jansorry feed with hashtag filters

jansorry screen with a receipt button and saved nagging cards

jansorry receipt screen with a QR code

jansorry statistics screen with average count and gender breakdown


[lyricit · real-time game built on song lyrics](https://geonwoo.kim/en/portfolio/lyricit) · [newjibs · real estate investment simulator](https://geonwoo.kim/en/portfolio/newjibs)
