Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions GithubAquarium/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@
# Include URL configurations from the 'repositories' and 'users' apps
path('api/repositories/', include('apps.repositories.urls')),
path('api/users/', include('apps.users.urls')),
path('api/aquatics/', include('apps.aquatics.urls')),

# --- DRF Auth ---
path('api-auth/', include('rest_framework.urls')),
]
11 changes: 5 additions & 6 deletions GithubAquarium/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
# GithubAquarium/views.py
"""
Core views for the GithubAquarium project.
"""

from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView
from django.conf import settings
from rest_framework.permissions import AllowAny

class GitHubLogin(SocialLoginView):
"""
Expand All @@ -18,4 +14,7 @@ class GitHubLogin(SocialLoginView):
"""
adapter_class = GitHubOAuth2Adapter
callback_url = settings.GITHUB_CALLBACK_URL
client_class = OAuth2Client
client_class = OAuth2Client
# [추가] 이 뷰는 로그인 전이므로 누구나 접근 가능해야 함
permission_classes = (AllowAny,)
authentication_classes = []
Empty file.
103 changes: 103 additions & 0 deletions apps/aquatics/renderer/sprite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import random
from .utils import strip_outer_svg

def render_fish_group(cf, width, height,mode):
species = cf.fish_species
raw_svg = species.svg_template
inner = strip_outer_svg(raw_svg)
fish_id = cf.id

# === 레이블 내용 ===
if mode == "aquarium":
label_text = cf.contributor.repository.name
else: # fishtank
label_text = cf.contributor.user.username

# === 랜덤 요소 생성 ===
start_x = random.uniform(width * 0.1, width * 0.9)
start_y = random.uniform(height * 0.2, height * 0.8)
amplitude_x = random.uniform(width * 0.25, width * 0.45) # x 왕복폭
amplitude_y = random.uniform(height * 0.05, height * 0.12) # y 파동
#speed = random.uniform(0.6, 1.6) # 느린 물고기 / 빠른 물고기
#phase = random.uniform(0, 6.28)
duration = random.uniform(8, 18) # 전체 이동 주기

# === 이동 keyframes ===
keyframes = f"""
@keyframes move-{fish_id} {{
0% {{
transform: translate({start_x}px, {start_y}px);
}}
25% {{
transform: translate({start_x + amplitude_x}px, {start_y + amplitude_y}px);
}}
50% {{
transform: translate({start_x}px, {start_y - amplitude_y}px) scale(-1,1);
}}
75% {{
transform: translate({start_x - amplitude_x}px, {start_y + amplitude_y}px) scale(-1,1);
}}
100% {{
transform: translate({start_x}px, {start_y}px);
}}
}}
"""

# === flip reverse keyframes ===
reverse_keyframes = f"""
@keyframes keep-label-upright-{fish_id} {{
0%,25% {{
transform: scale(1,1);
}}
50%,75% {{
transform: scale(-1,1); /* 물고기가 뒤집힐 때 라벨도 같이 뒤집혀서 정방향 유지 */
}}
100% {{
transform: scale(1,1);
}}
}}
"""

return f"""
<g id="fish-{fish_id}">
<style>
{keyframes}
{reverse_keyframes}

#fish-{fish_id} .motion {{
animation: move-{fish_id} {duration}s ease-in-out infinite;
transform-origin: center;
}}

#fish-{fish_id} .label {{
animation: keep-label-upright-{fish_id} {duration:.1f}s ease-in-out infinite;
transform-origin: center;
}}
#fish-{fish_id} .label text {{
font-size: 12px;
fill: white;
paint-order: stroke;
stroke: rgba(0, 0, 0, 0.7);
stroke-width: 2px;
}}
#fish-{fish_id} .label rect {{
fill: rgba(0, 0, 0, 0.5);
rx: 3px;
ry: 3px;
}}
</style>

<g class="motion">
<g class="sprite">
{inner}
</g>

<g class="label" transform="translate(0, 24)">
<rect x="-40" y="-14" width="80" height="18" />
<text text-anchor="middle" dominant-baseline="central">
{label_text}
</text>
</g>
</g>
</g>
"""
69 changes: 69 additions & 0 deletions apps/aquatics/renderer/tank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from apps.items.models import FishSpecies
from apps.aquatics.models import Aquarium,ContributionFish , Fishtank
from .sprite import render_fish_group
from .utils import strip_outer_svg , extract_svg_size

def render_aquarium_svg(user,width=512, height=512):
aquarium = Aquarium.objects.get(user=user)

if aquarium.background:
bg_svg = aquarium.background.background.svg_template
else:
bg_svg = '<svg width="512" height="512"></svg>'

width, height = extract_svg_size(bg_svg)
bg_inner = strip_outer_svg(bg_svg)

fishes = ContributionFish.objects.filter(
aquarium=aquarium,
is_visible=True
).select_related("fish_species", "contributor__repository")

fish_groups = [
render_fish_group(cf, width, height, mode="aquarium")
for cf in fishes
]

return f"""
<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">

<g id="background">
{bg_inner}
</g>
<g id="fish-container">
{''.join(fish_groups)}
</g>
</svg>
"""


def render_fishtank_svg(repository):
fishtank = Fishtank.objects.get(repository=repository)


setting = fishtank.settings.select_related("background__background").first()
if setting and setting.background:
bg_svg = setting.background.background.svg_template
else:
bg_svg = '<svg width="512" height="512"><rect width="100%" height="100%" fill="#001f3f"/></svg>'


width, height = extract_svg_size(bg_svg)
bg_inner = strip_outer_svg(bg_svg)

fishes = ContributionFish.objects.filter(
contributor__repository=repository,
is_visible=True
).select_related("fish_species", "contributor__user")

fish_groups = [
render_fish_group(cf, width, height, mode="fishtank")
for cf in fishes
]

return f"""
<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
<g id="background">{bg_inner}</g>
<g id="fish-container">{''.join(fish_groups)}</g>
</svg>
"""
52 changes: 52 additions & 0 deletions apps/aquatics/renderer/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import re

def extract_svg_size(svg_text: str):
"""
Extract width and height from <svg> tag.
Supports:
<svg width="512" height="512">
<svg viewBox="0 0 800 600">
Returns (width, height) as numbers.
Defaults to (512, 512) if not found.
"""
if not svg_text:
return (512, 512)

# width="512", height="512"
width_match = re.search(r'width="([\d\.]+)"', svg_text)
height_match = re.search(r'height="([\d\.]+)"', svg_text)

if width_match and height_match:
return (float(width_match.group(1)), float(height_match.group(1)))

# viewBox="0 0 800 600"
viewbox_match = re.search(r'viewBox="[^"]*?(\d+\.?\d*)\s+(\d+\.?\d*)"', svg_text)
if viewbox_match:
return (float(viewbox_match.group(1)), float(viewbox_match.group(2)))

# fallback
return (512, 512)


def strip_outer_svg(svg_text: str) -> str:
"""
species/background 템플릿이 풀 SVG로 들어있을 때
가장 바깥 <svg> 래퍼만 제거하고 안쪽 노드만 반환.
"""
if not svg_text:
return ""

# Find first tag after <svg ...>
start = svg_text.find(">") + 1
end = svg_text.rfind("</svg>")
if start <= 0 or end == -1:
return svg_text.strip()
return svg_text[start:end].strip()

def safe_attr(value):
"""
Convert None or empty to safe attribute values.
"""
if value is None:
return ""
return str(value)
72 changes: 72 additions & 0 deletions apps/aquatics/serializers_aquarium.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from rest_framework import serializers
from apps.aquatics.models import Aquarium, ContributionFish, OwnBackground
from apps.repositories.models import Contributor
from apps.repositories.models import Repository


class AquariumFishSerializer(serializers.ModelSerializer):
species = serializers.SerializerMethodField()
repository = serializers.SerializerMethodField()
my_commit_count = serializers.SerializerMethodField()
unlocked_at = serializers.SerializerMethodField()

class Meta:
model = ContributionFish
fields = [
"id",
"species",
"repository",
"my_commit_count",
"unlocked_at",
]

def get_species(self, obj):
s = obj.fish_species
return {
"id": s.id,
"name": s.name,
"maturity": s.maturity,
"required_commits": s.required_commits,
"svg_template": s.svg_template,
}

def get_repository(self, obj):
repo = obj.contributor.repository
return {
"id": repo.id,
"name": repo.name,
}

def get_my_commit_count(self, obj):
return obj.contributor.commit_count

def get_unlocked_at(self, obj):
# ContributionFish는 unlocked_at을 직접 갖지 않으므로 UnlockedFish에서 조회
unlocked = obj.contributor.user.owned_fishes.filter(
fish_species=obj.fish_species
).first()
return unlocked.unlocked_at if unlocked else None


class AquariumDetailSerializer(serializers.ModelSerializer):
background = serializers.SerializerMethodField()
fishes = AquariumFishSerializer(many=True)

class Meta:
model = Aquarium
fields = ["background", "svg_path", "fishes"]

def get_background(self, obj):
if obj.background:
return {
"id": obj.background.id,
"name": obj.background.background.name,
"svg_template": obj.background.background.svg_template,
}
return None


class AquariumBackgroundSerializer(serializers.ModelSerializer):
class Meta:
model = OwnBackground
fields = ["id", "background", "unlocked_at"]
55 changes: 55 additions & 0 deletions apps/aquatics/serializers_fishtank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# apps/aquatics/serializers_fishtank.py
from rest_framework import serializers
from apps.aquatics.models import Fishtank, ContributionFish, FishtankSetting, OwnBackground
from apps.repositories.models import Contributor, Repository
from apps.items.models import Background


class FishSpeciesSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
maturity = serializers.IntegerField()
required_commits = serializers.IntegerField()
svg_template = serializers.CharField()


class ContributionFishSerializer(serializers.ModelSerializer):
species = serializers.SerializerMethodField()

class Meta:
model = ContributionFish
fields = ["id", "is_visible", "species"]

def get_species(self, obj):
s = obj.fish_species
return {
"id": s.id,
"name": s.name,
"maturity": s.maturity,
"required_commits": s.required_commits,
"svg_template": s.svg_template,
}


class ContributorSerializer(serializers.ModelSerializer):
user = serializers.CharField(source="user.username")
fish = ContributionFishSerializer(source="contribution_fish", read_only=True)

class Meta:
model = Contributor
fields = ["id", "user", "commit_count", "fish"]


class FishtankDetailSerializer(serializers.ModelSerializer):
repository = serializers.CharField(source="repository.name")
contributors = ContributorSerializer(source="repository.contributors", many=True)

class Meta:
model = Fishtank
fields = ["id", "repository", "svg_path", "contributors"]


class FishtankBackgroundSerializer(serializers.ModelSerializer):
class Meta:
model = Background
fields = ["id", "name", "code", "svg_template"]
2 changes: 0 additions & 2 deletions apps/aquatics/tests.py

This file was deleted.

Loading