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

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 RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app COPY app ./app
COPY data ./data
EXPOSE 8000 EXPOSE 8000

View File

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

View File

@ -5,6 +5,7 @@
- ai_predictions GPT/Claude/Gemini 예측 (경기 × 모델) - ai_predictions GPT/Claude/Gemini 예측 (경기 × 모델)
- crowd_stats 군중 투표 분포 (경기당 1, 원자적 증분) - crowd_stats 군중 투표 분포 (경기당 1, 원자적 증분)
- user_predictions 유저 (이메일 식별, 채점/알림 플래그 포함) - user_predictions 유저 (이메일 식별, 채점/알림 플래그 포함)
- user_points 유저별 누적 포인트 (채점 갱신, 이메일당 1)
""" """
from __future__ import annotations from __future__ import annotations
@ -152,3 +153,28 @@ class UserPrediction(Base):
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now() 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 랭킹 규칙). """리더보드 API — 누적 포인트 1위 (docs/SCORING.md §3 랭킹 규칙).
user_points 테이블(채점 누적 갱신) 기준 랭킹.
1 정렬: 누적 포인트. 동점: 정확스코어 횟수 적중률 참여수 최초도달. 1 정렬: 누적 포인트. 동점: 정확스코어 횟수 적중률 참여수 최초도달.
주최측/임직원 배제(P1). 이메일은 마스킹하여 노출. 주최측/임직원 배제(P1). 이메일은 마스킹하여 노출.
""" """
from __future__ import annotations from __future__ import annotations
from collections import defaultdict
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy import select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from ..database import get_db from ..database import get_db
from ..models import UserPrediction from ..models import Match, UserPoints
from ..scoring import SCORE, is_excluded from ..scoring import is_excluded
from ..schemas import LeaderboardOut, StandingOut from ..schemas import LeaderboardOut, StandingOut
router = APIRouter(prefix="/api/leaderboard", tags=["leaderboard"]) 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) limit: int = Query(50, ge=1, le=200), db: AsyncSession = Depends(get_db)
) -> LeaderboardOut: ) -> LeaderboardOut:
rows = ( rows = (
await db.execute( await db.execute(select(UserPoints))
select(UserPrediction).where(
UserPrediction.points.is_not(None),
UserPrediction.email.is_not(None),
)
)
).scalars().all() ).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( ranked = sorted(
agg.items(), (r for r in rows if not is_excluded(r.email)),
key=lambda kv: ( key=lambda r: (
-kv[1]["points"], -r.total_points,
-kv[1]["exact"], -r.exact_count,
-(kv[1]["points"] / kv[1]["played"] if kv[1]["played"] else 0), -(r.total_points / r.matches_played if r.matches_played else 0),
-kv[1]["played"], -r.matches_played,
kv[1]["first"].timestamp() if kv[1]["first"] else 0, 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 = [ standings = [
StandingOut( StandingOut(
rank=i + 1, rank=i + 1,
emailMasked=_mask(email), emailMasked=_mask(r.email),
totalPoints=v["points"], totalPoints=r.total_points,
exactCount=v["exact"], exactCount=r.exact_count,
matchesPlayed=v["played"], 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) 그대로 구현. """채점 로직 — docs/SCORING.md (SSOT) 그대로 구현.
배점: 정확 스코어 5 · 근접(승패+득실차) 3 · 승패 2 · 부분( 득점 일치) 1 · 빗나감 0. 배점: 정확 스코어 5 · 근접(승패+득실차) 3 · 승패 2 · 부분( 득점 일치) 1 · 빗나감 0.
단조 증가(정확>근접>승패>부분>빗나감). 배점 수치는 SCORE 상수만 교체하면 . 단조 증가(정확>근접>승패>부분>빗나감).
배점·배제 대상은 backend/data/scoring.json 에서 기동 로드(없으면 아래 기본값).
""" """
from __future__ import annotations 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} 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: def is_excluded(email: str) -> bool:
e = email.strip().lower() e = email.strip().lower()
return e in STAFF_EMAILS or any(e.endswith("@" + d) for d in STAFF_DOMAINS) 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 from __future__ import annotations
@ -11,13 +12,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from ..domain import now_utc from ..domain import now_utc
from ..models import Match, UserPrediction 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") log = logging.getLogger("triplepick.grading")
async def apply_result(db: AsyncSession, match: Match, score_a: int, score_b: int) -> int: async def apply_result(db: AsyncSession, match: Match, score_a: int, score_b: int) -> int:
"""결과 기록 + 해당 경기 픽 전수 채점. 채점된 픽 수 반환.""" """결과 기록 + 해당 경기 픽 전수 채점 + 유저별 포인트 누적. 채점된 픽 수 반환."""
match.result_score_a = score_a match.result_score_a = score_a
match.result_score_b = score_b match.result_score_b = score_b
match.result_outcome = outcome_of(score_a, 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() ).scalars().all()
for p in picks: 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() p.scored_at = now_utc()
# 채점된 픽의 유저(이메일)별 누적 포인트 갱신 — 같은 트랜잭션
await accumulate_user_points(db, {p.email for p in picks})
await db.commit() await db.commit()
log.info("graded %d picks for %s (%d-%d)", len(picks), match.match_id, score_a, score_b) log.info("graded %d picks for %s (%d-%d)", len(picks), match.match_id, score_a, score_b)
return len(picks) 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 .database import SessionLocal, init_db
from .domain import ensure_aware, now_utc from .domain import ensure_aware, now_utc
from .models import AIPrediction, Match, UserPrediction from .models import AIPrediction, Match, UserPrediction
from .scoring import load_scoring_data
from .services.ai import MatchContext, PROVIDERS, ProviderUnavailable from .services.ai import MatchContext, PROVIDERS, ProviderUnavailable
from .services.email import EmailUnavailable, build_result_email, send_email from .services.email import EmailUnavailable, build_result_email, send_email
from .services.schedule_fetch import fetch_schedule from .services.schedule_fetch import fetch_schedule
@ -217,6 +218,7 @@ def build_scheduler() -> AsyncIOScheduler:
async def main() -> None: async def main() -> None:
load_scoring_data() # data/scoring.json → 배점·배제 대상 (채점·결과메일에 사용)
await init_db() # 테이블 보장 (idempotent). 시드는 API 가 담당. await init_db() # 테이블 보장 (idempotent). 시드는 API 가 담당.
# 기동 시 1회: 일정 동기화 → AI 예측 생성(키 있을 때 GPT·Gemini 즉시 채움) # 기동 시 1회: 일정 동기화 → AI 예측 생성(키 있을 때 GPT·Gemini 즉시 채움)
await sync_schedule_job() 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"; import type { Team } from "@/lib/types";
// 모든 경기에 일반화된 국기 렌더. // 모든 경기에 일반화된 국기 렌더.
// KOR = 실제 png 자산, CZE = 인라인 SVG(비율 보존), 그 외 = 이모지(컨테이너 높이에 비례, 안 잘림). // 월드컵 출전 48개국 SVG 자산(/assets/flags/{fifa코드}.svg, 예: kor.svg)을 사용하고,
// 자산이 없는 코드는 이모지 폴백(컨테이너 높이에 비례, 안 잘림).
export default function TeamFlag({ export default function TeamFlag({
team, team,
className = "", className = "",
@ -9,30 +11,20 @@ export default function TeamFlag({
team: Team; team: Team;
className?: string; className?: string;
}) { }) {
if (team.code === "KOR") { const [failed, setFailed] = useState(false);
if (!failed && team.code) {
return ( return (
<img <img
src="/icons/kor.png" src={`/assets/flags/${team.code.toLowerCase()}.svg`}
alt={team.name} 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 ( // 폴백 — 이모지 국기. 컨테이너 높이(cqh) 기준으로 크기를 잡아 잘림/찌그러짐 방지.
<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) 기준으로 크기를 잡아 잘림/찌그러짐 방지.
return ( return (
<span <span
className={`grid place-items-center overflow-hidden border border-[var(--line-d)] bg-white/[0.06] ${className}`} className={`grid place-items-center overflow-hidden border border-[var(--line-d)] bg-white/[0.06] ${className}`}