English | νκ΅μ΄
SoundBadgeμ κ΄μ¬μ κ°μ Έμ£Όμ μ κ°μ¬ν©λλ€! λ²κ·Έ 리ν¬νΈ, κΈ°λ₯ μ μ, μλ‘μ΄ ν λ§/ν νλ¦Ώ μΆκ° λ± μ΄λ€ ννμ κΈ°μ¬λ νμν©λλ€.
- Node.js 18+
- bun (npm/yarn μ¬μ© κΈμ§)
- YouTube Data API v3 ν€
# 1. Fork ν ν΄λ‘
git clone https://github.com/<your-username>/SoundBadge.git
cd SoundBadge
# 2. μμ‘΄μ± μ€μΉ
bun install
# 3. νκ²½ λ³μ μ€μ
cp .env.example .env.local
# .env.localμ YOUTUBE_API_KEY μ
λ ₯
# 4. κ°λ° μλ² μ€ν
bun devbun dev # κ°λ° μλ² (Turbopack)
bun run build # νλ‘λμ
λΉλ
bun run lint # ESLint
bun run typecheck # TypeScript νμ
체ν¬mainμμ μ λΈλμΉ μμ±git checkout -b feat/my-feature
- λ³κ²½μ¬ν μμ± λ° μ»€λ°
- lint & νμ
μ²΄ν¬ ν΅κ³Ό νμΈ
bun run lint && bun run typecheck - Push ν PR μμ±
git push origin feat/my-feature
| μ λμ¬ | μ©λ |
|---|---|
feat/ |
μ κΈ°λ₯ |
fix/ |
λ²κ·Έ μμ |
theme/ |
μ ν λ§/ν νλ¦Ώ |
docs/ |
λ¬Έμ μμ |
refactor/ |
리ν©ν λ§ |
ν΄λμ ν λ§λ μΈλ€μΌ + μ λͺ© + μ±λ μ λ³΄κ° ν¬ν¨λ μΉ΄λ μ€νμΌμ λλ€.
src/themes/presets/your-theme.ts:
import type { Theme } from "../types";
export const yourTheme: Theme = {
name: "your-theme", // ThemeName νμ
μ μΆκ° νμ
label: "Your Theme",
tokens: {
bg: "#ffffff", // λ°°κ²½ (λ¨μ λλ CSS κ·ΈλΌλ°μ΄μ
)
fg: "#1a1a1a", // λ©μΈ ν
μ€νΈ
muted: "#6b7280", // μλΈ ν
μ€νΈ
accent: "#3b82f6", // ν¬μΈνΈ μμ
border: "#e5e7eb", // ν
λ리 ("none" κ°λ₯)
shadow: "none", // κ·Έλ¦Όμ (CSS box-shadow λλ "none")
radius: 12, // μΉ΄λ λΌμ΄λ (px)
pattern: "none", // λ°°κ²½ ν¨ν΄: "none" | "noise" | "grid" | "dots"
},
};src/themes/types.tsμ ThemeName μ λμ¨μ μΆκ°:
export type ThemeName =
| "minimal"
// ...
| "your-theme";src/themes/index.tsμμ import ν λ±λ‘.
ν νλ¦Ώμ ν΄λμ ν λ§λ³΄λ€ μμ λ‘μ΄ SVG λ μ΄μμμ λλ€ (badge, stream, terminal λ±).
src/templates/your-template.ts:
import type { Template, TemplateRenderOptions } from "./types";
import { truncate, esc } from "./utils";
export const yourTemplate: Template = {
meta: {
id: "your-template",
displayName: "Your Template",
description: "ν
νλ¦Ώ μ€λͺ
",
category: "simple", // "simple" | "player" | "visual" | "developer"
supportsLayout: false,
supportsMultiTrack: false,
maxTracks: 1,
variants: [], // μμ λ³νμ΄ μμΌλ©΄ μΆκ°
previewDimensions: { width: 480, height: 180 },
},
render(options: TemplateRenderOptions): string {
const { track, tracks, variant } = options;
return `<svg xmlns="http://www.w3.org/2000/svg" width="480" height="180" viewBox="0 0 480 180">
<!-- μ¬κΈ°μ SVG λ λλ§ λ‘μ§ -->
</svg>`;
},
};src/templates/index.tsμμ import ν λ±λ‘.
src/templates/utils.tsμμ μ 곡:
truncate(text, maxLen)β ν μ€νΈ μλ₯΄κΈ° (λ§μ€μν ν¬ν¨)esc(text)β SVG/XML μ΄μ€μΌμ΄νfetchImageAsBase64(url)β μ΄λ―Έμ§λ₯Ό base64λ‘ λ³ν
- μ»΄ν¬λνΈ: PascalCase (
CardPreview.tsx) - μ νΈ/ν¨μ: camelCase (
parseYouTubeUrl) - νμΌλͺ
: kebab-case (
card-renderer.ts) - κ²½λ‘ alias:
@/*βsrc/* - API Route: Edge Runtime μ¬μ©
μ΄μ μμ± μ μ 곡λλ μ΄μ ν νλ¦Ώμ μ¬μ©ν΄μ£ΌμΈμ. ν¬ν¨ν λ΄μ©:
- μ¬ν λ¨κ³
- κΈ°λ λμ vs μ€μ λμ
- μ¬μ©ν ν λ§/ν νλ¦Ώ/νλΌλ―Έν°
- μ€ν¬λ¦°μ· (κ°λ₯νλ©΄)