diff --git a/.github/workflows/ci2develop.yml b/.github/workflows/ci2develop.yml
index dec35d9c6..cc65672fd 100644
--- a/.github/workflows/ci2develop.yml
+++ b/.github/workflows/ci2develop.yml
@@ -6,6 +6,7 @@ on:
- "deployment/**"
- "scripts/**"
- ".github/**"
+ - "kubernetes/**"
branches: [develop]
workflow_dispatch:
@@ -53,9 +54,7 @@ jobs:
context: ./backend
file: ./backend/Dockerfile
push: true
- tags: |
- ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/backend:${{ github.sha }}-dev
- ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/backend:latest
+ tags: ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/backend:${{ github.sha }}-dev
ci-frontend-dev:
needs: [detect-changes-by-component]
@@ -81,9 +80,84 @@ jobs:
context: ./frontend
file: ./frontend/Dockerfile
push: true
- tags: |
- ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/frontend:${{ github.sha }}-dev
- ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/frontend:latest
+ tags: ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/frontend:${{ github.sha }}-dev
build-args: |
SERVER_NAME=${{ secrets.DEV_SERVER_NAME }}
APP_VERSION=${{ github.sha }}-dev
+
+ ci-hub-auth-dev:
+ needs: [detect-changes-by-component]
+ if: ${{ needs.detect-changes-by-component.outputs.hub-auth == 'true' }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Login to Harbor
+ uses: docker/login-action@v1
+ with:
+ registry: ${{ secrets.HARBOR_REGISTRY }}
+ username: ${{ secrets.HARBOR_USERNAME }}
+ password: ${{ secrets.HARBOR_PASSWORD }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v2
+ with:
+ context: ./hub/auth_server
+ file: ./hub/auth_server/Dockerfile
+ push: true
+ tags: ${{ secrets.HARBOR_REGISTRY }}/code-place-dev/hub-auth:${{ github.sha }}-dev
+
+ update-dev-manifest:
+ needs: [ci-backend-dev, ci-frontend-dev, ci-hub-auth-dev]
+ if: |
+ always() &&
+ (needs.ci-backend-dev.result == 'success' || needs.ci-frontend-dev.result == 'success' || needs.ci-hub-auth-dev.result == 'success')
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.ACTION_TOKEN }}
+ fetch-depth: 0
+
+ # NOTE: 기존에 kustomize를 사용하였지만 포멧 문제로 yq로 변경 (junwoo)
+ # images.[] .name 에 맞는 항목의 newTag 값을 github.sha-dev로 변경
+ - name: Update Backend Image Tag
+ if: needs.ci-backend-dev.result == 'success'
+ uses: mikefarah/yq@master
+ with:
+ cmd: yq -i '(.images[] | select(.name == "backend").newTag) = "${{ github.sha }}-dev"' kubernetes/overlays/dev/kustomization.yaml
+
+ - name: Update Frontend Image Tag
+ if: needs.ci-frontend-dev.result == 'success'
+ uses: mikefarah/yq@master
+ with:
+ cmd: yq -i '(.images[] | select(.name == "frontend").newTag) = "${{ github.sha }}-dev"' kubernetes/overlays/dev/kustomization.yaml
+
+ - name: Update Hub Auth Image Tag
+ if: needs.ci-hub-auth-dev.result == 'success'
+ uses: mikefarah/yq@master
+ with:
+ cmd: yq -i '(.images[] | select(.name == "hub-auth").newTag) = "${{ github.sha }}-dev"' kubernetes/overlays/dev/kustomization.yaml
+
+ - name: Commit and push changes
+ run: |
+ git config --global user.name 'github-actions[bot]'
+ git config --global user.email 'github-actions[bot]@users.noreply.github.com'
+ git add kubernetes/overlays/dev/kustomization.yaml
+
+ # 변경사항이 없으면 종료
+ if git diff-index --quiet HEAD; then
+ echo "No changes to commit"
+ exit 0
+ fi
+
+ # 충돌 방지를 위해 최신 develop 브랜치로 rebase 후 push
+ # [skip ci] 태그를 커밋 메시지에 추가하여 추가 CI 실행을 방지
+ git commit -m "ci: Update dev image tags to ${{ github.sha }} [skip ci]"
+ git pull --rebase origin ${{ github.ref_name }}
+ git push origin ${{ github.ref_name }}
diff --git a/.github/workflows/ci2production-and-release.yml b/.github/workflows/ci2production-and-release.yml
index 6937c5b93..a06b52340 100644
--- a/.github/workflows/ci2production-and-release.yml
+++ b/.github/workflows/ci2production-and-release.yml
@@ -5,6 +5,7 @@ on:
paths-ignore:
- "deployment/**"
- "scripts/**"
+ - "kubernetes/**"
branches: [main]
workflow_dispatch:
@@ -81,9 +82,7 @@ jobs:
context: ./backend
file: ./backend/Dockerfile
push: true
- tags: |
- ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/backend:${{ github.sha }}-prod
- ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/backend:latest
+ tags: ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/backend:${{ github.sha }}-prod
ci-frontend:
needs: [detect-changes-by-component]
@@ -109,9 +108,7 @@ jobs:
context: ./frontend
file: ./frontend/Dockerfile
push: true
- tags: |
- ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/frontend:${{ github.sha }}-prod
- ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/frontend:latest
+ tags: ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/frontend:${{ github.sha }}-prod
build-args: |
SERVER_NAME=${{ secrets.PROD_SERVER_NAME }}
APP_VERSION=${{ github.sha }}
@@ -140,17 +137,65 @@ jobs:
context: ./hub/auth_server
file: ./hub/auth_server/Dockerfile
push: true
- tags: |
- ${{ secrets.HARBOR_REGISTRY }}/code-place-hub/auth-server:${{ github.sha }}
- ${{ secrets.HARBOR_REGISTRY }}/code-place-hub/auth-server:latest
+ tags: ${{ secrets.HARBOR_REGISTRY }}/code-place-prod/hub-auth:${{ github.sha }}-prod
+
+ update-prod-manifest:
+ needs: [check-release-branch, ci-backend, ci-frontend, ci-hub-auth]
+ if: |
+ always() &&
+ needs.check-release-branch.outputs.is_release == 'true' &&
+ (needs.ci-frontend.result == 'success' || needs.ci-backend.result == 'success' || needs.ci-hub-auth.result == 'success')
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ token: ${{ secrets.ACTION_TOKEN }}
+ fetch-depth: 0
+
+ # NOTE: 기존에 kustomize를 사용하였지만 포멧 문제로 yq로 변경 (junwoo)
+ # images.[] .name 에 맞는 항목의 newTag 값을 github.sha 로 변경
+ - name: Update Backend Image Tag
+ if: needs.ci-backend.result == 'success'
+ uses: mikefarah/yq@master
+ with:
+ cmd: yq -i '(.images[] | select(.name == "backend").newTag) = "${{ github.sha }}-prod"' kubernetes/overlays/prod/kustomization.yaml
+
+ - name: Update Frontend Image Tag
+ if: needs.ci-frontend.result == 'success'
+ uses: mikefarah/yq@master
+ with:
+ cmd: yq -i '(.images[] | select(.name == "frontend").newTag) = "${{ github.sha }}-prod"' kubernetes/overlays/prod/kustomization.yaml
+
+ - name: Update Hub Auth Image Tag
+ if: needs.ci-hub-auth.result == 'success'
+ uses: mikefarah/yq@master
+ with:
+ cmd: yq -i '(.images[] | select(.name == "hub-auth").newTag) = "${{ github.sha }}-prod"' kubernetes/overlays/prod/kustomization.yaml
+
+ - name: Commit and push changes
+ run: |
+ git config --global user.name 'github-actions[bot]'
+ git config --global user.email 'github-actions[bot]@users.noreply.github.com'
+ git add kubernetes/overlays/prod/kustomization.yaml
+
+ # 변경사항이 없으면 종료
+ if git diff-index --quiet HEAD; then
+ echo "No changes to commit"
+ exit 0
+ fi
+
+ # [skip ci] 태그를 커밋 메시지에 추가하여 추가 CI 실행을 방지
+ git commit -m "ci: Update prod image tags to ${{ github.sha }} [skip ci]"
+ git pull --rebase origin main
+ git push origin main
create-release:
- needs: [check-release-branch, ci-frontend, ci-backend]
+ needs: [check-release-branch, update-prod-manifest]
if: |
always() &&
needs.check-release-branch.outputs.is_release == 'true' &&
- (needs.ci-frontend.result == 'success' || needs.ci-frontend.result == 'skipped') &&
- (needs.ci-backend.result == 'success' || needs.ci-backend.result == 'skipped')
+ (needs.update-prod-manifest.result == 'success' || needs.update-prod-manifest.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Checkout Code
diff --git a/.style.yapf b/.style.yapf
index b3d849f2d..4ef8ba7ff 100644
--- a/.style.yapf
+++ b/.style.yapf
@@ -1,3 +1,6 @@
[style]
-based_on_style = google
+based_on_style = yapf
column_limit = 120
+spaces_before_comment = 4
+split_before_logical_operator = true
+indent_width = 4
\ No newline at end of file
diff --git a/backend/account/migrations/0001_initial.py b/backend/account/migrations/0001_initial.py
index a608a0a5f..f93d375b0 100644
--- a/backend/account/migrations/0001_initial.py
+++ b/backend/account/migrations/0001_initial.py
@@ -48,10 +48,9 @@ class Migration(migrations.Migration):
name='UserScore',
fields=[
('user',
- models.OneToOneField(on_delete=django.db.models.deletion.CASCADE,
- primary_key=True,
- serialize=False,
- to='account.user')),
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False,
+ to='account.user')),
('yesterday_score', models.IntegerField(default=0)),
('total_score', models.IntegerField(default=0)),
('fluctuation', models.IntegerField(default=0)),
@@ -77,10 +76,9 @@ class Migration(migrations.Migration):
name='UserSolved',
fields=[
('user',
- models.OneToOneField(on_delete=django.db.models.deletion.CASCADE,
- primary_key=True,
- serialize=False,
- to='account.user')),
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False,
+ to='account.user')),
('math_solved', models.BigIntegerField(default=0)),
('implementation_solved', models.BigIntegerField(default=0)),
('datastructure_solved', models.BigIntegerField(default=0)),
@@ -105,13 +103,13 @@ class Migration(migrations.Migration):
('oi_problems_status', models.JSONField(default=dict)),
('real_name', models.TextField(null=True)),
('student_id',
- models.CharField(max_length=9,
- null=True,
- validators=[
- django.core.validators.RegexValidator(code='invalid_student_id',
- message='학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.',
- regex='^\\d{6,9}$')
- ])),
+ models.CharField(
+ max_length=9,
+ null=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ code='invalid_student_id', message='학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.', regex='^\\d{6,9}$')
+ ])),
('avatar', models.TextField(default='/public/avatar/default.png')),
('blog', models.URLField(null=True)),
('mood', models.TextField(null=True)),
diff --git a/backend/account/models.py b/backend/account/models.py
index c0842f0d8..7cf728d0d 100644
--- a/backend/account/models.py
+++ b/backend/account/models.py
@@ -73,11 +73,11 @@ class Meta:
def get_default_field_score():
return {
- "0": 0, # "Math"
- "1": 0, # "Implementation"
- "2": 0, # "Datastructure"
- "3": 0, # "Search"
- "4": 0, # "Sorting"
+ "0": 0, # "Math"
+ "1": 0, # "Implementation"
+ "2": 0, # "Datastructure"
+ "3": 0, # "Search"
+ "4": 0, # "Sorting"
}
@@ -121,13 +121,12 @@ class UserProfile(models.Model):
oi_problems_status = JSONField(default=dict)
real_name = models.TextField(null=True)
- student_id = models.CharField(max_length=9,
- null=True,
- validators=[
- RegexValidator(regex=r'^\d{6,9}$',
- message='학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.',
- code='invalid_student_id'),
- ])
+ student_id = models.CharField(
+ max_length=9,
+ null=True,
+ validators=[
+ RegexValidator(regex=r'^\d{6,9}$', message='학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.', code='invalid_student_id'),
+ ])
avatar = models.TextField(default=f"{settings.AVATAR_URI_PREFIX}/default.png")
blog = models.URLField(null=True)
mood = models.TextField(null=True)
diff --git a/backend/account/serializers.py b/backend/account/serializers.py
index 8557e04b0..4f5e06909 100644
--- a/backend/account/serializers.py
+++ b/backend/account/serializers.py
@@ -23,22 +23,22 @@ class UserRegisterSerializer(serializers.Serializer):
username = serializers.CharField(min_length=3, max_length=8)
real_name = serializers.CharField(max_length=13)
email = serializers.EmailField(max_length=64)
- password = serializers.RegexField(regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
- min_length=8,
- error_messages={'invalid': '비밀번호는 8글자 이상이어야 하며, 영문, 숫자, 특수문자를 모두 포함해야 합니다.'})
- student_id = serializers.RegexField(regex=r'^\d{6,9}$',
- min_length=6,
- max_length=9,
- error_messages={'invalid': '학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.'})
+ password = serializers.RegexField(
+ regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
+ min_length=8,
+ error_messages={'invalid': '비밀번호는 8글자 이상이어야 하며, 영문, 숫자, 특수문자를 모두 포함해야 합니다.'})
+ student_id = serializers.RegexField(
+ regex=r'^\d{6,9}$', min_length=6, max_length=9, error_messages={'invalid': '학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.'})
collegeId = serializers.IntegerField()
departmentId = serializers.IntegerField()
class UserChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField()
- new_password = serializers.RegexField(regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
- min_length=8,
- error_messages={'invalid': '비밀번호는 8글자 이상이어야 하며, 영문, 숫자, 특수문자를 모두 포함해야 합니다.'})
+ new_password = serializers.RegexField(
+ regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
+ min_length=8,
+ error_messages={'invalid': '비밀번호는 8글자 이상이어야 하며, 영문, 숫자, 특수문자를 모두 포함해야 합니다.'})
class GenerateUserSerializer(serializers.Serializer):
@@ -130,25 +130,25 @@ class EditUserSerializer(serializers.Serializer):
id = serializers.IntegerField()
username = serializers.CharField(min_length=3, max_length=8)
real_name = serializers.CharField(max_length=13)
- password = serializers.RegexField(allow_null=True,
- allow_blank=True,
- regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
- min_length=8,
- error_messages={'invalid': '비밀번호는 8글자 이상이어야 하며, 영문, 숫자, 특수문자를 모두 포함해야 합니다.'})
+ password = serializers.RegexField(
+ allow_null=True,
+ allow_blank=True,
+ regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
+ min_length=8,
+ error_messages={'invalid': '비밀번호는 8글자 이상이어야 하며, 영문, 숫자, 특수문자를 모두 포함해야 합니다.'})
# email = serializers.EmailField(max_length=64)
admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN))
- problem_permission = serializers.ChoiceField(choices=(ProblemPermission.NONE, ProblemPermission.OWN,
- ProblemPermission.ALL))
+ problem_permission = serializers.ChoiceField(
+ choices=(ProblemPermission.NONE, ProblemPermission.OWN, ProblemPermission.ALL))
college = serializers.IntegerField(allow_null=True)
department = serializers.IntegerField(allow_null=True)
- student_id = serializers.CharField(max_length=9,
- allow_null=True,
- allow_blank=True,
- validators=[
- RegexValidator(regex=r'^\d{6,9}$',
- message='학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.',
- code='invalid_student_id'),
- ])
+ student_id = serializers.CharField(
+ max_length=9,
+ allow_null=True,
+ allow_blank=True,
+ validators=[
+ RegexValidator(regex=r'^\d{6,9}$', message='학번은 6자리 이상 9자리 이하의 숫자만 입력 가능합니다.', code='invalid_student_id'),
+ ])
open_api = serializers.BooleanField()
two_factor_auth = serializers.BooleanField()
is_disabled = serializers.BooleanField()
@@ -161,8 +161,8 @@ class ApplyResetPasswordSerializer(serializers.Serializer):
class ResetPasswordSerializer(serializers.Serializer):
token = serializers.CharField()
- password = serializers.RegexField(regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$',
- min_length=8)
+ password = serializers.RegexField(
+ regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$', min_length=8)
captcha = serializers.CharField()
diff --git a/backend/account/tasks.py b/backend/account/tasks.py
index fdf898b66..fdcf41dab 100644
--- a/backend/account/tasks.py
+++ b/backend/account/tasks.py
@@ -15,12 +15,13 @@ def send_email_async(from_name, to_email, to_name, subject, content):
if not SysOptions.smtp_config:
return
try:
- send_email(smtp_config=SysOptions.smtp_config,
- from_name=from_name,
- to_email=to_email,
- to_name=to_name,
- subject=subject,
- content=content)
+ send_email(
+ smtp_config=SysOptions.smtp_config,
+ from_name=from_name,
+ to_email=to_email,
+ to_name=to_name,
+ subject=subject,
+ content=content)
except Exception as e:
logger.exception(e)
diff --git a/backend/account/tests.py b/backend/account/tests.py
index 6eaaad219..7aecf0536 100644
--- a/backend/account/tests.py
+++ b/backend/account/tests.py
@@ -448,7 +448,7 @@ def test_successful_calculate_user_score_fluctuation(self):
user_score.refresh_from_db()
- self.assertEqual(user_score.fluctuation, 200) # 300 - 100 = 200
+ self.assertEqual(user_score.fluctuation, 200) # 300 - 100 = 200
def test_database_error_calculate_user_score_fluctuation(self):
user_score = self.user.userscore
@@ -460,7 +460,7 @@ def test_database_error_calculate_user_score_fluctuation(self):
with mock.patch('account.models.UserScore.objects.update', side_effect=DatabaseError("Test Database Error")):
with self.assertRaises(DatabaseError):
calculate_user_score_fluctuation()
- self.assertEqual(user_score.fluctuation, 0) # Fluctuation should not change if error occurs
+ self.assertEqual(user_score.fluctuation, 0) # Fluctuation should not change if error occurs
# class UserChangePasswordAPITest(APITestCase):
diff --git a/backend/account/views/admin.py b/backend/account/views/admin.py
index 22969ddb5..5fcf1b3dc 100644
--- a/backend/account/views/admin.py
+++ b/backend/account/views/admin.py
@@ -40,8 +40,9 @@ def get(self, request):
}
# 학과별 사용자 수 통계
- department_stats = (User.objects.values('userprofile__major').annotate(value=Count('id')).values(
- 'value', name=F('userprofile__major')).order_by('-value'))
+ department_stats = (
+ User.objects.values('userprofile__major').annotate(value=Count('id')).values(
+ 'value', name=F('userprofile__major')).order_by('-value'))
# None 값 처리 (학과가 지정되지 않은 사용자)
department_stats = list(department_stats)
@@ -56,8 +57,9 @@ def get(self, request):
start_date = datetime(current_year, 1, 1)
end_date = datetime(current_year, 12, 31)
- monthly_stats = (User.objects.filter(create_time__range=(start_date, end_date)).annotate(
- month=TruncMonth('create_time')).values('month').annotate(count=Count('id')).order_by('month'))
+ monthly_stats = (
+ User.objects.filter(create_time__range=(start_date, end_date)).annotate(
+ month=TruncMonth('create_time')).values('month').annotate(count=Count('id')).order_by('month'))
months = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']
counts = [0] * 12
@@ -70,11 +72,13 @@ def get(self, request):
# 주간 가입자 수 통계
today = datetime.now().date()
- start_of_week = today - timedelta(days=today.weekday()) # 이번 주 월요일
- end_of_week = start_of_week + timedelta(days=6) # 이번 주 일요일
+ start_of_week = today - timedelta(days=today.weekday()) # 이번 주 월요일
+ end_of_week = start_of_week + timedelta(days=6) # 이번 주 일요일
- weekly_stats = (User.objects.filter(create_time__date__range=(start_of_week, end_of_week)).annotate(
- weekday=ExtractWeekDay('create_time')).values('weekday').annotate(count=Count('id')).order_by('weekday'))
+ weekly_stats = (
+ User.objects.filter(create_time__date__range=(start_of_week, end_of_week)).annotate(
+ weekday=ExtractWeekDay('create_time')).values('weekday').annotate(
+ count=Count('id')).order_by('weekday'))
weekdays = ['월', '화', '수', '목', '금', '토', '일']
counts = [0] * 7
@@ -178,10 +182,8 @@ def put(self, request):
if data["college"] and data["department"]:
college = College.objects.get(id=data["college"])
department = Department.objects.get(id=data["department"])
- UserProfile.objects.filter(user=user).update(college=college,
- department=department,
- school=college.college_name,
- major=department.department_name)
+ UserProfile.objects.filter(user=user).update(
+ college=college, department=department, school=college.college_name, major=department.department_name)
if data["student_id"]:
UserProfile.objects.filter(user=user).update(student_id=data["student_id"])
@@ -231,15 +233,16 @@ def get(self, request):
user = user.filter(admin_type=AdminType.REGULAR_USER)
if admin_type == "Admin":
user = user.exclude(admin_type=AdminType.REGULAR_USER).order_by(
- Case(When(admin_type=AdminType.SUPER_ADMIN, then=Value(0)),
- default=Value(1),
- output_field=IntegerField()), 'id')
+ Case(
+ When(admin_type=AdminType.SUPER_ADMIN, then=Value(0)),
+ default=Value(1),
+ output_field=IntegerField()), 'id')
keyword = request.GET.get("keyword", None)
if keyword:
user = user.filter(
- Q(username__icontains=keyword) | Q(userprofile__real_name__icontains=keyword) |
- Q(email__icontains=keyword))
+ Q(username__icontains=keyword) | Q(userprofile__real_name__icontains=keyword)
+ | Q(email__icontains=keyword))
college = request.GET.get("college", None)
if college:
diff --git a/backend/account/views/oj.py b/backend/account/views/oj.py
index 933236ec4..3fdb5d215 100644
--- a/backend/account/views/oj.py
+++ b/backend/account/views/oj.py
@@ -246,13 +246,14 @@ def post(self, request):
user = User.objects.create(username=data["username"], email=data["email"])
user.set_password(data["password"])
user.save()
- user_profile = UserProfile.objects.create(user=user,
- school=college.college_name,
- major=department.department_name,
- college=college,
- department=department,
- real_name=data["real_name"],
- student_id=data["student_id"])
+ user_profile = UserProfile.objects.create(
+ user=user,
+ school=college.college_name,
+ major=department.department_name,
+ college=college,
+ department=department,
+ real_name=data["real_name"],
+ student_id=data["student_id"])
user_profile.save()
user_score = UserScore.objects.create(user=user)
user_score.save()
diff --git a/backend/announcement/models.py b/backend/announcement/models.py
index ac609a1d6..d2b487850 100644
--- a/backend/announcement/models.py
+++ b/backend/announcement/models.py
@@ -16,4 +16,7 @@ class Announcement(models.Model):
class Meta:
db_table = "announcement"
- ordering = ("-is_pinned", "-create_time",)
\ No newline at end of file
+ ordering = (
+ "-is_pinned",
+ "-create_time",
+ )
diff --git a/backend/announcement/tests.py b/backend/announcement/tests.py
index 786222be1..b18668a97 100644
--- a/backend/announcement/tests.py
+++ b/backend/announcement/tests.py
@@ -15,7 +15,13 @@ def test_announcement_list(self):
self.assertSuccess(response)
def create_announcement(self):
- return self.client.post(self.url, data={"title": "test", "content": "test", "visible": True, "is_pinned": False})
+ return self.client.post(
+ self.url, data={
+ "title": "test",
+ "content": "test",
+ "visible": True,
+ "is_pinned": False
+ })
def test_create_announcement(self):
resp = self.create_announcement()
@@ -45,14 +51,8 @@ def test_edit_announcement_not_exist(self):
"""
존재하지 않는 공지사항 수정 test
"""
- data = {
- "id": -9999,
- "title": "error trial",
- "content": "test content",
- "visible": False,
- "is_pinned": False
- }
- resp=self.client.put(self.url,data=data)
+ data = {"id": -9999, "title": "error trial", "content": "test content", "visible": False, "is_pinned": False}
+ resp = self.client.put(self.url, data=data)
self.assertFailed(resp)
self.assertEqual(resp.data["data"], "Announcement does not exist")
@@ -77,7 +77,7 @@ def test_get_one_specific_announcement_not_exist(self):
"""
존재하지 않는 id 로 공지사항 하나 조회 test
"""
- resp=self.client.get(self.url + "?id=-99999")
+ resp = self.client.get(self.url + "?id=-99999")
self.assertFailed(resp)
self.assertEqual(resp.data["data"], "Announcement does not exist")
@@ -85,26 +85,11 @@ def test_get_visible_announcement_list(self):
"""
visible 설정된 공지사항 리스트 가져오기 test
"""
- self.client.post(self.url, data={
- "title": "보임1",
- "content": "test",
- "visible": True,
- "is_pinned": False
- })
- self.client.post(self.url, data={
- "title": "안보임1",
- "content": "test",
- "visible": False,
- "is_pinned": False
- })
- self.client.post(self.url, data={
- "title": "보임2",
- "content": "test",
- "visible": True,
- "is_pinned": False
- })
+ self.client.post(self.url, data={"title": "보임1", "content": "test", "visible": True, "is_pinned": False})
+ self.client.post(self.url, data={"title": "안보임1", "content": "test", "visible": False, "is_pinned": False})
+ self.client.post(self.url, data={"title": "보임2", "content": "test", "visible": True, "is_pinned": False})
resp = self.client.get(self.url + "?visible=true")
- self.assertEqual(len(resp.data["data"]["results"]),2)
+ self.assertEqual(len(resp.data["data"]["results"]), 2)
titles = [announcement["title"] for announcement in resp.data["data"]["results"]]
self.assertIn("보임1", titles)
self.assertIn("보임2", titles)
@@ -113,43 +98,24 @@ def test_create_pinned_announcement(self):
"""
고정된 공지사항 생성 test
"""
- resp = self.client.post(self.url, data={
- "title": "고정 공지사항",
- "content": "test",
- "visible": True,
- "is_pinned": True
- })
+ resp = self.client.post(
+ self.url, data={
+ "title": "고정 공지사항",
+ "content": "test",
+ "visible": True,
+ "is_pinned": True
+ })
self.assertSuccess(resp)
self.assertEqual(resp.data["data"]["is_pinned"], True)
-
+
def test_get_notice_list_in_order(self):
"""
공지사항 리스트 조회 시, 고정 공지사항 우선 로드 여부 test
"""
- self.client.post(self.url, data={
- "title": "고정1",
- "content": "test",
- "visible": True,
- "is_pinned": True
- })
- self.client.post(self.url, data={
- "title": "고정아님1",
- "content": "test",
- "visible": True,
- "is_pinned": False
- })
- self.client.post(self.url, data={
- "title": "고정2",
- "content": "test",
- "visible": True,
- "is_pinned": True
- })
- self.client.post(self.url, data={
- "title": "고정아님2",
- "content": "test",
- "visible": True,
- "is_pinned": False
- })
+ self.client.post(self.url, data={"title": "고정1", "content": "test", "visible": True, "is_pinned": True})
+ self.client.post(self.url, data={"title": "고정아님1", "content": "test", "visible": True, "is_pinned": False})
+ self.client.post(self.url, data={"title": "고정2", "content": "test", "visible": True, "is_pinned": True})
+ self.client.post(self.url, data={"title": "고정아님2", "content": "test", "visible": True, "is_pinned": False})
resp = self.client.get(self.url)
self.assertSuccess(resp)
@@ -160,23 +126,19 @@ def test_get_notice_list_in_order(self):
self.assertTrue(results[1]["is_pinned"])
self.assertFalse(results[2]["is_pinned"])
self.assertFalse(results[3]["is_pinned"])
-
+
pinned_titles = {results[0]["title"], results[1]["title"]}
self.assertIn("고정1", pinned_titles)
self.assertIn("고정2", pinned_titles)
-
-
+
class AnnouncementAPITest(APITestCase):
def setUp(self):
self.create_school_fixtures(college_id=1, college_name="Test", department_id=1, department_name="Test")
self.user = self.create_super_admin()
- self.announcement = Announcement.objects.create(title="title",
- content="content",
- is_pinned=False,
- visible=True,
- created_by=self.user)
+ self.announcement = Announcement.objects.create(
+ title="title", content="content", is_pinned=False, visible=True, created_by=self.user)
self.url = self.reverse("announcement_api")
def test_get_announcement_list(self):
diff --git a/backend/announcement/views/admin.py b/backend/announcement/views/admin.py
index 5082e79e6..afd054a54 100644
--- a/backend/announcement/views/admin.py
+++ b/backend/announcement/views/admin.py
@@ -14,11 +14,12 @@ def post(self, request):
publish announcement
"""
data = request.data
- announcement = Announcement.objects.create(title=data["title"],
- content=data["content"],
- created_by=request.user,
- visible=data["visible"],
- is_pinned=data["is_pinned"])
+ announcement = Announcement.objects.create(
+ title=data["title"],
+ content=data["content"],
+ created_by=request.user,
+ visible=data["visible"],
+ is_pinned=data["is_pinned"])
return self.success(AnnouncementSerializer(announcement).data)
@validate_serializer(EditAnnouncementSerializer)
diff --git a/backend/banner/views/admin.py b/backend/banner/views/admin.py
index 788debd5f..6ae12f40d 100644
--- a/backend/banner/views/admin.py
+++ b/backend/banner/views/admin.py
@@ -52,10 +52,8 @@ def post(self, request):
filename = ContentUtil.saveContentWithRandomFileName(banner_image, settings.BANNER_DIR)
- new_banner = Banner(banner_image=f"{settings.BANNER_URI_PREFIX}/{filename}",
- link_url=link_url,
- visible=False,
- order=None)
+ new_banner = Banner(
+ banner_image=f"{settings.BANNER_URI_PREFIX}/{filename}", link_url=link_url, visible=False, order=None)
new_banner.save()
diff --git a/backend/community/migrations/0001_initial.py b/backend/community/migrations/0001_initial.py
index 9648f2b2e..062ddd0b5 100644
--- a/backend/community/migrations/0001_initial.py
+++ b/backend/community/migrations/0001_initial.py
@@ -23,14 +23,27 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('content', utils.models.RichTextField()),
- ('community_type', models.CharField(choices=[('GENERAL', '일반'), ('PROBLEM', '문제'), ('CONTEST', '대회')], default='GENERAL', max_length=20)),
- ('post_type', models.CharField(choices=[('QUESTION', '질문'), ('ARTICLE', '글'), ('ANNOUNCEMENT', '공지')], default='ARTICLE', max_length=20)),
- ('question_status', models.CharField(choices=[('OPEN', '진행중'), ('CLOSED', '해결됨')], default='OPEN', max_length=20)),
+ ('community_type',
+ models.CharField(
+ choices=[('GENERAL', '일반'), ('PROBLEM', '문제'), ('CONTEST', '대회')],
+ default='GENERAL',
+ max_length=20)),
+ ('post_type',
+ models.CharField(
+ choices=[('QUESTION', '질문'), ('ARTICLE', '글'), ('ANNOUNCEMENT', '공지')],
+ default='ARTICLE',
+ max_length=20)),
+ ('question_status',
+ models.CharField(choices=[('OPEN', '진행중'), ('CLOSED', '해결됨')], default='OPEN', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ('contest', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.contest')),
- ('problem', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='problem.problem')),
+ ('contest',
+ models.ForeignKey(
+ blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.contest')),
+ ('problem',
+ models.ForeignKey(
+ blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='problem.problem')),
],
options={
'ordering': ['-created_at'],
@@ -44,8 +57,16 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='community.comment')),
- ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='community.post')),
+ ('parent_comment',
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='replies',
+ to='community.comment')),
+ ('post',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='community.post')),
],
options={
'ordering': ['created_at'],
diff --git a/backend/community/models.py b/backend/community/models.py
index 08d07f20a..faf5655d8 100644
--- a/backend/community/models.py
+++ b/backend/community/models.py
@@ -48,7 +48,6 @@ class SortType(models.TextChoices):
OLDEST = "OLDEST", "오래된순"
COMMENT = "COMMENT", "댓글많은순"
-
title = models.CharField(max_length=200)
content = RichTextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -96,4 +95,4 @@ class Comment(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
- ordering = ["created_at"] # 댓글은 오래된 순서대로 정렬
+ ordering = ["created_at"] # 댓글은 오래된 순서대로 정렬
diff --git a/backend/community/tests.py b/backend/community/tests.py
index 5dbab2af0..90abacf61 100644
--- a/backend/community/tests.py
+++ b/backend/community/tests.py
@@ -311,38 +311,50 @@ def test_get_post_list_with_keyword(self):
"""키워드로 게시글을 필터링할 수 있다."""
# 테스트 데이터 준비
self.client.force_login(self.user)
- self.client.post(self.post_list_url, {"title":"HI Annyeong", "content" : "What Im saying is HIIIIIIII","post_type" : "ARTICLE" })
- self.client.post(self.post_list_url, {"title":"HI Annyeong", "content" : "What Im saying is HIIIIIIII","post_type" : "QUESTION" })
- self.client.post(self.post_list_url, {"title":"Bye Jalga", "content" : "What Im saying is B22222","post_type" : "ARTICLE" })
+ self.client.post(self.post_list_url, {
+ "title": "HI Annyeong",
+ "content": "What Im saying is HIIIIIIII",
+ "post_type": "ARTICLE"
+ })
+ self.client.post(self.post_list_url, {
+ "title": "HI Annyeong",
+ "content": "What Im saying is HIIIIIIII",
+ "post_type": "QUESTION"
+ })
+ self.client.post(self.post_list_url, {
+ "title": "Bye Jalga",
+ "content": "What Im saying is B22222",
+ "post_type": "ARTICLE"
+ })
# API 호출
- response = self.client.get(self.post_list_url, {"keyword" : "HI"})
+ response = self.client.get(self.post_list_url, {"keyword": "HI"})
# 상태값 검증
self.assertSuccess(response)
self.assertEqual(response.data["data"]["total"], 2)
# API 호출
- response = self.client.get(self.post_list_url, {"keyword" : "Bye"})
+ response = self.client.get(self.post_list_url, {"keyword": "Bye"})
# 상태값 검증
self.assertSuccess(response)
self.assertEqual(response.data["data"]["total"], 1)
# API 호출
- response = self.client.get(self.post_list_url, {"keyword" : "is"})
+ response = self.client.get(self.post_list_url, {"keyword": "is"})
# 상태값 검증
self.assertSuccess(response)
- self.assertEqual(response.data["data"]["total"],3)
-
+ self.assertEqual(response.data["data"]["total"], 3)
+
def test_get_post_list_order_by_newest(self):
"""게시글을 최신 순으로 정렬할 수 있다."""
#테스트 데이터 준비
self.client.force_login(self.user)
- self.client.post(self.post_list_url, {"title" : "Post1", "content": "content1", "post_type": "ARTICLE"})
- self.client.post(self.post_list_url, {"title" : "Post2", "content": "content2", "post_type": "QUESTION"})
- self.client.post(self.post_list_url, {"title" : "Post3", "content": "content3", "post_type": "ARTICLE"})
+ self.client.post(self.post_list_url, {"title": "Post1", "content": "content1", "post_type": "ARTICLE"})
+ self.client.post(self.post_list_url, {"title": "Post2", "content": "content2", "post_type": "QUESTION"})
+ self.client.post(self.post_list_url, {"title": "Post3", "content": "content3", "post_type": "ARTICLE"})
# API 호출
- response = self.client.get(self.post_list_url, {"sort_type" : "NEWEST"})
+ response = self.client.get(self.post_list_url, {"sort_type": "NEWEST"})
# 상태값 검증
self.assertSuccess(response)
results = response.data["data"]["results"]
@@ -356,12 +368,12 @@ def test_get_post_list_order_by_oldest(self):
self.general_post.delete()
#테스트 데이터 준비
self.client.force_login(self.user)
- self.client.post(self.post_list_url, {"title" : "Post1", "content": "content1", "post_type": "ARTICLE"})
- self.client.post(self.post_list_url, {"title" : "Post2", "content": "content2", "post_type": "QUESTION"})
- self.client.post(self.post_list_url, {"title" : "Post3", "content": "content3", "post_type": "ARTICLE"})
+ self.client.post(self.post_list_url, {"title": "Post1", "content": "content1", "post_type": "ARTICLE"})
+ self.client.post(self.post_list_url, {"title": "Post2", "content": "content2", "post_type": "QUESTION"})
+ self.client.post(self.post_list_url, {"title": "Post3", "content": "content3", "post_type": "ARTICLE"})
#API 호출
- response = self.client.get(self.post_list_url, {"sort_type" : "OLDEST"})
+ response = self.client.get(self.post_list_url, {"sort_type": "OLDEST"})
#상태값 검증
self.assertSuccess(response)
results = response.data["data"]["results"]
@@ -376,13 +388,13 @@ def test_get_post_list_order_by_comments(self):
#테스트 데이터 준비
self.client.force_login(self.user)
## Post1, 댓글 3개
- self.client.post(self.post_list_url, {"title" : "Post1", "content": "content1", "post_type": "ARTICLE"})
+ self.client.post(self.post_list_url, {"title": "Post1", "content": "content1", "post_type": "ARTICLE"})
post1 = Post.objects.get(title="Post1")
Comment.objects.create(post=post1, author=self.user, content="comment1")
Comment.objects.create(post=post1, author=self.user, content="comment2")
Comment.objects.create(post=post1, author=self.user, content="comment3")
## Post2, 댓글 5개
- self.client.post(self.post_list_url, {"title" : "Post2", "content": "content2", "post_type": "QUESTION"})
+ self.client.post(self.post_list_url, {"title": "Post2", "content": "content2", "post_type": "QUESTION"})
post2 = Post.objects.get(title="Post2")
Comment.objects.create(post=post2, author=self.user, content="comment1")
Comment.objects.create(post=post2, author=self.user, content="comment2")
@@ -390,10 +402,10 @@ def test_get_post_list_order_by_comments(self):
Comment.objects.create(post=post2, author=self.user, content="comment4")
Comment.objects.create(post=post2, author=self.user, content="comment5")
## Post3, 댓글 0개
- self.client.post(self.post_list_url, {"title" : "Post3", "content": "content3", "post_type": "ARTICLE"})
+ self.client.post(self.post_list_url, {"title": "Post3", "content": "content3", "post_type": "ARTICLE"})
#API 호출
- response = self.client.get(self.post_list_url, {"sort_type" : "COMMENT"})
+ response = self.client.get(self.post_list_url, {"sort_type": "COMMENT"})
#상태값 검증
self.assertSuccess(response)
results = response.data["data"]["results"]
@@ -465,11 +477,11 @@ def test_update_question_status_when_change_to_question_type(self):
# 작성한 일반글 url 생성
new_post_id = response.data["data"]["id"]
new_post_url = self.reverse("community_post_detail", kwargs={"post_id": new_post_id})
-
+
# 질문글로 변경
- response2 = self.client.patch(new_post_url, {"post_type":"QUESTION"})
+ response2 = self.client.patch(new_post_url, {"post_type": "QUESTION"})
self.assertSuccess(response2)
- self.assertEqual(response2.data["data"]["question_status"],"OPEN")
+ self.assertEqual(response2.data["data"]["question_status"], "OPEN")
def test_update_post_type_to_announcement_by_non_super_admin(self):
"""일반 사용자는 게시글을 ANNOUNCEMENT 타입으로 변경할 수 없다."""
diff --git a/backend/community/views/oj.py b/backend/community/views/oj.py
index 8ef8dda72..f60901123 100644
--- a/backend/community/views/oj.py
+++ b/backend/community/views/oj.py
@@ -95,11 +95,9 @@ def get(self, request):
if question_status:
posts = posts.filter(question_status=question_status)
-
+
if keyword:
- posts = posts.filter(
- Q(title__icontains=keyword) | Q(content__icontains=keyword)
- )
+ posts = posts.filter(Q(title__icontains=keyword) | Q(content__icontains=keyword))
if sort_type == Post.SortType.NEWEST:
posts = posts.order_by("-created_at")
@@ -166,7 +164,7 @@ def patch(self, request, post_id):
for key, value in data.items():
setattr(post, key, value)
-
+
# 질문 타입으로 변경 시, 상태가 없거나 이전 타입이 일반글이었다면 미해결로 초기화
if post.post_type == Post.PostType.QUESTION:
if old_post_type == Post.PostType.ARTICLE or not post.question_status:
@@ -200,8 +198,9 @@ def get(self, request, post_id):
except Post.DoesNotExist:
return self.error("Post does not exist")
- comments = (Comment.objects.filter(
- post_id=post_id).select_related("author").prefetch_related("replies__author").order_by("created_at"))
+ comments = (
+ Comment.objects.filter(
+ post_id=post_id).select_related("author").prefetch_related("replies__author").order_by("created_at"))
root_comments = comments.filter(parent_comment__isnull=True)
data = self.paginate_data(request, root_comments, CommentSerializer)
diff --git a/backend/conf/views.py b/backend/conf/views.py
index e33000bd3..503a45eb7 100644
--- a/backend/conf/views.py
+++ b/backend/conf/views.py
@@ -66,12 +66,13 @@ def post(self, request):
if not SysOptions.smtp_config:
return self.error("Please setup SMTP config at first")
try:
- send_email(smtp_config=SysOptions.smtp_config,
- from_name=SysOptions.website_name_shortcut,
- to_name=request.user.username,
- to_email=request.data["email"],
- subject="You have successfully configured SMTP",
- content="You have successfully configured SMTP")
+ send_email(
+ smtp_config=SysOptions.smtp_config,
+ from_name=SysOptions.website_name_shortcut,
+ to_name=request.user.username,
+ to_email=request.data["email"],
+ subject="You have successfully configured SMTP",
+ content="You have successfully configured SMTP")
except smtplib.SMTPResponseException as e:
# guess error message encoding
msg = b"Failed to send email"
@@ -225,9 +226,9 @@ class ReleaseNotesAPI(APIView):
def get(self, request):
try:
- resp = requests.get("https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" +
- str(time.time()),
- timeout=3)
+ resp = requests.get(
+ "https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" + str(time.time()),
+ timeout=3)
releases = resp.json()
except (RequestException, ValueError):
return self.success()
@@ -241,8 +242,9 @@ class DashboardInfoAPI(APIView):
def get(self, request):
today = datetime.today()
- today_submission_count = Submission.objects.filter(create_time__gte=datetime(
- today.year, today.month, today.day, 0, 0, tzinfo=timezone.get_current_timezone())).count()
+ today_submission_count = Submission.objects.filter(
+ create_time__gte=datetime(today.year, today.month, today.day, 0, 0,
+ tzinfo=timezone.get_current_timezone())).count()
recent_contest_count = Contest.objects.exclude(end_time__lt=timezone.now()).count()
judge_server_count = len(list(filter(lambda x: x.status == "normal", JudgeServer.objects.all())))
return self.success({
diff --git a/backend/contest/models.py b/backend/contest/models.py
index 211cdbf68..090beb481 100644
--- a/backend/contest/models.py
+++ b/backend/contest/models.py
@@ -1,4 +1,4 @@
-from utils.constants import ContestRuleType # noqa
+from utils.constants import ContestRuleType # noqa
from django.db import models
from django.utils.timezone import now
from utils.models import JSONField
diff --git a/backend/contest/views/admin.py b/backend/contest/views/admin.py
index 3bee351ea..a654aaef8 100644
--- a/backend/contest/views/admin.py
+++ b/backend/contest/views/admin.py
@@ -173,8 +173,8 @@ def delete(self, request):
contest_announcement_id = request.GET.get("id")
if contest_announcement_id:
if request.user.is_admin():
- ContestAnnouncement.objects.filter(id=contest_announcement_id,
- contest__created_by=request.user).delete()
+ ContestAnnouncement.objects.filter(
+ id=contest_announcement_id, contest__created_by=request.user).delete()
else:
ContestAnnouncement.objects.filter(id=contest_announcement_id).delete()
return self.success()
diff --git a/backend/contest/views/oj.py b/backend/contest/views/oj.py
index 6dbc370f6..4f04275c0 100644
--- a/backend/contest/views/oj.py
+++ b/backend/contest/views/oj.py
@@ -157,9 +157,8 @@ def get(self, request):
submissions = Submission.objects.filter(contest_id=contest_id)
- user_submissions = submissions.values('user_id',
- 'username').annotate(submission_count=Count('id'),
- last_submission_ip=Max('ip')).order_by('user_id')
+ user_submissions = submissions.values('user_id', 'username').annotate(
+ submission_count=Count('id'), last_submission_ip=Max('ip')).order_by('user_id')
# UserProfile 정보 가져오기
user_profiles = UserProfile.objects.filter(user_id__in=[sub['user_id'] for sub in user_submissions])
diff --git a/backend/judge/dispatcher.py b/backend/judge/dispatcher.py
index 9c4b157db..54a61927b 100644
--- a/backend/judge/dispatcher.py
+++ b/backend/judge/dispatcher.py
@@ -306,8 +306,8 @@ def update_problem_status(self):
user_profile.accepted_number += 1
elif oi_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED:
# minus last time score, add this time score
- user_profile.add_score(this_time_score=score,
- last_time_score=oi_problems_status[problem_id]["score"])
+ user_profile.add_score(
+ this_time_score=score, last_time_score=oi_problems_status[problem_id]["score"])
oi_problems_status[problem_id]["score"] = score
oi_problems_status[problem_id]["status"] = self.submission.result
if self.submission.result == JudgeStatus.ACCEPTED:
diff --git a/backend/oj/settings.py b/backend/oj/settings.py
index 9639550cd..51d705461 100644
--- a/backend/oj/settings.py
+++ b/backend/oj/settings.py
@@ -236,18 +236,18 @@ def make_key(key, key_prefix, version):
CELERY_BEAT_SCHEDULE = {
'calculate_user_score_basis': {
'task': 'account.tasks.calculate_user_score_basis',
- 'schedule': celery.schedules.crontab(minute=0, hour=0), # Every day at midnight
+ 'schedule': celery.schedules.crontab(minute=0, hour=0), # Every day at midnight
},
'calculate_user_score_fluctuation': {
'task': 'account.tasks.calculate_user_score_fluctuation',
- 'schedule': celery.schedules.crontab(minute='*/1'), # Every minute
+ 'schedule': celery.schedules.crontab(minute='*/1'), # Every minute
},
'update_weekly_stats': {
'task': 'problem.tasks.update_weekly_stats',
- 'schedule': celery.schedules.crontab(hour=0, minute=0, day_of_week='mon'), # Every Monday at midnight
+ 'schedule': celery.schedules.crontab(hour=0, minute=0, day_of_week='mon'), # Every Monday at midnight
},
'update_bonus_problem': {
'task': 'problem.tasks.update_bonus_problem',
- 'schedule': celery.schedules.crontab(hour=0, minute=0, day_of_week='mon'), # Every Monday at midnight
+ 'schedule': celery.schedules.crontab(hour=0, minute=0, day_of_week='mon'), # Every Monday at midnight
}
}
diff --git a/backend/popup/views/admin.py b/backend/popup/views/admin.py
index 0c0fd31e9..eba80d337 100644
--- a/backend/popup/views/admin.py
+++ b/backend/popup/views/admin.py
@@ -55,11 +55,12 @@ def post(self, request):
filename = ContentUtil.saveContentWithRandomFileName(popup_image, settings.POPUP_DIR)
- new_popup = Popup(popup_image=f"{settings.POPUP_URI_PREFIX}/{filename}",
- link_url=link_url,
- popup_image_width=image_width,
- visible=False,
- order=None)
+ new_popup = Popup(
+ popup_image=f"{settings.POPUP_URI_PREFIX}/{filename}",
+ link_url=link_url,
+ popup_image_width=image_width,
+ visible=False,
+ order=None)
new_popup.save()
diff --git a/backend/problem/tests.py b/backend/problem/tests.py
index f929b6128..b10f19409 100644
--- a/backend/problem/tests.py
+++ b/backend/problem/tests.py
@@ -126,8 +126,8 @@ def setUp(self):
self.create_super_admin()
def test_filter_file_name(self):
- self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in", ".DS_Store"], spj=False),
- ["1.in", "1.out"])
+ self.assertEqual(
+ self.api.filter_name_list(["1.in", "1.out", "2.in", ".DS_Store"], spj=False), ["1.in", "1.out"])
self.assertEqual(self.api.filter_name_list(["2.in", "2.out"], spj=False), [])
self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in"], spj=True), ["1.in", "2.in"])
diff --git a/backend/problem/views/admin.py b/backend/problem/views/admin.py
index fa41d629a..1127c47b6 100644
--- a/backend/problem/views/admin.py
+++ b/backend/problem/views/admin.py
@@ -596,10 +596,9 @@ class ExportProblemAPI(APIView):
def choose_answers(self, user, problem):
ret = []
for item in problem.languages:
- submission = Submission.objects.filter(problem=problem,
- user_id=user.id,
- language=item,
- result=JudgeStatus.ACCEPTED).order_by("-create_time").first()
+ submission = Submission.objects.filter(
+ problem=problem, user_id=user.id, language=item,
+ result=JudgeStatus.ACCEPTED).order_by("-create_time").first()
if submission:
ret.append({"language": submission.language, "code": submission.code})
return ret
@@ -608,20 +607,21 @@ def process_one_problem(self, zip_file, user, problem, index):
info = ExportProblemSerializer(problem).data
info["answers"] = self.choose_answers(user, problem=problem)
compression = zipfile.ZIP_DEFLATED
- zip_file.writestr(zinfo_or_arcname=f"{index}/problem.json",
- data=json.dumps(info, indent=4),
- compress_type=compression)
+ zip_file.writestr(
+ zinfo_or_arcname=f"{index}/problem.json", data=json.dumps(info, indent=4), compress_type=compression)
problem_test_case_dir = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id)
with open(os.path.join(problem_test_case_dir, "info")) as f:
info = json.load(f)
for k, v in info["test_cases"].items():
- zip_file.write(filename=os.path.join(problem_test_case_dir, v["input_name"]),
- arcname=f"{index}/testcase/{v['input_name']}",
- compress_type=compression)
+ zip_file.write(
+ filename=os.path.join(problem_test_case_dir, v["input_name"]),
+ arcname=f"{index}/testcase/{v['input_name']}",
+ compress_type=compression)
if not info["spj"]:
- zip_file.write(filename=os.path.join(problem_test_case_dir, v["output_name"]),
- arcname=f"{index}/testcase/{v['output_name']}",
- compress_type=compression)
+ zip_file.write(
+ filename=os.path.join(problem_test_case_dir, v["output_name"]),
+ arcname=f"{index}/testcase/{v['output_name']}",
+ compress_type=compression)
@validate_serializer(ExportProblemRequestSerialzier)
def get(self, request):
@@ -750,28 +750,29 @@ def _create_problem(self, problem_data, creator):
our_lang = "Python3"
template[our_lang] = TEMPLATE_BASE.format(prepend.get(lang, ""), t["code"], append.get(lang, ""))
spj = problem_data["spj"] is not None
- Problem.objects.create(_id=f"fps-{rand_str(4)}",
- title=problem_data["title"],
- description=problem_data["description"],
- input_description=problem_data["input"],
- output_description=problem_data["output"],
- hint=problem_data["hint"],
- test_case_score=problem_data["test_case_score"],
- time_limit=time_limit,
- memory_limit=problem_data["memory_limit"]["value"],
- samples=problem_data["samples"],
- template=template,
- rule_type=ProblemRuleType.ACM,
- source=problem_data.get("source", ""),
- spj=spj,
- spj_code=problem_data["spj"]["code"] if spj else None,
- spj_language=problem_data["spj"]["language"] if spj else None,
- spj_version=rand_str(8) if spj else "",
- visible=False,
- languages=SysOptions.language_names,
- created_by=creator,
- difficulty=Difficulty.MID,
- test_case_id=problem_data["test_case_id"])
+ Problem.objects.create(
+ _id=f"fps-{rand_str(4)}",
+ title=problem_data["title"],
+ description=problem_data["description"],
+ input_description=problem_data["input"],
+ output_description=problem_data["output"],
+ hint=problem_data["hint"],
+ test_case_score=problem_data["test_case_score"],
+ time_limit=time_limit,
+ memory_limit=problem_data["memory_limit"]["value"],
+ samples=problem_data["samples"],
+ template=template,
+ rule_type=ProblemRuleType.ACM,
+ source=problem_data.get("source", ""),
+ spj=spj,
+ spj_code=problem_data["spj"]["code"] if spj else None,
+ spj_language=problem_data["spj"]["language"] if spj else None,
+ spj_version=rand_str(8) if spj else "",
+ visible=False,
+ languages=SysOptions.language_names,
+ created_by=creator,
+ difficulty=Difficulty.MID,
+ test_case_id=problem_data["test_case_id"])
def post(self, request):
form = UploadProblemForm(request.POST, request.FILES)
diff --git a/backend/problem/views/oj.py b/backend/problem/views/oj.py
index 25775248b..582606339 100644
--- a/backend/problem/views/oj.py
+++ b/backend/problem/views/oj.py
@@ -81,7 +81,7 @@ def get(self, request):
.get(_id=problem_id, contest_id__isnull=True, visible=True)
problem_data = ProblemSerializer(problem).data
self._add_problem_status(request, problem_data)
- problem_data["allow_paste"] = True # 기본 문제는 복사 붙여넣기 허용
+ problem_data["allow_paste"] = True # 기본 문제는 복사 붙여넣기 허용
return self.success(problem_data)
except Problem.DoesNotExist:
return self.error("Problem does not exist")
@@ -133,9 +133,8 @@ def get(self, request):
problem_id = request.GET.get("problem_id")
if problem_id:
try:
- problem = Problem.objects.select_related("created_by").get(_id=problem_id,
- contest=self.contest,
- visible=True)
+ problem = Problem.objects.select_related("created_by").get(
+ _id=problem_id, contest=self.contest, visible=True)
except Problem.DoesNotExist:
return self.error("Problem does not exist.")
if self.contest.problem_details_permission(request.user):
@@ -145,7 +144,7 @@ def get(self, request):
])
else:
problem_data = ProblemSafeSerializer(problem).data
- problem_data["allow_paste"] = self.contest.allow_paste # 대회의 복사 붙여넣기 허용 여부 반환
+ problem_data["allow_paste"] = self.contest.allow_paste # 대회의 복사 붙여넣기 허용 여부 반환
return self.success(problem_data)
contest_problems = Problem.objects.select_related("created_by").filter(contest=self.contest, visible=True)
@@ -157,8 +156,8 @@ def get(self, request):
submission_state_info = {}
if request.user:
- contest_user_submissions = Submission.objects.filter(contest=self.contest,
- username__exact=request.user.username)
+ contest_user_submissions = Submission.objects.filter(
+ contest=self.contest, username__exact=request.user.username)
accepted_submissions = set()
partially_accepted_submissions = set()
failed_submissions = set()
@@ -232,8 +231,8 @@ def get(self, request):
# candidate_problems = Problem.objects.filter(
# difficulty__in=difficulty, visible=True, contest__isnull=True).exclude(_id__in=user_solved_problem)
- candidate_problems = Problem.objects.filter(visible=True,
- contest__isnull=True).exclude(_id__in=user_solved_problem)
+ candidate_problems = Problem.objects.filter(
+ visible=True, contest__isnull=True).exclude(_id__in=user_solved_problem)
recommend_problems = random.sample(list(candidate_problems), min(3, len(candidate_problems)))
if recommend_problems:
diff --git a/backend/profile/serializers.py b/backend/profile/serializers.py
index 6119f125a..61f120e98 100644
--- a/backend/profile/serializers.py
+++ b/backend/profile/serializers.py
@@ -29,10 +29,8 @@ class EditUserProfileSerializer(serializers.Serializer):
github = serializers.URLField(max_length=256, allow_blank=True, required=False, allow_null=True)
college = serializers.IntegerField(allow_null=True, required=False)
department = serializers.IntegerField(allow_null=True, required=False)
- language = serializers.ChoiceField(allow_null=True,
- allow_blank=True,
- required=False,
- choices=["C", "C++", "Java", "Python3", "JavaScript"])
+ language = serializers.ChoiceField(
+ allow_null=True, allow_blank=True, required=False, choices=["C", "C++", "Java", "Python3", "JavaScript"])
class ProfileProblemSerializer(serializers.Serializer):
diff --git a/backend/profile/views/oj.py b/backend/profile/views/oj.py
index d3a873ed4..1cde4d200 100644
--- a/backend/profile/views/oj.py
+++ b/backend/profile/views/oj.py
@@ -74,10 +74,10 @@ def get(self, request):
user_id = user_profile.user_id
user_score = UserScore.objects.filter(user_id=user_id).annotate(
total_rank=Count('total_score', filter=Q(total_score__gt=F('total_score'))) + 1,
- datastructure_rank=Count('datastructure_score',
- filter=Q(datastructure_score__gt=F('datastructure_score'))) + 1,
- implementation_rank=Count('implementation_score',
- filter=Q(implementation_score__gt=F('implementation_score'))) + 1,
+ datastructure_rank=Count(
+ 'datastructure_score', filter=Q(datastructure_score__gt=F('datastructure_score'))) + 1,
+ implementation_rank=Count(
+ 'implementation_score', filter=Q(implementation_score__gt=F('implementation_score'))) + 1,
math_rank=Count('math_score', filter=Q(math_score__gt=F('math_score'))) + 1,
search_rank=Count('search_score', filter=Q(search_score__gt=F('search_score'))) + 1,
sorting_rank=Count('sorting_score', filter=Q(sorting_score__gt=F('sorting_score'))) + 1,
@@ -190,8 +190,8 @@ def get(self, request):
return self.error("Invalid date format. Should be YYYY-MM-DD.")
if start_date and end_date:
- submissions = submissions.filter(create_time__gte=start_date,
- create_time__lt=(end_date + datetime.timedelta(days=1)))
+ submissions = submissions.filter(
+ create_time__gte=start_date, create_time__lt=(end_date + datetime.timedelta(days=1)))
elif start_date:
submissions = submissions.filter(create_time__gte=start_date)
elif end_date:
diff --git a/backend/ranking/serializers.py b/backend/ranking/serializers.py
index 362c31d8c..85fb7cdfd 100644
--- a/backend/ranking/serializers.py
+++ b/backend/ranking/serializers.py
@@ -20,11 +20,11 @@ def get_username(self, obj):
class UserRankListSerializer(serializers.ModelSerializer):
rank = serializers.SerializerMethodField()
avatar = serializers.SerializerMethodField()
- username = serializers.CharField() # 수정
- mood = serializers.CharField(source='userprofile.mood') # 수정
- score = serializers.IntegerField(source='userscore.total_score') # 수정
- major = serializers.CharField(source='userprofile.major') # 수정
- tier = serializers.CharField(source='userscore.tier') # 수정
+ username = serializers.CharField() # 수정
+ mood = serializers.CharField(source='userprofile.mood') # 수정
+ score = serializers.IntegerField(source='userscore.total_score') # 수정
+ major = serializers.CharField(source='userprofile.major') # 수정
+ tier = serializers.CharField(source='userscore.tier') # 수정
solved = serializers.SerializerMethodField()
growth = serializers.SerializerMethodField()
@@ -41,7 +41,7 @@ def get_avatar(self, obj):
def get_growth(self, obj):
return obj.userscore.fluctuation
- def get_solved(self, obj): # 추가
+ def get_solved(self, obj): # 추가
return obj.usersolved.total_solved
diff --git a/backend/submission/models.py b/backend/submission/models.py
index 4241bdf51..381510e5a 100644
--- a/backend/submission/models.py
+++ b/backend/submission/models.py
@@ -44,8 +44,8 @@ class Submission(models.Model):
first_failed_tc_idx = models.IntegerField(null=True, default=None)
def check_user_permission(self, user, check_share=True):
- if (self.user_id == user.id or user.is_super_admin() or user.can_mgmt_all_problem() or
- self.problem.created_by_id == user.id):
+ if (self.user_id == user.id or user.is_super_admin() or user.can_mgmt_all_problem()
+ or self.problem.created_by_id == user.id):
return True
if check_share:
diff --git a/backend/submission/tests.py b/backend/submission/tests.py
index 119025343..e0695f7a4 100644
--- a/backend/submission/tests.py
+++ b/backend/submission/tests.py
@@ -312,26 +312,28 @@ def setUp(self):
self.url = self.reverse("submission_rank_api")
def test_get_submission_rank_success(self):
- submission = self.create_submission(self.user,
- self.problem,
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": 100,
- "memory_cost": 1024
- })
+ submission = self.create_submission(
+ self.user,
+ self.problem,
+ result=JudgeStatus.ACCEPTED,
+ statistic_info={
+ "time_cost": 100,
+ "memory_cost": 1024
+ })
resp = self.client.get(f"{self.url}?submission_id={submission.id}")
self.assertSuccess(resp)
self.assertIn("solved_rank", resp.data["data"])
def test_statistic_info_invalid(self):
- submission = self.create_submission(self.user,
- self.problem,
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": "invalid",
- "memory_cost": "invalid"
- })
+ submission = self.create_submission(
+ self.user,
+ self.problem,
+ result=JudgeStatus.ACCEPTED,
+ statistic_info={
+ "time_cost": "invalid",
+ "memory_cost": "invalid"
+ })
resp = self.client.get(f"{self.url}?submission_id={submission.id}")
self.assertFailed(resp, "Invalid submission statistic_info")
@@ -344,26 +346,28 @@ def test_statistic_info_invalid(self):
self.assertFailed(resp, "Invalid submission statistic_info")
def test_reject_contest_problem(self):
- submission = self.create_submission(self.user,
- self.contest_problem,
- contest_id=self.contest["id"],
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": 100,
- "memory_cost": 1024
- })
+ submission = self.create_submission(
+ self.user,
+ self.contest_problem,
+ contest_id=self.contest["id"],
+ result=JudgeStatus.ACCEPTED,
+ statistic_info={
+ "time_cost": 100,
+ "memory_cost": 1024
+ })
resp = self.client.get(f"{self.url}?submission_id={submission.id}")
self.assertFailed(resp, "This API is not available for contest submissions")
def test_reject_unauthorized_user(self):
- submission = self.create_submission(user=self.user,
- problem=self.problem,
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": 100,
- "memory_cost": 1024
- })
+ submission = self.create_submission(
+ user=self.user,
+ problem=self.problem,
+ result=JudgeStatus.ACCEPTED,
+ statistic_info={
+ "time_cost": 100,
+ "memory_cost": 1024
+ })
# 다른 유저로 요청
other_user = self.create_user(
@@ -384,34 +388,28 @@ def test_get_submission_rank_multiple_users(self):
problem = self.problem
# user1: Accepted, time_cost 100, memory_cost 1024, 제출 시각 가장 빠름
- sub1 = self.create_submission(user1,
- problem,
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": 100,
- "memory_cost": 1024
- })
+ sub1 = self.create_submission(
+ user1, problem, result=JudgeStatus.ACCEPTED, statistic_info={
+ "time_cost": 100,
+ "memory_cost": 1024
+ })
# 인위적으로 생성 시간 조절 (예: sub1가 가장 먼저 제출)
Submission.objects.filter(id=sub1.id).update(create_time=timezone.now() - timedelta(minutes=10))
# user2: Accepted, time_cost 200, memory_cost 2048
- sub2 = self.create_submission(user2,
- problem,
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": 200,
- "memory_cost": 2048
- })
+ sub2 = self.create_submission(
+ user2, problem, result=JudgeStatus.ACCEPTED, statistic_info={
+ "time_cost": 200,
+ "memory_cost": 2048
+ })
Submission.objects.filter(id=sub2.id).update(create_time=timezone.now() - timedelta(minutes=5))
# user3: Accepted, time_cost 150, memory_cost 1024
- sub3 = self.create_submission(user3,
- problem,
- result=JudgeStatus.ACCEPTED,
- statistic_info={
- "time_cost": 150,
- "memory_cost": 1024
- })
+ sub3 = self.create_submission(
+ user3, problem, result=JudgeStatus.ACCEPTED, statistic_info={
+ "time_cost": 150,
+ "memory_cost": 1024
+ })
Submission.objects.filter(id=sub3.id).update(create_time=timezone.now() - timedelta(minutes=1))
# user3의 제출물로 랭크 조회
@@ -444,13 +442,11 @@ def test_get_submission_rank_nonexistent(self):
def test_get_submission_rank_total_zero(self):
# Accepted된 submission이 없는 상태: 새로 제출 생성 (Accepted 외 상태로)
- submission = self.create_submission(self.user,
- self.problem,
- result=JudgeStatus.PENDING,
- statistic_info={
- "time_cost": 100,
- "memory_cost": 1024
- })
+ submission = self.create_submission(
+ self.user, self.problem, result=JudgeStatus.PENDING, statistic_info={
+ "time_cost": 100,
+ "memory_cost": 1024
+ })
resp = self.client.get(f"{self.url}?submission_id={submission.id}")
self.assertSuccess(resp)
diff --git a/backend/submission/views/oj.py b/backend/submission/views/oj.py
index b4c201652..f8d22e9b2 100644
--- a/backend/submission/views/oj.py
+++ b/backend/submission/views/oj.py
@@ -73,13 +73,14 @@ def post(self, request):
return self.error("Problem not exist")
if data["language"] not in problem.languages:
return self.error(f"{data['language']} is now allowed in the problem")
- submission = Submission.objects.create(user_id=request.user.id,
- username=request.user.username,
- language=data["language"],
- code=data["code"],
- problem_id=problem.id,
- ip=request.session["ip"],
- contest_id=data.get("contest_id"))
+ submission = Submission.objects.create(
+ user_id=request.user.id,
+ username=request.user.username,
+ language=data["language"],
+ code=data["code"],
+ problem_id=problem.id,
+ ip=request.session["ip"],
+ contest_id=data.get("contest_id"))
# use this for debug
# JudgeDispatcher(submission.id, problem.id).judge()
@@ -184,7 +185,7 @@ def get(self, request):
# 사용자 필터링
if (myself and myself == "1") or not SysOptions.submission_list_show_all:
submissions = submissions.filter(user_id=request.user.id)
- elif username and not contest_id: # 대회에서는 username 검색 제한
+ elif username and not contest_id: # 대회에서는 username 검색 제한
submissions = submissions.filter(username__icontains=username)
if result:
@@ -242,8 +243,8 @@ def get(self, request):
if not request.GET.get("problem_id"):
return self.error("Parameter error, problem_id is required")
return self.success(
- request.user.is_authenticated and
- Submission.objects.filter(problem_id=request.GET["problem_id"], user_id=request.user.id).exists())
+ request.user.is_authenticated
+ and Submission.objects.filter(problem_id=request.GET["problem_id"], user_id=request.user.id).exists())
class SubmissionRankAPI(APIView):
diff --git a/backend/utils/api/__init__.py b/backend/utils/api/__init__.py
index 9384481ce..f7269029c 100644
--- a/backend/utils/api/__init__.py
+++ b/backend/utils/api/__init__.py
@@ -1,2 +1,2 @@
-from ._serializers import * # NOQA
-from .api import * # NOQA
+from ._serializers import * # NOQA
+from .api import * # NOQA
diff --git a/backend/utils/api/tests.py b/backend/utils/api/tests.py
index ce056ea63..abbb34cc9 100644
--- a/backend/utils/api/tests.py
+++ b/backend/utils/api/tests.py
@@ -22,20 +22,19 @@ def create_user(self,
admin_type=AdminType.REGULAR_USER,
login=True,
problem_permission=ProblemPermission.NONE):
- user = User.objects.create(email=email,
- username=username,
- admin_type=admin_type,
- problem_permission=problem_permission)
+ user = User.objects.create(
+ email=email, username=username, admin_type=admin_type, problem_permission=problem_permission)
user.set_password(password)
school = College.objects.get(id=college_id)
major = Department.objects.get(id=department_id)
- UserProfile.objects.create(user=user,
- real_name=real_name,
- student_id=student_id,
- college_id=college_id,
- department_id=department_id,
- school=school,
- major=major)
+ UserProfile.objects.create(
+ user=user,
+ real_name=real_name,
+ student_id=student_id,
+ college_id=college_id,
+ department_id=department_id,
+ school=school,
+ major=major)
UserScore.objects.create(user=user)
UserSolved.objects.create(user=user)
user.save()
@@ -50,20 +49,22 @@ def create_school_fixtures(self, college_id, college_name, department_id, depart
department.save()
def create_admin(self, email="admin@admin.com", username="admin", password="admin1234!", login=True):
- return self.create_user(email=email,
- username=username,
- password=password,
- admin_type=AdminType.ADMIN,
- problem_permission=ProblemPermission.OWN,
- login=login)
+ return self.create_user(
+ email=email,
+ username=username,
+ password=password,
+ admin_type=AdminType.ADMIN,
+ problem_permission=ProblemPermission.OWN,
+ login=login)
def create_super_admin(self, email="root@root.com", username="root", password="root1234!", login=True):
- return self.create_user(email=email,
- username=username,
- password=password,
- admin_type=AdminType.SUPER_ADMIN,
- problem_permission=ProblemPermission.ALL,
- login=login)
+ return self.create_user(
+ email=email,
+ username=username,
+ password=password,
+ admin_type=AdminType.SUPER_ADMIN,
+ problem_permission=ProblemPermission.ALL,
+ login=login)
def reverse(self, url_name, *args, **kwargs):
return reverse(url_name, *args, **kwargs)
diff --git a/backend/utils/cache.py b/backend/utils/cache.py
index 61b38d9ba..73b0f2fcb 100644
--- a/backend/utils/cache.py
+++ b/backend/utils/cache.py
@@ -1,5 +1,5 @@
-from django.core.cache import cache, caches # noqa
-from django.conf import settings # noqa
+from django.core.cache import cache, caches # noqa
+from django.conf import settings # noqa
from django_redis.cache import RedisCache
from django_redis.client.default import DefaultClient
diff --git a/backend/utils/constants.py b/backend/utils/constants.py
index ace1abd82..a5a95847a 100644
--- a/backend/utils/constants.py
+++ b/backend/utils/constants.py
@@ -68,11 +68,11 @@ class ProblemField:
class ProblemScore:
score = {
- 'VeryLow': 10, # VeryLow
- 'Low': 20, # Low
- 'Mid': 160, # Mid
- 'High': 640, # High
- 'VeryHigh': 1280, # VeryHigh
+ 'VeryLow': 10, # VeryLow
+ 'Low': 20, # Low
+ 'Mid': 160, # Mid
+ 'High': 640, # High
+ 'VeryHigh': 1280, # VeryHigh
}
diff --git a/backend/utils/contest_ranking_writer.py b/backend/utils/contest_ranking_writer.py
index bff96265e..0015514e8 100644
--- a/backend/utils/contest_ranking_writer.py
+++ b/backend/utils/contest_ranking_writer.py
@@ -41,7 +41,7 @@ def __init__(self, contest, data):
},
}
self.common_participant_info_tag = ["순위", "닉네임", "실명", "이메일", "단과대학", "학과명", "학번"]
- self.table_start_pos_y = "7" # 대회 결과 테이블이 작성되기 시작하는 row number
+ self.table_start_pos_y = "7" # 대회 결과 테이블이 작성되기 시작하는 row number
@staticmethod
def get_ac_state_name(is_ac):
diff --git a/backend/utils/management/commands/inituser.py b/backend/utils/management/commands/inituser.py
index 72329aa38..24e5d5fbb 100644
--- a/backend/utils/management/commands/inituser.py
+++ b/backend/utils/management/commands/inituser.py
@@ -1,7 +1,7 @@
from django.core.management.base import BaseCommand
from account.models import AdminType, ProblemPermission, User, UserProfile, UserScore, UserSolved
-from utils.shortcuts import rand_str # NOQA
+from utils.shortcuts import rand_str # NOQA
class Command(BaseCommand):
@@ -25,10 +25,11 @@ def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored"))
exit()
- user = User.objects.create(username=username,
- email=username,
- admin_type=AdminType.SUPER_ADMIN,
- problem_permission=ProblemPermission.ALL)
+ user = User.objects.create(
+ username=username,
+ email=username,
+ admin_type=AdminType.SUPER_ADMIN,
+ problem_permission=ProblemPermission.ALL)
user.set_password(password)
user.save()
UserProfile.objects.create(user=user)
@@ -37,10 +38,8 @@ def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS("User created"))
elif action == "create_admin":
- user = User.objects.create(username=username,
- email=username,
- admin_type=AdminType.ADMIN,
- problem_permission=ProblemPermission.OWN)
+ user = User.objects.create(
+ username=username, email=username, admin_type=AdminType.ADMIN, problem_permission=ProblemPermission.OWN)
user.set_password(password)
user.save()
UserProfile.objects.create(user=user)
diff --git a/backend/utils/models.py b/backend/utils/models.py
index e70dd97fc..7e8e78bbd 100644
--- a/backend/utils/models.py
+++ b/backend/utils/models.py
@@ -1,4 +1,4 @@
-from django.db.models import JSONField # NOQA
+from django.db.models import JSONField # NOQA
from django.db import models
from utils.xss_filter import XSSHtml
diff --git a/backend/utils/shortcuts.py b/backend/utils/shortcuts.py
index 2f65104b3..16b1f5027 100644
--- a/backend/utils/shortcuts.py
+++ b/backend/utils/shortcuts.py
@@ -80,15 +80,14 @@ def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
def send_email(smtp_config, from_name, to_email, to_name, subject, content):
- envelope = Envelope(from_addr=(smtp_config["email"], from_name),
- to_addr=(to_email, to_name),
- subject=subject,
- html_body=content)
- return envelope.send(smtp_config["server"],
- login=smtp_config["email"],
- password=smtp_config["password"],
- port=smtp_config["port"],
- tls=smtp_config["tls"])
+ envelope = Envelope(
+ from_addr=(smtp_config["email"], from_name), to_addr=(to_email, to_name), subject=subject, html_body=content)
+ return envelope.send(
+ smtp_config["server"],
+ login=smtp_config["email"],
+ password=smtp_config["password"],
+ port=smtp_config["port"],
+ tls=smtp_config["tls"])
def get_env(name, default=""):
diff --git a/backend/utils/testcase_cache.py b/backend/utils/testcase_cache.py
index 9206f8d01..bd5606a45 100644
--- a/backend/utils/testcase_cache.py
+++ b/backend/utils/testcase_cache.py
@@ -12,7 +12,7 @@ class TestCaseCacheManager:
"""테스트 케이스의 Input/Output을 캐싱하는 클래스입니다."""
CACHE_PREFIX = "testcase"
- DEFAULT_TIMEOUT = 60 * 60 # 1 시간
+ DEFAULT_TIMEOUT = 60 * 60 # 1 시간
@classmethod
def _get_cache_key(cls, testcase_dir: str, testcase_idx: int) -> str:
diff --git a/backend/utils/tests.py b/backend/utils/tests.py
index 3149843fd..7e2f85efc 100644
--- a/backend/utils/tests.py
+++ b/backend/utils/tests.py
@@ -82,7 +82,7 @@ def test_get_testcase_cache_miss_no_file_data(self, mock_read_file):
self.assertIsNone(result)
mock_read_file.assert_called_once_with(testcase_dir, testcase_idx)
- @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
+ @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
def test_read_testcase_invalid_directory_name(self, mock_logger):
"""잘못된 디렉토리 이름 검증 테스트"""
result = TestCaseCacheManager._read_testcase_from_file("problem-1", 1)
@@ -91,7 +91,7 @@ def test_read_testcase_invalid_directory_name(self, mock_logger):
mock_logger.error.assert_called_with(
"Invalid testcase directory name. Only alphanumeric characters are allowed.")
- @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
+ @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
def test_read_testcase_invalid_index_negative(self, mock_logger):
"""음수 인덱스 검증 테스트"""
result = TestCaseCacheManager._read_testcase_from_file("problem1", -1)
@@ -99,7 +99,7 @@ def test_read_testcase_invalid_index_negative(self, mock_logger):
self.assertIsNone(result)
mock_logger.error.assert_called_with("Invalid testcase index. It must be a non-negative integer.")
- @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
+ @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
def test_read_testcase_invalid_index_non_integer(self, mock_logger):
"""정수가 아닌 인덱스 검증 테스트"""
result = TestCaseCacheManager._read_testcase_from_file("problem1", "invalid")
@@ -109,14 +109,14 @@ def test_read_testcase_invalid_index_non_integer(self, mock_logger):
@override_settings(TEST_CASE_DIR="/test/cases")
@patch('os.path.abspath')
- @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
+ @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
def test_read_testcase_path_traversal_attack(self, mock_logger, mock_abspath):
"""경로 순회 공격 방지 테스트"""
# abspath 모킹 - 악성 경로로 설정
mock_abspath.side_effect = [
- "/test/cases", # base_dir
- "/etc/passwd", # input_file_path (악성)
- "/test/cases/problem1/1.out" # output_file_path
+ "/test/cases", # base_dir
+ "/etc/passwd", # input_file_path (악성)
+ "/test/cases/problem1/1.out" # output_file_path
]
result = TestCaseCacheManager._read_testcase_from_file("problem1", 1)
@@ -130,9 +130,9 @@ def test_read_testcase_path_traversal_attack(self, mock_logger, mock_abspath):
def test_read_testcase_from_file_success(self, mock_abspath, mock_file):
"""파일 읽기 성공 테스트"""
mock_abspath.side_effect = [
- "/test/cases", # base_dir
- "/test/cases/problem5/5.in", # input_file_path
- "/test/cases/problem5/5.out" # output_file_path
+ "/test/cases", # base_dir
+ "/test/cases/problem5/5.in", # input_file_path
+ "/test/cases/problem5/5.out" # output_file_path
]
mock_file.side_effect = [
@@ -151,13 +151,13 @@ def test_read_testcase_from_file_success(self, mock_abspath, mock_file):
@override_settings(TEST_CASE_DIR="/test/cases")
@patch("builtins.open", side_effect=FileNotFoundError("File not found"))
@patch('os.path.abspath')
- @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
+ @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
def test_read_testcase_from_file_not_found(self, mock_logger, mock_abspath, mock_open_file):
"""파일이 존재하지 않는 경우 테스트"""
mock_abspath.side_effect = [
- "/test/cases", # base_dir
- "/test/cases/problem6/6.in", # input_file_path
- "/test/cases/problem6/6.out" # output_file_path
+ "/test/cases", # base_dir
+ "/test/cases/problem6/6.in", # input_file_path
+ "/test/cases/problem6/6.out" # output_file_path
]
result = TestCaseCacheManager._read_testcase_from_file("problem6", 6)
@@ -170,13 +170,13 @@ def test_read_testcase_from_file_not_found(self, mock_logger, mock_abspath, mock
@override_settings(TEST_CASE_DIR="/test/cases")
@patch("builtins.open", side_effect=IOError("IO Error"))
@patch('os.path.abspath')
- @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
+ @patch('utils.testcase_cache.logger') # 실제 모듈 경로에 맞게 수정
def test_read_testcase_from_file_io_error(self, mock_logger, mock_abspath, mock_open_file):
"""IO 에러 발생 시 테스트"""
mock_abspath.side_effect = [
- "/test/cases", # base_dir
- "/test/cases/problem7/7.in", # input_file_path
- "/test/cases/problem7/7.out" # output_file_path
+ "/test/cases", # base_dir
+ "/test/cases/problem7/7.in", # input_file_path
+ "/test/cases/problem7/7.out" # output_file_path
]
result = TestCaseCacheManager._read_testcase_from_file("problem7", 7)
@@ -188,7 +188,7 @@ def test_read_testcase_from_file_io_error(self, mock_logger, mock_abspath, mock_
def test_cache_timeout_setting(self):
"""캐시 타임아웃 설정 테스트"""
- self.assertEqual(TestCaseCacheManager.DEFAULT_TIMEOUT, 3600) # 1시간
+ self.assertEqual(TestCaseCacheManager.DEFAULT_TIMEOUT, 3600) # 1시간
def test_cache_prefix_setting(self):
"""캐시 프리픽스 설정 테스트"""
@@ -221,9 +221,9 @@ def test_os_path_join_usage(self, mock_path_join):
with patch('os.path.abspath') as mock_abspath:
mock_abspath.side_effect = [
- "/test/cases", # base_dir
- "/test/cases/test/1.in", # input_file_path
- "/test/cases/test/1.out" # output_file_path
+ "/test/cases", # base_dir
+ "/test/cases/test/1.in", # input_file_path
+ "/test/cases/test/1.out" # output_file_path
]
with patch("builtins.open", side_effect=FileNotFoundError()):
diff --git a/docs/content/developer/infra/service-architecture/_index.md b/docs/content/developer/infra/service-architecture/_index.md
new file mode 100644
index 000000000..3d679dc79
--- /dev/null
+++ b/docs/content/developer/infra/service-architecture/_index.md
@@ -0,0 +1,102 @@
+---
+date: 2026-02-05T18:55:49+09:00
+draft: false
+title: "Service Architecture"
+weight: 1
+---
+
+{{< callout >}}
+이 문서에서는 CodePlace의 전체적인 Service Architecture와 트래픽 처리 흐름에 대해 설명합니다.
+{{< /callout >}}
+
+### 0. CodePlace Service-Architecture
+
+
+---
+
+### 1. Entry Point
+
+서비스의 모든 요청 트래픽은 가장 먼저 `Traefik`을 거치게 됩니다. CodePlace에서 `Traefik`은 크게 두 가지 핵심적인 네트워크 처리를 담당합니다.
+
+- 보안을 위해 HTTP(80) 요청을 강제로 HTTPS(443)로 Redirect합니다.
+- 외부에서 암호화되어 들어온 패킷을 여기서 복호화(TLS Termination)하여 내부망으로 전달합니다.
+
+덕분에 내부 컨테이너들은 복잡한 암호화/복호화 연산 없이 효율적으로 통신할 수 있습니다. `Traefik`은 위 과정을 거친 모든 요청을 Frontend 컨테이너 (`Nginx`)로 전달합니다.
+
+---
+
+### 2. Routing
+
+`Traefik`에서 전달된 요청은 Frontend 컨테이너의 `Nginx`에 도달하며, URL 경로에 따라 적절한 서비스로 분기됩니다.
+
+### 1) 페이지 요청 (/)
+
+루트 경로 등 페이지 관련 요청이 들어오면, Nginx는 사전에 빌드된 정적 파일을 브라우저에게 서빙합니다.
+이 과정을 이해하기 위해서는 아래의 내용을 이해하여야 합니다.
+
+- **Build Process**
+
+Frontend 서버의 궁극적인 목표는 **정적파일 서빙**입니다. **배포 전 단계에서** 수행되는, 코드베이스에서 여러 가지로 흩어져 있는 소스코드를 하나로 묶는 과정이 빌드입니다. Build는 Node.js 환경에서 `Webpack`을 통해서 수행됩니다.
빌드는 **코드 난독화, 압축, 번들링** 등 여러 가지를 포함하는데, 그중 JavaScript 버전 문법 호환성은 `Babel`을 통해 해결합니다.
+빌드 과정을 거친 결과물(`vendor.js`, `index.html`등)은 **nginx 폴더** 안에 저장이 됩니다. 이 결과물을 Nginx가 브라우저에 서빙하는 것입니다.
+
+
+
+- **SPA (Single Page Application)**
+
+Code Place는 `SPA`방식을 사용하고 있습니다. `SPA`는 **단일 페이지 애플리케이션**으로, MPA와 대비됩니다. 비교를 위해 `MPA`를 설명하겠습니다. `MPA`는 Multi Page Application으로서, 페이지 이동 시마다 서버에 정적파일을 새로 요청하여 브라우저에 렌더링 하는 방식입니다.
+반면 `SPA`는 최초 접속 시, 위의 빌드과정을 통해 사전에 만들어진 애플리케이션 구동에 필요한 모든 자원(`vendor.js`, `index.html` 등)을 한 번에 로드합니다.
+이때 모든 정적파일을 모두 다 하나의 파일에 담지는 않습니다. 초기 로딩 속도 최적화를 위해 자주 쓰이지 않거나 무거운 기능은 **Chunk** 단위로 잘라두었다가, 실제 해당 기능이 필요할 때, 브라우저가 요청하여 로드하도록 설계되어 있습니다.
+이후 페이지 전환 시에는, 이미 서빙된 **index.html** 파일 위에서 사전에 로드된 혹은 필요 시 Chunk로 로드된 `vendor.js` 등의 자바스크립트가 컴포넌트만 교체하여 화면 전환을 수행합니다. 이를 통해 서버 요청을 최소화하여 MPA에 비해 빠른 사용성을 제공합니다.
+
+이러한 빌드, SPA 방식을 통해서 화면 로드가 일어납니다.
+
+### 2) API 요청 (/api)
+
+`/api`로 시작하는 경로로 들어온 요청일 경우, Nginx는 이를 뒷단의 **Django 서버**로 프록시합니다. 전달받은 요청에 따라 Django 서버는 매핑된 URL 엔드포인트를 통해 비즈니스 로직 (DB 조회, 연산 등)을 수행한 후, 그 결과를 브라우저에게 반환합니다.
+
+---
+
+### 3. Celery
+
+`Celery`는 비동기 작업 큐 프레임워크로서, `Celery Beat`와 `Celery Worker`로 구성됩니다.
+백엔드 로직 중 실시간성이 필요하지 않거나 리소스를 많이 점유하는 작업은 Redis를 사용하여 비동기로 처리합니다.
주로 *채점*, *이메일 발송*, *세션 관리*, 그리고 *등수/점수 업데이트와 같은 스케줄링 Job*이 여기에 해당합니다.
+이 구조의 이해를 돕기 위해 Celery의 두 핵심 컴포넌트를 설명하겠습니다.
+
+- **Celery Worker :** Redis 큐에 쌓인 작업을 실제로 가져가서 수행하는 주체입니다. Django 서버와 실행환경은 유사하지만, Redis 큐에 있는 작업만 처리합니다.
+- **Celery Beat :** 정해진 시간마다 주기적으로 실행되어야 하는 작업을 Redis 큐에 자동으로 예약해주는 스케줄러입니다.
+
+
+
+**[동작 프로세스 예시]**
+
+1. **문제 제출 (`Celery Worker`)**
+사용자가 '제출하기' 버튼을 누르면, Django 서버는 직접 채점을 수행하지 않고 **채점 요청 메시지**를 `Redis Queue`에 등록만 하고 즉시 응답을 반환합니다. 대기 중이던 `Celery Worker`가 Redis Queue의 job을 감지하여 가져간 뒤 별도의 격리된 채점 서버와 통신하며 채점을 진행합니다.
+
+2. **통계 업데이트 (`Celery Beat`)**
+사용자가 직접 요청하지 않아도 시스템이 수행해야 하는 '랭킹 업데이트' 같은 작업은 `Celery Beat`가 담당합니다. `Celery Beat`는 설정된 시각이 되면 자동으로 Redis에 작업 요청을 넣고, `Celery Worker`가 이를 가져가서 점수를 갱신합니다.
+
+`Celery Worker`는 Django 서버와 본질적으로 유사한 환경(동일한 코드베이스, ORM 사용)을 가집니다. 다만 가장 큰 차이점은 수신 대상이 다르다는 것입니다.
+
+- **Django Server :** `HTTP Port`로 유입되는 클라이언트의 요청을 수신하여 처리합니다.
+- **Celery Worker :** `Redis`에 등록된 비동기 작업을 대기 상태로 감시하다가 새로운 작업이 들어오면 이를 가져와 처리합니다.
+
+
+
+**왜 이렇게 나누었을까요?**
+
+만약 Celery 없이 Django가 모든 작업을 직접 처리한다고 가정해봅시다.
+
+채점 요청이 동시에 1,000개 이상 들어오는 상황에서, CPU 연산량이 크고 실행 시간이 긴 **채점 작업**을 Django 서버 프로세스 안에서 수행하게 되면 요청 처리 흐름이 지연됩니다. 그 결과 로그인, 페이지 조회와 같은 일반적인 사용자 요청까지 영향을 받아 서비스 응답성이 급격히 저하될 수 있습니다.
+
+물론 Django 내부에서도 비동기 처리를 구현할 수는 있습니다.
+하지만 채점과 같이 **CPU 사용량이 크고 오래 걸리는 작업**을 HTTP 요청 처리 흐름과 함께 수행할 경우, 여전히 다른 사용자 요청에 영향을 주는 구조적 한계가 존재합니다.
+
+따라서 CodePlace는 이러한 문제를 방지하기 위해, 무거운 작업을 Django 서버와 분리된 별도의 실행 주체인 `Celery Worker`에서 처리하도록 설계하였습니다.
+
+---
+
+### 4. Container
+
+`Frontend 서버`, `Backend 서버`, `Redis`, `PostgreSQL`, `Celery Worker`, `Celery Beat`, `Judge Server` 모두 **Docker Container** 기반으로 운용됩니다.
+
+이를 통해 각 서비스의 실행 환경을 격리시켜 충돌을 방지하고, `docker-compose`를 이용해 각 컨테이너를 일관성 있게 배포하고 관리할 수 있는 확장 가능한 구조를 갖췄습니다.
diff --git a/frontend/.babelrc b/frontend/.babelrc
index 1a14ec59f..587e29ec5 100644
--- a/frontend/.babelrc
+++ b/frontend/.babelrc
@@ -1,27 +1,22 @@
{
"presets": [
- ["env", {
- "modules": false,
- "targets": {
- "browsers": ["> 1%", "last 2 versions", "not ie <= 9"]
- },
- "useBuiltIns": true
- }],
+ [
+ "env",
+ {
+ "modules": false,
+ "targets": {
+ "browsers": ["> 1%", "last 2 versions", "not ie <= 9"]
+ },
+ "useBuiltIns": true
+ }
+ ],
"stage-2"
],
- "plugins": [
- "transform-runtime",
- "syntax-dynamic-import"
- ],
+ "plugins": ["transform-runtime", "syntax-dynamic-import"],
"env": {
"test": {
- "presets": [
- "env",
- "stage-2"
- ],
- "plugins": [
- "istanbul"
- ]
+ "presets": ["env", "stage-2"],
+ "plugins": ["istanbul"]
}
}
}
diff --git a/frontend/.eslintignore b/frontend/.eslintignore
index 8a6f41a94..2b4cad210 100644
--- a/frontend/.eslintignore
+++ b/frontend/.eslintignore
@@ -1,4 +1,2 @@
-build/*.js
-config/*.js
-src/**/*.js
-src/**/*.vue
+/*
+!/src/
\ No newline at end of file
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
index c5242c4f9..7f7c03f88 100644
--- a/frontend/.eslintrc.js
+++ b/frontend/.eslintrc.js
@@ -1,31 +1,35 @@
module.exports = {
root: true,
- parser: 'babel-eslint',
+ // 각 환경의 전역 변수 인식
+ env: {
+ browser: true, // for browser
+ node: true, // for node.js
+ },
+ parser: "vue-eslint-parser",
parserOptions: {
- sourceType: 'module',
+ parser: "babel-eslint",
+ sourceType: "module",
},
- // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
- extends: ['standard', 'prettier'],
- lintOnSave: false,
- // required to lint *.vue files
- plugins: ['html'],
+ // Apply external rule-sets
+ extends: ["eslint:recommended", "plugin:vue/essential", "prettier"],
+ // Vue 파일 지원 ESLint 확장플러그인 등록
+ plugins: ["vue"],
// add your custom rules here
rules: {
- 'prettier/prettier': 'error',
// allow paren-less arrow functions
- 'arrow-parens': 0,
+ "arrow-parens": 0,
// allow async-await
- 'generator-star-spacing': 0,
+ "generator-star-spacing": 0,
// allow debugger during development
- 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
- 'no-irregular-whitespace': [
- 'error',
+ "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0,
+ "no-irregular-whitespace": [
+ "error",
{
skipComments: true,
skipTemplates: true,
},
],
- 'no-unused-vars': ['warn'],
- quotes: ['off', 'single'],
+ "no-unused-vars": ["warn"],
+ quotes: ["off", "single"],
},
}
diff --git a/frontend/.github/issue_template.md b/frontend/.github/issue_template.md
index c5646f5cf..653b25b0c 100644
--- a/frontend/.github/issue_template.md
+++ b/frontend/.github/issue_template.md
@@ -1,8 +1,15 @@
## **TODO(필수)**
+
- 할 일을 작성해주세요.
+
## **요청사항(부분 필수)**
+
- 본인이 아닌 다른 Assignee를 지정할 때 요청사항을 명확하게 작성합니다.
+
## **제약사항(선택)**
+
- 해야하는 일 중 제약사항이 있다면 작성합니다.
+
## **참고사항(선택)**
+
- 본인이 아닌 다른 Assignee를 지정할 때, 참고사항(파일위치 등)을 작성합니다.
diff --git a/frontend/.github/workflows/ci_develop.yml b/frontend/.github/workflows/ci_develop.yml
index 7b5aa5d5e..782f75498 100644
--- a/frontend/.github/workflows/ci_develop.yml
+++ b/frontend/.github/workflows/ci_develop.yml
@@ -14,7 +14,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
- node-version: '16.x'
+ node-version: "16.x"
- run: echo "The Job is installing dependencies."
- name: Install dependencies
run: npm install
@@ -23,4 +23,3 @@ jobs:
- name: Build Test
run: npm run build
- run: echo "The Job Finished."
-
diff --git a/frontend/.postcssrc.js b/frontend/.postcssrc.js
index ea9a5ab87..71c1dabfa 100644
--- a/frontend/.postcssrc.js
+++ b/frontend/.postcssrc.js
@@ -1,8 +1,8 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
- "plugins": {
+ plugins: {
// to edit target browsers: use "browserlist" field in package.json
- "autoprefixer": {}
- }
+ autoprefixer: {},
+ },
}
diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js
index 59bf861bb..ffe944199 100644
--- a/frontend/.prettierrc.js
+++ b/frontend/.prettierrc.js
@@ -1,7 +1,8 @@
module.exports = {
semi: false,
tabWidth: 2,
- trailingComma: 'all',
+ trailingComma: "all",
printWidth: 80,
- endOfLine: 'lf',
+ endOfLine: "lf",
+ singleAttributePerLine: false,
}
diff --git a/frontend/README.md b/frontend/README.md
index 863ce8cf7..042f9f781 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,4 +1,5 @@
# CSEP Front End
+
[](https://github.com/vuejs/vue)
[](https://vuex.vuejs.org/)
[](https://github.com/ecomfe/echarts)
@@ -8,8 +9,8 @@

-
## 1.소개
+
[운영중인 사이트](http://oj.pusan.ac.kr/)
[베타 사이트는 현재 부산대학교 내부에서만 접속이 가능합니다.](http://10.125.121.115:8080/)
@@ -37,18 +38,23 @@ Vue.js 2.5.13 버전으로 개발된 기존 [QingdaoU의 OnlineJudge 프론트
### 개발 기간
+
2023.12 ~ 2024.3 (현재 진행 중)
### 개발 인원
-| github | 사진 | 역할 | 이메일 주소 | 소속 |
-|----------|-----------------------------------------------------------------|--------------------|------------------------|-------------------|
-| [hunsy9](https://github.com/hunsy9) |  | PM, 프론트/백엔드 기획 및 개발 | juniper0917@gmail.com | 정보컴퓨터공학부 |
-| [minmunui](https://github.com/minmunui) |  | 프론트엔드(Client) 기획 및 개발 | ehdwls1638@pusan.ac.kr | 정보컴퓨터공학부 |
-| [llddang](https://github.com/llddang) |  | 프론트엔드(Admin) 기획 및 개발 | bodyness@naver.com | 정보컴퓨터공학부 |
+
+| github | 사진 | 역할 | 이메일 주소 | 소속 |
+| --------------------------------------- | --------------------------------------------------------------------- | ------------------------------- | ---------------------- | ---------------- |
+| [hunsy9](https://github.com/hunsy9) |  | PM, 프론트/백엔드 기획 및 개발 | juniper0917@gmail.com | 정보컴퓨터공학부 |
+| [minmunui](https://github.com/minmunui) |  | 프론트엔드(Client) 기획 및 개발 | ehdwls1638@pusan.ac.kr | 정보컴퓨터공학부 |
+| [llddang](https://github.com/llddang) |  | 프론트엔드(Admin) 기획 및 개발 | bodyness@naver.com | 정보컴퓨터공학부 |
## 2.프로젝트 설치
+
서버가 설치되어 있지 않은 경우, [OnlineJudgeBE 프로젝트]()를 이용하여 DB와 서버를 설치해야 합니다.
+
### Linux
+
```bash
npm install
# we use webpack DllReference to decrease the build time,
@@ -83,6 +89,7 @@ npm run dev
```
### 프로덕션 배포
+
[Deploy.md를 참고해주십시오](https://github.com/PNU-CSEP/CSEP_FE/blob/main/deploy/Deploy.md)
## 4.디렉터리 구조
@@ -106,15 +113,18 @@ npm run dev
│ ├── utils/ # 유틸리티
├── static/ # 정적 파일
```
+
## 5.Troubleshooting
### webpack.dll.conf 에서 fs 관련 에러
+
webpack.dll.conf.js 파일에서 fs 관련 에러가 발생할 경우, 다음과 같이 수정합니다.
프로젝트를 빌드할 때 설정한 코드가 제대로 동작하지 않아 발생하는 에러입니다.
+
```javascript
// build/webpack.dll.conf.js :L30
-oldDlls.forEach(f => {
- fs.unlink(f, ()=> {})
+oldDlls.forEach((f) => {
+ fs.unlink(f, () => {})
})
```
@@ -124,9 +134,11 @@ oldDlls.forEach(f => {
그나마 규칙성을 발견하여 아래에 기록했습니다. 새로운 기능을 추가할 때에는 다음과 같은 규칙을 적용하도록 하겠습니다.
### 권장되는 커밋 메세지
+
- 어느 커밋으로 checkout하더라도 프로젝트가 정상적으로 동작할 수 있도록 커밋을 작성해야 합니다.
- 커밋은 최대한 작은 단위로 나누어 작성해야 합니다.
- 커밋 메세지는 다음과 같은 형식을 따라야 합니다.
+
```bash
<타입>: <제목>
@@ -134,6 +146,7 @@ oldDlls.forEach(f => {
[꼬리말]
```
+
- 타입은 가급적 소문자로 작성합니다.
- feat: 새로운 기능 추가
- fix: 버그 수정
@@ -145,7 +158,9 @@ oldDlls.forEach(f => {
- [커밋 메시지 컨벤션](https://www.conventionalcommits.org/ko/v1.0.0/)
### 작명 규칙
- 모든 작명규칙은 기존의 프로젝트에 있던 것들을 참고하여 작성되었습니다. 일관성을 유지하기 위해 기존의 작명규칙을 따르도록 하겠습니다.
+
+모든 작명규칙은 기존의 프로젝트에 있던 것들을 참고하여 작성되었습니다. 일관성을 유지하기 위해 기존의 작명규칙을 따르도록 하겠습니다.
+
- 변수명, 함수명은 camelCase를 따릅니다.
- 컴포넌트명은 PascalCase를 따릅니다.
- 상수는 대문자로 작성하며, 단어 사이는 언더바로 구분합니다. 다른 곳에서 공통적으로 사용될 것으로 예상되는 상수는 `src/utils/constants.js`에 작성합니다.
@@ -153,7 +168,9 @@ oldDlls.forEach(f => {
- 서버로부터 받아온 데이터 object의 key값은 snake_case를 따릅니다.
### 기타 유의사항
+
1. component에 특정 문자열을 넣을 때에는 `src/i18n/US.js`에 해당 문자열을 추가한 후 사용합니다. 절대로 하드코딩하지 않습니다.
+
```javascript
// bad
| + {{ $t("m.Input") }} + | ++ {{ $t("m.Output") }} + | ++ {{ $t("m.Score") }} + | +
|---|---|---|
| + {{ row.input_name }} + | ++ {{ row.output_name }} + | ++ + | +