<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>개인 프로젝트 | eecczz's page</title><link>https://eecczz.github.io/ko/toy-projects/</link><atom:link href="https://eecczz.github.io/ko/toy-projects/index.xml" rel="self" type="application/rss+xml"/><description>개인 프로젝트</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>ko</language><lastBuildDate>Wed, 01 Jul 2026 00:00:00 +0000</lastBuildDate><image><url>https://eecczz.github.io/media/icon_hu107733296a429e9339b15589e162cf6f_1739_512x512_fill_lanczos_center_3.png</url><title>개인 프로젝트</title><link>https://eecczz.github.io/ko/toy-projects/</link></image><item><title>Wii Swordplay 모작</title><link>https://eecczz.github.io/ko/toy-projects/swordplay/</link><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/swordplay/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">UNITY · ANIMATION · PHYSICS CASE STUDY&lt;/p>
&lt;p>마우스 방향 기반 2D Blend Tree, 복합 Animation Rigging, 물리 추종 collider와 방향별 피격 반응을 결합한 Unity WebGL 검술 프로젝트입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 입력/2D 애니메이션 블렌딩/IK/물리 피격/WebGL&lt;/span>&lt;span>&lt;b>검증&lt;/b> WebGL 배포&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>마우스 방향에 따라 공격 모션이 섞이고, 양손과 상체 IK가 무기 자세를 따라가며, 충돌 위치에 맞춰 상대의 피격 방향과 넉백이 달라지는 검술 전투를 구현했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="무기와 양손 자세가 맞물리는 검술 대련 장면" srcset="
/ko/toy-projects/swordplay/featured_hu8fb47b9acb7a79f6427c3880623bc535_448955_c2d95009aa713055072e48c5cd088f23.webp 400w,
/ko/toy-projects/swordplay/featured_hu8fb47b9acb7a79f6427c3880623bc535_448955_9984561782833a905a514efec08f16a4.webp 760w,
/ko/toy-projects/swordplay/featured_hu8fb47b9acb7a79f6427c3880623bc535_448955_1200x1200_fit_q75_h2_lanczos.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/swordplay/featured_hu8fb47b9acb7a79f6427c3880623bc535_448955_c2d95009aa713055072e48c5cd088f23.webp"
width="760"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="타격 방향에 따라 상대가 반응하는 전투 화면" srcset="
/ko/toy-projects/swordplay/detail-hit-reaction_hu95869620e25e48a2321dbad1731b5fac_1029118_e84ac1a8cb1c5eb6f304b11e46a44cc6.webp 400w,
/ko/toy-projects/swordplay/detail-hit-reaction_hu95869620e25e48a2321dbad1731b5fac_1029118_438072cde58fb7288cb4bee6c73d7c1c.webp 760w,
/ko/toy-projects/swordplay/detail-hit-reaction_hu95869620e25e48a2321dbad1731b5fac_1029118_1200x1200_fit_q75_h2_lanczos.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/swordplay/detail-hit-reaction_hu95869620e25e48a2321dbad1731b5fac_1029118_e84ac1a8cb1c5eb6f304b11e46a44cc6.webp"
width="760"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="캐릭터·무기 셰이더와 대련 장면" srcset="
/ko/toy-projects/swordplay/detail-shader_hu9a74d59b2249f059cac2ec36bfb754ac_250432_e7a33c8a2481adf89562494a31421728.webp 400w,
/ko/toy-projects/swordplay/detail-shader_hu9a74d59b2249f059cac2ec36bfb754ac_250432_58c5bc112a73ec0e0bf84a8b33c68dec.webp 760w,
/ko/toy-projects/swordplay/detail-shader_hu9a74d59b2249f059cac2ec36bfb754ac_250432_1200x1200_fit_q75_h2_lanczos.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/swordplay/detail-shader_hu9a74d59b2249f059cac2ec36bfb754ac_250432_e7a33c8a2481adf89562494a31421728.webp"
width="760"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-손뿐-아니라-허리머리까지-서로-다른-목표를-만족하는-복합-ik가-필요함">1. 손뿐 아니라 허리·머리까지 서로 다른 목표를 만족하는 복합 IK가 필요함&lt;/h3>
&lt;p>손이 무기 손잡이를 추적하면 팔꿈치와 어깨뿐 아니라 허리도 검의 방향으로 회전하고 기울어져야 했습니다. 반면 머리는 허리 회전에 끌려가지 않고 지면과 수평한 기울기를 유지하면서 상대의 얼굴을 바라봐야 했습니다.&lt;/p>
&lt;p>양팔은 &lt;code>Two Bone IK&lt;/code>, 허리와 머리의 방향 제어는 &lt;code>Multi-Aim&lt;/code>, 무기 소켓과 머리의 기준 변환은 &lt;code>Multi-Parent&lt;/code>를 조합했습니다. 머리에는 캐릭터 루트 회전을 기준으로 자세를 다시 보정해 허리가 기울어도 수평을 유지하도록 했습니다. 평상시·가드·공격·피격 상태에 따라 Rig와 각 constraint의 weight를 코드에서 전환하거나 보간해 애니메이션과 IK가 서로 덮어쓰지 않게 조정했습니다.&lt;/p>
&lt;h3 id="2-2d-blend-tree의-검-궤적을-collider에-그대로-사용하면-물리-충돌-방향이-불안정함">2. 2D Blend Tree의 검 궤적을 collider에 그대로 사용하면 물리 충돌 방향이 불안정함&lt;/h3>
&lt;p>마우스 위치를 &lt;code>tx·ty&lt;/code> 좌표로 변환하고, 상단에는 종방향 공격, 좌우에는 횡방향 공격, 사이에는 대각 공격 모션을 배치한 2D Blend Tree로 검의 방향을 연속적으로 만들었습니다. 그러나 애니메이션이 매 프레임 결정한 검 transform은 물리 엔진 관점에서는 순간이동이므로, collider를 그대로 붙이면 충돌 속도와 넉백 방향이 실제 휘두른 궤적과 어긋났습니다.&lt;/p>
&lt;p>애니메이션 검을 target으로만 사용하고, 보이지 않는 Rigidbody·collider가 Position/Rotation Drive의 spring과 damping으로 target을 추적하도록 분리했습니다. 저장된 Scene의 추종 drive는 Spring 1000, Damper 50으로 설정되어 있으며, &lt;code>Configurable Joint&lt;/code>가 연결된 물리 검이 충돌을 일으키게 해 실제 이동 속도와 방향을 가진 타격으로 바꿨습니다.&lt;/p>
&lt;h3 id="3-충돌-위치와-방향에-맞는-피격-모션넉백을-동시에-만들어야-함">3. 충돌 위치와 방향에 맞는 피격 모션·넉백을 동시에 만들어야 함&lt;/h3>
&lt;p>충돌점을 캐릭터 로컬 좌표로 변환해 &lt;code>cx·cy&lt;/code>로 정규화하고, 왼쪽·오른쪽·상단 피격 모션을 배치한 별도의 2D Blend Tree에 전달했습니다. 왼쪽에 맞으면 오른쪽으로 중심을 잃고, 상단에 맞으면 뒤로 넘어지는 모션이 비율에 따라 섞이도록 했습니다.&lt;/p>
&lt;p>동시에 캐릭터 root의 Rigidbody와 &lt;code>Configurable Joint&lt;/code>가 타격 방향으로 밀리게 하고 joint 제한·damping으로 속도를 줄였습니다. 방향별 애니메이션과 물리 넉백을 함께 적용해 특정 부위에 맞은 자세로 기울면서 밀린 뒤 자연스럽게 멈추도록 구현했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="공격 방향·피격 위치·물리 추종 설정 핵심 코드"
src="https://eecczz.github.io/ko/toy-projects/swordplay/code-combat-pipeline.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h3 id="4-webgl-시작-시-메모리-crash와-materiallighting-문제가-발생">4. WebGL 시작 시 메모리 crash와 material/lighting 문제가 발생&lt;/h3>
&lt;p>메모리 설정과 material·sky·camera를 커밋 단위로 수정하고 다시 배포했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>2D Blend Tree (&lt;code>tx·ty&lt;/code>)&lt;/strong>&lt;/td>
&lt;td>마우스 방향에 따라 종·횡·대각 공격 모션을 연속적으로 혼합하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Two Bone IK · Multi-Aim · Multi-Parent&lt;/strong>&lt;/td>
&lt;td>양손·팔꿈치·허리·머리가 서로 다른 자세 목표를 동시에 만족하도록 구성하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Physics target follower&lt;/strong>&lt;/td>
&lt;td>애니메이션 검을 보이지 않는 Rigidbody·collider가 spring·damping으로 추적하게 하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Hit-point 2D Blend Tree (&lt;code>cx·cy&lt;/code>)&lt;/strong>&lt;/td>
&lt;td>충돌 위치에 따라 좌우·상단 피격 자세를 연속적으로 혼합하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Configurable Joint&lt;/strong>&lt;/td>
&lt;td>검 충돌과 캐릭터 root의 넉백·감속·자세 회복을 물리적으로 연결하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>&lt;code>tx·ty&lt;/code> 공격 Blend Tree, 복합 IK, 물리 추종 collider, &lt;code>cx·cy&lt;/code> 피격 Blend Tree를 하나의 전투 루프로 연결했습니다.&lt;/li>
&lt;li>캐릭터 상태에 따라 Rig·constraint weight와 물리 joint 제한을 실시간으로 전환했습니다.&lt;/li>
&lt;li>WebGL 빌드를 공개하고 시각 스타일·카메라·메모리 오류를 지속 수정했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Wii Swordplay 모작 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/swordplay/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>마우스 방향을 Animator의 &lt;code>tx·ty&lt;/code> 좌표로 변환합니다.&lt;/li>
&lt;li>2D Blend Tree가 종·횡·대각 공격 모션을 혼합해 애니메이션 검의 궤적을 만듭니다.&lt;/li>
&lt;li>보이지 않는 물리 collider가 spring·damping으로 애니메이션 검을 추적합니다.&lt;/li>
&lt;li>양팔·허리·머리 constraint의 target과 weight를 캐릭터 상태에 맞게 조정합니다.&lt;/li>
&lt;li>충돌점을 로컬 &lt;code>cx·cy&lt;/code>로 변환해 피격 2D Blend Tree에 전달합니다.&lt;/li>
&lt;li>root Configurable Joint가 타격 방향의 넉백과 감속을 처리합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Input&lt;/code>&lt;/td>
&lt;td>&lt;code>mouse → tx·ty&lt;/code>&lt;/td>
&lt;td>공격 방향 선택&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Animation&lt;/code>&lt;/td>
&lt;td>&lt;code>2D attack Blend Tree&lt;/code>&lt;/td>
&lt;td>종·횡·대각 모션 혼합&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Rig&lt;/code>&lt;/td>
&lt;td>&lt;code>Two Bone IK · Multi-Aim · Multi-Parent&lt;/code>&lt;/td>
&lt;td>손·허리·머리 자세 제어&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Physics&lt;/code>&lt;/td>
&lt;td>&lt;code>target follower · Configurable Joint&lt;/code>&lt;/td>
&lt;td>물리 검 충돌·넉백·damping&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Reaction&lt;/code>&lt;/td>
&lt;td>&lt;code>contact point → cx·cy&lt;/code>&lt;/td>
&lt;td>방향별 피격 Blend Tree&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>애니메이션 target과 물리 collider 사이의 거리·회전 오차·latency 계측&lt;/li>
&lt;li>충돌 상대속도·무기 질량 기반 impulse와 hit-stop 튜닝&lt;/li>
&lt;li>Rig constraint별 weight curve를 상태별 ScriptableObject로 분리&lt;/li>
&lt;li>Unity Robotics 시뮬레이션에서 end-effector tracking으로 전이 실험&lt;/li>
&lt;/ul></description></item><item><title>My Movie Web</title><link>https://eecczz.github.io/ko/toy-projects/my-movie-web/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/my-movie-web/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>TMDb API 영화 데이터를 검색·필터·정렬하고 위시리스트 상태로 연결한 React 웹 애플리케이션입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 외부 API/React 상태/정적 배포&lt;/span>&lt;span>&lt;b>검증&lt;/b> GitHub Pages 자동 배포&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>인기 영화와 장르별 콘텐츠를 한 화면에서 탐색하고 검색·상세·위시리스트로 이동하는 구조입니다. 메인 화면에서 콘텐츠 분류와 검색 결과를 확인하고 관심 영화를 별도 목록에 저장할 수 있습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="TMDb 영화 콘텐츠를 탐색하는 메인 화면" srcset="
/ko/toy-projects/my-movie-web/detail-explore_hub184b924ee337dbb1f8cc7087325df63_1695978_8dd4af248a9078f8669f9dfbbf36b8a7.webp 400w,
/ko/toy-projects/my-movie-web/detail-explore_hub184b924ee337dbb1f8cc7087325df63_1695978_47b14cbcb62b31fdcfc0831639f8e687.webp 760w,
/ko/toy-projects/my-movie-web/detail-explore_hub184b924ee337dbb1f8cc7087325df63_1695978_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/my-movie-web/detail-explore_hub184b924ee337dbb1f8cc7087325df63_1695978_8dd4af248a9078f8669f9dfbbf36b8a7.webp"
width="760"
height="380"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-정적-pages에서-spa-route-새로고침이-깨질-수-있음">1. 정적 Pages에서 SPA route 새로고침이 깨질 수 있음&lt;/h3>
&lt;p>배포 source와 router base를 맞추고 빌드 결과를 Pages 경로에 배치했습니다.&lt;/p>
&lt;h3 id="2-가입-비밀번호가-브라우저-저장소에-평문으로-남는-학습용-구현">2. 가입 비밀번호가 브라우저 저장소에 평문으로 남는 학습용 구현&lt;/h3>
&lt;p>문제를 인지하고 저장 방식을 수정했으며, 실제 인증은 서버에서 처리해야 한다고 한계로 명시했습니다.&lt;/p>
&lt;h3 id="3-환경변수와-api-key-노출-위험">3. 환경변수와 API key 노출 위험&lt;/h3>
&lt;p>로컬 .env 대신 예시 파일만 추적했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>TMDb API&lt;/strong>&lt;/td>
&lt;td>직접 데이터셋을 구축하지 않고 실제 영화 검색·상세 흐름을 만들기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>React Router&lt;/strong>&lt;/td>
&lt;td>목록·검색·상세·위시리스트 경로를 분리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Client state&lt;/strong>&lt;/td>
&lt;td>관심 영화 추가/삭제를 화면에 즉시 반영하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GitHub Actions/Pages&lt;/strong>&lt;/td>
&lt;td>develop 기반 정적 빌드를 자동 배포하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>검색·필터·위시리스트·라우팅의 전체 프론트엔드 흐름을 구현했습니다.&lt;/li>
&lt;li>GitHub Pages 자동 배포 workflow를 추가해 실제 접근 가능한 빌드를 만들었습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="My Movie Web 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/my-movie-web/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>사용자가 인기 목록 또는 검색어를 입력합니다.&lt;/li>
&lt;li>TMDb API 응답을 화면 모델로 정규화합니다.&lt;/li>
&lt;li>장르·평점·정렬 조건을 클라이언트에서 적용합니다.&lt;/li>
&lt;li>위시리스트 변경을 사용자 상태에 반영합니다.&lt;/li>
&lt;li>React Router가 상세/목록 전환을 담당합니다.&lt;/li>
&lt;li>자동 workflow가 정적 빌드를 Pages source에 배포합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Discover&lt;/code>&lt;/td>
&lt;td>&lt;code>popular/search&lt;/code>&lt;/td>
&lt;td>TMDb 호출&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Detail&lt;/code>&lt;/td>
&lt;td>&lt;code>/movie/:id&lt;/code>&lt;/td>
&lt;td>상세 경로&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Wishlist&lt;/code>&lt;/td>
&lt;td>&lt;code>add/remove&lt;/code>&lt;/td>
&lt;td>클라이언트 상태&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Deploy&lt;/code>&lt;/td>
&lt;td>&lt;code>GitHub Pages&lt;/code>&lt;/td>
&lt;td>정적 빌드&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>실서비스 인증은 서버/JWT·HttpOnly cookie로 전환&lt;/li>
&lt;li>React Query cache와 loading/error 상태 표준화&lt;/li>
&lt;li>웹 성능·접근성 Lighthouse 측정&lt;/li>
&lt;/ul></description></item><item><title>스포츠 분석 AI</title><link>https://eecczz.github.io/ko/toy-projects/sports-analyzer/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/sports-analyzer/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>YouTube 영상의 선택 구간만 추출하고 FastAPI가 AI 분석 결과를 SSE로 전달하는 Next.js 기반 MVP입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 웹/AI 서비스 분리·영상 구간 처리·스트리밍 응답&lt;/span>&lt;span>&lt;b>검증&lt;/b> 로컬 MVP 및 분석 스트리밍 진단&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>YouTube 스포츠 영상을 탐색한 뒤 분석할 구간을 지정하고, 선택 구간의 질의응답과 분석 결과를 확인하는 MVP입니다. 영상 탐색 화면에서 종목별 후보와 분석 대상을 빠르게 비교할 수 있습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="스포츠 영상을 탐색하고 분석 대상을 선택하는 화면" srcset="
/ko/toy-projects/sports-analyzer/featured_hu7a0eab84f910f694e465208809805b85_392252_3be21790baeef82b15f2caacf051dc74.webp 400w,
/ko/toy-projects/sports-analyzer/featured_hu7a0eab84f910f694e465208809805b85_392252_4949a3f168314c68aa8e02adc49512d0.webp 760w,
/ko/toy-projects/sports-analyzer/featured_hu7a0eab84f910f694e465208809805b85_392252_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/sports-analyzer/featured_hu7a0eab84f910f694e465208809805b85_392252_3be21790baeef82b15f2caacf051dc74.webp"
width="760"
height="357"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-전체-영상을-모델에-보내면-처리-시간비용맥락-잡음이-커짐">1. 전체 영상을 모델에 보내면 처리 시간·비용·맥락 잡음이 커짐&lt;/h3>
&lt;p>타임라인에서 선택한 구간만 ffmpeg로 잘라 전달했습니다.&lt;/p>
&lt;h3 id="2-분석-응답이-길어-ui가-멈춘-것처럼-보임">2. 분석 응답이 길어 UI가 멈춘 것처럼 보임&lt;/h3>
&lt;p>SSE로 상태와 답변을 점진 전달하고 진단 로그를 보강했습니다.&lt;/p>
&lt;h3 id="3-모델-api가-바뀌면-서비스-코드가-함께-흔들림">3. 모델 API가 바뀌면 서비스 코드가 함께 흔들림&lt;/h3>
&lt;p>provider adapter로 Gemini/Kimi 교체 지점을 분리했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Next.js&lt;/strong>&lt;/td>
&lt;td>검색·watch·타임라인과 server route를 한 웹앱에서 구성하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>yt-dlp · ffmpeg&lt;/strong>&lt;/td>
&lt;td>전체 영상 대신 사용자가 선택한 시간 구간만 서버에서 추출하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FastAPI&lt;/strong>&lt;/td>
&lt;td>미디어 처리와 AI provider 호출을 웹 UI에서 분리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SSE&lt;/strong>&lt;/td>
&lt;td>긴 분석이 끝날 때까지 기다리지 않고 진행 결과를 점진적으로 보여주기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>검색→구간 선택→clip 추출→AI 분석→SSE 응답의 사용자 흐름을 구현했습니다.&lt;/li>
&lt;li>health/analyze stub test와 Dockerfile/Compose로 서비스 경계를 확인했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="스포츠 분석 AI 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/sports-analyzer/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>웹에서 YouTube 영상을 검색하고 구간을 선택합니다.&lt;/li>
&lt;li>Next.js API가 영상 메타데이터와 분석 요청을 정리합니다.&lt;/li>
&lt;li>FastAPI가 yt-dlp·ffmpeg로 필요한 clip만 추출합니다.&lt;/li>
&lt;li>provider adapter가 Gemini/Kimi 중 설정된 모델을 호출합니다.&lt;/li>
&lt;li>분석 결과를 SSE로 브라우저에 스트리밍합니다.&lt;/li>
&lt;li>SQLite history와 debug endpoint로 최근 결과를 진단합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Web&lt;/code>&lt;/td>
&lt;td>&lt;code>GET /api/search, /feed, /watch/{id}/meta&lt;/code>&lt;/td>
&lt;td>검색·메타데이터&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Analysis&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /api/analyze&lt;/code>&lt;/td>
&lt;td>웹→AI service proxy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>AI&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /analyze&lt;/code>&lt;/td>
&lt;td>clip 추출·모델 분석&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Debug&lt;/code>&lt;/td>
&lt;td>&lt;code>GET /debug/analyze-history&lt;/code>&lt;/td>
&lt;td>최근 실행 진단&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>job queue와 취소·재시도 상태 추가&lt;/li>
&lt;li>임시 clip lifecycle 및 저장 비용 관리&lt;/li>
&lt;li>분석 latency·provider 오류율·SSE disconnect 모니터링&lt;/li>
&lt;/ul></description></item><item><title>MetaXR 액션 프로토타입</title><link>https://eecczz.github.io/ko/toy-projects/metaxr-project/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/metaxr-project/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>XR 컨트롤러의 위치·회전을 무기 충돌, 몬스터 반응, 탈출 조건으로 연결한 Unity 공간 상호작용 프로젝트입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · XR 입력/전투/씬 로직&lt;/span>&lt;span>&lt;b>검증&lt;/b> Meta XR 기기 프로토타입&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>XR 컨트롤러의 위치·회전을 손과 무기에 연결하고, 오브젝트 상호작용과 적 피격이 같은 공간 좌표계에서 이어지도록 구현했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="XR 캐릭터와 무기 상호작용 장면" srcset="
/ko/toy-projects/metaxr-project/featured_huf562b41212e31cf19bb7ce838834f0c1_612207_613ae85d120ed6175befab5caa029d60.webp 400w,
/ko/toy-projects/metaxr-project/featured_huf562b41212e31cf19bb7ce838834f0c1_612207_e3fd2b08b8cb1db65b2e0063d19ba398.webp 760w,
/ko/toy-projects/metaxr-project/featured_huf562b41212e31cf19bb7ce838834f0c1_612207_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/metaxr-project/featured_huf562b41212e31cf19bb7ce838834f0c1_612207_613ae85d120ed6175befab5caa029d60.webp"
width="760"
height="622"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="컨트롤러로 공간 오브젝트를 조작하는 화면" srcset="
/ko/toy-projects/metaxr-project/detail-unity_hu15b53b9daea9f254f7ba82e90be40f03_539154_045be04e9c4eb69e8c8dc89c18f48539.webp 400w,
/ko/toy-projects/metaxr-project/detail-unity_hu15b53b9daea9f254f7ba82e90be40f03_539154_d6da332894bf017e802bd88b317f7a37.webp 760w,
/ko/toy-projects/metaxr-project/detail-unity_hu15b53b9daea9f254f7ba82e90be40f03_539154_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/metaxr-project/detail-unity_hu15b53b9daea9f254f7ba82e90be40f03_539154_045be04e9c4eb69e8c8dc89c18f48539.webp"
width="760"
height="369"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="적과 근접 전투를 진행하는 XR 시점" srcset="
/ko/toy-projects/metaxr-project/detail-xr_hu878ec5f9d7dfc358aea345b8f968c19c_491936_95f19cda197f9ce00365c5fc66d01415.webp 400w,
/ko/toy-projects/metaxr-project/detail-xr_hu878ec5f9d7dfc358aea345b8f968c19c_491936_b12b4cb1c157f36b6ba69da3fbf5a9c1.webp 760w,
/ko/toy-projects/metaxr-project/detail-xr_hu878ec5f9d7dfc358aea345b8f968c19c_491936_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/metaxr-project/detail-xr_hu878ec5f9d7dfc358aea345b8f968c19c_491936_95f19cda197f9ce00365c5fc66d01415.webp"
width="760"
height="367"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-화면-입력과-달리-xr-pose는-매-프레임-위치회전-노이즈를-포함">1. 화면 입력과 달리 XR pose는 매 프레임 위치·회전 노이즈를 포함&lt;/h3>
&lt;p>입력·무기·피격 상태를 분리해 상호작용 경계를 명확히 했습니다.&lt;/p>
&lt;h3 id="2-외부-asset-store-패키지와-직접-작성-코드의-범위가-섞임">2. 외부 Asset Store 패키지와 직접 작성 코드의 범위가 섞임&lt;/h3>
&lt;p>README에 제외 패키지와 담당 스크립트를 명시하고 저장소 이력을 정리했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Meta XR&lt;/strong>&lt;/td>
&lt;td>6DoF controller pose를 직접 게임 상호작용으로 사용하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Unity physics&lt;/strong>&lt;/td>
&lt;td>검·방패·몬스터 접촉을 공간 충돌로 처리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Stateful enemy controller&lt;/strong>&lt;/td>
&lt;td>추적·공격·피격·사망 상태를 분리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scene interaction&lt;/strong>&lt;/td>
&lt;td>레버·탈출 조건을 전투 진행과 연결하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>검술 조작감에 대한 관심을 실제 XR controller 기반 상호작용으로 확장했습니다.&lt;/li>
&lt;li>EnemyController·LeverController·UI/Sound 관리 코드로 전투→탈출 흐름을 구성했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="MetaXR 액션 프로토타입 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/metaxr-project/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>XR controller pose를 Unity 좌표로 받습니다.&lt;/li>
&lt;li>입력 mapping이 공격·방어 상태를 결정합니다.&lt;/li>
&lt;li>무기 collider와 trail이 공간 궤적을 표현합니다.&lt;/li>
&lt;li>Enemy controller가 탐지·추적·피격 상태를 갱신합니다.&lt;/li>
&lt;li>레버와 탈출 조건이 scene progression을 바꿉니다.&lt;/li>
&lt;li>UI·sound manager가 체력과 결과를 피드백합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Input&lt;/code>&lt;/td>
&lt;td>&lt;code>controller pose/buttons&lt;/code>&lt;/td>
&lt;td>공격·방어&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Combat&lt;/code>&lt;/td>
&lt;td>&lt;code>weapon collider&lt;/code>&lt;/td>
&lt;td>타격 판정&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Enemy&lt;/code>&lt;/td>
&lt;td>&lt;code>detect/chase/hit&lt;/code>&lt;/td>
&lt;td>상태 전환&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Scene&lt;/code>&lt;/td>
&lt;td>&lt;code>lever/exit&lt;/code>&lt;/td>
&lt;td>진행 조건&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>controller jitter smoothing·물리 timestep 계측&lt;/li>
&lt;li>충돌 속도 기반 damage와 haptic feedback&lt;/li>
&lt;li>Quest 실기기 FPS·GC profiling&lt;/li>
&lt;/ul></description></item><item><title>Streaming API</title><link>https://eecczz.github.io/ko/toy-projects/streaming-api/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/streaming-api/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>React 기반 영상 탐색·재생 화면과 Spring Boot·AWS S3·Lambda·MediaConvert를 연결한 스트리밍 서비스입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 미디어 UX/API/도메인/클라우드 파이프라인&lt;/span>&lt;span>&lt;b>검증&lt;/b> 업로드·변환·조회·재생 흐름 구현&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>영상 목록·지연 preview·상세 player·업로드 화면을 하나의 서비스로 구성했습니다. 사용자는 목록에서 콘텐츠를 탐색하고, hover preview로 장면을 확인한 뒤 상세 재생 화면으로 이동하거나 multipart 업로드를 시작할 수 있습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="영상 카드와 카테고리로 구성한 콘텐츠 탐색 화면" srcset="
/ko/toy-projects/streaming-api/detail-library_hu41c70f00b40e78806d6c8c51035764d8_633206_022d34d8391fe1456702d8dedffb162a.webp 400w,
/ko/toy-projects/streaming-api/detail-library_hu41c70f00b40e78806d6c8c51035764d8_633206_9629ed57a67ee67dd0719a0ca9fc22e1.webp 760w,
/ko/toy-projects/streaming-api/detail-library_hu41c70f00b40e78806d6c8c51035764d8_633206_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/streaming-api/detail-library_hu41c70f00b40e78806d6c8c51035764d8_633206_022d34d8391fe1456702d8dedffb162a.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="영상 정보와 썸네일·파일을 입력하는 업로드 화면" srcset="
/ko/toy-projects/streaming-api/detail-upload_hu7ee5e3fbac015abcc71f450785679a0c_130555_73b0213dc5dc700469bfc2a34e634174.webp 400w,
/ko/toy-projects/streaming-api/detail-upload_hu7ee5e3fbac015abcc71f450785679a0c_130555_7ef9cfa579e46a558f071e6410815945.webp 760w,
/ko/toy-projects/streaming-api/detail-upload_hu7ee5e3fbac015abcc71f450785679a0c_130555_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/streaming-api/detail-upload_hu7ee5e3fbac015abcc71f450785679a0c_130555_73b0213dc5dc700469bfc2a34e634174.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="재생 영상·추천 목록·댓글을 함께 제공하는 상세 player" srcset="
/ko/toy-projects/streaming-api/detail-player_hufa4438dc2b9899083b5757f6f2fc8acc_617142_d977b669f22e3028e58eed18089c82d4.webp 400w,
/ko/toy-projects/streaming-api/detail-player_hufa4438dc2b9899083b5757f6f2fc8acc_617142_31be7b626b1f7953e36687bef4582d26.webp 760w,
/ko/toy-projects/streaming-api/detail-player_hufa4438dc2b9899083b5757f6f2fc8acc_617142_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/streaming-api/detail-player_hufa4438dc2b9899083b5757f6f2fc8acc_617142_d977b669f22e3028e58eed18089c82d4.webp"
width="760"
height="692"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-대용량-영상을-spring-서버가-직접-받아-변환하면-요청이-오래-점유됨">1. 대용량 영상을 Spring 서버가 직접 받아 변환하면 요청이 오래 점유됨&lt;/h3>
&lt;p>Spring 서버가 영상 바이트 전체를 받아 S3로 다시 전달하면 업로드 시간 동안 요청·메모리·네트워크를 함께 점유하고, 변환까지 같은 요청에 묶을 경우 실패 복구도 어려워집니다. 이를 제어 API와 데이터 전송 경로로 분리했습니다.&lt;/p>
&lt;p>React 클라이언트는 먼저 &lt;code>/initiate-upload&lt;/code>에서 &lt;code>uploadId&lt;/code>를 받고, 파일을 10MB 단위로 나눕니다. 각 part마다 &lt;code>/upload-signed-url&lt;/code>로 서명 URL을 받은 뒤 영상 바이트는 브라우저가 S3에 직접 &lt;code>PUT&lt;/code>합니다. 마지막으로 &lt;code>partNumber&lt;/code>와 &lt;code>ETag&lt;/code> 목록만 &lt;code>/complete-upload&lt;/code>에 전달해 multipart upload를 확정합니다. Spring은 권한과 업로드 생명주기만 제어하고 대용량 파일 본문은 통과시키지 않게 했습니다. 변환은 S3 업로드 완료 이후의 Lambda·MediaConvert 단계로 분리해 사용자의 HTTP 요청과 비동기 처리 경계를 나눴습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Streaming API multipart 업로드 코드"
src="https://eecczz.github.io/ko/toy-projects/streaming-api/code-multipart.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h3 id="2-aws-설정을-마친-뒤에도-브라우저의-대용량-업로드-요청이-차단됨">2. AWS 설정을 마친 뒤에도 브라우저의 대용량 업로드 요청이 차단됨&lt;/h3>
&lt;p>AWS 업로드 설정을 마친 뒤 대용량 파일 업로드를 테스트하자 웹에서 오류가 발생했습니다. 처음에는 AWS 설정을 의심했지만, 실제 원인은 프론트엔드와 Spring 서버의 origin이 달라 업로드 요청이 차단된 것이었습니다.&lt;/p>
&lt;p>&lt;code>WebConfig&lt;/code>에 CORS 설정을 추가해 프론트엔드의 요청을 허용했고, &lt;code>/files/**&lt;/code>는 빌드 결과물이 아니라 실제 업로드 디렉터리에서 제공하도록 resource handler를 연결했습니다. 이후 업로드 요청과 업로드 파일 조회가 정상 동작했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Streaming API CORS와 업로드 파일 제공 설정"
src="https://eecczz.github.io/ko/toy-projects/streaming-api/code-cors.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h3 id="3-영상-원본을-게시물-db에-넣지-않고-조회-가능한-게시물로-구성해야-함">3. 영상 원본을 게시물 DB에 넣지 않고 조회 가능한 게시물로 구성해야 함&lt;/h3>
&lt;p>AWS에서 얻은 영상 원본은 객체 스토리지에 두고, 게시물 테이블에는 제목·작성자·설명·썸네일과 &lt;code>videoUrl&lt;/code>만 문자열로 저장했습니다. 조회 화면은 게시물 데이터를 불러온 뒤 &lt;code>videoUrl&lt;/code>만 player에 연결해 영상을 재생하도록 구성했습니다. 이로써 대용량 영상 파일을 DB에 넣지 않으면서도 게시물 단위의 목록·상세 조회를 구현했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>S3 Multipart Upload&lt;/strong>&lt;/td>
&lt;td>대용량 파일을 애플리케이션 서버 메모리에 오래 유지하지 않기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Lambda event&lt;/strong>&lt;/td>
&lt;td>업로드 완료를 변환 시작 신호로 사용해 요청 처리와 무거운 작업을 분리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MediaConvert · HLS&lt;/strong>&lt;/td>
&lt;td>브라우저 재생과 화질별 rendition을 표준 방식으로 제공하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Spring Boot · MariaDB&lt;/strong>&lt;/td>
&lt;td>영상 메타데이터, 인증, 댓글·좋아요·구독 도메인을 관리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>React · Hover delay&lt;/strong>&lt;/td>
&lt;td>영상 목록과 player 상태를 분리하고 사용 의도가 확인된 preview만 로드하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>S3 업로드→Lambda 이벤트→MediaConvert→HLS 재생 구조를 코드와 서비스 화면으로 연결했습니다.&lt;/li>
&lt;li>영상 API 외에 댓글·좋아요·구독·사용자별 목록 endpoint까지 확장했습니다.&lt;/li>
&lt;li>썸네일 탐색→지연 preview→상세 player로 이어지는 사용자 흐름을 같은 프로젝트 안에서 구현했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Streaming API 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/streaming-api/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>React 클라이언트가 multipart upload를 시작하고 signed URL을 받습니다.&lt;/li>
&lt;li>브라우저가 분할 업로드를 완료하면 원본 객체가 S3에 저장됩니다.&lt;/li>
&lt;li>S3 이벤트가 Lambda를 호출해 MediaConvert job을 생성합니다.&lt;/li>
&lt;li>MediaConvert가 HLS manifest와 화질별 segment를 생성합니다.&lt;/li>
&lt;li>Spring Boot가 게시물 메타데이터와 &lt;code>videoUrl&lt;/code>을 저장하고 조회 API로 제공합니다.&lt;/li>
&lt;li>목록은 썸네일을 먼저 표시하고 hover가 유지된 카드만 preview를 지연 로드합니다.&lt;/li>
&lt;li>상세 player가 HLS를 재생하고 댓글·좋아요·구독 API를 호출합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Upload&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /initiate-upload, /upload-signed-url&lt;/code>&lt;/td>
&lt;td>multipart 준비&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Upload&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /complete-upload, /abort-upload&lt;/code>&lt;/td>
&lt;td>완료/취소&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Video&lt;/code>&lt;/td>
&lt;td>&lt;code>GET/POST /api/videos&lt;/code>&lt;/td>
&lt;td>영상 목록·생성&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Social&lt;/code>&lt;/td>
&lt;td>&lt;code>/comments, /like, /subscribe&lt;/code>&lt;/td>
&lt;td>시청 상호작용&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Preview&lt;/code>&lt;/td>
&lt;td>&lt;code>hover timer + video element&lt;/code>&lt;/td>
&lt;td>preview 지연 로드·정리&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>job status webhook과 재시도/실패 상태 모델 추가&lt;/li>
&lt;li>CloudFront signed URL·캐시 정책 적용&lt;/li>
&lt;li>업로드/변환 latency와 비용 계측&lt;/li>
&lt;li>IntersectionObserver로 화면 밖 preview의 네트워크·재생 자원 정리&lt;/li>
&lt;li>운영 도메인 기준으로 CORS origin·method·header 최소 허용&lt;/li>
&lt;/ul></description></item><item><title>맛집 찾기 ReAct Agent</title><link>https://eecczz.github.io/ko/toy-projects/restaurant-agent/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/restaurant-agent/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>사용자의 지역·가격·목적 조건을 해석하고 검색·필터·검증 도구를 호출하는 FastAPI AI Agent입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 과제 · Agent loop/Tool Registry/API/fallback&lt;/span>&lt;span>&lt;b>검증&lt;/b> CLI trace 및 FastAPI endpoint 검증&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>사용자가 지역·가격·목적을 자연어로 입력하면 Agent가 조건을 정리하고 검색·필터·검증 도구를 순서대로 호출합니다. 최종 추천 화면과 내부 ReAct trace를 함께 남겨 결과뿐 아니라 도구 선택 과정도 확인할 수 있게 했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="자연어 조건을 입력하고 맛집 추천을 받는 화면" srcset="
/ko/toy-projects/restaurant-agent/featured_hu16ace187a93bd94a5b9b5aa8cbaac406_30732_6017ef55e483d151a16d57513f62277e.webp 400w,
/ko/toy-projects/restaurant-agent/featured_hu16ace187a93bd94a5b9b5aa8cbaac406_30732_6037760380ad229e79e9bd7f2eff27b7.webp 760w,
/ko/toy-projects/restaurant-agent/featured_hu16ace187a93bd94a5b9b5aa8cbaac406_30732_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/restaurant-agent/featured_hu16ace187a93bd94a5b9b5aa8cbaac406_30732_6017ef55e483d151a16d57513f62277e.webp"
width="728"
height="747"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="추천 근거와 후보를 정리한 결과 화면" srcset="
/ko/toy-projects/restaurant-agent/detail-result_hu2b831af388afd60d77e516ce4378b425_353680_138d672532124e7ac1138acf7c89f4d9.webp 400w,
/ko/toy-projects/restaurant-agent/detail-result_hu2b831af388afd60d77e516ce4378b425_353680_fc4a5456e0a95087e1d252174672b02a.webp 760w,
/ko/toy-projects/restaurant-agent/detail-result_hu2b831af388afd60d77e516ce4378b425_353680_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/restaurant-agent/detail-result_hu2b831af388afd60d77e516ce4378b425_353680_138d672532124e7ac1138acf7c89f4d9.webp"
width="760"
height="433"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Agent의 Thought·Action·Observation 실행 기록" srcset="
/ko/toy-projects/restaurant-agent/detail-trace_hu138c4d8905a1b0e209fd1210b79ab3d6_183337_e7f60ea26fde780727811cf1cdc26e32.webp 400w,
/ko/toy-projects/restaurant-agent/detail-trace_hu138c4d8905a1b0e209fd1210b79ab3d6_183337_f76f725466a9b4c6ada6684c857b285e.webp 760w,
/ko/toy-projects/restaurant-agent/detail-trace_hu138c4d8905a1b0e209fd1210b79ab3d6_183337_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/restaurant-agent/detail-trace_hu138c4d8905a1b0e209fd1210b79ab3d6_183337_e7f60ea26fde780727811cf1cdc26e32.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-외부-api-key가-없으면-과제-전체를-실행할-수-없음">1. 외부 API key가 없으면 과제 전체를 실행할 수 없음&lt;/h3>
&lt;p>동일 계약의 로컬 dataset fallback을 두었습니다.&lt;/p>
&lt;h3 id="2-조건이-엄격해-후보가-0개가-됨">2. 조건이 엄격해 후보가 0개가 됨&lt;/h3>
&lt;p>카테고리·가격 조건을 단계적으로 완화하고 그 사실을 Observation에 남겼습니다.&lt;/p>
&lt;h3 id="3-agent가-반복-도구-호출에-빠질-수-있음">3. Agent가 반복 도구 호출에 빠질 수 있음&lt;/h3>
&lt;p>max_iterations guard와 도구 오류를 Observation으로 환류했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>ReAct loop&lt;/strong>&lt;/td>
&lt;td>Thought·Action·Observation을 남겨 추천 과정이 블랙박스가 되지 않게 하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tool Registry&lt;/strong>&lt;/td>
&lt;td>검색·필터·점수·reflection을 독립 도구로 등록해 실행 흐름을 확장하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kakao Local API&lt;/strong>&lt;/td>
&lt;td>샘플 데이터 밖의 실제 지역 검색을 지원하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Fallback dataset&lt;/strong>&lt;/td>
&lt;td>API key/네트워크 실패에도 agent 동작과 테스트를 재현하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>Perceive→Reason→Act→Observe와 Plan-and-Solve·Reflection·Memory를 실행 가능한 코드로 검증했습니다.&lt;/li>
&lt;li>응답뿐 아니라 trace_text를 제공해 도구 사용과 예외 처리를 확인할 수 있습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="맛집 찾기 ReAct Agent 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/restaurant-agent/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>POST /recommend가 자연어 요청을 받습니다.&lt;/li>
&lt;li>parser tool이 지역·음식·가격·목적·개수를 구조화합니다.&lt;/li>
&lt;li>Kakao keyword search를 우선 호출하고 실패하면 샘플 데이터로 전환합니다.&lt;/li>
&lt;li>filter와 distance score tool이 후보를 정렬합니다.&lt;/li>
&lt;li>reflection tool이 조건 충족 여부를 재검토합니다.&lt;/li>
&lt;li>최종 답변과 trace를 함께 반환합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Health&lt;/code>&lt;/td>
&lt;td>&lt;code>GET /health&lt;/code>&lt;/td>
&lt;td>서비스 상태&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Agent&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /recommend&lt;/code>&lt;/td>
&lt;td>추천·trace&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Search&lt;/code>&lt;/td>
&lt;td>&lt;code>Kakao keyword / local fallback&lt;/code>&lt;/td>
&lt;td>후보 수집&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Guard&lt;/code>&lt;/td>
&lt;td>&lt;code>max_iterations&lt;/code>&lt;/td>
&lt;td>무한 loop 방지&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>실제 LLM function calling과 schema validation 도입&lt;/li>
&lt;li>추천 근거·거리 계산의 정량 평가&lt;/li>
&lt;li>API rate limit·cache·관측 로그 추가&lt;/li>
&lt;/ul></description></item><item><title>쇼핑몰 백엔드</title><link>https://eecczz.github.io/ko/toy-projects/shopping-mall/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/shopping-mall/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>Spring Boot·JPA·Querydsl로 상품 탐색부터 장바구니·주문·PortOne 테스트 결제 검증까지 구현한 서비스입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 상품/장바구니/주문/결제/배포&lt;/span>&lt;span>&lt;b>검증&lt;/b> jcloud Ubuntu executable jar 배포&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>상품 목록에서 상세 옵션을 확인하고 장바구니에 담은 뒤 합계·수량을 검토해 주문으로 넘어가는 구매 흐름을 구현했습니다. 목록 조회와 장바구니 상태가 주문·결제 도메인으로 이어지도록 구성했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="상품을 조회하고 옵션을 선택하는 목록 화면" srcset="
/ko/toy-projects/shopping-mall/capture-list_huc75df1e51dcda34110788da60ce2c3a2_311519_cd87ae636250a1a71a21ffb8c554c104.webp 400w,
/ko/toy-projects/shopping-mall/capture-list_huc75df1e51dcda34110788da60ce2c3a2_311519_102505033b97c839af3b8bfe31cf4f9f.webp 760w,
/ko/toy-projects/shopping-mall/capture-list_huc75df1e51dcda34110788da60ce2c3a2_311519_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/shopping-mall/capture-list_huc75df1e51dcda34110788da60ce2c3a2_311519_cd87ae636250a1a71a21ffb8c554c104.webp"
width="760"
height="535"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="선택 상품과 합계를 확인하는 장바구니 화면" srcset="
/ko/toy-projects/shopping-mall/capture-cart_huba2632785718c0a7563e2d2fb2f0ee2c_36712_2ab63b126e0a6043c4902e1786d2037f.webp 400w,
/ko/toy-projects/shopping-mall/capture-cart_huba2632785718c0a7563e2d2fb2f0ee2c_36712_45fc20859a6466d55f9d5f200ab231a3.webp 760w,
/ko/toy-projects/shopping-mall/capture-cart_huba2632785718c0a7563e2d2fb2f0ee2c_36712_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/shopping-mall/capture-cart_huba2632785718c0a7563e2d2fb2f0ee2c_36712_2ab63b126e0a6043c4902e1786d2037f.webp"
width="760"
height="535"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-빌드할-때마다-querydsl-qentity-재생성이-불안정함">1. 빌드할 때마다 Querydsl QEntity 재생성이 불안정함&lt;/h3>
&lt;p>Spring Boot 3의 Jakarta 전환 이후에도 오래된 Querydsl Gradle 플러그인과 생성 경로 설정이 함께 남아 있어, Q타입이 중복 생성되거나 stale source가 참조됐습니다.&lt;/p>
&lt;p>서드파티 플러그인을 제거하고 &lt;code>querydsl-jpa:5.0.0:jakarta&lt;/code>와 APT annotation processor로 생성 경로를 하나로 통일했습니다. &lt;code>JavaCompile.generatedSourceOutputDirectory&lt;/code>를 &lt;code>build/generated/querydsl&lt;/code>로 고정하고 &lt;code>clean&lt;/code>에서 해당 디렉터리를 지우도록 해 재현 가능한 빌드로 바꿨습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Querydsl Q타입 생성 설정 변경 전후"
src="https://eecczz.github.io/ko/toy-projects/shopping-mall/code-querydsl.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h3 id="2-장바구니-변경-후-헤더-수량을-즉시-갱신하기-어려움">2. 장바구니 변경 후 헤더 수량을 즉시 갱신하기 어려움&lt;/h3>
&lt;p>Thymeleaf로 렌더링하는 공통 헤더가 어느 화면에서나 장바구니 수량을 표시해야 했습니다. 처음에는 각 controller가 로그인 사용자와 장바구니를 다시 조회해 &lt;code>cartCount&lt;/code>를 모델에 담았고, 상품 추가 뒤에는 &lt;code>window.location.reload()&lt;/code>로 화면 전체를 새로 그려 수량을 반영했습니다.&lt;/p>
&lt;p>이 과정에서 화면 공통 데이터와 장바구니 변경 처리가 한 흐름에 묶여 있다는 점을 확인했습니다. 다음 수정에서는 공통 헤더 데이터 주입을 controller 밖으로 분리하고, 추가·삭제 API가 최신 수량과 합계를 반환해 필요한 DOM만 갱신하도록 개선할 계획입니다.&lt;/p>
&lt;h3 id="3-비회원-장바구니를-회원-엔티티에-억지로-연결함">3. 비회원 장바구니를 회원 엔티티에 억지로 연결함&lt;/h3>
&lt;p>비회원도 상품을 담을 수 있게 하려고 장바구니가 참조할 임시 회원을 두고, 결제 버튼을 누를 때 로그인 화면으로 보내는 흐름을 구성했습니다. 덕분에 회원 장바구니와 같은 테이블 구조를 재사용할 수 있었지만, 초기 데이터에 의존하고 Thymeleaf에서 회원·비회원 장바구니 분기가 늘어났습니다.&lt;/p>
&lt;p>다음 수정에서는 비회원 장바구니를 HTTP session의 guest key로 분리하고, 로그인 시 실제 회원 장바구니로 병합하는 방식으로 변경할 계획입니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Spring MVC · Thymeleaf&lt;/strong>&lt;/td>
&lt;td>서버 렌더링으로 구매 흐름과 세션 인증을 빠르게 끝까지 검증하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>JPA · Querydsl&lt;/strong>&lt;/td>
&lt;td>도메인 관계를 모델링하고 검색·페이징 조건을 동적으로 조합하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PortOne sandbox&lt;/strong>&lt;/td>
&lt;td>실결제 없이 승인 식별자와 주문 상태 처리 흐름을 검증하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MariaDB&lt;/strong>&lt;/td>
&lt;td>회원·상품·장바구니·주문 데이터를 트랜잭션으로 관리하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>회원가입→상품 탐색→장바구니→테스트 결제→주문 상태의 end-to-end 흐름을 구현했습니다.&lt;/li>
&lt;li>jcloud Ubuntu에 executable jar로 배포했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="쇼핑몰 백엔드 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/shopping-mall/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>사용자가 상품을 검색하고 상세를 조회합니다.&lt;/li>
&lt;li>세션 로그인 사용자가 장바구니 수량을 변경합니다.&lt;/li>
&lt;li>서비스 계층이 합계와 주문 항목을 계산합니다.&lt;/li>
&lt;li>PortOne 테스트 결제 후 imp_uid를 서버에 전달합니다.&lt;/li>
&lt;li>서버가 결제 검증 결과에 따라 주문 상태를 변경합니다.&lt;/li>
&lt;li>Executable jar를 jcloud Ubuntu에서 실행합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Catalog&lt;/code>&lt;/td>
&lt;td>&lt;code>GET /demo/list, /item-detail/{id}&lt;/code>&lt;/td>
&lt;td>검색·상세&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Cart&lt;/code>&lt;/td>
&lt;td>&lt;code>GET /cart, POST /updatecart, /deletecart&lt;/code>&lt;/td>
&lt;td>장바구니&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Order&lt;/code>&lt;/td>
&lt;td>&lt;code>GET/POST /purchase, /order/payment&lt;/code>&lt;/td>
&lt;td>주문 생성&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Payment&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /payment/validation/{imp_uid}&lt;/code>&lt;/td>
&lt;td>결제 검증&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>공통 헤더 장바구니 조회와 전체 페이지 reload 제거&lt;/li>
&lt;li>guest cart와 member cart 병합 처리&lt;/li>
&lt;li>결제 검증과 주문 상태 변경의 idempotency 보장&lt;/li>
&lt;li>동시 수량 변경·재고 차감에 optimistic locking 적용&lt;/li>
&lt;li>CI/CD·health check·로그 수집 추가&lt;/li>
&lt;/ul></description></item><item><title>3D 공포 게임 프로토타입</title><link>https://eecczz.github.io/ko/toy-projects/my3dhorrorgame/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/my3dhorrorgame/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>1인칭 이동·raycast 상호작용·인벤토리·목표·오디오 타이밍을 연결한 Unity 공포 게임 프로토타입입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 플레이어/상호작용/인벤토리/연출&lt;/span>&lt;span>&lt;b>검증&lt;/b> 기능 프로토타입&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>1인칭 시점으로 공간을 탐색하면서 raycast로 오브젝트를 조사하고, 인벤토리와 목표 상태에 따라 공포 연출이 진행됩니다. 캐릭터 조우와 오디오 타이밍을 결합해 제한된 공간에서도 긴장감이 이어지도록 구성했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="병원 복도를 탐색하다 적 캐릭터와 마주치는 실제 플레이 장면" srcset="
/ko/toy-projects/my3dhorrorgame/featured_hu713c6924e6b0a6787d5beea2f9ffcfb6_9386_c6feb3ad3669f2d10ddf4c81645ae11c.webp 400w,
/ko/toy-projects/my3dhorrorgame/featured_hu713c6924e6b0a6787d5beea2f9ffcfb6_9386_cec2a16e108262b2a46040ec65fe1910.webp 760w,
/ko/toy-projects/my3dhorrorgame/featured_hu713c6924e6b0a6787d5beea2f9ffcfb6_9386_1200x1200_fit_q75_h2_lanczos_2.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/my3dhorrorgame/featured_hu713c6924e6b0a6787d5beea2f9ffcfb6_9386_c6feb3ad3669f2d10ddf4c81645ae11c.webp"
width="760"
height="276"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-asset-기반-fps-코드와-직접-구현한-gameplay-코드의-경계가-불명확">1. Asset 기반 FPS 코드와 직접 구현한 gameplay 코드의 경계가 불명확&lt;/h3>
&lt;p>생성 Library 파일을 제거하고 직접 작성한 Equip/Input/Interaction/Inventory 스크립트를 저장소 전면에 정리했습니다.&lt;/p>
&lt;h3 id="2-공포-연출이-단순-어두운-조명에-의존">2. 공포 연출이 단순 어두운 조명에 의존&lt;/h3>
&lt;p>목표·아이템·event·audio timing을 분리해 플레이 흐름에서 긴장감을 만들었습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>FPS controller&lt;/strong>&lt;/td>
&lt;td>시야와 이동이 공포 공간 탐색의 핵심이기 때문에&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Raycast interaction&lt;/strong>&lt;/td>
&lt;td>플레이어가 바라보는 문·아이템을 일관된 방식으로 선택하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Inventory state&lt;/strong>&lt;/td>
&lt;td>획득한 도구가 다음 상호작용 조건을 바꾸게 하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Event/Audio manager&lt;/strong>&lt;/td>
&lt;td>공포감을 모델보다 타이밍과 정보 노출로 제어하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>플레이어→상호작용→인벤토리→목표→연출의 기능 흐름을 프로토타입으로 완성했습니다.&lt;/li>
&lt;li>Unity 생성 파일을 제거하고 재현 가능한 저장소 구조와 README를 정리했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="3D 공포 게임 프로토타입 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/my3dhorrorgame/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>입력이 FPS controller의 이동·시점을 갱신합니다.&lt;/li>
&lt;li>화면 중심 ray가 상호작용 가능한 객체를 탐색합니다.&lt;/li>
&lt;li>아이템을 획득하면 inventory와 장착 상태가 바뀝니다.&lt;/li>
&lt;li>Objective manager가 다음 목표를 활성화합니다.&lt;/li>
&lt;li>Game flow event가 적·문·장면을 전환합니다.&lt;/li>
&lt;li>Audio·HUD가 시점에 맞춰 긴장감을 전달합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Player&lt;/code>&lt;/td>
&lt;td>&lt;code>move/look&lt;/code>&lt;/td>
&lt;td>1인칭 탐색&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Interaction&lt;/code>&lt;/td>
&lt;td>&lt;code>raycast + interface&lt;/code>&lt;/td>
&lt;td>문·아이템&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Inventory&lt;/code>&lt;/td>
&lt;td>&lt;code>collect/equip&lt;/code>&lt;/td>
&lt;td>조건 상태&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Flow&lt;/code>&lt;/td>
&lt;td>&lt;code>objective/event/audio&lt;/code>&lt;/td>
&lt;td>연출 진행&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>상호작용 interface와 save/load contract 정리&lt;/li>
&lt;li>공간 audio와 적 AI perception 고도화&lt;/li>
&lt;li>Profiler로 scene loading·GC spike 점검&lt;/li>
&lt;/ul></description></item><item><title>Job API Project</title><link>https://eecczz.github.io/ko/toy-projects/job-api/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://eecczz.github.io/ko/toy-projects/job-api/</guid><description>&lt;div class="case-study-lead">
&lt;p class="case-study-kicker">BACKEND · AI · SYSTEM CASE STUDY&lt;/p>
&lt;p>사람인 채용공고를 수집·중복 제거하고 Querydsl 다중 조건 검색과 JWT 기반 지원·북마크를 제공하는 REST API입니다.&lt;/p>
&lt;div class="case-study-meta">&lt;span>&lt;b>역할&lt;/b> 개인 프로젝트 · 크롤러/검색 API/인증/배포&lt;/span>&lt;span>&lt;b>검증&lt;/b> jcloud 배포 및 실제 데이터 수집 검증&lt;/span>&lt;/div>
&lt;/div>
&lt;h2 id="주요-화면">주요 화면&lt;/h2>
&lt;p>수집한 채용공고를 직무·경력·지역·기술 키워드로 조회하고, REST 응답과 실제 목록 화면이 같은 검색 조건을 사용하도록 구성했습니다. 외부 크롤링이 중단된 뒤에는 SQL dump를 입력 데이터로 전환해 검색 기능을 계속 검증했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="채용공고 검색 API와 응답 구조" srcset="
/ko/toy-projects/job-api/api-source_hu14ba695e1acf4d6e686af5ee7c9983fd_110107_6ef4a0361b6f6ff1e5cfc8cfd3bdf7d8.webp 400w,
/ko/toy-projects/job-api/api-source_hu14ba695e1acf4d6e686af5ee7c9983fd_110107_1746e81f3aa5773ab900af850143e11a.webp 760w,
/ko/toy-projects/job-api/api-source_hu14ba695e1acf4d6e686af5ee7c9983fd_110107_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/job-api/api-source_hu14ba695e1acf4d6e686af5ee7c9983fd_110107_6ef4a0361b6f6ff1e5cfc8cfd3bdf7d8.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="SQL dump를 적재하고 결과를 확인한 실행 화면" srcset="
/ko/toy-projects/job-api/crawl-dump_hu7f94d799cce0186227dcecd1b002426b_78426_fe95500ed66097d73ddea07f9ef27bb6.webp 400w,
/ko/toy-projects/job-api/crawl-dump_hu7f94d799cce0186227dcecd1b002426b_78426_dcc44e6ad1f4b773321a91888e85ce1b.webp 760w,
/ko/toy-projects/job-api/crawl-dump_hu7f94d799cce0186227dcecd1b002426b_78426_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/job-api/crawl-dump_hu7f94d799cce0186227dcecd1b002426b_78426_fe95500ed66097d73ddea07f9ef27bb6.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="다중 조건으로 채용공고를 조회하는 목록 화면" srcset="
/ko/toy-projects/job-api/job-list_hu790750dfa26deecad750ee8670e658a4_73445_cc2570cab9d6d42f800e45b7e348cc39.webp 400w,
/ko/toy-projects/job-api/job-list_hu790750dfa26deecad750ee8670e658a4_73445_8734e9c4360886f811662922ac0f3e69.webp 760w,
/ko/toy-projects/job-api/job-list_hu790750dfa26deecad750ee8670e658a4_73445_1200x1200_fit_q75_h2_lanczos_3.webp 1200w"
src="https://eecczz.github.io/ko/toy-projects/job-api/job-list_hu790750dfa26deecad750ee8670e658a4_73445_cc2570cab9d6d42f800e45b7e348cc39.webp"
width="760"
height="428"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="트러블-슈팅">트러블 슈팅&lt;/h2>
&lt;h3 id="1-빠른-병렬-크롤링이-대상-사이트-차단-위험을-키움">1. 빠른 병렬 크롤링이 대상 사이트 차단 위험을 키움&lt;/h3>
&lt;p>초기에는 10개 thread로 페이지를 병렬 요청했지만, 사람인 응답 실패와 IP 차단을 경험했습니다. 외부 HTML 구조와 접근 정책에 핵심 데이터가 종속되면 서비스 기능 검증까지 함께 멈추는 구조였습니다.&lt;/p>
&lt;p>병렬 수집을 제거하고 페이지별 순차 처리, 최대 5회 재시도, URL 기반 중복 제거로 요청 강도를 낮췄습니다. 차단된 개발 환경에서는 확보해 둔 DB dump·샘플 공고를 적재해 검색·지원·북마크 API 개발을 계속했습니다. 즉 크롤러는 데이터 공급 수단으로 격리하고, Job API 자체는 저장된 데이터만으로 동작하게 분리했습니다.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Job API 순차 수집과 실패 격리 코드"
src="https://eecczz.github.io/ko/toy-projects/job-api/code-crawler-guard.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h3 id="2-같은-공고가-반복-수집되어-데이터가-누적">2. 같은 공고가 반복 수집되어 데이터가 누적&lt;/h3>
&lt;p>공고 URL을 식별자로 확인한 뒤 새 항목만 saveAll하도록 바꿨습니다.&lt;/p>
&lt;h3 id="3-로컬-mysql과-jcloud-환경-차이">3. 로컬 MySQL과 jcloud 환경 차이&lt;/h3>
&lt;p>DB schema와 jar 배포 경로를 분리하고 환경변수 기반 접속 정보로 정리했습니다.&lt;/p>
&lt;h2 id="기술-선택과-이유">기술 선택과 이유&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>기술&lt;/th>
&lt;th>선택 이유&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Jsoup&lt;/strong>&lt;/td>
&lt;td>사람인 HTML에서 공고 필드를 직접 파싱하고 페이지 단위 수집을 통제하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Querydsl&lt;/strong>&lt;/td>
&lt;td>키워드·회사·직무·기술·지역·경력·급여 조건을 조합하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>JWT access/refresh&lt;/strong>&lt;/td>
&lt;td>REST client의 인증 상태를 서버 세션과 분리하기 위해&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MariaDB&lt;/strong>&lt;/td>
&lt;td>공고·회원·지원·북마크 관계를 영속화하기 위해&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="검증-결과">검증 결과&lt;/h2>
&lt;ul>
&lt;li>공고 수집·동적 검색·인증·지원·북마크의 전체 API 흐름을 구현했습니다.&lt;/li>
&lt;li>실제 jcloud Ubuntu에 jar와 DB dump를 배포해 서버 환경에서 검증했습니다.&lt;/li>
&lt;/ul>
&lt;h2 id="시스템-흐름">시스템 흐름&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Job API Project 시스템 흐름"
src="https://eecczz.github.io/ko/toy-projects/job-api/architecture.svg"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ol>
&lt;li>POST /jobs/crawl이 수집 작업을 시작합니다.&lt;/li>
&lt;li>Jsoup이 회사·제목·지역·경력·마감·기술·URL을 파싱합니다.&lt;/li>
&lt;li>URL 존재 여부를 확인해 중복 공고를 제외합니다.&lt;/li>
&lt;li>순차 요청과 최대 5회 retry로 페이지 실패를 격리하며, 접근 불가 시 DB dump·샘플 데이터로 전환합니다.&lt;/li>
&lt;li>GET /jobs가 Querydsl 조건과 정렬·페이지 값을 조합합니다.&lt;/li>
&lt;li>인증 사용자는 지원·북마크 상태를 생성하거나 취소합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="api--시스템-경계">API · 시스템 경계&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>영역&lt;/th>
&lt;th>API/계약&lt;/th>
&lt;th>책임&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>Auth&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /auth/register, /login, /refresh&lt;/code>&lt;/td>
&lt;td>JWT lifecycle&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Jobs&lt;/code>&lt;/td>
&lt;td>&lt;code>GET /jobs, /jobs/{id}&lt;/code>&lt;/td>
&lt;td>검색·상세&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Crawler&lt;/code>&lt;/td>
&lt;td>&lt;code>POST /jobs/crawl&lt;/code>&lt;/td>
&lt;td>사람인 수집&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Activity&lt;/code>&lt;/td>
&lt;td>&lt;code>POST/DELETE /applications/{id}, /bookmarks/{id}&lt;/code>&lt;/td>
&lt;td>지원·저장&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="다음-구현-계획">다음 구현 계획&lt;/h2>
&lt;ul>
&lt;li>robots.txt·이용약관·공식 API 여부를 먼저 확인하고 외부 사이트 수집을 운영 기능과 분리&lt;/li>
&lt;li>크롤러를 scheduler/queue로 분리하고 실행 상태 endpoint 추가&lt;/li>
&lt;li>검색 query 실행계획·index·응답시간 계측&lt;/li>
&lt;li>외부 사이트 정책 변경을 감지하는 parser contract test&lt;/li>
&lt;/ul></description></item></channel></rss>