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
9 changes: 4 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -177,10 +177,9 @@ cython_debug/

# My

# VS Code
.vscode/
*.code-workspace
/logs/
all.txt
*.txt

# End of My
16 changes: 0 additions & 16 deletions GithubAquarium/asgi.py

This file was deleted.

20 changes: 19 additions & 1 deletion GithubAquarium/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -136,6 +145,10 @@
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'OPTIONS': {
# DB가 잠겨있으면 20초까지 대기하다가 풀리면 씁니다.
'timeout': 20,
}
}
}

Expand Down Expand Up @@ -351,4 +364,9 @@
'timeout': 6000, # sec
'retry': 36000,
'orm': 'default', # temporary use ORM as broker
}
}

# --- Game Logic Settings ---
DEFAULT_FISH_GROUP = "ShrimpWich"

RENDER_DOMAIN = "http://localhost:8000" if DEBUG else "https://githubaquarium.store"
2 changes: 1 addition & 1 deletion GithubAquarium/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')),

Expand Down
34 changes: 28 additions & 6 deletions GithubAquarium/views.py
Original file line number Diff line number Diff line change
@@ -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)
58 changes: 8 additions & 50 deletions GithubAquarium/webhook_views.py
Original file line number Diff line number Diff line change
@@ -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)
async_task('apps.repositories.tasks.process_webhook_event_task', event_type, request.data)
return Response({'detail': 'Event queued'}, status=200)
38 changes: 21 additions & 17 deletions apps/aquatics/admin.py
Original file line number Diff line number Diff line change
@@ -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')
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'
83 changes: 83 additions & 0 deletions apps/aquatics/logic.py
Original file line number Diff line number Diff line change
@@ -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
Loading