diff --git a/.gitignore b/.gitignore
index e36ce56..33b47b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,10 @@
*.pyc
__pycache__/
local_settings.py
-*.sqlite3
-db.sqlite3-journal
-media
staticfiles_collected/
+*.sqlite3
+*.sqlite3-journal
+media_files/
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
@@ -177,10 +177,9 @@ cython_debug/
# My
-# VS Code
.vscode/
*.code-workspace
/logs/
-all.txt
+*.txt
# End of My
\ No newline at end of file
diff --git a/GithubAquarium/asgi.py b/GithubAquarium/asgi.py
deleted file mode 100644
index 1258dfd..0000000
--- a/GithubAquarium/asgi.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-ASGI config for GithubAquarium project.
-
-It exposes the ASGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
-"""
-
-import os
-
-from django.core.asgi import get_asgi_application
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'GithubAquarium.settings')
-
-application = get_asgi_application()
diff --git a/GithubAquarium/settings.py b/GithubAquarium/settings.py
index b6622e6..e43d37a 100644
--- a/GithubAquarium/settings.py
+++ b/GithubAquarium/settings.py
@@ -31,6 +31,15 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG')
+
+# --- Custom Service Settings ---
+# SVG 렌더링 시 이미지 절대 경로 생성을 위한 도메인 설정
+if DEBUG:
+ SITE_DOMAIN = 'http://localhost:8000'
+else:
+ # 실제 운영 도메인 (Nginx가 처리할 도메인)
+ SITE_DOMAIN = 'https://githubaquarium.store'
+
# Allowed hosts for the application
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'githubaquarium.store', 'www.githubaquarium.store']
@@ -136,6 +145,10 @@
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
+ 'OPTIONS': {
+ # DB가 잠겨있으면 20초까지 대기하다가 풀리면 씁니다.
+ 'timeout': 20,
+ }
}
}
@@ -351,4 +364,9 @@
'timeout': 6000, # sec
'retry': 36000,
'orm': 'default', # temporary use ORM as broker
-}
\ No newline at end of file
+}
+
+# --- Game Logic Settings ---
+DEFAULT_FISH_GROUP = "ShrimpWich"
+
+RENDER_DOMAIN = "http://localhost:8000" if DEBUG else "https://githubaquarium.store"
\ No newline at end of file
diff --git a/GithubAquarium/urls.py b/GithubAquarium/urls.py
index 41a4417..e219f3b 100644
--- a/GithubAquarium/urls.py
+++ b/GithubAquarium/urls.py
@@ -61,7 +61,7 @@
# --- Local App APIs ---
# 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/users/', include('apps.users.urls')),
path('api/aquatics/', include('apps.aquatics.urls')),
path('api/shop/', include('apps.shop.urls')),
diff --git a/GithubAquarium/views.py b/GithubAquarium/views.py
index 0293bd3..b16288e 100644
--- a/GithubAquarium/views.py
+++ b/GithubAquarium/views.py
@@ -1,20 +1,42 @@
+# GithubAquarium/views.py
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
+from drf_yasg.utils import swagger_auto_schema
+from drf_yasg import openapi
class GitHubLogin(SocialLoginView):
"""
- Custom view for handling GitHub social login.
-
- This view integrates with django-allauth and dj-rest-auth to perform
- social authentication using a GitHub account. It specifies the adapter,
- callback URL, and client class necessary for the OAuth2 flow.
+ GitHub OAuth2를 이용한 소셜 로그인/회원가입을 처리합니다.
"""
adapter_class = GitHubOAuth2Adapter
callback_url = settings.GITHUB_CALLBACK_URL
client_class = OAuth2Client
- # [추가] 이 뷰는 로그인 전이므로 누구나 접근 가능해야 함
permission_classes = (AllowAny,)
authentication_classes = []
+
+ @swagger_auto_schema(
+ operation_summary="GitHub 소셜 로그인",
+ operation_description="GitHub에서 받은 access_token을 전달하여 JWT 토큰을 발급받습니다.",
+ tags=["Authentication"],
+ responses={
+ 200: openapi.Response(
+ description="로그인 성공. access 및 refresh 토큰이 쿠키 또는 바디로 반환됩니다.",
+ schema=openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ 'access': openapi.Schema(type=openapi.TYPE_STRING),
+ 'refresh': openapi.Schema(type=openapi.TYPE_STRING),
+ 'user': openapi.Schema(type=openapi.TYPE_OBJECT, properties={
+ 'pk': openapi.Schema(type=openapi.TYPE_INTEGER),
+ 'username': openapi.Schema(type=openapi.TYPE_STRING),
+ })
+ }
+ )
+ )
+ }
+ )
+ def post(self, request, *args, **kwargs):
+ return super().post(request, *args, **kwargs)
\ No newline at end of file
diff --git a/GithubAquarium/webhook_views.py b/GithubAquarium/webhook_views.py
index c131dfb..52bbd1e 100644
--- a/GithubAquarium/webhook_views.py
+++ b/GithubAquarium/webhook_views.py
@@ -1,70 +1,28 @@
# GithubAquarium/webhook_views.py
import hashlib
import hmac
-import logging # Import the logging module
from django.conf import settings
from django_q.tasks import async_task
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
-
-
from drf_yasg.utils import swagger_auto_schema
-# Get an instance of a logger for this module
-logger = logging.getLogger(__name__)
-
class GitHubWebhookView(APIView):
- """
- Receives and processes webhook events from GitHub.
-
- This view verifies the integrity of the request using the webhook secret,
- then processes 'star', 'push', and 'meta' events to keep the database
- in sync with GitHub.
- """
-
@swagger_auto_schema(
- summary="GitHub Webhook Handler",
- description="""
- Handles incoming webhook events from GitHub to keep the application's database in sync.
-
- ### Supported Events:
- - **star**: Triggered when a repository is starred or unstarred. Updates the `stargazers_count`.
- - **push**: Triggered on a push to a repository. Updates repository details and records new commits.
- - **meta**: Triggered for webhook administrative events (e.g., deletion).
-
- The request body should be the raw JSON payload from the GitHub webhook, and the `X-Hub-Signature-256` header must be present for verification.
- """,
- operation_id="handle_github_webhook",
+ operation_summary="GitHub 웹훅 수신",
+ operation_description="GitHub로부터 push, star 이벤트를 수신하여 데이터를 비동기로 동기화합니다.",
tags=["Webhooks"],
+ responses={200: "수신 완료 및 큐 등록", 403: "서명 불일치"}
)
def post(self, request, *args, **kwargs):
- # 1. Verify Signature (보안 검증은 동기로 즉시 처리해야 함)
signature_header = request.headers.get('X-Hub-Signature-256')
if not signature_header:
- return Response({'detail': 'Signature header missing'}, status=status.HTTP_403_FORBIDDEN)
-
- sha_name, signature = signature_header.split('=', 1)
- if sha_name != 'sha256':
- return Response({'detail': 'Invalid signature format'}, status=status.HTTP_403_FORBIDDEN)
-
+ return Response({'detail': 'Signature missing'}, status=403)
mac = hmac.new(settings.GITHUB_WEBHOOK_SECRET.encode('utf-8'), msg=request.body, digestmod=hashlib.sha256)
- if not hmac.compare_digest(mac.hexdigest(), signature):
- return Response({'detail': 'Invalid signature'}, status=status.HTTP_403_FORBIDDEN)
+ if not hmac.compare_digest(f"sha256={mac.hexdigest()}", signature_header):
+ return Response({'detail': 'Invalid signature'}, status=403)
- # 2. 작업 Queue에 등록
event_type = request.headers.get('X-GitHub-Event')
- payload = request.data
-
- # 비동기 Task 호출
- async_task(
- 'apps.repositories.tasks.process_webhook_event_task', # Task 함수 경로
- event_type,
- payload,
- group='webhooks' # 그룹을 지정하면 나중에 관리하기 편함
- )
-
- logger.info(f"Webhook event '{event_type}' queued for processing.")
-
- # 3. GitHub에 즉시 성공 응답 반환
- return Response({'detail': 'Event queued'}, status=status.HTTP_200_OK)
\ No newline at end of file
+ async_task('apps.repositories.tasks.process_webhook_event_task', event_type, request.data)
+ return Response({'detail': 'Event queued'}, status=200)
\ No newline at end of file
diff --git a/apps/aquatics/admin.py b/apps/aquatics/admin.py
index e9e3e7f..283b2ee 100644
--- a/apps/aquatics/admin.py
+++ b/apps/aquatics/admin.py
@@ -1,37 +1,41 @@
from django.contrib import admin
-from .models import UnlockedFish, OwnBackground, Aquarium, Fishtank, ContributionFish, FishtankSetting
+from .models import UnlockedFish, OwnBackground, Aquarium, Fishtank, ContributionFish
@admin.register(UnlockedFish)
class UnlockedFishAdmin(admin.ModelAdmin):
list_display = ('user', 'fish_species', 'unlocked_at')
- list_filter = ('user', 'fish_species')
+ list_filter = ('unlocked_at', 'fish_species__rarity')
search_fields = ('user__username', 'fish_species__name')
@admin.register(OwnBackground)
class OwnBackgroundAdmin(admin.ModelAdmin):
list_display = ('user', 'background', 'unlocked_at')
- list_filter = ('user', 'background')
search_fields = ('user__username', 'background__name')
@admin.register(Aquarium)
class AquariumAdmin(admin.ModelAdmin):
- list_display = ('user', 'background', 'svg_path')
- search_fields = ('user__username',)
+ list_display = ('user', 'background', 'fish_count')
+
+ def fish_count(self, obj):
+ return obj.fishes.count()
@admin.register(Fishtank)
class FishtankAdmin(admin.ModelAdmin):
- list_display = ('repository', 'svg_path')
- search_fields = ('repository__name',)
+ list_display = ('repository', 'fish_count')
+
+ def fish_count(self, obj):
+ return obj.repository.contributors.count()
@admin.register(ContributionFish)
class ContributionFishAdmin(admin.ModelAdmin):
- # list_display 및 list_filter 수정
- list_display = ('contributor', 'fish_species', 'aquarium', 'is_visible_in_fishtank', 'is_visible_in_aquarium')
- list_filter = ('is_visible_in_fishtank', 'is_visible_in_aquarium', 'fish_species')
- search_fields = ('contributor__user__username', 'fish_species__name')
-
-@admin.register(FishtankSetting)
-class FishtankSettingAdmin(admin.ModelAdmin):
- list_display = ('fishtank', 'contributor', 'background')
- list_filter = ('fishtank', 'contributor')
- search_fields = ('fishtank__repository__name', 'contributor__username')
\ No newline at end of file
+ list_display = ('id', 'get_user', 'get_repo', 'fish_species', 'is_visible_in_fishtank', 'is_visible_in_aquarium')
+ list_filter = ('is_visible_in_fishtank', 'is_visible_in_aquarium', 'fish_species__maturity')
+ search_fields = ('contributor__user__username', 'contributor__repository__full_name')
+
+ def get_user(self, obj):
+ return obj.contributor.user.username
+ get_user.short_description = 'User'
+
+ def get_repo(self, obj):
+ return obj.contributor.repository.name
+ get_repo.short_description = 'Repository'
diff --git a/apps/aquatics/logic.py b/apps/aquatics/logic.py
new file mode 100644
index 0000000..2de52ab
--- /dev/null
+++ b/apps/aquatics/logic.py
@@ -0,0 +1,83 @@
+# apps/aquatics/logic.py
+import logging
+import random
+from django.conf import settings
+from apps.items.models import FishSpecies
+from apps.aquatics.models import ContributionFish, UnlockedFish, Aquarium
+
+logger = logging.getLogger(__name__)
+
+def update_or_create_contribution_fish(contributor):
+ """
+ 기여자의 커밋 수에 맞춰 물고기 종을 결정하고 ContributionFish를 업데이트합니다.
+
+ 1. 신규 획득 시: DB에 존재하는 물고기 그룹 중 하나를 '랜덤'으로 배정합니다.
+ 2. 기존 보유 시: 이미 배정받은 그룹 내에서 진화(Maturity 증가)만 수행합니다.
+ 3. 획득한 물고기는 자동으로 유저의 아쿠아리움에 배치됩니다.
+ """
+ commit_count = contributor.commit_count
+ user = contributor.user
+
+ # 1. 물고기 그룹(Group Code) 결정
+ # contributor에게 이미 할당된 물고기가 있는지 확인 (Related Object 참조)
+ current_fish = getattr(contributor, 'contribution_fish', None)
+
+ if current_fish:
+ # 이미 물고기가 있다면, 기존 그룹을 유지 (진화만 함)
+ group_code = current_fish.fish_species.group_code
+ else:
+ # [수정] 신규 할당: 등록된 모든 물고기 그룹 중 하나를 랜덤 선택
+ available_groups = list(FishSpecies.objects.values_list('group_code', flat=True).distinct())
+
+ if available_groups:
+ group_code = random.choice(available_groups)
+ else:
+ # DB에 물고기 데이터가 하나도 없을 경우 대비 (Fallback)
+ group_code = getattr(settings, "DEFAULT_FISH_GROUP", "ShrimpWich")
+
+ # 2. 커밋 수에 맞는 가장 높은 단계(Maturity)의 물고기 조회
+ # 예: 해당 그룹에서 내 커밋 수보다 요구량이 작거나 같은 것 중 가장 높은 단계
+ target_species = FishSpecies.objects.filter(
+ group_code=group_code,
+ required_commits__lte=commit_count
+ ).order_by('-maturity').first()
+
+ if not target_species:
+ # 조건에 맞는 게 없다면(커밋 0개 등), 해당 그룹의 1단계(Lv.1) 강제 할당
+ target_species = FishSpecies.objects.filter(group_code=group_code, maturity=1).first()
+
+ if not target_species:
+ logger.error(f"No FishSpecies found for group {group_code}")
+ return None
+
+ # 3. 유저의 아쿠아리움 가져오기 (없으면 생성)
+ user_aquarium, _ = Aquarium.objects.get_or_create(user=user)
+
+ # 4. ContributionFish 생성 또는 업데이트
+ if not current_fish:
+ # 신규 생성
+ current_fish = ContributionFish.objects.create(
+ contributor=contributor,
+ fish_species=target_species,
+ aquarium=user_aquarium, # [핵심] 생성 시 아쿠아리움에 바로 넣기
+ is_visible_in_aquarium=True
+ )
+ else:
+ # 기존 물고기 업데이트
+ if current_fish.fish_species != target_species:
+ current_fish.fish_species = target_species
+ current_fish.save()
+
+ # 혹시 아쿠아리움 연결이 끊겨있다면 다시 연결 (데이터 보정)
+ if not current_fish.aquarium:
+ current_fish.aquarium = user_aquarium
+ current_fish.is_visible_in_aquarium = True
+ current_fish.save()
+
+ # 5. 도감(UnlockedFish) 업데이트 (Fishdex)
+ UnlockedFish.objects.get_or_create(
+ user=user,
+ fish_species=target_species
+ )
+
+ return current_fish
\ No newline at end of file
diff --git a/apps/aquatics/migrations/0001_initial.py b/apps/aquatics/migrations/0001_initial.py
index 30ceca4..c96e61d 100644
--- a/apps/aquatics/migrations/0001_initial.py
+++ b/apps/aquatics/migrations/0001_initial.py
@@ -1,8 +1,7 @@
-# Generated by Django 5.2.6 on 2025-11-15 00:41
+# Generated by Django 4.2.27 on 2025-12-19 07:15
-import django.db.models.deletion
-from django.conf import settings
from django.db import migrations, models
+import django.db.models.deletion
class Migration(migrations.Migration):
@@ -11,8 +10,6 @@ class Migration(migrations.Migration):
dependencies = [
('items', '0001_initial'),
- ('repositories', '0001_initial'),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@@ -21,25 +18,23 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('svg_path', models.CharField(blank=True, help_text='The relative path to the generated SVG file.', max_length=512)),
- ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='aquarium', to=settings.AUTH_USER_MODEL)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='ContributionFish',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('is_visible', models.BooleanField(default=True, help_text='Determines whether this fish is visible in the fishtank.')),
- ('aquarium', models.ForeignKey(blank=True, help_text='The aquarium this fish has been added to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fishes', to='aquatics.aquarium')),
- ('contributor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='contribution_fish', to='repositories.contributor')),
- ('fish_species', models.ForeignKey(help_text='The species assigned to this contributor.', on_delete=django.db.models.deletion.PROTECT, to='items.fishspecies')),
+ ('is_visible_in_fishtank', models.BooleanField(default=True, help_text='Determines whether this fish is visible in the repository fishtank.')),
+ ('is_visible_in_aquarium', models.BooleanField(default=True, help_text='Determines whether this fish is visible in the personal aquarium.')),
],
),
migrations.CreateModel(
name='Fishtank',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('svg_path', models.CharField(blank=True, help_text='The relative path to the generated SVG file.', max_length=512)),
- ('repository', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='fishtank', to='repositories.repository')),
+ ('svg_path', models.CharField(blank=True, help_text='유저의 설정이 반영되어 생성된 SVG 파일 경로.', max_length=512)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
@@ -47,29 +42,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('unlocked_at', models.DateTimeField(auto_now_add=True, help_text='Timestamp when the user unlocked this background.')),
- ('background', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='items.background')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_backgrounds', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'unique_together': {('user', 'background')},
- },
- ),
- migrations.AddField(
- model_name='aquarium',
- name='background',
- field=models.ForeignKey(blank=True, help_text='The background chosen by the user from their collection.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='aquatics.ownbackground'),
- ),
- migrations.CreateModel(
- name='FishtankSetting',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ('fishtank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='aquatics.fishtank')),
- ('background', models.ForeignKey(blank=True, help_text='The background chosen by the contributor for this fishtank.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='aquatics.ownbackground')),
],
- options={
- 'unique_together': {('fishtank', 'contributor')},
- },
),
migrations.CreateModel(
name='UnlockedFish',
@@ -77,10 +50,6 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('unlocked_at', models.DateTimeField(auto_now_add=True, help_text='Timestamp when the user unlocked this fish species.')),
('fish_species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='items.fishspecies')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_fishes', to=settings.AUTH_USER_MODEL)),
],
- options={
- 'unique_together': {('user', 'fish_species')},
- },
),
]
diff --git a/apps/aquatics/migrations/0002_initial.py b/apps/aquatics/migrations/0002_initial.py
new file mode 100644
index 0000000..8f108f4
--- /dev/null
+++ b/apps/aquatics/migrations/0002_initial.py
@@ -0,0 +1,87 @@
+# Generated by Django 4.2.27 on 2025-12-19 07:15
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('repositories', '0001_initial'),
+ ('items', '0001_initial'),
+ ('aquatics', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='unlockedfish',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_fishes', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='ownbackground',
+ name='background',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='items.background'),
+ ),
+ migrations.AddField(
+ model_name='ownbackground',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_backgrounds', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='fishtank',
+ name='background',
+ field=models.ForeignKey(blank=True, help_text='이 수족관 뷰에 대해 유저가 설정한 배경입니다.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='aquatics.ownbackground'),
+ ),
+ migrations.AddField(
+ model_name='fishtank',
+ name='repository',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fishtanks', to='repositories.repository'),
+ ),
+ migrations.AddField(
+ model_name='fishtank',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fishtanks', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='contributionfish',
+ name='aquarium',
+ field=models.ForeignKey(blank=True, help_text='The aquarium this fish has been added to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fishes', to='aquatics.aquarium'),
+ ),
+ migrations.AddField(
+ model_name='contributionfish',
+ name='contributor',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='contribution_fish', to='repositories.contributor'),
+ ),
+ migrations.AddField(
+ model_name='contributionfish',
+ name='fish_species',
+ field=models.ForeignKey(help_text='The species assigned to this contributor.', on_delete=django.db.models.deletion.PROTECT, to='items.fishspecies'),
+ ),
+ migrations.AddField(
+ model_name='aquarium',
+ name='background',
+ field=models.ForeignKey(blank=True, help_text='The background chosen by the user from their collection.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='aquatics.ownbackground'),
+ ),
+ migrations.AddField(
+ model_name='aquarium',
+ name='user',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='aquarium', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterUniqueTogether(
+ name='unlockedfish',
+ unique_together={('user', 'fish_species')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='ownbackground',
+ unique_together={('user', 'background')},
+ ),
+ migrations.AlterUniqueTogether(
+ name='fishtank',
+ unique_together={('repository', 'user')},
+ ),
+ ]
diff --git a/apps/aquatics/migrations/0002_remove_contributionfish_is_visible_and_more.py b/apps/aquatics/migrations/0002_remove_contributionfish_is_visible_and_more.py
deleted file mode 100644
index 57929a2..0000000
--- a/apps/aquatics/migrations/0002_remove_contributionfish_is_visible_and_more.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Generated by Django 5.2.6 on 2025-11-29 04:41
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('aquatics', '0001_initial'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='contributionfish',
- name='is_visible',
- ),
- migrations.AddField(
- model_name='contributionfish',
- name='is_visible_in_aquarium',
- field=models.BooleanField(default=True, help_text='Determines whether this fish is visible in the personal aquarium.'),
- ),
- migrations.AddField(
- model_name='contributionfish',
- name='is_visible_in_fishtank',
- field=models.BooleanField(default=True, help_text='Determines whether this fish is visible in the repository fishtank.'),
- ),
- ]
diff --git a/apps/aquatics/models.py b/apps/aquatics/models.py
index e7a3850..83dd8fc 100644
--- a/apps/aquatics/models.py
+++ b/apps/aquatics/models.py
@@ -78,6 +78,7 @@ class Aquarium(models.Model):
blank=True,
help_text="The relative path to the generated SVG file."
)
+ updated_at = models.DateTimeField(auto_now=True) # 추가
def __str__(self):
return f"{self.user.username}'s Aquarium"
@@ -85,22 +86,39 @@ def __str__(self):
class Fishtank(models.Model):
"""
- Represents a shared fishtank for a single repository.
- It contains fish representing each contributor.
+ 특정 레포지토리에 대한 '개별 유저의 뷰'를 담당하는 수족관입니다.
+ 유저마다 같은 레포지토리를 서로 다른 배경으로 볼 수 있습니다.
"""
- repository = models.OneToOneField(
+ repository = models.ForeignKey(
Repository,
on_delete=models.CASCADE,
- related_name='fishtank'
+ related_name='fishtanks'
+ )
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name='fishtanks'
+ )
+ background = models.ForeignKey(
+ OwnBackground,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ help_text="이 수족관 뷰에 대해 유저가 설정한 배경입니다."
)
svg_path = models.CharField(
max_length=512,
blank=True,
- help_text="The relative path to the generated SVG file."
+ help_text="유저의 설정이 반영되어 생성된 SVG 파일 경로."
)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ unique_together = ('repository', 'user')
def __str__(self):
- return f"Fishtank for {self.repository.name}"
+ return f"{self.user.username}'s view of {self.repository.name}"
+
class ContributionFish(models.Model):
"""
@@ -137,32 +155,4 @@ class ContributionFish(models.Model):
def __str__(self):
if self.aquarium:
return f"Fish for {self.contributor.user.username} in {self.contributor.repository.name}'s Fishtank (in {self.aquarium})"
- return f"Fish for {self.contributor.user.username} in {self.contributor.repository.name}'s Fishtank"
-
-
-class FishtankSetting(models.Model):
- """
- Stores a contributor's chosen background for a specific Fishtank.
- """
- fishtank = models.ForeignKey(
- Fishtank,
- on_delete=models.CASCADE,
- related_name='settings'
- )
- contributor = models.ForeignKey(
- settings.AUTH_USER_MODEL,
- on_delete=models.CASCADE
- )
- background = models.ForeignKey(
- OwnBackground,
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- help_text="The background chosen by the contributor for this fishtank."
- )
-
- class Meta:
- unique_together = ('fishtank', 'contributor')
-
- def __str__(self):
- return f"Setting by {self.contributor.username} for {self.fishtank}"
\ No newline at end of file
+ return f"Fish for {self.contributor.user.username} in {self.contributor.repository.name}'s Fishtank"
\ No newline at end of file
diff --git a/apps/aquatics/renderer/__init__.py b/apps/aquatics/renderer/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/apps/aquatics/renderer/sprite.py b/apps/aquatics/renderer/sprite.py
deleted file mode 100644
index 8995e65..0000000
--- a/apps/aquatics/renderer/sprite.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# apps/aquatics/renderer/sprite.py
-import random
-from .utils import strip_outer_svg
-
-def apply_sprite_id(svg_template: str, fish_id: int) -> str:
- """
- species.svg_template 안의 *{id} 플레이스홀더를
- 개별 물고기 id(예: 3, 7, 12)로 치환한다.
- """
- if not svg_template:
- return ""
- return svg_template.replace("*{id}", str(fish_id))
-
-
-def render_fish_group(cf, width, height,mode):
- species = cf.fish_species
- fish_id = cf.id
- raw_svg = species.svg_template
- templated_svg=apply_sprite_id(raw_svg, fish_id)
- inner = strip_outer_svg(templated_svg)
-
-
- # === 레이블 내용 ===
- 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"""
-
-
-
-
-
- {inner}
-
-
-
-
-
- {label_text}
-
-
-
-
- """
\ No newline at end of file
diff --git a/apps/aquatics/renderer/tank.py b/apps/aquatics/renderer/tank.py
deleted file mode 100644
index e1b684e..0000000
--- a/apps/aquatics/renderer/tank.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# apps/aquatics/renderer/tank.py
-from apps.aquatics.models import Aquarium,ContributionFish , Fishtank
-from .sprite import render_fish_group
-
-def render_aquarium_svg(user,width=512, height=512):
- aquarium = Aquarium.objects.get(user=user)
-
- if aquarium.background and aquarium.background.background.background_image:
- bg_url = aquarium.background.background.background_image.url
- else:
- bg_url = ""
- #width, height = extract_svg_size(bg_svg)
- #bg_inner = strip_outer_svg(bg_svg)
- width = 512
- height = 512
- fishes = ContributionFish.objects.filter(
- aquarium=aquarium,
- is_visible_in_aquarium=True
- ).select_related("fish_species", "contributor__repository")
-
- fish_groups = [
- render_fish_group(cf, width, height, mode="aquarium")
- for cf in fishes
- ]
-
- return f"""
-
- """
-
-
-def render_fishtank_svg(repository):
- fishtank = Fishtank.objects.get(repository=repository)
-
-
- setting = fishtank.settings.select_related("background__background").first()
- if setting and setting.background and setting.background.background.background_image:
- bg_url = setting.background.background.background_image.url
- else:
- bg_url = ""
-
- #width, height = extract_svg_size(bg_svg)
- #bg_inner = strip_outer_svg(bg_svg)
- width = 512
- height = 512
- fishes = ContributionFish.objects.filter(
- contributor__repository=repository,
- is_visible_in_fishtank=True
- ).select_related("fish_species", "contributor__user")
-
- fish_groups = [
- render_fish_group(cf, width, height, mode="fishtank")
- for cf in fishes
- ]
-
- return f"""
-
- """
\ No newline at end of file
diff --git a/apps/aquatics/renderer/utils.py b/apps/aquatics/renderer/utils.py
deleted file mode 100644
index 0af216a..0000000
--- a/apps/aquatics/renderer/utils.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# apps/aquatics/renderer/utils.py
-import re
-
-def extract_svg_size(svg_text: str):
- """
- Extract width and height from