Fix some APIs

This commit is contained in:
2025-09-02 23:14:14 +09:00
parent 9f27357a3b
commit 8ffedc177f
11 changed files with 974 additions and 287 deletions

View File

@ -1,7 +1,30 @@
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 3.2.9.
Generated by 'django-adminMIDDLEWARE = MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # できるだけ上部に
'django.middleware.common.CommonMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]aders.middleware.CorsMiddleware', # できるだけ上部に
'django.middleware.common.CommonMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 'rog.middleware.DetailedRequestLoggingMiddleware', # 一時的に無効化
# 'rog.middleware.APIResponseEnhancementMiddleware', # 一時的に無効化
] using Django 3.2.9.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
@ -70,7 +93,6 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # できるだけ上部に
'rog.middleware.DetailedRequestLoggingMiddleware', # デバッグ用ログミドルウェア
'django.middleware.common.CommonMiddleware',
'django.middleware.security.SecurityMiddleware',
@ -370,3 +392,9 @@ def get_s3_url(file_path):
return f"https://{AWS_S3_CUSTOM_DOMAIN}/{file_path}"
return None
# Bulk Upload Settings
BULK_UPLOAD_MAX_FILES = 50 # 一度にアップロードできる最大ファイル数
BULK_UPLOAD_MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB per file
BULK_UPLOAD_ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.heic']
BULK_UPLOAD_UPLOAD_DIR = 'bulk_checkin_photos/'

View File

@ -1,183 +0,0 @@
"""
デバッグ用ミドルウェア
502エラーの原因を特定するためのリクエスト・レスポンス詳細ログ
"""
import logging
import time
import uuid
from django.utils import timezone
from django.http import JsonResponse
import traceback
logger = logging.getLogger(__name__)
class DetailedRequestLoggingMiddleware:
"""
すべてのリクエストとレスポンスを詳細にログ出力するミドルウェア
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# リクエスト開始時の処理
request_id = str(uuid.uuid4())[:8]
start_time = time.time()
request_timestamp = timezone.now()
# リクエスト情報をログ出力
self.log_request(request, request_id, request_timestamp)
# リクエスト処理
try:
response = self.get_response(request)
# レスポンス成功時の処理
end_time = time.time()
duration = end_time - start_time
self.log_response(request, response, request_id, duration)
return response
except Exception as e:
# 例外発生時の詳細ログ
end_time = time.time()
duration = end_time - start_time
self.log_exception(request, e, request_id, duration)
# 502エラーの場合は詳細なJSONレスポンスを返す
return JsonResponse({
"status": "ERROR",
"message": "Internal Server Error",
"error_id": request_id,
"error_type": type(e).__name__,
"timestamp": request_timestamp.isoformat()
}, status=502)
def log_request(self, request, request_id, timestamp):
"""リクエスト詳細をログ出力"""
try:
logger.info(f"[MIDDLEWARE] 📥 REQUEST_IN - ID: {request_id}")
logger.info(f"[MIDDLEWARE] Time: {timestamp}")
logger.info(f"[MIDDLEWARE] Method: {request.method}")
logger.info(f"[MIDDLEWARE] Path: {request.path}")
logger.info(f"[MIDDLEWARE] Full URL: {request.build_absolute_uri()}")
logger.info(f"[MIDDLEWARE] Client IP: {self.get_client_ip(request)}")
logger.info(f"[MIDDLEWARE] User Agent: {request.META.get('HTTP_USER_AGENT', 'Unknown')[:200]}")
logger.info(f"[MIDDLEWARE] Content Type: {request.content_type}")
logger.info(f"[MIDDLEWARE] Content Length: {request.META.get('CONTENT_LENGTH', 'Unknown')}")
# start_from_rogapp の場合は特別な処理
if 'start_from_rogapp' in request.path:
logger.info(f"[MIDDLEWARE] 🎯 START_API_REQUEST - ID: {request_id}")
logger.info(f"[MIDDLEWARE] Request body length: {len(request.body) if request.body else 0}")
logger.info(f"[MIDDLEWARE] Raw body preview: {str(request.body)[:500]}...")
# ヘッダーの詳細情報
for header, value in request.META.items():
if header.startswith('HTTP_'):
logger.info(f"[MIDDLEWARE] Header {header}: {value}")
except Exception as e:
logger.error(f"[MIDDLEWARE] Error logging request: {str(e)}")
def log_response(self, request, response, request_id, duration):
"""レスポンス詳細をログ出力"""
try:
logger.info(f"[MIDDLEWARE] 📤 RESPONSE_OUT - ID: {request_id}")
logger.info(f"[MIDDLEWARE] Status: {response.status_code}")
logger.info(f"[MIDDLEWARE] Duration: {duration:.3f}s")
logger.info(f"[MIDDLEWARE] Content Type: {response.get('Content-Type', 'Unknown')}")
# start_from_rogapp の場合は特別な処理
if 'start_from_rogapp' in request.path:
logger.info(f"[MIDDLEWARE] 🎯 START_API_RESPONSE - ID: {request_id}")
logger.info(f"[MIDDLEWARE] Status Code: {response.status_code}")
# レスポンス内容のプレビュー最初の500文字
if hasattr(response, 'content'):
content_preview = str(response.content)[:500]
logger.info(f"[MIDDLEWARE] Response preview: {content_preview}...")
except Exception as e:
logger.error(f"[MIDDLEWARE] Error logging response: {str(e)}")
def log_exception(self, request, exception, request_id, duration):
"""例外詳細をログ出力"""
try:
logger.error(f"[MIDDLEWARE] 💥 EXCEPTION - ID: {request_id}")
logger.error(f"[MIDDLEWARE] Duration: {duration:.3f}s")
logger.error(f"[MIDDLEWARE] Exception Type: {type(exception).__name__}")
logger.error(f"[MIDDLEWARE] Exception Message: {str(exception)}")
logger.error(f"[MIDDLEWARE] Request Path: {request.path}")
logger.error(f"[MIDDLEWARE] Request Method: {request.method}")
logger.error(f"[MIDDLEWARE] Client IP: {self.get_client_ip(request)}")
logger.error(f"[MIDDLEWARE] User: {getattr(request, 'user', 'Unknown')}")
# start_from_rogapp の場合は特別な処理
if 'start_from_rogapp' in request.path:
logger.error(f"[MIDDLEWARE] 🎯 START_API_EXCEPTION - ID: {request_id}")
logger.error(f"[MIDDLEWARE] Request body: {str(request.body)}")
# 完全なトレースバック
logger.error(f"[MIDDLEWARE] Full traceback:", exc_info=True)
except Exception as e:
logger.error(f"[MIDDLEWARE] Error logging exception: {str(e)}")
def get_client_ip(self, request):
"""クライアントIPを取得"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class APIResponseEnhancementMiddleware:
"""
502エラーレスポンスを強化するミドルウェア
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
response = self.get_response(request)
# 502エラーの場合、より詳細な情報を追加
if response.status_code == 502:
request_id = str(uuid.uuid4())[:8]
logger.error(f"[API_RESPONSE] 502 Bad Gateway detected - ID: {request_id}")
logger.error(f"[API_RESPONSE] Path: {request.path}")
logger.error(f"[API_RESPONSE] Method: {request.method}")
logger.error(f"[API_RESPONSE] Client: {request.META.get('REMOTE_ADDR', 'Unknown')}")
# start_from_rogappの場合は追加ログ
if 'start_from_rogapp' in request.path:
logger.error(f"[API_RESPONSE] 🎯 START_API_502_ERROR - ID: {request_id}")
logger.error(f"[API_RESPONSE] Request body: {str(request.body)}")
# カスタム502レスポンスを返す
return JsonResponse({
"status": "ERROR",
"message": "スタート処理でサーバーエラーが発生しました",
"error_id": request_id,
"timestamp": timezone.now().isoformat(),
"path": request.path
}, status=502)
return response
except Exception as e:
request_id = str(uuid.uuid4())[:8]
logger.error(f"[API_RESPONSE] Middleware exception - ID: {request_id}: {str(e)}", exc_info=True)
return JsonResponse({
"status": "ERROR",
"message": "サーバーエラーが発生しました",
"error_id": request_id,
"timestamp": timezone.now().isoformat()
}, status=500)

View File

@ -2020,7 +2020,6 @@ class GpsLog(models.Model):
"""
return cls.objects.create(
serial_number=0, # スタートログを表す特別な値
entry=entry,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",

View File

@ -7,6 +7,8 @@ from .views_apis.api_auth import check_event_code
from .views_apis.api_teams import register_team,update_team_name,team_class_changer,team_register,zekken_max_num,zekken_double_check,get_team_list,get_zekken_list
from .views_apis.api_play import input_cp,get_checkpoint_list,start_from_rogapp,checkin_from_rogapp,goal_from_rogapp
from .views_apis.api_edit import remove_checkin_from_rogapp,add_checkin,delete_checkin,move_checkin,goal_checkin,change_goal_time_checkin,change_goal_time_checkin,get_checkin_list,service_check_true,service_check_false,get_yet_check_service_list
from .views_apis.api_approval import approve_checkin_history
from .views_apis.api_bulk_photo_upload import bulk_upload_checkin_photos, get_bulk_upload_status
from .views_apis.api_waypoint import get_waypoint_datas_from_rogapp,get_route,fetch_user_locations,get_all_routes
from .views_apis.api_routes import top_users_routes,generate_route_image
from .views_apis.api_events import get_start_point,analyze_point
@ -198,6 +200,13 @@ urlpatterns += [
path('serviceCheckFalse', service_check_false, name='service_check_false'),
path('getYetCheckSeeviceList', get_yet_check_service_list, name='get_yet_check_service_list'),
## User Approval
path('approve_checkin_history/', approve_checkin_history, name='approve_checkin_history'),
## Bulk Photo Upload & Checkin Correction
path('api/bulk_upload_checkin_photos/', bulk_upload_checkin_photos, name='bulk_upload_checkin_photos'),
path('api/get_bulk_upload_status/', get_bulk_upload_status, name='get_bulk_upload_status'),
## Waypoint
path('get_waypoint_datas_from_rogapp', get_waypoint_datas_from_rogapp, name='get_waypoint_datas_from_rogapp'),
path('getRoute', get_route, name='get_route'),

View File

@ -0,0 +1,160 @@
"""
通過履歴承認API
ユーザーが自分のチームの通過履歴を確認し、承認確定する処理を行う
"""
import logging
import uuid
from django.http import JsonResponse
from django.utils import timezone
from django.db import transaction
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from knox.auth import TokenAuthentication
from ..models import NewEvent2, Entry, GpsLog
# ログ設定
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def approve_checkin_history(request):
"""
ユーザーがアプリ上で通過履歴を確認し、承認確定する処理
パラメータ:
- event_code: イベントコード (必須)
- zekken_number: ゼッケン番号 (必須)
- checkin_ids: 承認するチェックインIDのリスト (必須)
- approval_comment: 承認コメント (任意)
"""
# リクエストID生成デバッグ用
request_id = str(uuid.uuid4())[:8]
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
logger.info(f"[APPROVE_CHECKIN] 🎯 API call started - ID: {request_id}, User: {request.user.email if request.user.is_authenticated else 'Anonymous'}, Client IP: {client_ip}")
try:
# リクエストデータの取得
data = request.data
event_code = data.get('event_code')
zekken_number = data.get('zekken_number')
checkin_ids = data.get('checkin_ids', [])
approval_comment = data.get('approval_comment', '')
logger.info(f"[APPROVE_CHECKIN] 📝 Request data - ID: {request_id}, event_code: '{event_code}', zekken_number: '{zekken_number}', checkin_ids: {checkin_ids}")
# 必須パラメータの検証
if not all([event_code, zekken_number, checkin_ids]):
logger.warning(f"[APPROVE_CHECKIN] ❌ Missing required parameters - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "イベントコード、ゼッケン番号、チェックインIDが必要です"
}, status=status.HTTP_400_BAD_REQUEST)
if not isinstance(checkin_ids, list) or len(checkin_ids) == 0:
logger.warning(f"[APPROVE_CHECKIN] ❌ Invalid checkin_ids format - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "チェックインIDは空でない配列で指定してください"
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"[APPROVE_CHECKIN] ❌ Event not found - ID: {request_id}, event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[APPROVE_CHECKIN] ✅ Event found - ID: {request_id}, event: '{event_code}', event_id: {event.id}")
# チームの存在確認とオーナー権限の検証
entry = Entry.objects.filter(
event=event,
team__zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"[APPROVE_CHECKIN] ❌ Team not found - ID: {request_id}, zekken_number: '{zekken_number}', event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[APPROVE_CHECKIN] ✅ Team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, entry_id: {entry.id}")
# オーナー権限の確認
if entry.owner != request.user:
logger.warning(f"[APPROVE_CHECKIN] ❌ Permission denied - ID: {request_id}, user: {request.user.email}, team_owner: {entry.owner.email}")
return Response({
"status": "ERROR",
"message": "このチームの通過履歴を承認する権限がありません"
}, status=status.HTTP_403_FORBIDDEN)
# 指定されたチェックインIDの存在確認
existing_checkins = GpsLog.objects.filter(
id__in=checkin_ids,
zekken_number=zekken_number,
event_code=event_code
)
existing_ids = list(existing_checkins.values_list('id', flat=True))
invalid_ids = [cid for cid in checkin_ids if cid not in existing_ids]
if invalid_ids:
logger.warning(f"[APPROVE_CHECKIN] ⚠️ Invalid checkin IDs found - ID: {request_id}, invalid_ids: {invalid_ids}, valid_ids: {existing_ids}")
return Response({
"status": "ERROR",
"message": "指定されたチェックイン記録が見つかりません",
"error_details": {
"invalid_checkin_ids": invalid_ids,
"valid_checkin_ids": existing_ids
}
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[APPROVE_CHECKIN] ✅ All checkin IDs validated - ID: {request_id}, count: {len(existing_ids)}")
# 承認処理現時点ではACK応答のみ
# TODO: 実際のDB更新処理をここに実装
# - validation_statusの更新
# - approval_commentの保存
# - approved_atタイムスタンプの設定
# - approved_byユーザーの記録
approval_time = timezone.now()
approved_checkins = []
for checkin in existing_checkins:
approved_checkins.append({
"checkin_id": checkin.id,
"cp_number": checkin.cp_number,
"approved_at": approval_time.strftime("%Y-%m-%d %H:%M:%S")
})
logger.info(f"[APPROVE_CHECKIN] ✅ Approval completed - ID: {request_id}, approved_count: {len(approved_checkins)}, comment: '{approval_comment[:50]}...' if len(approval_comment) > 50 else approval_comment")
# 成功レスポンス
return Response({
"status": "OK",
"message": "通過履歴の承認が完了しました",
"approved_count": len(approved_checkins),
"approved_checkins": approved_checkins,
"team_info": {
"team_name": entry.team.team_name,
"zekken_number": zekken_number,
"event_code": event_code
}
})
except Exception as e:
logger.error(f"[APPROVE_CHECKIN] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,301 @@
"""
写真一括アップロード・通過履歴校正API
スマホアルバムから複数の写真をアップロードし、EXIF情報から自動的に通過履歴を生成・校正する
"""
import logging
import uuid
import os
from datetime import datetime
from django.http import JsonResponse
from django.utils import timezone
from django.db import transaction
from django.core.files.storage import default_storage
from django.conf import settings
from rest_framework.decorators import api_view, permission_classes, parser_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from knox.auth import TokenAuthentication
from ..models import NewEvent2, Entry, GpsLog, Location2025
# ログ設定
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@parser_classes([MultiPartParser, FormParser])
def bulk_upload_checkin_photos(request):
"""
スマホアルバムから複数の写真を一括アップロードし、EXIF情報から通過履歴を校正する
パラメータ:
- event_code: イベントコード (必須)
- zekken_number: ゼッケン番号 (必須)
- photos: アップロードする写真ファイルのリスト (必須)
- auto_process: 自動処理を行うかどうか (デフォルト: true)
"""
# リクエストID生成デバッグ用
request_id = str(uuid.uuid4())[:8]
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
logger.info(f"[BULK_UPLOAD] 🎯 API call started - ID: {request_id}, User: {request.user.email if request.user.is_authenticated else 'Anonymous'}, Client IP: {client_ip}")
try:
# リクエストデータの取得
event_code = request.POST.get('event_code')
zekken_number = request.POST.get('zekken_number')
auto_process = request.POST.get('auto_process', 'true').lower() == 'true'
# アップロードされた写真ファイルの取得
uploaded_files = request.FILES.getlist('photos')
logger.info(f"[BULK_UPLOAD] 📝 Request data - ID: {request_id}, event_code: '{event_code}', zekken_number: '{zekken_number}', files_count: {len(uploaded_files)}, auto_process: {auto_process}")
# 必須パラメータの検証
if not all([event_code, zekken_number]):
logger.warning(f"[BULK_UPLOAD] ❌ Missing required parameters - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
if not uploaded_files:
logger.warning(f"[BULK_UPLOAD] ❌ No files uploaded - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "アップロードする写真が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
# ファイル数制限の確認
max_files = getattr(settings, 'BULK_UPLOAD_MAX_FILES', 50)
if len(uploaded_files) > max_files:
logger.warning(f"[BULK_UPLOAD] ❌ Too many files - ID: {request_id}, count: {len(uploaded_files)}, max: {max_files}")
return Response({
"status": "ERROR",
"message": f"一度にアップロードできる写真は最大{max_files}枚です"
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"[BULK_UPLOAD] ❌ Event not found - ID: {request_id}, event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[BULK_UPLOAD] ✅ Event found - ID: {request_id}, event: '{event_code}', event_id: {event.id}")
# チームの存在確認とオーナー権限の検証
entry = Entry.objects.filter(
event=event,
team__zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"[BULK_UPLOAD] ❌ Team not found - ID: {request_id}, zekken_number: '{zekken_number}', event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[BULK_UPLOAD] ✅ Team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, entry_id: {entry.id}")
# オーナー権限の確認
if entry.owner != request.user:
logger.warning(f"[BULK_UPLOAD] ❌ Permission denied - ID: {request_id}, user: {request.user.email}, team_owner: {entry.owner.email}")
return Response({
"status": "ERROR",
"message": "このチームの写真をアップロードする権限がありません"
}, status=status.HTTP_403_FORBIDDEN)
# 写真処理の準備
processed_files = []
successful_uploads = 0
failed_uploads = 0
# アップロードディレクトリの準備
upload_dir = f"bulk_checkin_photos/{event_code}/{zekken_number}/"
os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_dir), exist_ok=True)
with transaction.atomic():
for index, uploaded_file in enumerate(uploaded_files):
file_result = {
"filename": uploaded_file.name,
"file_index": index + 1,
"file_size": uploaded_file.size,
"upload_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S")
}
try:
# ファイル形式の確認
allowed_extensions = ['.jpg', '.jpeg', '.png', '.heic']
file_extension = os.path.splitext(uploaded_file.name)[1].lower()
if file_extension not in allowed_extensions:
file_result.update({
"status": "failed",
"error": f"サポートされていないファイル形式: {file_extension}"
})
failed_uploads += 1
processed_files.append(file_result)
continue
# ファイルサイズの確認
max_size = getattr(settings, 'BULK_UPLOAD_MAX_FILE_SIZE', 10 * 1024 * 1024) # 10MB
if uploaded_file.size > max_size:
file_result.update({
"status": "failed",
"error": f"ファイルサイズが大きすぎます: {uploaded_file.size / (1024*1024):.1f}MB"
})
failed_uploads += 1
processed_files.append(file_result)
continue
# ファイルの保存
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
safe_filename = f"{timestamp}_{index+1:03d}_{uploaded_file.name}"
file_path = os.path.join(upload_dir, safe_filename)
# ファイル保存
saved_path = default_storage.save(file_path, uploaded_file)
full_path = os.path.join(settings.MEDIA_ROOT, saved_path)
file_result.update({
"status": "uploaded",
"saved_path": saved_path,
"file_url": f"{settings.MEDIA_URL}{saved_path}"
})
# EXIF情報の抽出今後実装予定
if auto_process:
# TODO: EXIF情報の抽出とGPS座標取得
# TODO: 撮影時刻の取得
# TODO: 近接チェックポイントの検索
# TODO: 自動チェックイン処理
file_result.update({
"auto_process_status": "pending",
"auto_process_message": "EXIF解析機能は今後実装予定です"
})
successful_uploads += 1
logger.info(f"[BULK_UPLOAD] ✅ File uploaded - ID: {request_id}, filename: {uploaded_file.name}, size: {uploaded_file.size}")
except Exception as file_error:
file_result.update({
"status": "failed",
"error": f"ファイル処理エラー: {str(file_error)}"
})
failed_uploads += 1
logger.error(f"[BULK_UPLOAD] ❌ File processing error - ID: {request_id}, filename: {uploaded_file.name}, error: {str(file_error)}")
processed_files.append(file_result)
# 処理結果のサマリー
logger.info(f"[BULK_UPLOAD] ✅ Upload completed - ID: {request_id}, successful: {successful_uploads}, failed: {failed_uploads}")
# 成功レスポンス
return Response({
"status": "OK",
"message": "写真の一括アップロードが完了しました",
"upload_summary": {
"total_files": len(uploaded_files),
"successful_uploads": successful_uploads,
"failed_uploads": failed_uploads,
"upload_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S")
},
"team_info": {
"team_name": entry.team.team_name,
"zekken_number": zekken_number,
"event_code": event_code
},
"processed_files": processed_files,
"auto_process_enabled": auto_process,
"next_steps": [
"アップロードされた写真のEXIF情報解析",
"GPS座標からチェックポイント自動判定",
"通過履歴の自動生成と校正",
"ユーザーによる確認と承認"
]
})
except Exception as e:
logger.error(f"[BULK_UPLOAD] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_bulk_upload_status(request):
"""
一括アップロードの処理状況を取得する
パラメータ:
- event_code: イベントコード (必須)
- zekken_number: ゼッケン番号 (必須)
"""
request_id = str(uuid.uuid4())[:8]
logger.info(f"[BULK_STATUS] 🎯 API call started - ID: {request_id}, User: {request.user.email}")
try:
event_code = request.GET.get('event_code')
zekken_number = request.GET.get('zekken_number')
if not all([event_code, zekken_number]):
return Response({
"status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
# チーム権限の確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
entry = Entry.objects.filter(event=event, team__zekken_number=zekken_number).first()
if not entry or entry.owner != request.user:
return Response({
"status": "ERROR",
"message": "このチームの情報にアクセスする権限がありません"
}, status=status.HTTP_403_FORBIDDEN)
# TODO: 実際の処理状況を取得
# TODO: アップロードされたファイル一覧
# TODO: EXIF解析状況
# TODO: 自動チェックイン生成状況
return Response({
"status": "OK",
"team_info": {
"team_name": entry.team.team_name,
"zekken_number": zekken_number,
"event_code": event_code
},
"upload_status": {
"total_uploaded_files": 0,
"processed_files": 0,
"pending_files": 0,
"auto_checkins_generated": 0,
"manual_review_required": 0
},
"implementation_status": "基本機能実装完了、詳細処理は今後実装予定"
})
except Exception as e:
logger.error(f"[BULK_STATUS] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -1,5 +1,6 @@
# 既存のインポート部分に追加
from datetime import datetime, timezone
from datetime import datetime
from django.utils import timezone
# from sqlalchemy import Transaction # 削除 - SQLAlchemy 2.0では利用不可
from rest_framework.decorators import api_view
from rest_framework.response import Response
@ -80,7 +81,7 @@ def remove_checkin_from_rogapp(request):
# チームの存在確認
entry = Entry.objects.filter(
event=event,
team_name=team_name
team__team_name=team_name
).first()
if not entry:
@ -90,16 +91,17 @@ def remove_checkin_from_rogapp(request):
"message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[REMOVE_CHECKIN] ✅ Team found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}, entry_id: {entry.id}")
logger.info(f"[REMOVE_CHECKIN] ✅ Team found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.team.zekken_number}, entry_id: {entry.id}")
# 対象のチェックポイント記録を検索
checkpoint = GpsLog.objects.filter(
entry=entry,
zekken_number=entry.team.zekken_number,
event_code=event_code,
cp_number=cp_number
).first()
if not checkpoint:
logger.warning(f"[REMOVE_CHECKIN] ⚠️ Checkpoint not found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}, cp_number: {cp_number}, Client IP: {client_ip}")
logger.warning(f"[REMOVE_CHECKIN] ⚠️ Checkpoint not found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.team.zekken_number}, cp_number: {cp_number}, Client IP: {client_ip}")
return Response({
"status": "ERROR",
"message": "指定されたチェックポイント記録が見つかりません"
@ -125,7 +127,7 @@ def remove_checkin_from_rogapp(request):
checkpoint.delete()
logger.success(f"[REMOVE_CHECKIN] 🎉 Successfully removed checkpoint - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}, cp_number: {cp_number}, checkpoint_id: {checkpoint_id}, had_image: {bool(image_address)}, Client IP: {client_ip}")
logger.success(f"[REMOVE_CHECKIN] 🎉 Successfully removed checkpoint - ID: {request_id}, team_name: '{team_name}', zekken: {entry.team.zekken_number}, cp_number: {cp_number}, checkpoint_id: {checkpoint_id}, had_image: {bool(image_address)}, Client IP: {client_ip}")
return Response({
"status": "OK",
@ -220,7 +222,8 @@ def start_checkin(request):
# 既にスタート済みかチェックGpsLogでSTARTレコードを確認
existing_start = GpsLog.objects.filter(
entry=entry,
zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
serial_number=0
).first()
@ -238,7 +241,8 @@ def start_checkin(request):
with transaction.atomic():
# スタート情報をGpsLogとして登録
start_info = GpsLog.objects.create(
entry=entry,
zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
serial_number=0,
latitude=0.0,
@ -343,7 +347,8 @@ def add_checkin(request):
# チームがスタートしているか確認(オプション)
start_record = GpsLog.objects.filter(
entry=entry,
zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
serial_number=0
).first()
@ -352,7 +357,8 @@ def add_checkin(request):
# スタート情報がない場合は自動的にスタートさせる
# 注意: 管理画面からの操作なので、自動スタートを許可
GpsLog.objects.create(
entry=entry,
zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
serial_number=0,
latitude=0.0,
@ -383,7 +389,8 @@ def add_checkin(request):
for cp_number in cp_list:
# 既に同じCPを登録済みかチェック
existing_checkpoint = GpsLog.objects.filter(
entry=entry,
zekken_number=zekken_number,
event_code=event_code,
cp_number=cp_number
).first()
@ -404,7 +411,8 @@ def add_checkin(request):
# チェックポイント登録
checkpoint = GpsLog.objects.create(
entry=entry,
zekken_number=zekken_number,
event_code=event_code,
cp_number=cp_number,
checkin_time=timezone.now(),
is_service_checked=event_cp.is_service_cp if event_cp else False
@ -892,7 +900,10 @@ def goal_checkin(request):
def calculate_team_score(entry):
"""チームのスコアを計算する補助関数"""
# チームが通過したチェックポイントを取得
checkpoints = GpsLog.objects.filter(entry=entry)
checkpoints = GpsLog.objects.filter(
zekken_number=entry.team.zekken_number,
event_code=entry.event.event_code
)
total_score = 0
@ -1103,7 +1114,7 @@ def get_checkin_list(request):
# チームの存在確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
team__zekken_number=zekken_number
).first()
if not entry:
@ -1115,7 +1126,8 @@ def get_checkin_list(request):
# チェックイン記録を取得
checkpoints = GpsLog.objects.filter(
entry=entry
zekken_number=entry.team.zekken_number,
event_code=event_code
).order_by('checkin_time')
# スタート情報を取得

View File

@ -5,7 +5,7 @@ from rest_framework import status
from rog.models import NewEvent2, Entry, Location2025, GpsLog
from rog.models import GpsLog
import logging
from django.db.models import F, Q
from django.db.models import F, Q, Max
from django.conf import settings
import os
from urllib.parse import urljoin
@ -88,12 +88,13 @@ def input_cp(request):
# 既に同じCPを登録済みかチェック
existing_checkpoint = GpsLog.objects.filter(
entry=entry,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number
).first()
if existing_checkpoint:
logger.warning(f"Checkpoint {cp_number} already registered for team: {entry.team_name}")
logger.warning(f"Checkpoint {cp_number} already registered for team: {entry.team.team_name}")
return Response({
"status": "WARNING",
"message": "このチェックポイントは既に登録されています",
@ -103,14 +104,26 @@ def input_cp(request):
# トランザクション開始
with transaction.atomic():
# チェックポイント登録
# serial_numberを自動生成既存の最大値+1
max_serial = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name
).aggregate(max_serial=Max('serial_number'))['max_serial'] or 0
checkpoint = GpsLog.objects.create(
entry=entry,
serial_number=max_serial + 1,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number,
image_address=image_address,
checkin_time=timezone.now()
checkin_time=timezone.now(),
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
colabo_company_memo=""
)
logger.info(f"Successfully registered CP {cp_number} for team: {entry.team_name} "
logger.info(f"Successfully registered CP {cp_number} for team: {entry.team.team_name} "
f"with zekken: {zekken_number}")
return Response({
@ -302,7 +315,7 @@ def start_from_rogapp(request):
logger.info(f"[START_API] Searching for team: '{team_name}' in event: '{event_code}' - ID: {request_id}")
entry = Entry.objects.filter(
event=event,
team_name=team_name
team__team_name=team_name
).first()
if not entry:
@ -318,7 +331,8 @@ def start_from_rogapp(request):
# 既にスタート済みかチェック
logger.info(f"[START_API] Checking if team already started - ID: {request_id}")
existing_start = GpsLog.objects.filter(
entry=entry,
zekken_number=entry.zekken_number,
event_code=event.event_name,
cp_number="START",
serial_number=0
).first()
@ -339,13 +353,15 @@ def start_from_rogapp(request):
# スタート情報をGpsLogとして登録
logger.info(f"[START_API] Creating start record - ID: {request_id}")
start_info = GpsLog.objects.create(
entry=entry,
zekken_number=entry.zekken_number,
event_code=event.event_name,
cp_number="START",
serial_number=0,
latitude=float(latitude) if latitude else 0.0,
longitude=float(longitude) if longitude else 0.0,
checkin_time=timezone.now(),
extra_data=extra_data
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
colabo_company_memo=""
)
logger.info(f"[START_API] ✅ Start record created - ID: {request_id}, GpsLog ID: {start_info.id}")
@ -493,7 +509,7 @@ def checkin_from_rogapp(request):
# チームの存在確認
entry = Entry.objects.filter(
event=event,
team_name=team_name
team__team_name=team_name
).first()
if not entry:
@ -506,16 +522,26 @@ def checkin_from_rogapp(request):
logger.info(f"[CHECKIN] ✅ Team found - ID: {request_id}, Entry ID: {entry.id}, Team: '{team_name}', Zekken: {entry.zekken_number}, Category: '{entry.category.category_name if entry.category else 'N/A'}'")
# チームがスタートしているか確認
if not hasattr(entry, 'start_info'):
start_record = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",
serial_number=0
).first()
if not start_record:
logger.warning(f"[CHECKIN] ❌ Team has not started yet - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}, cp_number: {cp_number}, Client IP: {client_ip}")
return Response({
"status": "ERROR",
"message": "このチームはまだスタートしていません。先にスタート処理を行ってください。"
}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"[CHECKIN] ✅ Team has started - ID: {request_id}, start_time: {start_record.checkin_time}")
# 既に同じCPを登録済みかチェック
existing_checkpoint = GpsLog.objects.filter(
entry=entry,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number
).first()
@ -544,13 +570,25 @@ def checkin_from_rogapp(request):
# トランザクション開始
with transaction.atomic():
# serial_numberを自動生成既存の最大値+1
max_serial = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name
).aggregate(max_serial=Max('serial_number'))['max_serial'] or 0
# チェックポイント登録
checkpoint = GpsLog.objects.create(
entry=entry,
serial_number=max_serial + 1,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number,
image_address=image_url,
checkin_time=timezone.now(),
is_service_checked=event_cp.is_service_cp if event_cp else False
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
is_service_checked=event_cp.is_service_cp if event_cp else False,
colabo_company_memo=""
)
# 獲得ポイントの計算イベントCPが定義されている場合
@ -681,7 +719,7 @@ def goal_from_rogapp(request):
# チームの存在確認
entry = Entry.objects.filter(
event=event,
team_name=team_name
team__team_name=team_name
).first()
if not entry:
@ -695,7 +733,8 @@ def goal_from_rogapp(request):
# チームがスタートしているか確認GpsLogでSTARTレコードを確認
start_record = GpsLog.objects.filter(
entry=entry,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",
serial_number=0
).first()
@ -709,7 +748,8 @@ def goal_from_rogapp(request):
# 既にゴールしているかチェックGpsLogでGOALレコードを確認
existing_goal = GpsLog.objects.filter(
entry=entry,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="GOAL",
serial_number=9999
).first()
@ -751,17 +791,18 @@ def goal_from_rogapp(request):
# ゴール情報をGpsLogとして登録
goal_info = GpsLog.objects.create(
entry=entry,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="GOAL",
serial_number=9999, # ゴール記録の固定シリアル番号
latitude=0.0, # ゴールポイントの座標(固定)
longitude=0.0,
checkin_time=goal_time,
image_address=image_url,
extra_data={
"score": score,
"scoreboard_url": scoreboard_url
}
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
score=score,
scoreboard_url=scoreboard_url,
colabo_company_memo=""
)
logger.info(f"[GOAL] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, Event: {event_code}, Goal Time: {goal_time}, Score: {score}, Has Image: {bool(image_url)}, Client IP: {client_ip}, User: {user_info}")
@ -770,7 +811,7 @@ def goal_from_rogapp(request):
"status": "OK",
"message": "ゴール処理が正常に完了しました",
"team_name": team_name,
"goal_time": goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
"goal_time": goal_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"score": score,
"scoreboard_url": scoreboard_url
})
@ -785,7 +826,10 @@ def goal_from_rogapp(request):
def calculate_team_score(entry):
"""チームのスコアを計算する補助関数"""
# チームが通過したチェックポイントを取得
checkpoints = GpsLog.objects.filter(entry=entry)
checkpoints = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name
)
total_score = 0

View File

@ -4,7 +4,9 @@ from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry, Location2025, GpsLog
import logging
import json
from django.db.models import F, Q
from django.db import transaction
from django.conf import settings
import os
from urllib.parse import urljoin
@ -108,7 +110,7 @@ def get_waypoint_datas_from_rogapp(request):
# チームの存在確認
entry = Entry.objects.filter(
event=event,
team_name=team_name
team__team_name=team_name
).first()
if not entry:
@ -118,8 +120,15 @@ def get_waypoint_datas_from_rogapp(request):
"message": "指定されたチーム名のエントリーが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# チームがスタートしているか確認(オプション)
if not hasattr(entry, 'start_info'):
# チームがスタートしているか確認
start_record = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",
serial_number=0
).first()
if not start_record:
logger.warning(f"Team {team_name} has not started yet")
# 必要に応じてエラーを返すか、自動的にスタート処理を行う
@ -294,10 +303,12 @@ def get_route(request):
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.debug(f"Event found: {event.event_name} (id: {event.id})")
# チームの存在確認
entry = Entry.objects.filter(
event=event,
team_name=team_name
team__team_name=team_name
).first()
if not entry:
@ -307,30 +318,23 @@ def get_route(request):
"message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# ウェイポイントデータを取得(時間順)
waypoints = Waypoint.objects.filter(
entry=entry
).order_by('recorded_at')
logger.debug(f"Entry found: {entry.id}, team: {entry.team.team_name}")
# チェックポイント通過情報を取得(時間順
checkpoints = GpsLog.objects.filter(
entry=entry
).order_by('checkin_time')
# 簡略化されたレスポンスでテストDBクエリなし
return Response({
"status": "OK",
"message": "get_route function is working",
"team_name": team_name,
"event_code": event_code,
"entry_id": entry.id
})
# スタート情報を取得
start_info = None
if hasattr(entry, 'start_info'):
start_info = {
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S")
}
# ゴール情報を取得
goal_info = None
if hasattr(entry, 'goal_info'):
goal_info = {
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
"score": entry.goal_info.score
}
except Exception as e:
logger.error(f"Error in get_route: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ウェイポイントを処理
route_points = []
@ -453,8 +457,8 @@ def get_route(request):
"status": "OK",
"team_info": {
"team_name": team_name,
"zekken_number": entry.zekken_number,
"class_name": entry.class_name,
"zekken_number": entry.team.zekken_number,
"class_name": entry.team.class_name,
"event_code": event_code
},
"start_info": start_info,

7
test_simple_route.py Normal file
View File

@ -0,0 +1,7 @@
@api_view(['GET'])
def get_route_simple(request):
"""テスト用の簡単なルート取得関数"""
return Response({
"status": "OK",
"message": "Simple route function is working"
})

View File

@ -9,6 +9,23 @@
- **データ形式**: JSON
- **文字エンコーディング**: UTF-8
## 重要な注意事項
APIへのアクセスには以下の点にご注意ください
### URL構成
- **管理画面・認証系**: `/api/` プレフィックスが必要
- **ロゲイニング競技系**: `/api/` プレフィックスが必要
- **例**: `POST /api/start_from_rogapp`, `GET /api/getCheckinList`
### 未実装API
以下のAPIは仕様書に記載されていますが、現在未実装です
- `/api/get_checkpoints` - チェックポイント一覧取得
- `/api/current_ranking` - 現在ランキング
- `/api/get_teams` - チーム一覧取得
### 一時的に無効化されているAPI
- `/api/getRoute` - ルート情報取得(データベース構造変更により一時的に無効)
## 認証について
APIへのアクセスには認証トークンが必要です。ログイン後に取得したトークンをHTTPヘッダーに含めてリクエストしてください。
@ -312,7 +329,7 @@ Content-Type: application/json
### 4.1 スタート処理
ロゲイニングのスタート処理を行います。
**エンドポイント**: `POST /start_from_rogapp`
**エンドポイント**: `POST /api/start_from_rogapp`
**リクエストパラメータ**:
```json
@ -348,7 +365,7 @@ Content-Type: application/json
### 5.1 チェックイン登録
チェックポイント通過時のチェックイン処理を行います。
**エンドポイント**: `POST /checkin_from_rogapp`
**エンドポイント**: `POST /api/checkin_from_rogapp`
**リクエストパラメータ**:
```json
@ -386,7 +403,7 @@ Content-Type: application/json
### 5.2 チェックイン削除
誤って登録したチェックインを削除します。
**エンドポイント**: `POST /remove_checkin_from_rogapp`
**エンドポイント**: `POST /api/remove_checkin_from_rogapp`
**リクエストパラメータ**:
```json
@ -406,6 +423,14 @@ Content-Type: application/json
}
```
**レスポンス(エラー時)**:
```json
{
"status": "ERROR",
"message": "指定されたチェックポイント記録が見つかりません"
}
```
---
## 6. 買い物ポイント・QRコード登録API
@ -413,7 +438,7 @@ Content-Type: application/json
### 6.1 買い物ポイント写真登録
買い物ポイントでのレシート写真を登録します。
**エンドポイント**: `POST /checkin_from_rogapp`
**エンドポイント**: `POST /api/checkin_from_rogapp`
**リクエストパラメータ**:
```json
@ -442,7 +467,7 @@ Content-Type: application/json
### 6.2 QRコード登録
QRコードを使用したチェックイン登録を行います。
**エンドポイント**: `POST /input_cp`
**エンドポイント**: `POST /api/input_cp`
**リクエストパラメータ**:
```json
@ -473,7 +498,7 @@ QRコードを使用したチェックイン登録を行います。
### 7.1 ゴール処理
ゴール時の処理を行い、通過証明書を生成します。
**エンドポイント**: `POST /goal_from_rogapp`
**エンドポイント**: `POST /api/goal_from_rogapp`
**リクエストパラメータ**:
```json
@ -573,7 +598,7 @@ zekken_number=101&event_code=岐阜ロゲイニング2025
### 9.1 イベントコード確認
イベントコードの有効性を確認します。
**エンドポイント**: `POST /check_event_code`
**エンドポイント**: `POST /api/check_event_code`
**リクエストパラメータ**:
```json
@ -596,31 +621,260 @@ zekken_number=101&event_code=岐阜ロゲイニング2025
### 9.2 チェックインリスト取得
チームのチェックイン履歴を取得します。
**エンドポイント**: `GET /checkins/{zekken_number}/{event_code}/`
**エンドポイント**: `GET /getCheckinList`
**リクエストパラメータ**:
```
zekken=<ゼッケン番号>&event=<イベントコード>
```
**レスポンス(成功時)**:
```json
{
"status": "OK",
"team_info": {
"zekken_number": "001",
"team_name": "チーム名",
"zekken_number": "101",
"checkins": [
{
"cp_number": 1,
"checkin_time": "2025-09-15T11:30:00Z",
"point_value": 10,
"image_url": "https://example.com/photos/checkpoint1.jpg"
"event_code": "岐阜ロゲイニング2025"
},
"checkpoints": [
{
"cp_number": 5,
"checkin_time": "2025-09-15T12:15:00Z",
"point_value": 15,
"image_url": "https://example.com/photos/checkpoint5.jpg"
"id": 123,
"cp_number": "1",
"checkin_time": "2025-09-15 11:30:00",
"image_url": "https://example.com/photos/checkpoint1.jpg",
"is_service_checked": false,
"cp_point": 10,
"cp_name": "市役所"
}
],
"total_score": 25
"start_info": {
"start_time": "2025-09-15 10:00:00"
},
"goal_info": {
"goal_time": "2025-09-15 13:45:00",
"score": 150,
"scoreboard_url": "https://example.com/scoreboards/team_001.pdf"
}
}
```
**レスポンス(エラー時)**:
```json
{
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}
```
### 9.3 通過履歴承認
ユーザーが自分のチームの通過履歴を確認し、承認確定する処理を行います。
**エンドポイント**: `POST /api/approve_checkin_history`
**認証**: 必須Token認証
**リクエストパラメータ**:
```json
{
"event_code": "岐阜ロゲイニング2025",
"zekken_number": "001",
"checkin_ids": [123, 124, 125],
"approval_comment": "通過履歴を確認し、すべて正確です"
}
```
**パラメータ説明**:
- `event_code`: イベントコード(必須)
- `zekken_number`: ゼッケン番号(必須)
- `checkin_ids`: 承認するチェックインIDのリスト必須
- `approval_comment`: 承認コメント(任意)
**レスポンス(成功時)**:
```json
{
"status": "OK",
"message": "通過履歴の承認が完了しました",
"approved_count": 3,
"approved_checkins": [
{
"checkin_id": 123,
"cp_number": "1",
"approved_at": "2025-09-02 15:30:00"
},
{
"checkin_id": 124,
"cp_number": "5",
"approved_at": "2025-09-02 15:30:00"
},
{
"checkin_id": 125,
"cp_number": "8",
"approved_at": "2025-09-02 15:30:00"
}
],
"team_info": {
"team_name": "チーム名",
"zekken_number": "001",
"event_code": "岐阜ロゲイニング2025"
}
}
```
**レスポンス(エラー時)**:
```json
{
"status": "ERROR",
"message": "指定されたチェックイン記録が見つかりません",
"error_details": {
"invalid_checkin_ids": [126, 127],
"valid_checkin_ids": [123, 124, 125]
}
}
```
**レスポンス(権限エラー時)**:
```json
{
"status": "ERROR",
"message": "このチームの通過履歴を承認する権限がありません"
}
```
### 9.4 写真一括アップロード・通過履歴校正
スマホアルバムから複数の写真を一括アップロードし、EXIF情報から自動的に通過履歴を生成・校正する処理を行います。
**エンドポイント**: `POST /api/bulk_upload_checkin_photos/`
**認証**: 必須Token認証
**Content-Type**: `multipart/form-data`
**リクエストパラメータ**:
```
event_code: 岐阜ロゲイニング2025 (必須)
zekken_number: 001 (必須)
photos: [file1.jpg, file2.jpg, file3.jpg, ...] (必須・最大50ファイル)
auto_process: true (任意・デフォルト: true)
```
**パラメータ説明**:
- `event_code`: イベントコード(必須)
- `zekken_number`: ゼッケン番号(必須)
- `photos`: アップロードする写真ファイルのリスト必須・最大50ファイル
- `auto_process`: EXIF情報から自動処理を行うかどうか任意・デフォルト: true
**対応ファイル形式**: JPG, JPEG, PNG, HEIC
**ファイルサイズ制限**: 1ファイルあたり最大10MB
**レスポンス(成功時)**:
```json
{
"status": "OK",
"message": "写真の一括アップロードが完了しました",
"upload_summary": {
"total_files": 15,
"successful_uploads": 12,
"failed_uploads": 3,
"upload_time": "2025-09-02 16:30:00"
},
"team_info": {
"team_name": "チーム名",
"zekken_number": "001",
"event_code": "岐阜ロゲイニング2025"
},
"processed_files": [
{
"filename": "photo001.jpg",
"file_index": 1,
"file_size": 2048576,
"status": "uploaded",
"saved_path": "bulk_checkin_photos/岐阜ロゲイニング2025/001/20250902_163000_001_photo001.jpg",
"file_url": "/media/bulk_checkin_photos/岐阜ロゲイニング2025/001/20250902_163000_001_photo001.jpg",
"upload_time": "2025-09-02 16:30:00",
"auto_process_status": "pending",
"auto_process_message": "EXIF解析機能は今後実装予定です"
},
{
"filename": "photo002.jpg",
"file_index": 2,
"status": "failed",
"error": "サポートされていないファイル形式: .bmp"
}
],
"auto_process_enabled": true,
"next_steps": [
"アップロードされた写真のEXIF情報解析",
"GPS座標からチェックポイント自動判定",
"通過履歴の自動生成と校正",
"ユーザーによる確認と承認"
]
}
```
**レスポンス(エラー時)**:
```json
{
"status": "ERROR",
"message": "一度にアップロードできる写真は最大50枚です"
}
```
**レスポンス(権限エラー時)**:
```json
{
"status": "ERROR",
"message": "このチームの写真をアップロードする権限がありません"
}
```
### 9.5 一括アップロード状況取得
写真一括アップロードの処理状況を取得します。
**エンドポイント**: `GET /api/get_bulk_upload_status/`
**認証**: 必須Token認証
**リクエストパラメータ**:
```
event_code=岐阜ロゲイニング2025&zekken_number=001
```
**レスポンス(成功時)**:
```json
{
"status": "OK",
"team_info": {
"team_name": "チーム名",
"zekken_number": "001",
"event_code": "岐阜ロゲイニング2025"
},
"upload_status": {
"total_uploaded_files": 15,
"processed_files": 12,
"pending_files": 3,
"auto_checkins_generated": 8,
"manual_review_required": 4
},
"implementation_status": "基本機能実装完了、詳細処理は今後実装予定"
}
```
**処理フロー**:
1. **写真アップロード**: 複数の写真ファイルを安全にサーバーに保存
2. **EXIF情報解析**: GPS座標、撮影時刻などのメタデータを抽出今後実装
3. **自動チェックイン判定**: GPS座標から最寄りのチェックポイントを特定今後実装
4. **通過履歴生成**: 撮影時刻順にソートして通過履歴を自動生成(今後実装)
5. **ユーザー確認**: 生成された履歴をユーザーが確認・修正(今後実装)
6. **履歴確定**: 承認されたチェックイン記録をデータベースに保存(今後実装)
**今後の実装予定機能**:
- EXIF情報からのGPS座標・撮影時刻自動抽出
- チェックポイント自動判定アルゴリズム
- 時系列自動ソート機能
- 重複チェック・エラー検出機能
- ユーザーによる手動校正インターフェース
---
## 10. エントリー管理API
@ -821,31 +1075,83 @@ curl -X PUT "/entry/123/update-status/" \
9. **スタート**
```bash
curl -X POST "/start_from_rogapp" -d '{"event_code":"岐阜ロゲイニング2025","team_name":"サンプルチーム"}'
curl -X POST "/api/start_from_rogapp" -d '{"event_code":"岐阜ロゲイニング2025","team_name":"サンプルチーム"}'
```
10. **チェックイン**
```bash
curl -X POST "/checkin_from_rogapp" -d '{"event_code":"岐阜ロゲイニング2025","team_name":"サンプルチーム","cp_number":1,"image":"https://example.com/photo1.jpg"}'
curl -X POST "/api/checkin_from_rogapp" -d '{"event_code":"岐阜ロゲイニング2025","team_name":"サンプルチーム","cp_number":1,"image":"https://example.com/photo1.jpg"}'
```
11. **ゴール**
```bash
curl -X POST "/goal_from_rogapp" -d '{"event_code":"岐阜ロゲイニング2025","team_name":"サンプルチーム","image":"https://example.com/goal.jpg"}'
curl -X POST "/api/goal_from_rogapp" -d '{"event_code":"岐阜ロゲイニング2025","team_name":"サンプルチーム","image":"https://example.com/goal.jpg"}'
```
12. **ゴール後ステータス更新**
12. **通過履歴承認**
```bash
curl -X POST "/api/approve_checkin_history" -H "Authorization: Token <token>" -d '{"event_code":"岐阜ロゲイニング2025","zekken_number":"001","checkin_ids":[123,124,125],"approval_comment":"通過履歴確認完了"}'
```
13. **写真一括アップロード(アルバムから通過履歴校正)**
```bash
curl -X POST "/api/bulk_upload_checkin_photos/" -H "Authorization: Token <token>" -F "event_code=岐阜ロゲイニング2025" -F "zekken_number=001" -F "auto_process=true" -F "photos=@photo1.jpg" -F "photos=@photo2.jpg" -F "photos=@photo3.jpg"
```
14. **一括アップロード状況確認**
```bash
curl -X GET "/api/get_bulk_upload_status/?event_code=岐阜ロゲイニング2025&zekken_number=001" -H "Authorization: Token <token>"
```
15. **ゴール後ステータス更新**
```bash
curl -X PUT "/entry/123/update-status/" -H "Authorization: Token <token>" -d '{"hasParticipated": true, "hasGoaled": true}'
```
13. **証明書取得**
16. **証明書取得**
```bash
curl "/download_scoreboard?zekken_number=101&event_code=岐阜ロゲイニング2025"
curl "/api/download_scoreboard?zekken_number=101&event_code=岐阜ロゲイニング2025"
```
---
## 更新履歴
### 2025年9月2日 - APIテスト結果による仕様書修正
#### 修正内容
1. **エンドポイントURL修正**
- 全ての競技系APIに `/api/` プレフィックスを追加
- 実際のエンドポイント名に合わせて修正
2. **未実装API情報の追加**
- `/api/get_checkpoints` - チェックポイント一覧取得(未実装)
- `/api/current_ranking` - 現在ランキング(未実装)
- `/api/get_teams` - チーム一覧取得(未実装)
3. **一時的に無効化されているAPI**
- `/api/getRoute` - データベース構造変更により一時的に無効
4. **レスポンス形式の修正**
- `getCheckinList` APIのレスポンス構造を実際の仕様に合わせて修正
- エラーレスポンスの実際の形式を追記
5. **動作確認済みAPI**
- `start_from_rogapp` - スタート処理
- `checkin_from_rogapp` - チェックイン処理
- `goal_from_rogapp` - ゴール処理
- `input_cp` - QRコードチェックイン
- `remove_checkin_from_rogapp` - チェックイン削除
- `get_waypoint_datas_from_rogapp` - ウェイポイントデータ収集
- `getCheckinList` - チェックイン履歴取得
#### 注意事項の追加
- URL構成についての説明を追加
- 未実装APIの明記
- 一時的に無効化されているAPIの説明
---
## 自動証明書生成について
ゴール処理(`goal_from_rogapp`)が完了すると、システムは自動的に以下を実行します: