-
[2026-02-27] ๊ฒ์ ์ปจ์ ํ์ : ์ํ ํผ์ฆ (Wool Sort / Tangle ๊ณ์ด)
-
[2026-02-27] ๊ธฐ์ ์คํ ํ์ : React + Vite + HTML5 Canvas
-
[2026-02-27] ์์ธ ๊ธฐํ์ ์์ฑ ์๋ฃ (v1.0)
-
[2026-02-27] ์์ฅ ์กฐ์ฌ + ๋ ํผ๋ฐ์ค ๋ถ์ ์๋ฃ
-
[2026-02-27] ํ๋ก์ ํธ ์ด๊ธฐ ์ธํ ์๋ฃ โ Frontend Developer
- React 19 + Vite + Zustand + Howler ์์กด์ฑ ์ค์น
- ์ ์ฒด ํด๋ ๊ตฌ์กฐ ์์ฑ
- GameLoop, Renderer, InputManager, GameCanvas, HomeScreen, GameplayScreen ๊ตฌํ
- Zustand ์คํ ์ด, ๊ฒ์ ์์, utils/math, utils/tween ๊ตฌํ
- Thread.js, Bobbin.js, Puzzle.js, PuzzleGenerator.js (์ญ๋ฐฉํฅ ์์ฑ + BFS ์๋ฒ) ๊ตฌํ
- git init + ์ฒซ ์ปค๋ฐ ์๋ฃ
-
[2026-02-27] ์ฝ์ด ๊ฒ์ํ๋ ์ด ๊ตฌํ ์๋ฃ โ Frontend Developer
- ์ค(Thread) ๋ ๋๋ง: ํ๋น ๋ฒ ์ง์ด ๊ณก์ , DESIGN.md 8์ ํ๋ ํธ, ์ธ๊ณฝ์ +ํ์ด๋ผ์ดํธ ๊ดํ ๋ ์ด์ด
- ์ค ๊ฒน์นจ z-order: zIndex ๊ธฐ๋ฐ ์ ๋ ฌ ๋ ๋๋ง
- Free End ๋ง์ปค: ํ์ค ์ ๋๋ฉ์ด์ (1.5์ด ์ฃผ๊ธฐ), ๋์ ํํธ ์์ญ (r=28px)
- ์ค ์ ํ ์๊ฐ ํผ๋๋ฐฑ: ์ ํ ์ค ๋ฐ๊ด+๊ตต์ด์ง, ๋น์ ํ ์ค ๋ฐํฌ๋ช (opacity 0.4)+blur
- ๋ณด๋น ๋ ๋๋ง: ๋๋ฌด ๋ณด๋น (์/ํ ํ๋์ง+Core), ์ค ์ฑ์ ๋จ๊ณ ์๊ฐํ (์ค๋ฌด๋ฌ ์ง๊ฐ), ์์ ๋ผ๋ฒจ ์
- ํญ-ํญ ์กฐ์: ์ค ๋ ํญ โ ๋ณด๋น ํญ ์ด๋, ์๋ชป๋ ๋ณด๋น ๊ฑฐ๋ถ ํ๋ค๋ฆผ ์ ๋๋ฉ์ด์
- ์ค ๊ฐ๊ธฐ ์ ๋๋ฉ์ด์ : De Casteljau subdivision์ผ๋ก ๋ฒ ์ง์ด ๊ฒฝ๋ก ์ ์ง ์๋ฉธ (500ms Tween)
- ๋ณด๋น ํ์ด๋ผ์ดํธ: ์ ํ ์ค ์์์ ๋ง๋ ๋ณด๋น๋ง ๊ฐ์กฐ, ํ๋ฆฐ ๋ณด๋น ๋ฐํฌ๋ช
- ํํฐํด ์์คํ : ๋ณด๋น ์์ฑ ํํฐํด 16๊ฐ fanout, ์คํ ์ด์ง ํด๋ฆฌ์ด 60๊ฐ ํํฐํด
- ํผ์ฆ ๋ก์ง ์ฐ๋: PuzzleGenerator ์ญ๋ฐฉํฅ ์์ฑ, Puzzle.move/undo/isCleared/isFailed/isDeadlocked
- Undo: ๋ฌด๋ฃ ๋๋๋ฆฌ๊ธฐ (Move ์๋ชจ ์์)
- Move ์นด์ดํฐ HUD: ์์ฌ 5 ์ดํ ์ ๋นจ๊ฐ์ pulse ๊ฒฝ๊ณ
- ๊ฒ์ ํ๋ฆ: ์คํ ์ด์ง ์์ โ ํผ์ฆ ์์ฑ โ ํ๋ ์ด โ ํด๋ฆฌ์ด/์คํจ โ ResultScreen
- ๋ณ์ ๊ณ์ฐ: Move 50% ์ด๋ด 3์ฑ, 70% ์ด๋ด 2์ฑ, ์ด๊ณผ 1์ฑ
- ๊ฒฐ๊ณผ ํ๋ฉด: ๋ณ ์์ฐจ ๋ฑ์ฅ ์ ๋๋ฉ์ด์ , ์ฝ์ธ ๋ณด์ ํ์, ๋ค์ ์คํ ์ด์ง/๋ค์ํ๊ธฐ/์คํ ์ด์ง ์ ํ ๋ฒํผ
- ์คํ ์ด์ง ์ ํ ํ๋ฉด: 10์คํ ์ด์ง ๊ทธ๋ฆฌ๋, ๋ณ์ ํ์, ์ธ๋ฝ ์์คํ , ์งํ๋ฅ ๋ฐ
- ๋ฉ์ธ ํ๋ฉด: ์ฝ์ธ ํ์, ์คํ๋ SVG ์ผ๋ฌ์คํธ, ์คํ ์ด์ง ์ ํ/๋ฐ๋ก ์์ ๋ฒํผ
- ์ผ์์ ์ง ๋ชจ๋ฌ: ๊ณ์ํ๊ธฐ/์ฌ์์/์คํ ์ด์ง ์ ํ/ํ์ผ๋ก ๋ฒํผ
- DESIGN.md ์ปฌ๋ฌ/ํฐํธ ์์คํ : CSS Custom Properties ์ ๋ฉด ์ ์ฉ
- ๋ชจ๋ฐ์ผ+๋ฐ์คํฌํฑ ํฐ์น/๋ง์ฐ์ค ์ง์ (InputManager)
- ๋ฐ์ํ: ResizeObserver ๊ธฐ๋ฐ, devicePixelRatio ์ค์ผ์ผ๋ง
-
[2026-02-27] ๋ฐํ์ ๋ฒ๊ทธ 5์ข ์์ ์๋ฃ โ Frontend Developer
- [A] CSS
@importGoogle Fonts โindex.html<link>ํ๊ทธ๋ก ์ด์ (๋ ๋ ๋ธ๋กํน ์ ๊ฑฐ) - [B]
Puzzle.isCleared: spare ๋ณด๋น(colorIndex=-1)์ ํด๋ฆฌ์ด ์ฒดํฌ์์ ์ ์ธ (ํผ์ฆ ํด๋ฆฌ์ด ๋ถ๊ฐ ๋ฒ๊ทธ ์์ ) - [C]
PuzzleGenerator:THREAD_COLORS[i](๊ฐ์ฒด ์ซ์ ์ธ๋ฑ์ค โ undefined) โTHREAD_COLOR_LIST[i]๋ฐฐ์ด๋ก ๊ต์ฒด, import ์์ - [D]
Renderer.resize:ctx.scale()๋์ โctx.setTransform()์ผ๋ก ๊ต์ฒด - [E]
GameCanvas: ๋ง์ดํธ ์งํclientWidth/clientHeight๊ฐ 0์ธ ๊ฒฝ์ฐ rAF 1ํ๋ ์ ์ฌ์๋ ๋ก์ง ์ถ๊ฐ - ๋น๋ ๋ฐ ๊ฐ๋ฐ ์๋ฒ ๋์ ํ์ธ ์๋ฃ (http://localhost:5176)
- [A] CSS
-
[2026-02-27] ์กฐ์ ๋ฐฉ์ ๋ณ๊ฒฝ: 2ํญ โ 1ํญ ์๋ ๋งค์นญ โ Frontend Developer
- GameController.js:
handleTap๋ฆฌํฉํฐ๋ง โ Free End ํญ ์ ์ฆ์_attemptAutoMoveํธ์ถ - ์๋ ๋งค์นญ ์ฐ์ ์์: ๊ฐ์ ์ ๋ณด๋น(0) โ ์ผ๋ฐ ๋น ๋ณด๋น(1) โ spare ๋น ๋ณด๋น(2) ์ ์ ๋ ฌ
- ๊ฑฐ๋ถ ํผ๋๋ฐฑ: ์ด๋ ๊ฐ๋ฅ ๋ณด๋น ์์ ์ Free End ์์ฒด ์ข์ฐ ํ๋ค๋ฆผ + ๋นจ๊ฐ flash (
_shakeFreeEnd) - freeEndShakeState: Controller์์ update(), Renderer์์ ๋ ๋๋ง ๋ถ๋ฆฌ ๊ด๋ฆฌ
- GameRenderer.js:
selectedBobbinId๊ธฐ๋ฐ ํ์ด๋ผ์ดํธ/๋ค/๋ฐ๊ด ํจ๊ณผ ์ ๋ฉด ์ ๊ฑฐ โ ๋ชจ๋ ์ค ๋๋ฑ ๋ ๋ - Free End ๋ง์ปค: ๊ฑฐ๋ถ ์ ๋นจ๊ฐ ์ธ๊ณฝ ์ flash + ๋ง์ปค ์์ ์ ํ (
DANGER์) - GameCanvas.jsx: ๋ ๋ ํธ์ถ์์
selectedBobbinId์ ๊ฑฐ,freeEndShakeState์ถ๊ฐ ์ ๋ฌ selectedBobbinId์ํ ํ๋ ์์ ์ ๊ฑฐ (Undo, ์ด๋ ์๋ฃ ํ null ์ฒ๋ฆฌ ์ฝ๋ ๋ฑ)- ๋น๋ ์๋ฌ ์์ ํ์ธ (http://localhost:5174)
- GameController.js:
-
[2026-02-27] ๋น์ฃผ์ผ ๋์์ธ ๋ฆฌ์คํจ ์๋ฃ โ Frontend Developer
- ๋์์ธ ํ ํฐ ๊ต์ฒด: tenqube reward-webview ๋๋์ง์ก๊ธฐ ์คํ์ผ๋ก ์ ๋ฉด ์ ์ฉ
- ๋ฐฐ๊ฒฝ: ์ ํ๋ฉด
#FFF9F0/#FFF4E6/#F5EFE6์ํค โ#ffffffํด๋ฆฐ ํ์ดํธ - ์ฃผ์ ์์: terracotta
#E8734A๊ณ์ด โ ๋ธ๋ฃจ#2761FF๊ณ์ด ์ ๋ฉด ๊ต์ฒด - ํ
์คํธ:
#2C1A0Eโ#20222E, ๋ณด์กฐ#7A6A5Aโ#868686 - ๋ฒํผ: height 52px, border-radius 16px, font-weight 600 ํต์ผ (๊ธฐ์กด 56px/28px pill ํํ์์ ๋ณ๊ฒฝ)
- ๋นํ์ฑ/๋ณด์กฐ ๋ฒํผ:
#E8EAEDbg ๋๋ white with border ์คํ์ผ - ์งํ๋ฐ fill:
#C4845Cโ#2761FF - ์คํ
์ด์ง ์นด๋ ํ์ฌ ํ์ด๋ผ์ดํธ: ์ค๋ ์ง โ
#2761FF(glow ringrgba(39,97,255,0.12)) - HUD/ํด๋ฐ: ๋ฐฐ๊ฒฝ white, ๋ณด๋
#E8EAED - ํํธ ๋ฒํผ: ์ค๋ ์ง FAB โ
#2761FF๋ธ๋ฃจ pill - ์ผ์์ ์ง ๋ชจ๋ฌ: ํฐํธ
Noto Serif KR์ ๊ฑฐ โ system font, ๋ฒํผ#2761FFprimary - SVG ์์ด์ฝ: PauseIcon
#7A6A5Aโ#868686, UndoIcon โ#20222E - ์บ๋ฒ์ค ๋ด๋ถ ๊ฒ์ ์์: ๋ณ๊ฒฝ ์์ (์ค ์์, ๋ณด๋น ๋น์ฃผ์ผ ์ ์ง)
- ๋์ ํ์ผ: App.jsx, HomeScreen.jsx, StageSelectScreen.jsx, GameplayScreen.jsx, ResultScreen.jsx
-
[2026-02-28] ๊ฒ์ ์์คํ ์ ๋ฉด ํผ๋ฒ: ๋ณด๋น ๊ธฐ๋ฐ โ ๊ตฌํ ์ค๋ญ์น(Yarn Ball) โ Frontend Developer
- ์ ๊ท ํ์ผ ์์ฑ (6๊ฐ):
src/utils/arc.jsโ ์ํธ ์ ํธ: normalizeAngle, angleInArc, arcsOverlap, arcMidAngle, arcSpansrc/game/entities/WindingThread.jsโ ์ค๋ญ์น ์ ์ค ํ๋ (id, colorIndex, color, layer, arcStart, arcEnd, windingLoops, freeEndAngle)src/game/entities/YarnBall.jsโ ํผ์ฆ ์ํ (accessibleThreads, isAccessible, removeThread, undo, isCleared, isFailed)src/game/generators/YarnBallGenerator.jsโ ๋ ์ด์ด๋ณ arc ๋ฐฐ์น + ํด๊ฒฐ ๊ฐ๋ฅ์ฑ greedy ๊ฒ์ฆsrc/game/systems/YarnBallRenderer.jsโ ๊ตฌ์ฒด ๋ ๋๋ง (๋๋กญ ์๋, ๋ฐฉ์ฌํ ๊ทธ๋ผ๋์ธํธ, ๋ ์ด์ด๋ณ ์ค arc, Free End marker, ๊ตฌ์ฒด ํ์ด๋ผ์ดํธ)src/game/systems/YarnBallController.jsโ ์ํ ๋จธ์ (idleโunwindingโidle), ํ๊ธฐ Tween ์ ๋๋ฉ์ด์ 600ms
- ์์ ํ์ผ (4๊ฐ):
src/constants/game.jsโ YARN_BALL ์์ ๋ธ๋ก ์ถ๊ฐ (BOBBIN_LAYOUT ๋ณด์กด)src/constants/stages.jsโ 10๊ฐ ์คํ ์ด์ง ์ ๋ฉด ๊ต์ฒด (threadCount/layerCount/arcDensity ํ๋ผ๋ฏธํฐ)src/ui/components/GameCanvas.jsxโ YarnBallRenderer + YarnBallController ๋ก ๊ต์ฒด, puzzleโyarnBall propsrc/ui/screens/GameplayScreen.jsxโ generateYarnBall ์ฌ์ฉ, ๋ณด๋น UI ์ ๊ฑฐ
- ๋น๋ ์ฑ๊ณต ํ์ธ (237kB, vite v7.3.1)
- ๊ตฌ๋ฒ์ ํ์ผ ๋ณด์กด: Puzzle.js, Bobbin.js, Thread.js, PuzzleGenerator.js, GameController.js, GameRenderer.js, ThreadLayout.js
- ์ ๊ท ํ์ผ ์์ฑ (6๊ฐ):
-
[2026-02-28] ์ค๋ญ์น ๋ ๋๋ง 3D ๋์(great circle) ๋ชจ๋ธ๋ก ์ ๋ฉด ๊ต์ฒด โ Frontend Developer
- WindingThread.js: tiltAngle + rotOffset + arcStart/arcEnd(๋์ ํ๋ผ๋ฏธํฐ) ํ๋ ์ถ๊ฐ, latitude/freeEndAngle ์ ๊ฑฐ
- YarnBallGenerator.js: rotOffset ๊ท ๋ฑ ๋ฐฐ์น(์งํฐ ํฌํจ), tiltAngle ๋ ์ด์ด๋ณ ๋ฒ์ ํ ๋น, ํด๊ฒฐ ๊ฐ๋ฅ์ฑ ๊ฒ์ฆ threadsOverlap ๊ธฐ๋ฐ์ผ๋ก ์ ํ
- YarnBallRenderer.js: _drawThread ์์ ์ฌ์์ฑ โ _sampleGreatCircle(๊ธฐ์ธ์ด์ง ๋์ 3D ์ขํ โ Y์ถ ํ์ โ ์ ์ฌ์), _splitFrontSegments(์๋ฉด ์ธ๊ทธ๋จผํธ ๋ถ๋ฆฌ), ๊น์ด ๊ธฐ๋ฐ ๊ตต๊ธฐ/๋ฐ๊ธฐ, ๋ฃจํ๋ณ tilt ์คํ์ ๋ค์ค ๋ ์ด์ด ๋ ๋
- YarnBallRenderer.js: getFreeEndPosition โ arcEnd 3D ์ ์์ ๋ฒ์ ๋ฐฉํฅ์ผ๋ก ๋ป๊ธฐ, ๋ท๋ฉด์ด๋ฉด arcStart ์ฌ์ฉ
- YarnBallRenderer.js: _drawTextureArcs โ ๋ฐฐ๊ฒฝ ํ ์ค์ฒ๋ ๋์ ๊ณก์ ์ผ๋ก ๊ต์ฒด
- YarnBall.js: isAccessible โ arcsOverlap ๋์ threadsOverlap ์ฌ์ฉ
- arc.js: threadsOverlap ํจ์ ์ถ๊ฐ (rotOffset ๊ทผ์ ๋ ๊ธฐ๋ฐ, ์๊ณ๊ฐ PI*0.4)
- game.js: YARN_BALL์ LAYER_DELTA(5), TILT_MIN(0.52), TILT_MAX(1.40) ์ถ๊ฐ
- ๋น๋ ์ฑ๊ณต ํ์ธ (238kB)
- ๊ฒฐ๊ณผ: ์ค์ด ๊ตฌ์ฒด๋ฅผ ์ฌ์ ์ผ๋ก ๊ฐ๋ก์ง๋ฅด๋ ๋์ ํธ ํํ๋ก ๋ ๋๋ง, ์๋ฉด๋ง ํ์(back-face culling), ๊น์ด ์์ ์ ์ฉ
-
[2026-02-28] JavaScript โ TypeScript ์ ๋ฉด ์ ํ + Pretendard ํฐํธ ์ ์ฉ โ Frontend Developer (ํ)
- ์ธํ๋ผ: tsconfig.json (strict, @/* path alias), vite.config.ts (tsconfigPaths ํ๋ฌ๊ทธ์ธ), .prettierrc, src/vite-env.d.ts
- ์์กด์ฑ ์ถ๊ฐ: typescript, vite-tsconfig-paths, prettier (devDependencies)
- ํฐํธ ๊ต์ฒด: Google Fonts CDN (Noto Sans KR/Serif KR) โ ๋ก์ปฌ Pretendard woff2-subset 9์ข (100~900)
- ํ์ ์ ์: src/types/game.ts ์ ๊ท ์์ฑ (ThreadColor, Stage, WindingThreadParams, GameStoreState ๋ฑ 17+ ์ธํฐํ์ด์ค)
- ์์ค ์ ํ (31ํ์ผ): .jsโ.ts (24๊ฐ), .jsxโ.tsx (7๊ฐ), ์ ์ฒด ํด๋์ค/ํจ์์ ํ์ ์ถ๊ฐ
- Zustand v5: create()() ์ด์ค ํธ์ถ ํจํด ์ ์ฉ
- ๊ฒ์ฆ: tsc --noEmit ์๋ฌ 0๊ฐ, prettier ํฌ๋งทํ , npm run build ์ฑ๊ณต (241KB)
- package.json: "typecheck": "tsc --noEmit", "build": "tsc --noEmit && vite build"
- (๋ค์ ์์ ๋ฐฐ์ ๋๊ธฐ)
- ์ฌ์ด๋ ๋ฏธ๊ตฌํ (Howler.js ์ค์น๋จ, SFX ์์ ๋ฏธํ๋ณด)
- ํํธ ๋ฒํผ UI๋ง ์๊ณ ๊ธฐ๋ฅ ๋ฏธ๊ตฌํ
- ๊ด๊ณ SDK ๋ฏธ์ฐ๋ (Poki SDK)
- YarnBallGenerator: ์์ฑ ์คํจ ์ forceSolvable ํด๋ฐฑ์ผ๋ก ๋์จํ๊ฒ ์์ฑ (๊ทน๋จ์ ํ๋ผ๋ฏธํฐ์์ ๋๋ฌผ๊ฒ ๋ฐ์ ๊ฐ๋ฅ)