๋ฃจ๋ฏธํ๋ธ(Rummikub) ๋ณด๋๊ฒ์ ๊ธฐ๋ฐ ๋ฉํฐ LLM ์ ๋ต ์คํ ํ๋ซํผ
Human + AI ํผํฉ 2~4์ธ ์ค์๊ฐ ๋์ ์ง์ ยท ๋ค์ํ LLM ๋ชจ๋ธ ์ ๋ต ๋น๊ต/๋ถ์
๐ ๋ชฉ์ฐจ
- ํ๋ก์ ํธ ๊ฐ์
- ์ ์ฒด ์ํคํ
์ฒ
- ๊ธฐ์ ์คํ ์์ธ ์ค๋ช
- ํ๋ก์ ํธ ๊ตฌ์กฐ
- ๊ฒ์ ๊ท์น & ํ์ผ ์ธ์ฝ๋ฉ
- AI ์บ๋ฆญํฐ ์์คํ
- API ๋ช
์ธ ๊ฐ์
- ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ ์คํ
- Kubernetes ๋ฐฐํฌ (Helm)
- ํ
์คํธ ํํฉ
1. ํ๋ก์ ํธ ๊ฐ์
RummiArena๋ ๊ณ ์ ๋ณด๋๊ฒ์ ๋ฃจ๋ฏธํ๋ธ(Rummikub)๋ฅผ ์จ๋ผ์ธ ๋ฉํฐํ๋ ์ด ํ๊ฒฝ์ผ๋ก ๊ตฌํํ๊ณ , ์ฌ๋ฌ LLM(Large Language Model) AI๋ฅผ ํ๋ ์ด์ด๋ก ์ฐธ์ฌ์์ผ ์ ๋ต์ ๋น๊ตยท๋ถ์ํ๋ ์คํ ํ๋ซํผ์
๋๋ค.
ํต์ฌ ์ค๊ณ ์์น
| ์์น | ์ค๋ช
|
|---|
| LLM ์ ๋ขฐ ๊ธ์ง | LLM ์๋ต์ ํญ์ Game Engine์ผ๋ก ์ ํจ์ฑ ๊ฒ์ฆ. Invalid move โ ์ฌ์์ฒญ (์ต๋ 3ํ) โ ์คํจ ์ ๊ฐ์ ๋๋ก์ฐ |
| AI Adapter ๋ถ๋ฆฌ | Game Engine์ ํน์ LLM์ ์์กดํ์ง ์์. ๊ณตํต ์ธํฐํ์ด์ค(MoveRequest / MoveResponse)๋ก ๋ชจ๋ธ ๊ต์ฒด ๊ฐ๋ฅ |
| Stateless ์๋ฒ | ๋ชจ๋ ๊ฒ์ ์ํ๋ Redis์ ์ ์ฅ. Pod ์ฌ์์์๋ ๊ฒ์ ์ ์ง |
| GitOps | ArgoCD๊ฐ Helm chart ๊ธฐ๋ฐ ๋ฐฐํฌ ๋ด๋น |
2. ์ ์ฒด ์ํคํ
์ฒ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Client Layer โ
โ Frontend (Next.js 15) Admin Panel (Next.js) โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ WebSocket / REST โ ๊ด๋ฆฌ API
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
โ Game Server (Go / gin) โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ Game Engine โ โ REST API (gin) โ โ WebSocket Hub โ โ
โ โ (๊ท์น ๊ฒ์ฆ) โ โ โ โ (์ค์๊ฐ ๋๊ธฐํ) โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโ
โผ โผ โผ
โโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โ Redis โ โPostgres โ โ AI Adapter (NestJS) โ
โ(๊ฒ์ โ โ(์์ โ โ OpenAI / Claude โ
โ ์ํ) โ โ ๋ฐ์ดํฐ) โ โ DeepSeek / Ollama โ
โโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
|
์ธํ๋ผ ๊ตฌ์ฑ
1
2
3
4
5
6
7
8
9
10
11
| Docker Desktop Kubernetes
โโโ Namespace: rummikub
โโโ frontend (Next.js โ NodePort 30000)
โโโ game-server (Go โ NodePort 30080)
โโโ ai-adapter (NestJS โ NodePort 30081)
โโโ postgres (PostgreSQL 16 โ NodePort 30432)
โโโ redis (Redis 7)
CI/CD: GitLab CI + GitLab Runner
GitOps: ArgoCD + Helm 3
Ingress: Traefik v3
|
3. ๊ธฐ์ ์คํ ์์ธ ์ค๋ช
์ ์ฒด ์คํ ์์ฝ
| ๊ณ์ธต | ๊ธฐ์ |
|---|
| Frontend | Next.js 15, TailwindCSS, Framer Motion, dnd-kit, Zustand |
| Backend (game-server) | Go 1.24, gin, gorilla/websocket, GORM, zap |
| Backend (ai-adapter) | NestJS, TypeScript, class-validator |
| Database | PostgreSQL 16, Redis 7 |
| AI Models | OpenAI API, Claude API, DeepSeek API, Ollama (LLaMA) |
| Infra | Docker Desktop Kubernetes, Helm 3, ArgoCD, Traefik v3 |
| CI/CD | GitLab CI + GitLab Runner |
| Quality | SonarQube, Trivy, OWASP ZAP |
| Auth | Google OAuth 2.0 (NextAuth.js) |
3.1 Frontend โ Next.js 15
๊ฒ์ UI์ ์ค์๊ฐ ์ํธ์์ฉ์ ๋ด๋นํ๋ ํด๋ผ์ด์ธํธ ๋ ์ด์ด์
๋๋ค.
Next.js 15 (App Router)
- ์๋ฒ ์ปดํฌ๋ํธ(RSC): ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ๋์ด๊ณ JS ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ์ต์ํํฉ๋๋ค.
- ์๋ฒ ์ก์
(Server Actions): ํผ ์ ์ถ๊ณผ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ ์๋ฒ์์ ์ง์ ์ฒ๋ฆฌํฉ๋๋ค.
- Partial Prerendering(PPR): ์ ์ ์ฝํ
์ธ ๋ ์ฆ์ ์ ๋ฌํ๊ณ , ๋์ ์ฝํ
์ธ ๋ง ์คํธ๋ฆฌ๋ฐํ์ฌ ์ต์์ ์ฑ๋ฅ์ ๋
๋๋ค.
TailwindCSS
- ์ ํธ๋ฆฌํฐ ํด๋์ค๋ง์ผ๋ก ๋น ๋ฅด๊ฒ ์คํ์ผ๋งํ ์ ์์ด ๋ณ๋์ CSS ํ์ผ์ด ๋ถํ์ํฉ๋๋ค.
- ๋ฐํ์ JS ์์ด ์ ์ CSS๋ฅผ ์์ฑํ๋ฏ๋ก ์๋ฒ ์ปดํฌ๋ํธ์ ๊ถํฉ์ด ์ข์ต๋๋ค.
Framer Motion
layout prop ํ๋๋ก ํ์ผ์ด ์ด๋ํ ๋ ๋ถ๋๋ฌ์ด ์ฌ๋ผ์ด๋ฉ ํจ๊ณผ๋ฅผ ๊ตฌํํฉ๋๋ค.layoutId๋ฅผ ํ์ฉํด ํ์ผ ๋๋๊ทธ ์ข
๋ฃ ํ ์์ฐ์ค๋ฌ์ด ์ฌ๋ฐฐ์น ์ ๋๋ฉ์ด์
์ ์ ์ฉํฉ๋๋ค.- ์ฃผ์: ๋ฐ๋์
'use client' ์ปดํฌ๋ํธ์์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
dnd-kit (Drag & Drop)
- ๋ชจ๋ํ ์ค๊ณ๋ก ํ์ํ ๊ธฐ๋ฅ๋ง ์ ํํด ๊ฐ๋ณ๊ฒ ์ฌ์ฉํฉ๋๋ค.
- ํค๋ณด๋ ์กฐ์๊ณผ ์คํฌ๋ฆฐ ๋ฆฌ๋๋ฅผ ๊ธฐ๋ณธ ์ง์ํ์ฌ ์ ๊ทผ์ฑ(A11y)์ ํ๋ณดํฉ๋๋ค.
react-beautiful-dnd๋ณด๋ค ์ฑ๋ฅ๊ณผ ์ ์ง๋ณด์ ๋ฉด์์ ์ฐ์ํฉ๋๋ค.
Zustand (์ํ ๊ด๋ฆฌ)
- Redux๋ณด๋ค ํจ์ฌ ๊ฐ๋ณ๊ณ ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ์ ์ต๋๋ค.
- ๋๋๊ทธ ์ค์ธ ํ์ผ ์์น, ๋ชจ๋ฌ ์ํ, ์ํจ(hand tile) ๋ชฉ๋ก ๋ฑ ์ ์ญ ์ํ ๊ด๋ฆฌ์ ์ฌ์ฉํฉ๋๋ค.
- Selector ๊ธฐ๋ฐ ์ ๊ทผ์ผ๋ก ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์งํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // store/useTaskStore.ts โ Zustand + dnd-kit ์กฐํฉ ์์
import { create } from 'zustand';
import { arrayMove } from '@dnd-kit/sortable';
interface TaskStore {
tasks: string[];
reorderTasks: (activeId: string, overId: string) => void;
}
export const useTaskStore = create<TaskStore>((set) => ({
tasks: ['Task 1', 'Task 2', 'Task 3'],
reorderTasks: (activeId, overId) => set((state) => {
const oldIndex = state.tasks.indexOf(activeId);
const newIndex = state.tasks.indexOf(overId);
return { tasks: arrayMove(state.tasks, oldIndex, newIndex) };
}),
}));
|
ํ๋ก ํธ์๋ ์๋์ง ํ๋ฆ
1
2
3
4
5
| 1. Zustand ์คํ ์ด์ ํ์ผ ๋ฐฐ์ด ์ ์ฅ
2. dnd-kit SortableContext๋ก ํ์ผ ๋ชฉ๋ก ๊ฐ์ธ๊ธฐ
3. onDragEnd โ Zustand arrayMove๋ก ๋ฐฐ์ด ์์ ์
๋ฐ์ดํธ
4. Framer Motion layout ์ ๋๋ฉ์ด์
์ผ๋ก ๋ถ๋๋ฌ์ด ์ฌ๋ฐฐ์น
5. Next.js Server Action์ผ๋ก ๋ณ๊ฒฝ ์ฌํญ DB์ ์ ์ฅ (useOptimistic ํ์ฉ)
|
3.2 Backend: Game Server โ Go 1.24
์ค์๊ฐ ๊ฒ์ ๋ก์ง, WebSocket ํต์ , ๊ท์น ๊ฒ์ฆ์ ๋ด๋นํ๋ ํต์ฌ ์๋ฒ์
๋๋ค.
Go 1.24
- ๊ณ ๋ฃจํด(Goroutines): ์๋ง ๋ช
์ ๋์ ์ ์์๋ฅผ ๊ฐ๋ฒผ์ด ์ค๋ ๋๋ก ์ฒ๋ฆฌํฉ๋๋ค.
- ๋ฎ์ ์ง์ฐ ์๊ฐ(Low Latency): ์ปดํ์ผ ์ธ์ด ํน์ฑ์ ์คํ ์๋๊ฐ ๋น ๋ฅด๋ฉฐ, ๊ฒ์ ์๋ฒ์ ํต์ฌ ์๊ตฌ์ฌํญ์ธ ๋น ๋ฅธ ์๋ต์ ๋ณด์ฅํฉ๋๋ค.
- Go 1.24์ ์ ๋ค๋ฆญ ์ฑ๋ฅ ํฅ์๊ณผ ๋ฉ๋ชจ๋ฆฌ ์ต์ ํ๊ฐ ๋ฐ์๋์์ต๋๋ค.
Gin (HTTP ํ๋ ์์ํฌ)
- ๋ก๊ทธ์ธ, ๋ฐฉ ์์ฑ, ํ๋กํ ์กฐํ ๋ฑ REST API ์ฒ๋ฆฌ์ ์ฌ์ฉํฉ๋๋ค.
- ๋ถํ์ํ ๊ธฐ๋ฅ์ ์ ๊ฑฐํ๊ณ ์๋์ ์ง์คํ ๊ฒฝ๋ ํ๋ ์์ํฌ์
๋๋ค.
Gorilla/WebSocket
- ๊ฒ์ ๋ด ์ฑํ
, ํ์ผ ์ด๋ ๋๊ธฐํ, ์ค์๊ฐ ์ ํฌ ๋ฑ ์๋ฐฉํฅ ํต์ ์ ๋ด๋นํฉ๋๋ค.
- Go ์ํ๊ณ์์ ๊ฐ์ฅ ์์ ์ ์ด๊ณ ๋๋ฆฌ ์ฌ์ฉ๋๋ WebSocket ๊ตฌํ์ฒด์
๋๋ค.
- ๊ณ ๋ฃจํด๊ณผ ๊ฒฐํฉํ์ฌ ๋ค์์ ํด๋ผ์ด์ธํธ๋ฅผ ๋์์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
GORM (ORM)
- SQL ์์ด Go ๊ตฌ์กฐ์ฒด(Struct)๋ก DB๋ฅผ ์กฐ์ํฉ๋๋ค.
- ์ ์ ๋ฐ์ดํฐ, ์์ดํ
์ธ๋ฒคํ ๋ฆฌ, ๊ฒ์ ์ ์ ๋ฑ์ PostgreSQL๊ณผ ์ฐ๋ํฉ๋๋ค.
- Auto Migration์ผ๋ก ๋ฐ์ดํฐ ๋ชจ๋ธ ๋ณ๊ฒฝ ์ ํ
์ด๋ธ ๊ตฌ์กฐ๋ฅผ ์๋ ์
๋ฐ์ดํธํฉ๋๋ค.
Zap (๋ก๊น
)
- Uber์์ ๊ฐ๋ฐํ ์ด๊ณ ์ ๊ตฌ์กฐํ ๋ก๊น
๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.
- ์ต์ํ์ ๋ฉ๋ชจ๋ฆฌ ํ ๋น์ผ๋ก ๋ก๊ทธ๋ฅผ ๊ธฐ๋กํ์ฌ ์ฑ๋ฅ ์ ํ๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
- JSON ํ์ ์ถ๋ ฅ์ผ๋ก ๋๋์ ๊ฒ์ ๋ก๊ทธ๋ฅผ ์์งยท๋ถ์ํ๊ธฐ ์ฉ์ดํฉ๋๋ค.
๋ก์ปฌ ์คํ
1
2
| cd src/game-server
go build ./cmd/server && ./server
|
Go๋ ์ปดํ์ผ ์ธ์ด์ด๋ฏ๋ก go build๋ก ๋ฐ์ด๋๋ฆฌ๋ฅผ ์์ฑํ ํ ์คํํฉ๋๋ค.
3.3 Backend: AI Adapter โ NestJS
AI ๋ชจ๋ธ(OpenAI, Claude, DeepSeek, Ollama)๊ณผ ๊ฒ์ ์๋ฒ ์ฌ์ด์์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ์ค๊ฐํ๋ ์๋ฒ์
๋๋ค.
NestJS
- ๋ชจ๋/์ปจํธ๋กค๋ฌ/์๋น์ค ๊ตฌ์กฐ๋ก ์ญํ ์ ๋ช
ํํ ๋ถ๋ฆฌํฉ๋๋ค.
- AI ์๋ต ๋๊ธฐ ์๊ฐ์ด ๊ธธ์ด์ง๋ ํน์ฑ์ ๊ฐํ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ํ ๊ฐ๋ฐ๊ณผ ํ๋ก์ ํธ ํ์ฅ ์ ์ฝ๋ ์ผ๊ด์ฑ์ ์ ์งํ๊ธฐ ์ข์ต๋๋ค.
TypeScript
- AI ๋ชจ๋ธ์ ๋ณด๋ด๋ ํ๋กฌํํธ์ ์๋ต ๋ฐ์ดํฐ์ ํ์
์ ์ ์ํ์ฌ ๋ฐํ์ ์๋ฌ๋ฅผ ๋ฐฉ์งํฉ๋๋ค.
- ๋ณต์กํ AI ์๋ต ๊ฐ์ฒด ๊ตฌ์กฐ๋ฅผ ์ธํฐํ์ด์ค๋ก ๊ด๋ฆฌํ์ฌ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๋์
๋๋ค.
class-validator
- ํ๋ก ํธ์๋ ์์ฒญ์ด ์๋ฒ์ ๋๋ฌํ๊ธฐ ์ , ์
๊ตฌ์์ ์ฆ์ ์ ํจ์ฑ์ ๊ฒ์ฌํฉ๋๋ค.
- ๋ฐ์ฝ๋ ์ดํฐ ๋ฐฉ์์ผ๋ก ์ง๊ด์ ์ธ ๊ฒ์ฆ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
| // DTO ์ค์ ์์
import { IsString, IsInt, Min, Max } from 'class-validator';
export class ChatRequestDto {
@IsString()
message: string; // AI์๊ฒ ์ ๋ฌํ ๋ฉ์์ง
@IsInt()
@Min(1)
@Max(500)
maxTokens: number; // ์ต๋ ํ ํฐ ์ ์ ํ ๊ฒ์ฆ
}
|
ValidationPipe ์ค์ ์, ์๋ชป๋ ๋ฐ์ดํฐ ์
๋ ฅ์ ์๋์ผ๋ก 400 Bad Request ์๋ต์ ๋ฐํํฉ๋๋ค.
AI Adapter ์ฒ๋ฆฌ ํ๋ฆ
1
2
3
4
5
| 1. class-validator โ ์์ฒญ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ
2. NestJS Service โ ํด๋น AI ๋ชจ๋ธ SDK ํธ์ถ (OpenAI / Claude / DeepSeek / Ollama)
3. ์๋ต ํ์ฑ ๋ฐ ๋ณํ โ MoveResponse ํํ๋ก ์ ํํ
4. Game Server ๋ฐํ โ ๊ฒฐ๊ณผ ์ ๋ฌ
5. Game Engine ์ต์ข
๊ฒ์ฆ โ ์ต๋ 3ํ ์ฌ์์ฒญ ํ ์คํจ ์ ๊ฐ์ ๋๋ก์ฐ
|
๋ก์ปฌ ์คํ
1
2
| cd src/ai-adapter
npm install && npm run start:dev
|
start:dev๋ Watch mode๋ก ์คํ๋์ด ์ฝ๋ ์์ ์ ์๋ฒ๊ฐ ์๋ ์ฌ์์๋ฉ๋๋ค.
3.4 ์ธ์ฆ โ Google OAuth 2.0 (NextAuth.js)
NextAuth.js v5(Auth.js) ๊ธฐ๋ฐ์ผ๋ก Google ์์
๋ก๊ทธ์ธ์ ๊ตฌํํฉ๋๋ค.
Google Cloud Console ์ค์
- Google Cloud Console์์ ํ๋ก์ ํธ ์์ฑ
- API ๋ฐ ์๋น์ค > OAuth ๋์ ํ๋ฉด โ ์ธ๋ถ(External) ์ ํ ํ ํ์ ์ ๋ณด ์
๋ ฅ
- ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด ๋ง๋ค๊ธฐ > OAuth ํด๋ผ์ด์ธํธ ID ์์ฑ
- ์ ํ๋ฆฌ์ผ์ด์
์ ํ: ์น ์ ํ๋ฆฌ์ผ์ด์
- ์น์ธ๋ ๋ฆฌ๋๋ ์
URI:
http://localhost:3000/api/auth/callback/google
- ๋ฐ๊ธ๋ ํด๋ผ์ด์ธํธ ID์ ๋ณด์ ๋น๋ฐ๋ฒํธ๋ฅผ
.env์ ์ ์ฅ
ํ๊ฒฝ ๋ณ์ ์ค์ (.env)
AUTH_GOOGLE_ID=๋ฐ๊ธ๋ฐ์_ํด๋ผ์ด์ธํธ_ID
AUTH_GOOGLE_SECRET=๋ฐ๊ธ๋ฐ์_ํด๋ผ์ด์ธํธ_๋ณด์_๋น๋ฐ๋ฒํธ
AUTH_SECRET=๋๋ค_์ํฌ๋ฆฟ_ํค # openssl rand -base64 32 ์ผ๋ก ์์ฑ ๊ถ์ฅ
์ฝ๋ ๊ตฌํ
1
| npm install next-auth@beta
|
1
2
3
4
5
6
7
| // auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Google],
})
|
1
2
3
| // app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
|
1
2
3
4
5
6
| // ๋ก๊ทธ์ธ ๋ฒํผ (Client Component)
import { signIn } from "next-auth/react"
export default function LoginButton() {
return <button onClick={() => signIn("google")}>Google๋ก ๋ก๊ทธ์ธ</button>
}
|
1
2
3
4
5
6
7
8
| // ์ธ์
ํ์ธ (Server Component)
import { auth } from "@/auth"
export default async function Page() {
const session = await auth()
if (!session) return <div>๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.</div>
return <div>์๋
ํ์ธ์, {session.user?.name}๋!</div>
}
|
๋ณด์ ์ฃผ์: AUTH_SECRET์ ์ธ์
์ฟ ํค ์ํธํ์ ์ฌ์ฉ๋ฉ๋๋ค. ์ ๋ ์ธ๋ถ์ ๋
ธ์ถํ์ง ๋ง์ธ์.
3.5 ๋ณด์ & ํ์ง ๋๊ตฌ โ DevSecOps
CI/CD ํ์ดํ๋ผ์ธ์ ํตํฉ๋ ์๋ํ ๋ณด์ ๋ฐ ํ์ง ๊ฒ์ฌ ๋๊ตฌ์
๋๋ค.
๋๊ตฌ ๋น๊ต
| ๋๊ตฌ | ๋ถ์ ๋ฐฉ์ | ์ฃผ์ ๋์ | ์ฃผ์ ๋ชฉ์ |
|---|
| SonarQube | ์ ์ (SAST) | ์์ค ์ฝ๋ | ์ฝ๋ ํ์ง ํฅ์ ๋ฐ ์ด๊ธฐ ๋ฒ๊ทธ ๋ฐฉ์ง |
| Trivy | ์ ์ / ํ๊ฒฝ | ์ปจํ
์ด๋, ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ๋ฐฐํฌ ํ๊ฒฝ ๋ฐ ์ธํ๋ผ ๋ณด์ ๊ฐํ |
| OWASP ZAP | ๋์ (DAST) | ์คํ ์ค์ธ ์น ์ฑ | ์ค์ ์๋น์ค ํ๊ฒฝ์ ๊ณต๊ฒฉ ๋ฐฉ์ด ๋ฐ ๊ฒ์ฆ |
SonarQube โ ์ ์ ์ฝ๋ ๋ถ์ (SAST)
์์ค ์ฝ๋๋ฅผ ์คํํ์ง ์๊ณ ๋ถ์ํ์ฌ ํ์ง๊ณผ ๋ณด์ ์ทจ์ฝ์ ์ ์ฐพ์ต๋๋ค.
- ๋ฒ๊ทธ ๋ฐ ์ฝ๋ ์ค๋ฉ ํ์ง: ์ ์ฌ์ ์ค๋ฅ์ ์ ์ง๋ณด์๊ฐ ์ด๋ ค์ด ์ฝ๋ ๊ตฌ์กฐ๋ฅผ ์๋ณํฉ๋๋ค.
- ๋ณด์ ์ทจ์ฝ์ ๋ถ์: SQL Injection, XSS ๋ฑ ์ฝ๋ ์์ค์ ๋ณด์ ์ํ์ ๊ฒฝ๊ณ ํฉ๋๋ค.
- ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง: ํ
์คํธ๊ฐ ์ค์ ๋ก์ง์ ์ผ๋ง๋ ๊ฒ์ฆํ๋์ง ์๊ฐํํฉ๋๋ค.
- CI/CD ํตํฉ: GitLab CI์ ์ฐ๊ฒฐํ์ฌ ์ปค๋ฐ๋ง๋ค ์๋ ๊ฒ์ฌ๋ฅผ ์ํํฉ๋๋ค.
Trivy โ ์ปจํ
์ด๋ ๋ฐ ์ธํ๋ผ ๋ณด์
Aqua Security์์ ๊ฐ๋ฐํ ์คํ์์ค ๋ณด์ ์ค์บ๋์
๋๋ค.
- OS ํจํค์ง ์ทจ์ฝ์ ์ค์บ: ์ปจํ
์ด๋ ์ด๋ฏธ์ง(Alpine, Ubuntu ๋ฑ) ๋ด ํจํค์ง ๋ณด์ ๊ฒฐํจ์ ํ์งํฉ๋๋ค.
- ์ ํ๋ฆฌ์ผ์ด์
์ข
์์ฑ ์ค์บ: Go, Node.js ๋ฑ ํ๋ก์ ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ทจ์ฝ์ ์ ํ์งํฉ๋๋ค.
- ์ค์ ์ค๋ฅ ์ ๊ฒ: Dockerfile, Kubernetes YAML์ ๋ณด์ ์ํ ๊ตฌ์ฑ์ ์ฐพ์ต๋๋ค.
- ๋น ๋ฅธ ์๋: ๊ฐ๋ณ๊ณ ๋นจ๋ผ ์ปจํ
์ด๋ ๋น๋ ์ ์ฆ๊ฐ์ ์ธ ๊ฒ์ฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
OWASP ZAP โ ๋์ ์น ๋ณด์ ํ
์คํธ (DAST)
์น ์ ํ๋ฆฌ์ผ์ด์
์ ์ค์ ์คํํ ์ํ์์ ๊ณต๊ฒฉ์ ์๋ฎฌ๋ ์ด์
ํฉ๋๋ค.
- ์๋ ์ค์บ: ์น ์ฌ์ดํธ๋ฅผ ํฌ๋กค๋งํ๋ฉฐ OWASP Top 10 ์ทจ์ฝ์ ์ ์๋ ํ
์คํธํฉ๋๋ค.
- ์คํ์ด๋๋ง: ๋ชจ๋ ํ์ด์ง์ URL ๊ตฌ์กฐ๋ฅผ ์๋์ผ๋ก ๋ถ์ํฉ๋๋ค.
- API/CLI ์ง์: ์๋ํ๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ์ ๋ณด์ ์ ๊ฒ์ ํฌํจ์ํฌ ์ ์์ต๋๋ค.
4. ํ๋ก์ ํธ ๊ตฌ์กฐ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| RummiArena/
โโโ docs/
โ โโโ 01-planning/ # ๊ธฐํ (ํ์ฅ, ์๊ตฌ์ฌํญ, WBS, ๋ฐฑ๋ก๊ทธ)
โ โโโ 02-design/ # ์ค๊ณ (์ํคํ
์ฒ, DB, API, WebSocket, AI Adapter)
โ โโโ 03-development/ # ๊ฐ๋ฐ ๊ฐ์ด๋
โ โโโ 04-testing/ # ํ
์คํธ ์ ๋ต + ๋ณด๊ณ ์
โ โโโ 05-deployment/ # ๋ฐฐํฌ ๊ฐ์ด๋ + K8s ์ํคํ
์ฒ
โโโ src/
โ โโโ game-server/ # Go ๋ฐฑ์๋ (REST API + Game Engine)
โ โ โโโ cmd/server/ # ๋ฉ์ธ ์ํธ๋ฆฌํฌ์ธํธ
โ โโโ ai-adapter/ # NestJS AI ์๋น์ค
โ โโโ frontend/ # Next.js ๊ฒ์ UI
โ โโโ admin/ # ๊ด๋ฆฌ์ ๋์๋ณด๋
โโโ helm/
โ โโโ charts/
โ โโโ postgres/ # PostgreSQL Helm ์ฐจํธ (Bitnami ๊ธฐ๋ฐ)
โ โโโ redis/ # Redis Helm ์ฐจํธ (Bitnami ๊ธฐ๋ฐ)
โ โโโ game-server/ # ์ปค์คํ
Helm ์ฐจํธ (์ง์ ์์ฑ)
โ โโโ ai-adapter/ # ์ปค์คํ
Helm ์ฐจํธ (์ง์ ์์ฑ)
โ โโโ frontend/ # ์ปค์คํ
Helm ์ฐจํธ (์ง์ ์์ฑ)
โโโ work_logs/ # ์ธ์
/ ๋ฐ์ผ๋ฆฌ / ์คํฌ๋ผ ๋ก๊ทธ
|
Helm ์ฐจํธ ์ข
๋ฅ
| ์ฐจํธ | ์ถ์ฒ | ๋น๊ณ |
|---|
charts/postgres | Bitnami ๊ณต์ฉ ์ฐจํธ ํ์ฉ | bitnami/postgresql ๊ธฐ๋ฐ |
charts/redis | Bitnami ๊ณต์ฉ ์ฐจํธ ํ์ฉ | bitnami/redis ๊ธฐ๋ฐ |
charts/game-server | ์ง์ ์์ฑ | Go ์๋ฒ ์ด๋ฏธ์ง, ํฌํธ, ํ๊ฒฝ ๋ณ์ ์ค์ |
charts/ai-adapter | ์ง์ ์์ฑ | NestJS ์๋ฒ ์ด๋ฏธ์ง, AI API ํค ์ค์ |
charts/frontend | ์ง์ ์์ฑ | Next.js ์ด๋ฏธ์ง, NodePort ์ค์ |
์ปค์คํ
์ฐจํธ๋ helm create charts/game-server๋ก ๊ธฐ๋ณธ ํ
ํ๋ฆฟ ์์ฑ ํ values.yaml์ ์์ ํ์ฌ ์ฌ์ฉํฉ๋๋ค.
5. ๊ฒ์ ๊ท์น & ํ์ผ ์ธ์ฝ๋ฉ
์ปดํจํฐ์ AI๊ฐ ํ์ผ์ ๊ณ ์ ํ๊ฒ ์๋ณํ๊ธฐ ์ํ ์ฝ๋ ๊ท์น์
๋๋ค.
ํ์ผ ์ฝ๋ ๊ตฌ์กฐ: {Color}{Number}{Set}
| ๊ตฌ์ฑ ์์ | ๊ฐ | ์ค๋ช
|
|---|
| Color | R, B, Y, K | Red, Blue, Yellow, Black |
| Number | 1 ~ 13 | ํ์ผ์ ์ ํ ์ซ์ |
| Set | a, b | ๋์ผํ ํ์ผ์ด 2๊ฐ์ฉ ์กด์ฌํ๋ฏ๋ก ๊ตฌ๋ถ ํ์ |
| Joker | JK1, JK2 | ์กฐ์ปค ํ์ผ 2๊ฐ ๊ตฌ๋ถ |
Black์ โKโ๋ก ํ๊ธฐํ๋ ์ด์ : โBโ๊ฐ ์ด๋ฏธ Blue์ ์ฌ์ฉ๋์ด ์ถฉ๋์ ๋ฐฉ์งํ๊ธฐ ์ํจ์
๋๋ค.
ํ์ผ ์ฝ๋ ์์
| ์ฝ๋ | ์๋ฏธ |
|---|
R7a | ๋นจ๊ฐ์ 7๋ฒ ์ฒซ ๋ฒ์งธ ํ์ผ |
R7b | ๋นจ๊ฐ์ 7๋ฒ ๋ ๋ฒ์งธ ํ์ผ |
B13b | ํ๋์ 13๋ฒ ๋ ๋ฒ์งธ ํ์ผ |
Y1a | ๋
ธ๋์ 1๋ฒ ์ฒซ ๋ฒ์งธ ํ์ผ |
K10b | ๊ฒ์ ์ 10๋ฒ ๋ ๋ฒ์งธ ํ์ผ |
JK1 | ์ฒซ ๋ฒ์งธ ์กฐ์ปค |
JK2 | ๋ ๋ฒ์งธ ์กฐ์ปค |
๊ฐ๋ฐ ์ ํ์ฉ
- Go ์๋ฒ: WebSocket ํจํท์ ๋ฌธ์์ด ๋ฐฐ์ด๋ก ์ ์กํ์ฌ ํจํท ํฌ๊ธฐ๋ฅผ ์ต์ํํฉ๋๋ค.
์) ["R7a", "R8a", "R9a"] โ ๊ฐ์ ์ ์ฐ์ 3์ฅ ๊ทธ๋ฃน ๊ตฌ์ฑ - AI ์ด๋ํฐ: LLM ํ๋กฌํํธ์ ์ธ์ฝ๋ฉ ๊ท์น์ ํฌํจํ์ฌ AI๊ฐ ๊ฒ์ ์ํ๋ฅผ ์ ํํ ํ์
ํ๋๋ก ํฉ๋๋ค.
- Zustand: ํ์ผ ์ฝ๋๋ฅผ
key ๊ฐ์ผ๋ก ์ฌ์ฉํ์ฌ ๋๋๊ทธ ์ค ๋๋กญ ์ํ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
6. AI ์บ๋ฆญํฐ ์์คํ
๋ค์ํ ์ ๋ต ์คํ์ผ์ ๊ฐ์ง AI ์บ๋ฆญํฐ๋ก ๊ฒ์ ๋ค์์ฑ์ ํ๋ณดํฉ๋๋ค.
| ์บ๋ฆญํฐ | ์คํ์ผ | ์ค๋ช
|
|---|
| Rookie | ๋ณด์์ | ์์ ํ ์๋ง ์ ํ |
| Calculator | ํ๋ฅ ๊ธฐ๋ฐ | ๊ธฐ๋๊ฐ ๊ณ์ฐ์ผ๋ก ์ต์ ์ ๋์ถ |
| Shark | ๊ณต๊ฒฉ์ | ์๋ ๊ฒฌ์ + ๋๋ ๋ฐฐ์น ์ ๋ต |
| Fox | ๊ธฐ๋ง์ | ์๋์ ์ง์ฐ + ์ญ์ ํจํด ๊ตฌ์ฌ |
| Wall | ์๋น์ | ์ต์ ๋ฐฐ์น + ์์ ๋น์ถ |
| Wildcard | ์์ธก ๋ถ๊ฐ | ๋๋ค ์ ๋ต ํผํฉ |
- ๋์ด๋: ํ์ / ์ค์ / ๊ณ ์
- ์ฌ๋ฆฌ์ ๋ ๋ฒจ: 0 ~ 3๋จ๊ณ
7. API ๋ช
์ธ ๊ฐ์
Room Management
1
2
3
4
5
6
7
| POST /api/rooms # ๋ฐฉ ์์ฑ
GET /api/rooms # ๋ฐฉ ๋ชฉ๋ก ์กฐํ
GET /api/rooms/:id # ๋ฐฉ ์์ธ ์กฐํ
POST /api/rooms/:id/join # ๋ฐฉ ์
์ฅ
POST /api/rooms/:id/leave # ๋ฐฉ ํด์ฅ
POST /api/rooms/:id/start # ๊ฒ์ ์์
DELETE /api/rooms/:id # ๋ฐฉ ์ญ์
|
Game Actions
1
2
3
4
5
| GET /api/games/:id # ๊ฒ์ ์ํ ์กฐํ (1์ธ์นญ ๋ทฐ)
POST /api/games/:id/place # ํ์ผ ์์ ๋ฐฐ์น
POST /api/games/:id/confirm # ํด ํ์ (Game Engine ์ ํจ์ฑ ๊ฒ์ฆ)
POST /api/games/:id/draw # ํ์ผ ๋๋ก์ฐ
POST /api/games/:id/reset # ํด ๋๋๋ฆฌ๊ธฐ
|
Health Check
1
2
| GET /health # ์๋ฒ ์ํ + Redis ์ฐ๊ฒฐ ํ์ธ
GET /ready # ์ค๋น ์ํ ํ์ธ
|
Service Endpoints (NodePort)
| ์๋น์ค | URL | Port |
|---|
| Frontend | http://localhost:30000 | 30000 |
| Game Server | http://localhost:30080 | 30080 |
| AI Adapter | http://localhost:30081 | 30081 |
| PostgreSQL | localhost:30432 | 30432 |
8. ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ ์คํ
์ฌ์ ์๊ตฌ์ฌํญ
- Docker Desktop (Kubernetes ํ์ฑํ)
- Helm 3
- Node.js 20+
- Go 1.24+
1. Game Server ์คํ (Go)
1
2
| cd src/game-server
go build ./cmd/server && ./server
|
go build ./cmd/server: ์์ค ์ฝ๋๋ฅผ ์ปดํ์ผํ์ฌ ๋ฐ์ด๋๋ฆฌ ์์ฑ&& ./server: ๋น๋ ์ฑ๊ณต ์ ์ฆ์ ์๋ฒ ์คํ- WebSocket ํต์ ๋๊ธฐ ์ํ๊ฐ ๋ฉ๋๋ค.
2. AI Adapter ์คํ (NestJS)
1
2
| cd src/ai-adapter
npm install && npm run start:dev
|
npm install: package.json์ ๋ช
์๋ ๋ชจ๋ ํจํค์ง ์ค์นnpm run start:dev: ๊ฐ๋ฐ ๋ชจ๋(Watch mode) ์คํ. ์ฝ๋ ์ ์ฅ ์ ์๋ ์ฌ์์
3. Frontend ์คํ (Next.js)
1
2
| cd src/frontend
npm install && npm run dev
|
npm run dev: Next.js ๊ฐ๋ฐ ์๋ฒ ์คํ (http://localhost:3000)- Hot Module Replacement(HMR) ์ง์. ์ฝ๋ ์์ ์ ์ฆ์ ํ๋ฉด ๋ฐ์
์ฃผ์: Framer Motion, dnd-kit, Zustand๋ ๋ธ๋ผ์ฐ์ API๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋ฐ๋์ 'use client' ์ปดํฌ๋ํธ์์ ๊ตฌํํด์ผ ํฉ๋๋ค.
9. Kubernetes ๋ฐฐํฌ (Helm)
๋ค์์คํ์ด์ค ์์ฑ
1
| kubectl create namespace rummikub
|
rummikub ์ ์ฉ ๊ฒฉ๋ฆฌ ๊ณต๊ฐ์ ์์ฑํฉ๋๋ค. ์ดํ ๋ชจ๋ ๋ฆฌ์์ค๋ ์ด ๋ค์์คํ์ด์ค ์์์ ๊ด๋ฆฌ๋ฉ๋๋ค.
Helm ์ฐจํธ ๋ฐฐํฌ (5๊ฐ ์๋น์ค)
1
2
3
4
5
6
7
8
9
10
11
12
| cd helm
# 1. ์ธํ๋ผ ๊ณ์ธต (DB, ์บ์)
helm install postgres charts/postgres -n rummikub
helm install redis charts/redis -n rummikub
# 2. ๋ฐฑ์๋ ์๋น์ค
helm install game-server charts/game-server -n rummikub
helm install ai-adapter charts/ai-adapter -n rummikub
# 3. ํ๋ก ํธ์๋
helm install frontend charts/frontend -n rummikub
|
๋ฐฐํฌ ํ์ธ
1
2
3
4
5
6
7
8
| # ํ๋ ์ํ ํ์ธ
kubectl get pods -n rummikub
# ์๋น์ค ํ์ธ
kubectl get services -n rummikub
# ํน์ ํ๋ ๋ก๊ทธ ํ์ธ
kubectl logs -f <pod-name> -n rummikub
|
๋ฐฐํฌ ์
๋ฐ์ดํธ / ์ญ์
1
2
3
4
5
6
7
8
| # ์ค์ ๋ณ๊ฒฝ ํ ์
๋ฐ์ดํธ
helm upgrade game-server charts/game-server -n rummikub
# ํน์ ์๋น์ค ์ญ์
helm uninstall game-server -n rummikub
# ์ ์ฒด ๋ค์์คํ์ด์ค ์ญ์
kubectl delete namespace rummikub
|
10. ํ
์คํธ ํํฉ
| ํ
์คํธ ๋ถ๋ฅ | ๊ฒฐ๊ณผ | ์ปค๋ฒ๋ฆฌ์ง |
|---|
| Engine Unit Tests | โ
69/69 PASS | 96.5% |
| Smoke Tests | โ
16/16 PASS | โ |
| Integration Tests | โ
31/31 PASS | REST API + DB/Redis |
๊ด๋ จ ๋ฌธ์
| ๋ฌธ์ | ๊ฒฝ๋ก |
|---|
| ํ๋ก์ ํธ ํ์ฅ | docs/01-planning/01-project-charter.md |
| ์ํคํ
์ฒ ์ค๊ณ | docs/02-design/01-architecture.md |
| DB ์ค๊ณ | docs/02-design/02-database-design.md |
| API ์ค๊ณ | docs/02-design/03-api-design.md |
| WebSocket ํ๋กํ ์ฝ | docs/02-design/10-websocket-protocol.md |
| ํ
์คํธ ์ ๋ต | docs/04-testing/01-test-strategy.md |
| ํตํฉ ํ
์คํธ ๋ณด๊ณ ์ | docs/04-testing/06-integration-test-report.md |
์ด ๋ฌธ์๋ RummiArena ํ๋ก์ ํธ์ ๊ธฐ์ ์คํ, ์ํคํ
์ฒ, ๊ฐ๋ฐ ๋ฐ ๋ฐฐํฌ ๊ฐ์ด๋๋ฅผ ํฌํจํฉ๋๋ค.
๋ฌธ์ ๋ฐ ๊ธฐ์ฌ: GitHub โ RummiArena