포인트 시스템 추가, 프론트 국기 반영

develop
jwkim 2026-06-10 15:11:56 +09:00
parent 6ffe0dfca3
commit 1a803b7fa0
58 changed files with 326 additions and 68 deletions

View File

@ -15,6 +15,7 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY data ./data
EXPOSE 8000

View File

@ -14,6 +14,7 @@ from fastapi.middleware.cors import CORSMiddleware
from .config import settings
from .database import init_db
from .routers import admin, leaderboard, matches, predictions
from .scoring import load_scoring_data
from .seed import seed_if_empty
logging.basicConfig(level=logging.INFO)
@ -22,6 +23,7 @@ log = logging.getLogger("triplepick")
@asynccontextmanager
async def lifespan(app: FastAPI): # noqa: ANN201
load_scoring_data() # data/scoring.json → 배점·배제 대상
await init_db()
await seed_if_empty()
log.info("API ready")

View File

@ -5,6 +5,7 @@
- ai_predictions GPT/Claude/Gemini 예측 (경기 × 모델)
- crowd_stats 군중 투표 분포 (경기당 1, 원자적 증분)
- user_predictions 유저 (이메일 식별, 채점/알림 플래그 포함)
- user_points 유저별 누적 포인트 (채점 갱신, 이메일당 1)
"""
from __future__ import annotations
@ -152,3 +153,28 @@ class UserPrediction(Base):
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class UserPoints(Base):
"""유저별 누적 포인트 — 채점(grade_prediction) 결과를 이메일 단위로 집계.
등급별 횟수 컬럼은 scoring.json key(score_*) 1:1 대응.
exact_count 리더보드 동점 보정 1순위(정확 스코어 횟수) 사용.
"""
__tablename__ = "user_points"
email: Mapped[str] = mapped_column(String, primary_key=True) # 소문자 정규화
total_points: Mapped[int] = mapped_column(Integer, default=0)
exact_count: Mapped[int] = mapped_column(Integer, default=0)
close_count: Mapped[int] = mapped_column(Integer, default=0)
outcome_count: Mapped[int] = mapped_column(Integer, default=0)
partial_count: Mapped[int] = mapped_column(Integer, default=0)
miss_count: Mapped[int] = mapped_column(Integer, default=0)
matches_played: Mapped[int] = mapped_column(Integer, default=0)
first_scored_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)

View File

@ -1,19 +1,18 @@
"""리더보드 API — 누적 포인트 1위 (docs/SCORING.md §3 랭킹 규칙).
user_points 테이블(채점 누적 갱신) 기준 랭킹.
1 정렬: 누적 포인트. 동점: 정확스코어 횟수 적중률 참여수 최초도달.
주최측/임직원 배제(P1). 이메일은 마스킹하여 노출.
"""
from __future__ import annotations
from collections import defaultdict
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..database import get_db
from ..models import UserPrediction
from ..scoring import SCORE, is_excluded
from ..models import Match, UserPoints
from ..scoring import is_excluded
from ..schemas import LeaderboardOut, StandingOut
router = APIRouter(prefix="/api/leaderboard", tags=["leaderboard"])
@ -30,51 +29,36 @@ async def leaderboard(
limit: int = Query(50, ge=1, le=200), db: AsyncSession = Depends(get_db)
) -> LeaderboardOut:
rows = (
await db.execute(
select(UserPrediction).where(
UserPrediction.points.is_not(None),
UserPrediction.email.is_not(None),
)
)
await db.execute(select(UserPoints))
).scalars().all()
agg: dict[str, dict] = defaultdict(
lambda: {"points": 0, "exact": 0, "played": 0, "first": None}
)
matches: set[str] = set()
for r in rows:
email = (r.email or "").strip().lower()
if not email or is_excluded(email):
continue
matches.add(r.match_id)
a = agg[email]
a["points"] += r.points or 0
a["played"] += 1
if (r.points or 0) >= SCORE["EXACT"]:
a["exact"] += 1
ts = r.scored_at or r.created_at
if a["first"] is None or (ts and ts < a["first"]):
a["first"] = ts
ranked = sorted(
agg.items(),
key=lambda kv: (
-kv[1]["points"],
-kv[1]["exact"],
-(kv[1]["points"] / kv[1]["played"] if kv[1]["played"] else 0),
-kv[1]["played"],
kv[1]["first"].timestamp() if kv[1]["first"] else 0,
(r for r in rows if not is_excluded(r.email)),
key=lambda r: (
-r.total_points,
-r.exact_count,
-(r.total_points / r.matches_played if r.matches_played else 0),
-r.matches_played,
r.first_scored_at.timestamp() if r.first_scored_at else 0,
),
)
scored_matches = (
await db.execute(
select(func.count())
.select_from(Match)
.where(Match.result_outcome.is_not(None))
)
).scalar() or 0
standings = [
StandingOut(
rank=i + 1,
emailMasked=_mask(email),
totalPoints=v["points"],
exactCount=v["exact"],
matchesPlayed=v["played"],
emailMasked=_mask(r.email),
totalPoints=r.total_points,
exactCount=r.exact_count,
matchesPlayed=r.matches_played,
)
for i, (email, v) in enumerate(ranked[:limit])
for i, r in enumerate(ranked[:limit])
]
return LeaderboardOut(standings=standings, scoredMatches=len(matches))
return LeaderboardOut(standings=standings, scoredMatches=scored_matches)

View File

@ -1,11 +1,18 @@
"""채점 로직 — docs/SCORING.md (SSOT) 그대로 구현.
배점: 정확 스코어 5 · 근접(승패+득실차) 3 · 승패 2 · 부분( 득점 일치) 1 · 빗나감 0.
단조 증가(정확>근접>승패>부분>빗나감). 배점 수치는 SCORE 상수만 교체하면 .
단조 증가(정확>근접>승패>부분>빗나감).
배점·배제 대상은 backend/data/scoring.json 에서 기동 로드(없으면 아래 기본값).
"""
from __future__ import annotations
# 팀 확정 후 이 상수만 교체 (docs/SCORING.md §2)
import json
import logging
from pathlib import Path
log = logging.getLogger("triplepick.scoring")
# 기본 배점 — backend/data/scoring.json 로드 시 덮어씀 (docs/SCORING.md §2)
SCORE = {"EXACT": 5, "CLOSE": 3, "OUTCOME": 2, "PARTIAL": 1, "MISS": 0}
@ -40,3 +47,87 @@ STAFF_DOMAINS = ["o2o.kr", "aio2o.kr"]
def is_excluded(email: str) -> bool:
e = email.strip().lower()
return e in STAFF_EMAILS or any(e.endswith("@" + d) for d in STAFF_DOMAINS)
# ── 외부 설정 로드 (backend/data/scoring.json, key-value) ──────
DATA_FILE = Path(__file__).resolve().parent.parent / "data" / "scoring.json"
# 등급 → scoring.json 의 key
_JSON_KEY = {
"EXACT": "score_exact",
"CLOSE": "score_close",
"OUTCOME": "score_outcome",
"PARTIAL": "score_partial",
"MISS": "score_miss",
}
def grade_prediction(
pick_a: int, pick_b: int, result_a: int, result_b: int,
path: Path | None = None,
) -> tuple[str, int]:
"""유저 투표(픽) vs 실제 결과 → scoring.json 에서 해당 등급의 (key, value).
score_prediction 같은 판정 기준. : 승패+득실차 일치 ("score_close", 3).
scoring.json 없거나 키가 빠지면 기본 SCORE 값으로 대체.
"""
p = path or DATA_FILE
try:
data = json.loads(p.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
log.warning("scoring data 읽기 실패 (%s): %s — 기본 배점 사용", p, e)
data = {}
if pick_a == result_a and pick_b == result_b:
grade = "EXACT"
elif outcome_of(pick_a, pick_b) == outcome_of(result_a, result_b):
grade = "CLOSE" if (pick_a - pick_b) == (result_a - result_b) else "OUTCOME"
elif pick_a == result_a or pick_b == result_b:
grade = "PARTIAL"
else:
grade = "MISS"
key = _JSON_KEY[grade]
value = data[key] if isinstance(data.get(key), int) else SCORE[grade]
return key, value
_SCORE_KEYS = {
"score_exact": "EXACT",
"score_close": "CLOSE",
"score_outcome": "OUTCOME",
"score_partial": "PARTIAL",
"score_miss": "MISS",
}
def load_scoring_data(path: Path | None = None) -> dict:
"""data/scoring.json 을 읽어 배점(SCORE)·배제 대상을 갱신. 기동 시 1회 호출.
SCORE/STAFF_EMAILS/STAFF_DOMAINS 모듈이 import 객체라 재할당 대신
in-place 갱신. 파일이 없거나 깨져도 기본값으로 동작(부팅 실패 방지).
"""
p = path or DATA_FILE
try:
data = json.loads(p.read_text(encoding="utf-8"))
except FileNotFoundError:
log.warning("scoring data 없음 (%s) — 기본 배점 사용", p)
return {}
except (OSError, json.JSONDecodeError) as e:
log.warning("scoring data 읽기 실패 (%s): %s — 기본 배점 사용", p, e)
return {}
for key, score_key in _SCORE_KEYS.items():
if isinstance(data.get(key), int):
SCORE[score_key] = data[key]
if isinstance(data.get("excluded_emails"), list):
STAFF_EMAILS.clear()
STAFF_EMAILS.update(e.strip().lower() for e in data["excluded_emails"])
if isinstance(data.get("excluded_domains"), list):
STAFF_DOMAINS[:] = [
d.strip().lower().lstrip("@") for d in data["excluded_domains"]
]
log.info(
"scoring data 로드 (%s): 배점 %s · 배제 이메일 %d · 도메인 %s",
p, SCORE, len(STAFF_EMAILS), STAFF_DOMAINS,
)
return data

View File

@ -1,6 +1,7 @@
"""채점 서비스 — 결과 입력 시 해당 경기 유저 픽 전수 채점.
"""채점 서비스 — 결과 입력 시 해당 경기 유저 픽 전수 채점 + 유저별 누적.
관리자 setResult 워커가 공용으로 사용. scoring.score_prediction 적용.
관리자 setResult 워커가 공용으로 사용. scoring.grade_prediction 적용
(scoring.json 배점) services.points user_points 누적 갱신.
"""
from __future__ import annotations
@ -11,13 +12,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..domain import now_utc
from ..models import Match, UserPrediction
from ..scoring import outcome_of, score_prediction
from ..scoring import grade_prediction, outcome_of
from .points import accumulate_user_points
log = logging.getLogger("triplepick.grading")
async def apply_result(db: AsyncSession, match: Match, score_a: int, score_b: int) -> int:
"""결과 기록 + 해당 경기 픽 전수 채점. 채점된 픽 수 반환."""
"""결과 기록 + 해당 경기 픽 전수 채점 + 유저별 포인트 누적. 채점된 픽 수 반환."""
match.result_score_a = score_a
match.result_score_b = score_b
match.result_outcome = outcome_of(score_a, score_b)
@ -31,9 +33,13 @@ async def apply_result(db: AsyncSession, match: Match, score_a: int, score_b: in
).scalars().all()
for p in picks:
p.points = score_prediction(p.score_a, p.score_b, score_a, score_b)
key, value = grade_prediction(p.score_a, p.score_b, score_a, score_b)
p.points = value
p.scored_at = now_utc()
# 채점된 픽의 유저(이메일)별 누적 포인트 갱신 — 같은 트랜잭션
await accumulate_user_points(db, {p.email for p in picks})
await db.commit()
log.info("graded %d picks for %s (%d-%d)", len(picks), match.match_id, score_a, score_b)
return len(picks)

View File

@ -0,0 +1,99 @@
"""유저별 포인트 누적 — grade_prediction 의 (key, value) 를 user_points 에 반영.
채점(apply_result)에서 호출. 결과 정정(재채점)에도 안전하도록 영향받은
이메일의 채점 가능한 전체를 재집계해 upsert 한다(idempotent 번을
다시 돌려도 같은 결과, 증분 방식의 이중 누적 위험 없음).
"""
from __future__ import annotations
import logging
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..domain import now_utc
from ..models import Match, UserPoints, UserPrediction
from ..scoring import grade_prediction
log = logging.getLogger("triplepick.points")
# scoring.json 의 key → user_points 등급별 횟수 컬럼
_KEY_TO_COL = {
"score_exact": "exact_count",
"score_close": "close_count",
"score_outcome": "outcome_count",
"score_partial": "partial_count",
"score_miss": "miss_count",
}
def _empty() -> dict:
return {
"total_points": 0,
"exact_count": 0,
"close_count": 0,
"outcome_count": 0,
"partial_count": 0,
"miss_count": 0,
"matches_played": 0,
"first_scored_at": None,
}
async def accumulate_user_points(
db: AsyncSession, emails: set[str | None]
) -> int:
"""이메일별 누적 포인트 재집계 → user_points upsert. 갱신 행 수 반환.
커밋은 호출자(apply_result) 책임 채점과 누적이 트랜잭션으로 묶인다.
"""
targets = {e.strip().lower() for e in emails if e and e.strip()}
if not targets:
return 0
rows = (
await db.execute(
select(UserPrediction, Match)
.join(Match, UserPrediction.match_id == Match.match_id)
.where(
func.lower(UserPrediction.email).in_(targets),
Match.result_score_a.is_not(None),
Match.result_score_b.is_not(None),
)
)
).all()
agg: dict[str, dict] = {e: _empty() for e in targets}
for pick, match in rows:
key, value = grade_prediction(
pick.score_a, pick.score_b, match.result_score_a, match.result_score_b
)
t = agg[pick.email.strip().lower()]
t["total_points"] += value
t[_KEY_TO_COL[key]] += 1
t["matches_played"] += 1
ts = pick.scored_at or pick.created_at
if ts and (t["first_scored_at"] is None or ts < t["first_scored_at"]):
t["first_scored_at"] = ts
existing = {
up.email: up
for up in (
await db.execute(select(UserPoints).where(UserPoints.email.in_(targets)))
).scalars()
}
for email, t in agg.items():
row = existing.get(email)
if row is None:
row = UserPoints(email=email)
db.add(row)
for col, val in t.items():
setattr(row, col, val)
row.updated_at = now_utc()
log.info(
"user_points: %d명 누적 갱신 (%s)",
len(agg),
", ".join(f"{e}={t['total_points']}p" for e, t in agg.items()),
)
return len(agg)

View File

@ -29,6 +29,7 @@ from .config import settings
from .database import SessionLocal, init_db
from .domain import ensure_aware, now_utc
from .models import AIPrediction, Match, UserPrediction
from .scoring import load_scoring_data
from .services.ai import MatchContext, PROVIDERS, ProviderUnavailable
from .services.email import EmailUnavailable, build_result_email, send_email
from .services.schedule_fetch import fetch_schedule
@ -217,6 +218,7 @@ def build_scheduler() -> AsyncIOScheduler:
async def main() -> None:
load_scoring_data() # data/scoring.json → 배점·배제 대상 (채점·결과메일에 사용)
await init_db() # 테이블 보장 (idempotent). 시드는 API 가 담당.
# 기동 시 1회: 일정 동기화 → AI 예측 생성(키 있을 때 GPT·Gemini 즉시 채움)
await sync_schedule_job()

View File

@ -0,0 +1,7 @@
{
"score_exact": 5,
"score_close": 3,
"score_outcome": 2,
"score_partial": 1,
"score_miss": 0
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#fff" d="M0 0h900v600H0z"/><path fill="#063" d="M0 0h450v600H0z"/><path fill="#d21034" d="M579.904 225a150 150 0 1 0 0 150 120 120 0 1 1 0-150m5.772 75L450 255.916l83.853 115.413V228.671L450 344.084z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -0,0 +1 @@
<svg width="800" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" height="500"><path fill="#74acdf" d="M0 0h800v500H0z"/><path fill="#fff" d="M0 166.67h800v166.67H0z"/><g id="c"><path id="a" stroke-width="1.112" stroke="#85340a" fill="#f6b40e" d="m396.84 251.31 28.454 61.992s.49 1.185 1.28.859c.79-.327.299-1.512.299-1.512l-23.715-63.956m-.68 24.12c-.347 9.428 5.452 14.613 4.694 23.032-.757 8.42 3.867 13.18 4.94 16.454 1.073 3.274-1.16 5.232-.198 5.698.963.466 3.07-2.12 2.383-6.775-.687-4.655-4.22-6.037-3.39-16.32.83-10.283-4.206-12.678-2.98-22.058"/><use xlink:href="#a" transform="rotate(22.5 400 250)"/><use xlink:href="#a" transform="rotate(45 400 250)"/><use xlink:href="#a" transform="rotate(67.5 400 250)"/><path id="b" fill="#85340a" d="M404.31 274.41c.453 9.054 5.587 13.063 4.579 21.314 2.213-6.525-3.124-11.583-2.82-21.22m-7.649-23.757 19.487 42.577-16.329-43.887"/><use xlink:href="#b" transform="rotate(22.5 400 250)"/><use xlink:href="#b" transform="rotate(45 400 250)"/><use xlink:href="#b" transform="rotate(67.5 400 250)"/></g><use xlink:href="#c" transform="rotate(90 400 250)"/><use xlink:href="#c" transform="rotate(180 400 250)"/><use xlink:href="#c" transform="rotate(270 400 250)"/><circle r="27.778" stroke="#85340a" cy="250" cx="400" stroke-width="1.5" fill="#f6b40e"/><path id="h" fill="#843511" d="M409.47 244.06c-1.897 0-3.713.822-4.781 2.531 2.136 1.923 6.856 2.132 10.062-.219a7.333 7.333 0 0 0-5.281-2.312zm-.031.438c1.846-.034 3.571.814 3.812 1.656-2.136 2.35-5.55 2.146-7.687.437.935-1.495 2.439-2.067 3.875-2.094z"/><use xlink:href="#d" transform="matrix(-1 0 0 1 800.25 0)"/><use xlink:href="#e" transform="matrix(-1 0 0 1 800.25 0)"/><use xlink:href="#f" transform="translate(18.862)"/><use xlink:href="#g" transform="matrix(-1 0 0 1 800.25 0)"/><path d="M395.75 253.84c-.913.167-1.563.977-1.563 1.906 0 1.062.878 1.906 1.938 1.906a1.89 1.89 0 0 0 1.563-.812c.739.556 1.764.615 2.312.625.084.002.193 0 .25 0 .548-.01 1.573-.069 2.313-.625.36.516.935.812 1.562.812 1.06 0 1.938-.844 1.938-1.906 0-.929-.65-1.74-1.563-1.906.513.18.844.676.844 1.219a1.28 1.28 0 0 1-1.281 1.281c-.68 0-1.242-.54-1.282-1.219-.208.417-1.034 1.655-2.656 1.719-1.622-.064-2.447-1.302-2.656-1.719-.04.679-.6 1.219-1.281 1.219a1.28 1.28 0 0 1-1.281-1.281c0-.542.33-1.038.843-1.219zM397.84 259.53c-2.138 0-2.983 1.937-4.906 3.219 1.068-.427 1.91-1.27 3.406-2.125 1.496-.855 2.772.187 3.625.187h.031c.853 0 2.13-1.041 3.625-.187 1.497.856 2.369 1.698 3.438 2.125-1.924-1.282-2.8-3.219-4.938-3.219-.426 0-1.271.23-2.125.656h-.031c-.853-.426-1.698-.656-2.125-.656z" fill="#85340a"/><path d="M397.12 262.06c-.844.037-1.96.207-3.563.688 3.848-.855 4.697.437 6.407.437h.03c1.71 0 2.56-1.292 6.407-.438-4.274-1.282-5.124-.437-6.406-.437h-.031c-.802 0-1.437-.312-2.844-.25z" fill="#85340a"/><path d="M393.75 262.72c-.248.003-.519.005-.813.031 4.488.428 2.331 3 7.032 3h.03c4.702 0 2.575-2.572 7.063-3-4.7-.426-3.214 2.344-7.062 2.344h-.031c-3.608 0-2.496-2.421-6.22-2.375zM403.85 269.66a3.848 3.848 0 0 0-3.846-3.846 3.848 3.848 0 0 0-3.847 3.846 3.955 3.955 0 0 1 3.847-3.04 3.952 3.952 0 0 1 3.846 3.04z" fill="#85340a"/><path id="e" fill="#85340a" d="M382.73 244.02c4.915-4.273 11.11-4.915 14.53-1.709.837 1.121 1.373 2.32 1.593 3.57.43 2.433-.33 5.062-2.236 7.756.215-.001.643.212.856.427 1.697-3.244 2.297-6.577 1.74-9.746a13.815 13.815 0 0 0-.67-2.436c-4.7-3.845-11.11-4.272-15.81 2.138z"/><path id="d" fill="#85340a" d="M390.42 242.74c2.777 0 3.419.642 4.7 1.71 1.284 1.068 1.924.854 2.137 1.068.213.215 0 .854-.426.64s-1.284-.64-2.564-1.708c-1.283-1.07-2.563-1.069-3.846-1.069-3.846 0-5.983 3.205-6.41 2.991-.426-.214 2.137-3.632 6.41-3.632z"/><use xlink:href="#h" transform="translate(-19.181)"/><circle id="f" cy="246.15" cx="390.54" r="1.923" fill="#85340a"/><path id="g" fill="#85340a" d="M385.29 247.44c3.633 2.778 7.265 2.564 9.402 1.282 2.136-1.282 2.136-1.709 1.71-1.709-.427 0-.853.427-2.564 1.281-1.71.856-4.273.856-8.546-.854z"/></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="640" viewBox="0 0 10080 5040"><defs><clipPath id="a"><path d="M0 0h6v3H0z"/></clipPath><clipPath id="b"><path d="M0 0v1.5h6V3zm6 0H3v3H0z"/></clipPath><path id="c" d="m0-360 69.421 215.845 212.038-80.301L155.99-35.603l194.985 115.71-225.881 19.651 31.105 224.59L0 160l-156.198 164.349 31.105-224.59-225.881-19.651 194.986-115.711-125.471-188.853 212.038 80.301z"/><path id="d" d="M0-210 54.86-75.508l144.862 10.614L88.765 28.842l34.67 141.052L0 93.334l-123.435 76.56 34.67-141.052-110.957-93.736L-54.86-75.508z"/></defs><path fill="#012169" d="M0 0h10080v5040H0z"/><path d="m0 0 6 3m0-3L0 3" stroke="#fff" stroke-width=".6" clip-path="url(#a)" transform="scale(840)"/><path d="m0 0 6 3m0-3L0 3" stroke="#e4002b" stroke-width=".4" clip-path="url(#b)" transform="scale(840)"/><path d="M2520 0v2520M0 1260h5040" stroke="#fff" stroke-width="840"/><path d="M2520 0v2520M0 1260h5040" stroke="#e4002b" stroke-width="504"/><g fill="#fff"><use xlink:href="#c" transform="matrix(2.1 0 0 2.1 2520 3780)"/><use xlink:href="#c" x="7560" y="4200"/><use xlink:href="#c" x="6300" y="2205"/><use xlink:href="#c" x="7560" y="840"/><use xlink:href="#c" x="8680" y="1869"/><use xlink:href="#d" x="8064" y="2730"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#c8102e" d="M0 0h900v600H0z"/><path fill="#fff" d="M0 200h900v200H0z"/></svg>

After

Width:  |  Height:  |  Size: 154 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="780"><path fill="#ef3340" d="M0 0h900v780H0z"/><path fill="#fdda25" d="M0 0h600v780H0z"/><path d="M0 0h300v780H0z"/></svg>

After

Width:  |  Height:  |  Size: 182 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="400" viewBox="0 0 16 8"><path fill="#002395" d="M0 0h16v8H0z"/><path d="M4.24 0h8v8z" fill="#fecb00"/><g id="b"><path d="M2.353.525 2.8-.85 3.247.525l-1.17-.85h1.446z" fill="#fff" id="a"/><use xlink:href="#a" x="1" y="1"/><use xlink:href="#a" x="2" y="2"/></g><use xlink:href="#b" x="3" y="3"/><use xlink:href="#b" x="6" y="6"/></svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="700" viewBox="-2100 -1470 4200 2940"><defs><path id="j" fill-rule="evenodd" d="M-31.5 0h33a30 30 0 0 0 30-30v-10a30 30 0 0 0-30-30h-33zm13-13h19a19 19 0 0 0 19-19v-6a19 19 0 0 0-19-19h-19z"/><path id="k" d="M0 0h63v-13H12v-18h40v-12H12v-14h48v-13H0z" transform="translate(-31.5)"/><path id="m" d="M-26.25 0h52.5v-12h-40.5v-16h33v-12h-33v-11H25v-12h-51.25z"/><path id="l" d="M-31.5 0h12v-48l14 48h11l14-48V0h12v-70H14L0-22l-14-48h-17.5z"/><path id="b" fill-rule="evenodd" d="M0 0a31.5 35 0 0 0 0-70A31.5 35 0 0 0 0 0m0-13a18.5 22 0 0 0 0-44 18.5 22 0 0 0 0 44"/><path id="c" fill-rule="evenodd" d="M-31.5 0h13v-26h28a22 22 0 0 0 0-44h-40zm13-39h27a9 9 0 0 0 0-18h-27z"/><path id="o" d="M-15.75-22C-15.75-15-9-11.5 1-11.5s14.74-3.25 14.75-7.75c0-14.25-46.75-5.25-46.5-30.25C-30.5-71-6-70 3-70s26 4 25.75 21.25H13.5c0-7.5-7-10.25-15-10.25-7.75 0-13.25 1.25-13.25 8.5-.25 11.75 46.25 4 46.25 28.75C31.5-3.5 13.5 0 0 0c-11.5 0-31.55-4.5-31.5-22z"/><use xlink:href="#f" id="p" transform="scale(31.5)"/><use xlink:href="#f" id="q" transform="scale(26.25)"/><use xlink:href="#f" id="u" transform="scale(21)"/><use xlink:href="#f" id="r" transform="scale(15)"/><use xlink:href="#f" id="v" transform="scale(10.5)"/><g id="n"><clipPath id="a"><path d="M-31.5 0v-70h63V0zM0-47v12h31.5v-12z"/></clipPath><use xlink:href="#b" clip-path="url(#a)"/><path d="M5-35h26.5v10H5z"/><path d="M21.5-35h10V0h-10z"/></g><g id="i"><use xlink:href="#c"/><path d="M28 0c0-10 0-32-15-32H-6c22 0 22 22 22 32"/></g><g id="f" fill="#fff"><g id="e"><path id="d" d="M0-1v1h.5" transform="rotate(18 0 -1)"/><use xlink:href="#d" transform="scale(-1 1)"/></g><use xlink:href="#e" transform="rotate(72)"/><use xlink:href="#e" transform="rotate(-72)"/><use xlink:href="#e" transform="rotate(144)"/><use xlink:href="#e" transform="rotate(216)"/></g></defs><clipPath id="h"><circle r="735"/></clipPath><path fill="#009440" d="M-2100-1470h4200v2940h-4200z"/><path fill="#ffcb00" d="M-1743 0 0 1113 1743 0 0-1113Z"/><circle r="735" fill="#302681"/><path fill="#fff" d="M-2205 1470a1785 1785 0 0 1 3570 0h-105a1680 1680 0 1 0-3360 0z" clip-path="url(#h)"/><g fill="#009440" transform="translate(-420 1470)"><use xlink:href="#b" y="-1697.5" transform="rotate(-7)"/><use xlink:href="#i" y="-1697.5" transform="rotate(-4)"/><use xlink:href="#j" y="-1697.5" transform="rotate(-1)"/><use xlink:href="#k" y="-1697.5" transform="rotate(2)"/><use xlink:href="#l" y="-1697.5" transform="rotate(5)"/><use xlink:href="#m" y="-1697.5" transform="rotate(9.75)"/><use xlink:href="#c" y="-1697.5" transform="rotate(14.5)"/><use xlink:href="#i" y="-1697.5" transform="rotate(17.5)"/><use xlink:href="#b" y="-1697.5" transform="rotate(20.5)"/><use xlink:href="#n" y="-1697.5" transform="rotate(23.5)"/><use xlink:href="#i" y="-1697.5" transform="rotate(26.5)"/><use xlink:href="#k" y="-1697.5" transform="rotate(29.5)"/><use xlink:href="#o" y="-1697.5" transform="rotate(32.5)"/><use xlink:href="#o" y="-1697.5" transform="rotate(35.5)"/><use xlink:href="#b" y="-1697.5" transform="rotate(38.5)"/></g><use xlink:href="#p" x="-600" y="-132"/><use xlink:href="#p" x="-535" y="177"/><use xlink:href="#q" x="-625" y="243"/><use xlink:href="#r" x="-463" y="132"/><use xlink:href="#q" x="-382" y="250"/><use xlink:href="#u" x="-404" y="323"/><use xlink:href="#p" x="228" y="-228"/><use xlink:href="#p" x="515" y="258"/><use xlink:href="#u" x="617" y="265"/><use xlink:href="#q" x="545" y="323"/><use xlink:href="#q" x="368" y="477"/><use xlink:href="#u" x="367" y="551"/><use xlink:href="#u" x="441" y="419"/><use xlink:href="#q" x="500" y="382"/><use xlink:href="#u" x="365" y="405"/><use xlink:href="#q" x="-280" y="30"/><use xlink:href="#u" x="200" y="-37"/><use xlink:href="#p" y="330"/><use xlink:href="#q" x="85" y="184"/><use xlink:href="#q" y="118"/><use xlink:href="#u" x="-74" y="184"/><use xlink:href="#r" x="-37" y="235"/><use xlink:href="#q" x="220" y="495"/><use xlink:href="#u" x="283" y="430"/><use xlink:href="#u" x="162" y="412"/><use xlink:href="#p" x="-295" y="390"/><use xlink:href="#v" y="575"/></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="600" viewBox="0 0 9600 4800"><path fill="red" d="M0 0h2400l99 99h4602l99-99h2400v4800H7200l-99-99H2499l-99 99H0z"/><path fill="#fff" d="M2400 0h4800v4800H2400zm2490 4430-45-863a95 95 0 0 1 111-98l859 151-116-320a65 65 0 0 1 20-73l941-762-212-99a65 65 0 0 1-34-79l186-572-542 115a65 65 0 0 1-73-38l-105-247-423 454a65 65 0 0 1-111-57l204-1052-327 189a65 65 0 0 1-91-27l-332-652-332 652a65 65 0 0 1-91 27l-327-189 204 1052a65 65 0 0 1-111 57l-423-454-105 247a65 65 0 0 1-73 38l-542-115 186 572a65 65 0 0 1-34 79l-212 99 941 762a65 65 0 0 1 20 73l-116 320 859-151a95 95 0 0 1 111 98l-45 863z"/></svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#009e60" d="M0 0h900v600H0z"/><path fill="#fff" d="M0 0h600v600H0z"/><path fill="#f77f00" d="M0 0h300v600H0z"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><path style="fill:#007fff" d="M0 0h800v600H0z"/><path d="M36 120h84l26-84 26 84h84l-68 52 26 84-68-52-68 52 26-84-68-52zM750 0 0 450v150h50l750-450V0h-50" style="fill:#f7d618"/><path d="M800 0 0 480v120l800-480V0" style="fill:#ce1021"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#ffcd00" d="M0 0h900v600H0z"/><path fill="#003087" d="M0 300h900v300H0z"/><path fill="#c8102e" d="M0 450h900v150H0z"/></svg>

After

Width:  |  Height:  |  Size: 201 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1020" height="600"><path fill="#003893" d="M0 0h1020v600H0z"/><path fill="#fff" d="M0 300h1020v150H0z"/><path fill="#cf2027" d="M0 350h1020v50H0z"/><path fill="#f7d116" d="m382.5 198.715 5.933 18.119 19.066.043-15.4 11.242 5.852 18.148-15.452-11.173-15.45 11.173 5.851-18.148-15.4-11.242 19.066-.043zm-88.168 28.646 5.933 18.121 19.066.043-15.4 11.242 5.852 18.147-15.452-11.172-15.45 11.172 5.851-18.147-15.4-11.242 19.066-.043zm176.336 0 5.933 18.121 19.066.043-15.4 11.242 5.852 18.147-15.452-11.172-15.45 11.172 5.851-18.147-15.4-11.242 19.066-.043zm-230.826 73.863 5.933 18.12 19.067.043-15.4 11.242 5.85 18.148-15.45-11.174-15.452 11.174 5.852-18.148-15.4-11.242 19.066-.043zm285.316 0 5.934 18.12 19.066.043-15.4 11.242 5.851 18.148-15.451-11.174-15.451 11.174 5.851-18.148-15.4-11.242 19.066-.043zm-285.316 100 5.933 18.119 19.067.044-15.4 11.242 5.85 18.148-15.45-11.173-15.452 11.173 5.852-18.148-15.4-11.242 19.066-.043zm285.316 0 5.934 18.119 19.066.044-15.4 11.242 5.851 18.148-15.451-11.173-15.451 11.173 5.851-18.148-15.4-11.242 19.066-.043zm-230.826 68.842 5.933 18.121 19.066.044-15.4 11.242 5.852 18.146-15.452-11.172-15.45 11.172 5.851-18.146-15.4-11.242 19.066-.044zm176.336 0 5.933 18.121 19.066.044-15.4 11.242 5.852 18.146-15.452-11.172-15.45 11.172 5.851-18.146-15.4-11.242 19.066-.044zM382.5 498.715l5.933 18.119 19.066.043-15.4 11.242 5.852 18.149-15.452-11.174-15.45 11.174 5.851-18.149-15.4-11.242 19.066-.043z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="0 0 54 36"><path fill="#002b7f" d="M0 0h54v36H0"/><path fill="#f9e814" d="M0 22.5h54V27H0"/><path fill="#fff" d="m4.2 8.4.1-.1L6 3l1.8 5.4-4.6-3.3h5.7m.7 10.1L12 8l2.4 7.2-6.2-4.4h7.6"/></svg>

After

Width:  |  Height:  |  Size: 266 B

View File

@ -0,0 +1 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#d7141a" d="M0 0h900v600H0z"/><path fill="#fff" d="M0 0h900v300H0z"/><path d="M450 300 0 0v600z" fill="#11457e"/></svg>

After

Width:  |  Height:  |  Size: 210 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 212 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="480"><path fill="#FFF" d="M0 0h800v480H0"/><path stroke="#C8102E" stroke-width="96" d="M0 240h800M400 0v480"/></svg>

After

Width:  |  Height:  |  Size: 176 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#CE1126" d="M0 0h900v600H0"/><path fill="#fff" d="M0 0h600v600H0"/><path fill="#002654" d="M0 0h300v600H0"/></svg>

After

Width:  |  Height:  |  Size: 191 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" viewBox="0 0 5 3"><path d="M0 0h5v3H0z"/><path fill="#D00" d="M0 1h5v2H0z"/><path fill="#FFCE00" d="M0 2h5v1H0z"/></svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#006b3f" d="M0 0h900v600H0"/><path fill="#fcd116" d="M0 0h900v400H0"/><path fill="#ce1126" d="M0 0h900v200H0"/><path d="m450 200 64.98 200-170.13-123.61h210.3L385.02 400"/></svg>

After

Width:  |  Height:  |  Size: 255 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="630" height="360"><path fill="#da0000" d="M0 0h630v360H0z"/><path fill="#fff" d="M0 0h630v240H0z"/><path fill="#239f40" d="M0 0h630v120H0z"/><g transform="translate(8.4 100.4)"><g id="e"><g id="c" fill="none" stroke="#fff" stroke-width="2"><path id="b" d="M0 1h26M1 10V5h8v4h8V5h-5M4 9h2m20 0h-5V5h8m0-5v9h8V0m-4 0v9" transform="scale(1.4)"/><path id="a" d="M0 7h9m1 0h9" transform="scale(2.8)"/><use xlink:href="#a" y="120"/><use xlink:href="#b" y="145.2"/></g><g id="d"><use xlink:href="#c" x="56"/><use xlink:href="#c" x="112"/><use xlink:href="#c" x="168"/></g></g><use xlink:href="#d" x="168"/><use xlink:href="#e" x="392"/></g><g fill="#da0000" transform="matrix(45 0 0 45 315 180)"><g id="f"><path d="M1.016-.016a.78.78 0 0 1-.414.687A1.004 1.004 0 0 0 .444-.741L.4-.775a.776.776 0 0 1 .617.76"/><path d="M.656-.047A.927.927 0 0 1-.557.834l.05.001A1.016 1.016 0 0 0 .333-.749a.92.92 0 0 1 .322.702M.262-.944A.142.142 0 0 1 0-.869l-.017-.017L0-.969A.132.132 0 0 0 .25-1a.14.14 0 0 1 .012.056"/><path d="M.12-.714A.3.3 0 0 1 0-.81l-.05.794L0 1 .079.892l.01-.25L.1.38.101.348V.335L.105.281.111.117l.005-.132.002-.039.002-.048zM0-.55Zm0 .865Z"/></g><use xlink:href="#f" transform="scale(-1 1)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="0 0 11880 7920"><path fill="#fff" d="M0 0h11880v7920H0z"/><path fill="#cd1125" d="M0 0h11880v2640H0z"/><path d="M0 5280h11880v2640H0z"/><path fill="#017b3d" d="M5864 4515H3929a288 248 0 0 1-365 215c271-133 254-268 83-568 95-34 110-43 206-108-68 206 176 181 356 181 0-72 7-154-47-165 70-25 76-33 187-127v277h1335v-190a40 40 0 0 0-80 0v110a30 30 0 0 1-30 30H4554v-180l766-740c-5 38 74 140 107 157-25 4-53-1-71 17l-627 606h695c0-161 150-161 220-218 70 57 220 57 220 218zm145 0V3250c71 39 126 84 214 106-4 50-49 66-49 101v778c98 22 120-35 167-64 12 124 91 246 88 344zm1322-845 155-130v680h110v-773c54-45 124-94 155-151v1219h-975c-14-252-14-511 280-455v-103c0-24-36-5-36-27l201-168v458h110zm-51-348c-19 1-48-103-41-123 7-23 33-23 44-12 18 17 16 134-3 135zm-181 141c-55-32-46-45 2-31 83 25 125 4 185-57l45 23c59 30 95 17 116-55 6-22 24-16 29 9 19 100-57 131-134 103-42-14-49-14-70 2-46 36-112 42-173 6zm797 1052V3250c71 39 126 84 214 106-4 50-49 66-49 101v778c98 22 120-35 167-64 12 124 91 246 88 344zm-3791 140a1 1 0 0 1 118 0 1 1 0 0 1-118 0zm2861-460a45 34 0 0 0 90 0 45 34 0 0 0-90 0z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="600" viewBox="0 0 10080 5040"><path fill="#fff" d="M0 0h10080v5040H0z"/><path d="M0 0h10080v1680H0z"/><path fill="#007a3d" d="M0 3360h10080v1680H0z"/><path fill="#ce1126" d="M5040 2520 0 5040V0m1557 2160-78 198-203-62 106 184-176 120 211 32-16 212 156-144 157 144-16-212 210-32-175-120 106-184-203 62z"/></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#fff" d="M0 0h900v600H0z"/><circle fill="#bc002d" cx="450" cy="300" r="180"/></svg>

After

Width:  |  Height:  |  Size: 160 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="-72 -48 144 96"><path fill="#fff" d="M-72-48v96H72v-96z"/><g stroke="#000" stroke-width="4"><path d="M-34.946-37.72-48.26-17.75m4.992 3.328 13.313-19.97m4.992 3.329-13.312 19.969m63.236 42.157 6.101-9.152m1.11-1.664 6.101-9.153m4.993 3.328-6.102 9.153m-1.11 1.664-6.101 9.152m4.992 3.329 6.102-9.153m1.11-1.664 6.1-9.153M-48.259 17.75l13.313 19.97m4.992-3.329-6.102-9.152m-1.109-1.664-6.102-9.153m4.993-3.328 13.312 19.97m63.236-42.158-6.101-9.153m-1.11-1.664-6.101-9.152m4.992-3.328 13.313 19.969m4.992-3.328-6.102-9.153m-1.11-1.664-6.1-9.153"/></g><path fill="#cd2e3a" d="M9.985 6.656A18 18 0 1 1-19.97-13.313a24 24 0 1 1 39.938 26.626"/><path fill="#0047a0" d="M0 0a12 12 0 1 1 19.97 13.313 24 24 0 1 1-39.94-26.626A12 12 0 1 0 0 0"/></svg>

After

Width:  |  Height:  |  Size: 817 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="0 0 90000 60000"><path fill="#c1272d" d="M0 0h90000v60000H0z"/><path fill="none" stroke="#006233" stroke-width="1426" d="m45000 17308 7460 22960-19531-14190h24142L37540 40268z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 140 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="0 0 9 6"><path fill="#21468B" d="M0 0h9v6H0z"/><path fill="#FFF" d="M0 0h9v4H0z"/><path fill="#AE1C28" d="M0 0h9v2H0z"/></svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 16"><path fill="#ba0c2f" d="M0 0h22v16H0z"/><path d="M0 8h22M8 0v16" stroke="#fff" stroke-width="4"/><path d="M0 8h22M8 0v16" stroke="#00205b" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="600" width="1200"><defs><clipPath id="b"><path d="M0 0h600v300H0z"/></clipPath><clipPath id="c"><path d="m0 0 300 150H0zm300 0h300L300 150zm0 150h300v150zm0 0v150H0z"/></clipPath><g id="d"><g id="a"><path d="M0 0v.5L1 0z" transform="translate(0 -.325)"/><path d="M0 0v-.5L1 0z" transform="rotate(-36 .5 -.162)"/></g><use xlink:href="#a" transform="scale(-1 1)"/><use xlink:href="#a" transform="rotate(72 0 0)"/><use xlink:href="#a" transform="rotate(-72 0 0)"/><use xlink:href="#a" transform="scale(-1 1) rotate(72)"/></g></defs><path fill="#012169" d="M0 0h1200v600H0z"/><path stroke="#FFF" d="m0 0 600 300M0 300 600 0" stroke-width="60" clip-path="url(#b)"/><path stroke="#C8102E" d="m0 0 600 300M0 300 600 0" stroke-width="40" clip-path="url(#c)"/><path stroke="#FFF" d="M300 0v300M0 150h600" stroke-width="100" clip-path="url(#b)"/><path stroke="#C8102E" d="M300 0v300M0 150h600" stroke-width="60" clip-path="url(#b)"/><use xlink:href="#d" fill="#FFF" transform="matrix(45.4 0 0 45.4 900 120)"/><use xlink:href="#d" fill="#C8102E" transform="matrix(30 0 0 30 900 120)"/><g transform="rotate(82 900 240)"><use xlink:href="#d" fill="#FFF" transform="rotate(-82 519.022 -457.666) scale(40.4)"/><use xlink:href="#d" fill="#C8102E" transform="rotate(-82 519.022 -457.666) scale(25)"/></g><g transform="rotate(82 900 240)"><use xlink:href="#d" fill="#FFF" transform="rotate(-82 668.57 -327.666) scale(45.4)"/><use xlink:href="#d" fill="#C8102E" transform="rotate(-82 668.57 -327.666) scale(30)"/></g><use xlink:href="#d" fill="#FFF" transform="matrix(50.4 0 0 50.4 900 480)"/><use xlink:href="#d" fill="#C8102E" transform="matrix(35 0 0 35 900 480)"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="0 0 12 8"><path fill="#fff" d="M0 4V0h6l6 4v4H6z"/><path fill="#da121a" d="M6 0h6v4H6zm3 5 .65 2-1.702-1.236h2.103L8.352 7z"/><path fill="#072357" d="M0 4h6v4H0zm3-3 .65 2-1.702-1.236h2.104L2.35 3z"/></svg>

After

Width:  |  Height:  |  Size: 280 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="550" preserveAspectRatio="none" viewBox="0 0 75 18"><path fill="#8a1538" d="M0 0h75v18H0"/><path fill="#fff" d="M22 18H0V0h22l6 1-6 1 6 1-6 1 6 1-6 1 6 1-6 1 6 1-6 1 6 1-6 1 6 1-6 1 6 1-6 1 6 1z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600" viewBox="0 0 90 60"><defs><clipPath id="b"><path d="m0 0 45 30L0 60z"/></clipPath><clipPath id="a"><path d="M0 0h90v60H0z"/></clipPath></defs><path fill="#E03C31" d="M0 0h90v30H45z"/><path fill="#001489" d="M0 60h90V30H45z"/><g fill="none" clip-path="url(#a)"><path stroke="#FFF" stroke-width="20" d="M90 30H45L0 0v60l45-30"/><path fill="#000" stroke="#ffb81c" stroke-width="20" d="m0 0 45 30L0 60" clip-path="url(#b)"/><path stroke="#007749" stroke-width="12" d="m0 0 45 30h45M0 60l45-30"/></g></svg>

After

Width:  |  Height:  |  Size: 566 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600"><rect width="100%" height="100%" fill="#005EB8"/><path stroke="#fff" stroke-width="120" d="m0 0 1000 600M0 600 1000 0"/></svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><path fill="#e31b23" d="M0 0h900v600H0"/><path fill="#fdef42" d="M0 0h600v600H0"/><path fill="#00853f" d="M0 0h300v600H0m391-218 59-181 59 181-154-112h190"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1 @@
<svg width="512" height="512" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h32v32H0z" fill="red"/><path d="M13 6h6v7h7v6h-7v7h-6v-7H6v-6h7z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 183 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 8 5"><path fill="#005293" d="M0 0h8v5H0Z"/><path stroke="#fecb00" d="M0 2.5h8M3 0v5"/></svg>

After

Width:  |  Height:  |  Size: 172 B

View File

@ -0,0 +1 @@
<svg width="1200" height="800" viewBox="-60 -40 120 80" xmlns="http://www.w3.org/2000/svg" fill="#e70013"><path d="M-60-40H60v80H-60z"/><circle fill="#fff" r="20"/><circle r="15"/><circle fill="#fff" cx="4" r="12"/><path d="m-5 0 16.281-5.29L1.22 8.56V-8.56L11.28 5.29z"/></svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 -30000 90000 60000"><path fill="#e30a17" d="M0-30000h90000v60000H0z"/><path fill="#fff" d="m41750 0 13568-4408-8386 11541V-7133l8386 11541zm925 8021a15000 15000 0 1 1 0-16042 12000 12000 0 1 0 0 16042z"/></svg>

After

Width:  |  Height:  |  Size: 287 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1235" height="650" viewBox="0 0 7410 3900"><path fill="#b31942" d="M0 0h7410v3900H0"/><path stroke="#FFF" stroke-width="300" d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0"/><path fill="#0a3161" d="M0 0h2964v2100H0"/><g fill="#FFF"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="m247 90 70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="500"><path fill="#308738" d="M0 0h1000v500H0z"/><path fill="#3081f7" d="M0 0h1000v250H0z"/><path fill="#ee162e" d="M0 160h1000v180H0z"/><path fill="#fff" d="M0 170h1000v160H0z"/><circle cx="140" cy="80" r="60" fill="#fff"/><circle cx="160" cy="80" r="60" fill="#3081f7"/><g fill="#fff" transform="matrix(2 0 0 2 272 128)"><g id="e"><g id="d"><g id="c"><g id="b"><path id="a" d="M0-6v6h3" transform="rotate(18 0 -6)"/><use xlink:href="#a" width="100%" height="100%" transform="scale(-1 1)"/></g><use xlink:href="#b" width="100%" height="100%" transform="rotate(72)"/></g><use xlink:href="#b" width="100%" height="100%" transform="rotate(-72)"/><use xlink:href="#c" width="100%" height="100%" transform="rotate(144)"/></g><use xlink:href="#d" width="100%" height="100%" y="-24"/><use xlink:href="#d" width="100%" height="100%" y="-48"/></g><use xlink:href="#e" width="100%" height="100%" x="24"/><use xlink:href="#e" width="100%" height="100%" x="48"/><use xlink:href="#d" width="100%" height="100%" x="-48"/><use xlink:href="#d" width="100%" height="100%" x="-24"/><use xlink:href="#d" width="100%" height="100%" x="-24" y="-24"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,7 +1,9 @@
import { useState } from "react";
import type { Team } from "@/lib/types";
// 모든 경기에 일반화된 국기 렌더.
// KOR = 실제 png 자산, CZE = 인라인 SVG(비율 보존), 그 외 = 이모지(컨테이너 높이에 비례, 안 잘림).
// 월드컵 출전 48개국 SVG 자산(/assets/flags/{fifa코드}.svg, 예: kor.svg)을 사용하고,
// 자산이 없는 코드는 이모지 폴백(컨테이너 높이에 비례, 안 잘림).
export default function TeamFlag({
team,
className = "",
@ -9,30 +11,20 @@ export default function TeamFlag({
team: Team;
className?: string;
}) {
if (team.code === "KOR") {
const [failed, setFailed] = useState(false);
if (!failed && team.code) {
return (
<img
src="/icons/kor.png"
src={`/assets/flags/${team.code.toLowerCase()}.svg`}
alt={team.name}
className={`flag-img object-cover ${className}`}
onError={() => setFailed(true)}
className={`flag-img object-cover border border-[var(--line-d)] ${className}`}
/>
);
}
if (team.code === "CZE") {
return (
<svg
viewBox="0 0 6 4"
className={`border border-[var(--line-d)] ${className}`}
preserveAspectRatio="xMidYMid meet"
aria-label={team.name}
>
<rect width="6" height="2" y="0" fill="#ffffff" />
<rect width="6" height="2" y="2" fill="#d7141a" />
<polygon points="0,0 3,2 0,4" fill="#11457e" />
</svg>
);
}
// 그 외 국가 — 이모지 국기. 컨테이너 높이(cqh) 기준으로 크기를 잡아 잘림/찌그러짐 방지.
// 폴백 — 이모지 국기. 컨테이너 높이(cqh) 기준으로 크기를 잡아 잘림/찌그러짐 방지.
return (
<span
className={`grid place-items-center overflow-hidden border border-[var(--line-d)] bg-white/[0.06] ${className}`}