- you can see my working flow (pr, review ...) from the original organization repository!
- 📌 Overview
- 💚 Demo
- 👉 Why do you need BoogiBoogi?
- 🧪 Scientific Background
- 📋 How was the detection threshold set?
- 📊 Detection Sensitivity Levels
- 🛠 Tech Stack
- 🧑🤝🧑 Team members and roles
- 💻 My contribution
Most web-based posture correction systems rely on a side-view camera setup, which often requires additional space or inconvenient camera placement.
This application estimates forward head posture using only a front-facing webcam,
allowing users to measure posture naturally during everyday computer use without extra hardware or environmental adjustments.
Plus, no video or photo data of the user is serving to server for the privacy!
- Processed MediaPipe landmark coordinates
- Calculated neck angle deviation based on shoulder–ear alignment
- Applied threshold-based classification for forward head posture detection
- Designed component-based UI structure in Next.js
- Implemented real-time feedback system (visual + audio alerts)
- Optimized rendering to handle continuous webcam input
- Designed a type-safe backend API layer using Next.js Server Actions and Zod validation
- Integrated PostgreSQL (NeonDB) via Prisma ORM for scalable data management and secure transactions
- Connected PostgreSQL (NeonDB) for data storage
- Released with Vercel.
Have you seen this woman?
This image was created by British behavioral futurist William Higham,
which explored how the body of an office worker might change after 20 years of desk-based work.
It shows how long hours of office work and poor posture can gradually affect the human body.
Today, posture-related problems are becoming increasingly common among modern workers.
According to the Korea Disease Control and Prevention Agency (KDCA),
forward head posture is reported in more than 70% of adults aged 25 to 42.
For this reason, there is a clear need for a tool that helps users monitor and improve their posture easily during everyday work.
Forward head posture (FHP) is commonly assessed using the craniovertebral angle (CVA).
In many clinical studies, FHP is identified when the CVA falls below approximately 45° to 50°.
One of the early studies frequently referenced for this range is:
The craniovertebral angle (CVA) is a widely used measurement for evaluating forward head posture.
It is generally defined as the angle formed between:
- a horizontal line through the C7 vertebra
- a line connecting C7 to the tragus of the ear
A recent study published in 2024 classified severe forward head posture as a CVA below 45°.
The same study also reported that CVA tends to be measured lower in the sitting position than in the standing position,
regardless of whether the participant has forward head posture.
Since this application is mainly intended for users who are sitting at a desk,
this difference was considered when setting the detection thresholds.
Reference (Evaluation of the Craniovertebral Angle in Standing versus Sitting Positions in Young Adults with and without Severe Forward Head Posture David A. Titcomb et al. / January 2024)
This application provides three posture sensitivity levels
so that users can choose how strictly posture changes should be monitored:
- Low: 45°
- Middle: 48°
- High: 50°
- 45° was used for Low, based on the threshold used in the paper for severe forward head posture
- 48° was used for Middle as a more moderate threshold for everyday monitoring
- 50° was used for High because many studies treat angles above 50° as being within a normal posture range
This allows the application to support different levels of detection sensitivity depending on the user's preference.
| Category | Technology |
|---|---|
| Frontend | Next.js, React, TypeScript, TailwindCSS |
| Pose Estimation | MediaPipe Pose Landmarker |
| 3D Visualization | Three.js |
| Backend / DB | Next.js, PostgreSQL (NeonDB), prisma |
| Collaboration | GitHub, Jira, notion |
🔗 Jira Board:
https://kge0211114.atlassian.net/jira/software/projects/TNA/boards/34
- Gaeun Kim: AI, Backend Lead, Frontend
김가은 | kge0211114@gmail.com - Jimin Nam: Frontend Lead
남지민 | dnpsel2737@gmail.com - Seunghyun park: Frontend(Three.js)
박승현 | seunghyuni00@khu.ac.kr - Jun Hur: AI Lead
허준 | heojun8500@naver.com
- 51% out of 4 team members (based on commit numbers)
- state managing with zustand ( MeasurementController, PipController, Soundcontroller )
- custom hooks ( useFriendsData, useHandleHotKey ... )
- UI component/page ( HelpMessageModal, CharacterSelectionPage ... )
I am working on my code with this rules.
😭 before refactoring
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && selectedCharacter) {
handleConfirm();
} else if (e.key === "Escape") {
handleSkip();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedCharacter]);🥰 after refactoring
useHandleHotKey("Enter", () => {
if (selectedCharacter) {
handleConfirm();
}
});
useHandleHotKey("Escape", () => {
handleSkip();
});😭 before refactoring
const handleConfirm = () => {
if (selectedCharacter) {
if (typeof window !== "undefined") {
localStorage.setItem("selectedCharacter", selectedCharacter);
}
router.push("/");
}
};
const handleSkip = () => {
if (typeof window !== "undefined") {
localStorage.setItem("selectedCharacter", "remy");
}
router.push("/");
};🥰 after refactoring
const saveCharacterAndRedirect = (characterId: string) => {
if (typeof window != "undefined") {
localStorage.setItem("selectedCharacter", characterId);
}
router.replace("/");
};
const handleConfirm = () => {
if (selectedCharacter) {
saveCharacterAndRedirect(selectedCharacter);
}
};
const handleSkip = () => {
saveCharacterAndRedirect("remy");
};3. Similar levels of abstraction: Make sure that level of abstraction is similar in a component, page ...etc.
😭 before refactoring
<div>
<Button>
...
</Button>
<div
className={`grid transition-all duration-300 ease-in-out ${
isAccordionOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden">
<div className="px-3.5 pb-3.5 pt-0 text-[13px] leading-relaxed text-[var(--green)]">
<ul className="flex flex-col gap-1.5">
{item.descriptions.map((desc, idx) => (
<li key={idx} className="flex gap-1.5 items-start">
<span className="mt-0.5 text-[var(--green-mid)] text-[10px]">●</span>
<span>{desc}</span>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</>🥰 after refactoring
<div>
<Button>
...
</Button>
<div className="custom-scrollbar flex max-h-[400px] flex-col gap-2.5 overflow-y-auto pr-1">
{GUIDE_DATA.map((item) => (
<HelpAccordionItem
key={item.id}
item={item}
isOpen={openAccordionId === item.id}
onToggle={() => toggleAccordion(item.id)}
/>
))}
</div>
</div>
</div>- All logic on BE side.
- Parallelized AI & Camera Loading: Eliminated waterfall loading bottlenecks by executing
PoseLandmarker.createFromOptions(MediaPipe AI) andnavigator.mediaDevices.getUserMedia(Camera API) asynchronously in parallel. - Unblocked UI Rendering: Seperated the
CanvasRenderingContext2D.drawImagelogic from the AI worker's readiness,
providing an instant camera feed to users while the model downloads in the background.
|
=> |
|
| Before | After |
- Dynamic FPS Throttling: Leveraged the Page Visibility API (document.hidden) to dynamically reduce the measurement polling rate
(e.g., 10fps down to 5fps) when the tab is inactive, significantly optimizing CPU usage and battery consumption.
|
=> |
|
| Before | After |
|
=> |
|
| Before | After |
- Web Worker Architecture (
poseDetection.worker.ts): Offloaded heavy landmark computations to a background thread.
UtilizedcreateImageBitmapto transfer video frames efficiently, preventing main thread blocking.
- Memory Leak Prevention: Enforced strict useEffect cleanups in Estimate.tsx to reliably execute
worker.terminate(),PiP window.close(), andMediaStreamTrack.stop()upon component unmount.
This project processes user health-related data. We got a lot of feedbacks concerning about privacy problem. To minimize security risks and protect sensitive information, we implemented the following strategies:
Regularly collected posture data is stored in IndexedDB on the client side instead of being continuously transmitted to the server.
This approach:
- Reduces exposure to network-based interception (e.g., MITM attacks)
- Minimizes unnecessary data transmission up to 90%
- Limits server-side accumulation of sensitive health data
- Hardly being influcned by network disconnection.
Only essential or aggregated data is persisted to the backend when necessary.
We adopted Next.js as a full-stack framework to unify frontend and backend logic within a single controlled environment.
This provides:
- Reduced attack surface by avoiding publicly exposed REST endpoints
- Sensitive logic never exposed to the client bundle
- Strong type safety between client and server
By consolidating the stack, we minimized configuration inconsistencies and improved maintainability.
- Enforce strict input validation
- Prevent malformed or malicious payloads
- Ensure runtime data integrity beyond TypeScript's compile-time checks
This approach:
- Prevents content-type sniffing attacks with
X-Content-Type-Options: nosniff - Reduces cross-origin referrer leakage with a
strict referrer policy - Restricts access to sensitive browser capabilities through
Permissions-Policy
This middleware-based approach made security rules easier to maintain and ensured they were applied uniformly across the application.
Constructed robust APIs using Next.js Server Actions, strictly validating client payloads via Zod to ensure runtime safety.
//actions/summaryActions.ts
const PostDailySummarySchema = z.object({
dateISO: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "wrong date formation"),
sumWeighted: z.number().refine(Number.isFinite),
weightSeconds: z.number().refine((n) => Number.isFinite(n) && n > 0),
count: z.number().int().nonnegative(),
});
export type PostDailySummaryInput = z.infer<typeof PostDailySummarySchema>;
export async function postDailySummaryAction(_prevState: ActionState<unknown>, data: PostDailySummaryInput) {
const session = await auth();
if (!session?.user?.id) {
return { ok: false, status: 401, message: SERVER_MESSAGES.AUTH_REQUIRED } as const;
}
const parsed = PostDailySummarySchema.safeParse(data);
if (!parsed.success) {
return { ok: false, status: 400, message: SERVER_MESSAGES.SYSTEM_MESSAGES } as const;
}
try {
const result = await upsertDailySummary({
...parsed.data,
userId: session.user.id,
});
revalidateTag("daily_summary");
return { ok: true, data: result } as const;
} catch (error: unknown) {
logger.error("[postDailySummaryAction] Error:", error);
return { ok: false, status: 500, message: SERVER_MESSAGES.INTERNAL_SERVER_ERROR } as const;
}
}-
Eliminated Separate API Layer
Reduced complexity by removing the need for dedicated REST endpoints. -
End-to-End Type Safety
Shared TypeScript types between client and server without manual request/response contracts. -
Smaller Client Bundle Size
Database logic stays on the server, preventing unnecessary client-side code bloat. -
Improved Security
Sensitive logic and database access are never exposed to the browser. -
Seamless React Integration
Works natively with React Server Components, Suspense, and streaming. -
Built-in Caching & Revalidation
LeveragedrevalidatePathandrevalidateTagfor automatic cache invalidation without external libraries. ( No need to add caching library like TQ )
Implemented scalable friendship management ensuring data consistency and integrity using Prisma $transaction.
// friends.service.ts
return prisma.$transaction(async (tx) => {
const updated = await tx.friendRequest.update({
where: { id: requestId },
data: { status: "ACCEPTED", respondedAt: new Date() },
select: { id: true, status: true, respondedAt: true, fromUserId: true, toUserId: true },
});Integrated a centralized logging utility with Sentry, providing context-aware error tracking and i18n-ready,
user-friendly error messages while securing sensitive stack traces in production.
error message that developers see:
return json({ error: "UNAUTHORIZED" }, 401);error message that users see:
FRIEND_NOT_FOUND: {
ko: "아직 저희 서비스를 이용하지 않는 친구 같아요. 이 기회에 초대해 보는 건 어떨까요? 💌",
en: "It looks like your friend hasn't joined us yet. Why not invite them? 💌",
},- I added our coding philosopy to Claude Code skills.
- I use this skills for repeated, simple tasks. Such as ...
- Change
<button>into<Button>in whole code base. - Move componets that has to do with 'estimate'page into app/estimate/components folder.
- Change
- I also consider Claude Code as a senior developer I can discuss about the refactoring.
- I analyzed our CVA data and removed the noises.











