From b77324836e2bcaef8e4eb26f9899b649967c2e6d Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 10:05:20 +0900 Subject: [PATCH 001/380] =?UTF-8?q?[docs]=20requirements.txt=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..51e46b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +asgiref==3.11.0 +Django==5.2.10 +sqlparse==0.5.5 +tzdata==2025.3 From 3db4cb546320b3bfc3f82733822b07e414a80731 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 11:41:01 +0900 Subject: [PATCH 002/380] =?UTF-8?q?[chore]=20djano=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/__init__.py | 0 config/asgi.py | 16 ++++++ config/settings.py | 122 +++++++++++++++++++++++++++++++++++++++++++++ config/urls.py | 22 ++++++++ config/wsgi.py | 16 ++++++ manage.py | 22 ++++++++ 6 files changed, 198 insertions(+) create mode 100644 config/__init__.py create mode 100644 config/asgi.py create mode 100644 config/settings.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 manage.py diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..cd6907c --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..794cf1a --- /dev/null +++ b/config/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 5.2.10. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-)76zvrn)+9frg^h5=7wq!l=xlrqi-57@#7yjq3f(s14$$khudk" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "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", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = "ko" + +TIME_ZONE = "Asia/Seoul" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..cbd63dc --- /dev/null +++ b/config/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..27c0377 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..d28672e --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() From 12c1e0b3dbba5a5130db87a3612bb31aa29fc5ec Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 11:43:37 +0900 Subject: [PATCH 003/380] =?UTF-8?q?[chore]=20accounts,=20learning,=20roadm?= =?UTF-8?q?aps,=20teams,=20reflections=20=EC=95=B1=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/__init__.py | 0 accounts/admin.py | 3 +++ accounts/apps.py | 6 ++++++ accounts/migrations/__init__.py | 0 accounts/models.py | 3 +++ accounts/tests.py | 3 +++ accounts/views.py | 3 +++ learning/__init__.py | 0 learning/admin.py | 3 +++ learning/apps.py | 6 ++++++ learning/migrations/__init__.py | 0 learning/models.py | 3 +++ learning/tests.py | 3 +++ learning/views.py | 3 +++ reflections/__init__.py | 0 reflections/admin.py | 3 +++ reflections/apps.py | 6 ++++++ reflections/migrations/__init__.py | 0 reflections/models.py | 3 +++ reflections/tests.py | 3 +++ reflections/views.py | 3 +++ roadmaps/__init__.py | 0 roadmaps/admin.py | 3 +++ roadmaps/apps.py | 6 ++++++ roadmaps/migrations/__init__.py | 0 roadmaps/models.py | 3 +++ roadmaps/tests.py | 3 +++ roadmaps/views.py | 3 +++ teams/__init__.py | 0 teams/admin.py | 3 +++ teams/apps.py | 6 ++++++ teams/migrations/__init__.py | 0 teams/models.py | 3 +++ teams/tests.py | 3 +++ teams/views.py | 3 +++ 35 files changed, 90 insertions(+) create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/tests.py create mode 100644 accounts/views.py create mode 100644 learning/__init__.py create mode 100644 learning/admin.py create mode 100644 learning/apps.py create mode 100644 learning/migrations/__init__.py create mode 100644 learning/models.py create mode 100644 learning/tests.py create mode 100644 learning/views.py create mode 100644 reflections/__init__.py create mode 100644 reflections/admin.py create mode 100644 reflections/apps.py create mode 100644 reflections/migrations/__init__.py create mode 100644 reflections/models.py create mode 100644 reflections/tests.py create mode 100644 reflections/views.py create mode 100644 roadmaps/__init__.py create mode 100644 roadmaps/admin.py create mode 100644 roadmaps/apps.py create mode 100644 roadmaps/migrations/__init__.py create mode 100644 roadmaps/models.py create mode 100644 roadmaps/tests.py create mode 100644 roadmaps/views.py create mode 100644 teams/__init__.py create mode 100644 teams/admin.py create mode 100644 teams/apps.py create mode 100644 teams/migrations/__init__.py create mode 100644 teams/models.py create mode 100644 teams/tests.py create mode 100644 teams/views.py diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..0cb51e6 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "accounts" diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/learning/__init__.py b/learning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learning/admin.py b/learning/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/learning/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/learning/apps.py b/learning/apps.py new file mode 100644 index 0000000..b212832 --- /dev/null +++ b/learning/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LearningConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "learning" diff --git a/learning/migrations/__init__.py b/learning/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learning/models.py b/learning/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/learning/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/learning/tests.py b/learning/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/learning/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/learning/views.py b/learning/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/learning/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/reflections/__init__.py b/reflections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reflections/admin.py b/reflections/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/reflections/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/reflections/apps.py b/reflections/apps.py new file mode 100644 index 0000000..f4f277f --- /dev/null +++ b/reflections/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ReflectionsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "reflections" diff --git a/reflections/migrations/__init__.py b/reflections/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reflections/models.py b/reflections/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/reflections/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/reflections/tests.py b/reflections/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/reflections/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/reflections/views.py b/reflections/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/reflections/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/roadmaps/__init__.py b/roadmaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/roadmaps/admin.py b/roadmaps/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/roadmaps/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/roadmaps/apps.py b/roadmaps/apps.py new file mode 100644 index 0000000..8bae0cc --- /dev/null +++ b/roadmaps/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RoadmapsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "roadmaps" diff --git a/roadmaps/migrations/__init__.py b/roadmaps/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/roadmaps/models.py b/roadmaps/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/roadmaps/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/roadmaps/tests.py b/roadmaps/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/roadmaps/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/roadmaps/views.py b/roadmaps/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/roadmaps/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/teams/__init__.py b/teams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teams/admin.py b/teams/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/teams/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/teams/apps.py b/teams/apps.py new file mode 100644 index 0000000..be7aa05 --- /dev/null +++ b/teams/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TeamsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "teams" diff --git a/teams/migrations/__init__.py b/teams/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teams/models.py b/teams/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/teams/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/teams/tests.py b/teams/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/teams/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/teams/views.py b/teams/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/teams/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From a39f390474db9a041155d5ee4c2fd4538bf9c447 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 13:06:44 +0900 Subject: [PATCH 004/380] =?UTF-8?q?chore:=20settings.py=EC=97=90=20?= =?UTF-8?q?=EC=8B=A0=EA=B7=9C=20=EC=95=B1=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/config/settings.py b/config/settings.py index 794cf1a..51fd08d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -37,6 +37,13 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + + # Local apps + "accounts.apps.AccountsConfig", + "learning.apps.LearningConfig", + "reflections.apps.ReflectionsConfig", + "roadmaps.apps.RoadsConfig", + "teams.apps.TeamsConfig", ] MIDDLEWARE = [ @@ -54,7 +61,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -114,7 +121,13 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ -STATIC_URL = "static/" +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_DIRS = [BASE_DIR / "static"] + +# Media files +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field From 684991807ace996992b90e1589435ef86077e70c Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 13:26:53 +0900 Subject: [PATCH 005/380] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 46 ++++++++++++++++++++++++++++- learning/models.py | 69 ++++++++++++++++++++++++++++++++++++++++++- reflections/models.py | 38 +++++++++++++++++++++++- roadmaps/models.py | 69 ++++++++++++++++++++++++++++++++++++++++++- teams/models.py | 32 +++++++++++++++++++- 5 files changed, 249 insertions(+), 5 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index 71a8362..d22d934 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,47 @@ +from django.contrib.auth.models import AbstractUser from django.db import models -# Create your models here. + +class User(AbstractUser): + """ + Custom User for StartLine.dev + + - allauth 사용 + - 로그인 후 프로필 설정 화면에서 nickname / profile_image 입력 + """ + + # 프로필 정보 (로그인 직후에는 비어 있을 수 있음) + nickname = models.CharField( + max_length=30, + unique=True, + null=True, + blank=True, + help_text="서비스 내 표시 닉네임 (프로필 설정 시 입력)", + ) + + profile_image = models.TextField( + null=True, + blank=True, + help_text="프로필 이미지 URL 또는 media 경로", + ) + + # 학습 레벨 + user_level = models.PositiveSmallIntegerField( + default=0, + help_text="사용자 학습 레벨 (0~6)", + ) + + # 공통 타임스탬프 + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def is_profile_completed(self) -> bool: + """ + 프로필 설정 완료 여부 + - 프론트에서 /me 응답 보고 판단해도 되고 + - 백엔드에서도 재사용 가능 + """ + return bool(self.nickname) + + def __str__(self) -> str: + return self.nickname or self.username diff --git a/learning/models.py b/learning/models.py index 71a8362..776419b 100644 --- a/learning/models.py +++ b/learning/models.py @@ -1,3 +1,70 @@ from django.db import models -# Create your models here. + +class Tag(models.Model): + name = models.CharField(max_length=50, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return self.name + + +class LearningResource(models.Model): + class Platform(models.TextChoices): + YOUTUBE = "YOUTUBE", "YOUTUBE" + INFLEARN = "INFLEARN", "INFLEARN" + VELOG = "VELOG", "VELOG" + BLOG = "BLOG", "BLOG" + ETC = "ETC", "ETC" + + class ContentType(models.TextChoices): + VIDEO = "VIDEO", "VIDEO" + ARTICLE = "ARTICLE", "ARTICLE" + COURSE = "COURSE", "COURSE" + + class Track(models.TextChoices): + WEB_FRONT = "WEB_FRONT", "WEB_FRONT" + WEB_BACK = "WEB_BACK", "WEB_BACK" + APP_FRONT = "APP_FRONT", "APP_FRONT" + APP_BACK = "APP_BACK", "APP_BACK" + GAME = "GAME", "GAME" + + title = models.CharField(max_length=200) + url = models.TextField(null=True, blank=True) + + platform = models.CharField(max_length=50, choices=Platform.choices, null=True, blank=True) + content_type = models.CharField(max_length=20, choices=ContentType.choices) + track = models.CharField(max_length=20, choices=Track.choices) + + level_min = models.PositiveSmallIntegerField(default=0) + level_max = models.PositiveSmallIntegerField(default=6) + + estimated_time = models.PositiveIntegerField(null=True, blank=True) + learning_style = models.CharField(max_length=50, null=True, blank=True) + + tags = models.ManyToManyField(Tag, through="LearningResourceTag", related_name="resources") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + indexes = [ + models.Index(fields=["platform"]), + models.Index(fields=["content_type"]), + models.Index(fields=["track"]), + models.Index(fields=["level_min", "level_max"]), + ] + + def __str__(self) -> str: + return self.title + + +class LearningResourceTag(models.Model): + resource = models.ForeignKey(LearningResource, on_delete=models.CASCADE, related_name="resource_tags") + tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="resource_tags") + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["resource", "tag"], name="uq_learning_resource_tags"), + ] + indexes = [ + models.Index(fields=["tag"]), + ] diff --git a/reflections/models.py b/reflections/models.py index 71a8362..c2d3585 100644 --- a/reflections/models.py +++ b/reflections/models.py @@ -1,3 +1,39 @@ +from django.conf import settings from django.db import models -# Create your models here. + +class Reflection(models.Model): + class Track(models.TextChoices): + WEB_FRONT = "WEB_FRONT", "WEB_FRONT" + WEB_BACK = "WEB_BACK", "WEB_BACK" + APP_FRONT = "APP_FRONT", "APP_FRONT" + APP_BACK = "APP_BACK", "APP_BACK" + GAME = "GAME", "GAME" + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reflections") + + # nullable 허용(아이템과 느슨 연결) + roadmap_item = models.ForeignKey( + "roadmaps.RoadmapItem", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="reflections", + ) + + track = models.CharField(max_length=20, choices=Track.choices) + content = models.TextField() + starred = models.BooleanField(default=False) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + indexes = [ + models.Index(fields=["user", "created_at"]), + models.Index(fields=["user", "starred"]), + models.Index(fields=["track", "created_at"]), + ] + + def __str__(self) -> str: + return f"Reflection({self.user_id}, {self.track})" diff --git a/roadmaps/models.py b/roadmaps/models.py index 71a8362..a9d346a 100644 --- a/roadmaps/models.py +++ b/roadmaps/models.py @@ -1,3 +1,70 @@ +from django.conf import settings from django.db import models -# Create your models here. + +class Roadmap(models.Model): + class Track(models.TextChoices): + WEB_FRONT = "WEB_FRONT", "WEB_FRONT" + WEB_BACK = "WEB_BACK", "WEB_BACK" + APP_FRONT = "APP_FRONT", "APP_FRONT" + APP_BACK = "APP_BACK", "APP_BACK" + GAME = "GAME", "GAME" + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="roadmaps") + track = models.CharField(max_length=20, choices=Track.choices) + + level = models.PositiveSmallIntegerField() + current_index = models.PositiveIntegerField(default=0) + + is_completed = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + # include-only 태그(선호/방향) + tags = models.ManyToManyField("learning.Tag", through="RoadmapTag", related_name="roadmaps") + + class Meta: + indexes = [ + models.Index(fields=["user", "track", "is_completed"]), + ] + + def __str__(self) -> str: + return f"Roadmap({self.user_id}, {self.track})" + + +class RoadmapTag(models.Model): + roadmap = models.ForeignKey(Roadmap, on_delete=models.CASCADE, related_name="roadmap_tags") + tag = models.ForeignKey("learning.Tag", on_delete=models.CASCADE, related_name="roadmap_tags") + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["roadmap", "tag"], name="uq_roadmap_tags"), + ] + indexes = [ + models.Index(fields=["tag"]), + ] + + +class RoadmapItem(models.Model): + roadmap = models.ForeignKey(Roadmap, on_delete=models.CASCADE, related_name="items") + resource = models.ForeignKey( + "learning.LearningResource", + on_delete=models.PROTECT, + related_name="roadmap_items", + help_text="리소스 삭제 시 과거 로드맵 기록 보호 위해 PROTECT 권장", + ) + + order_no = models.PositiveIntegerField() + is_completed = models.BooleanField(default=False) + completed_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["roadmap", "order_no"], name="uq_roadmap_items_order_no"), + ] + indexes = [ + models.Index(fields=["roadmap", "is_completed"]), + ] + + def __str__(self) -> str: + return f"RoadmapItem({self.roadmap_id}, #{self.order_no})" diff --git a/teams/models.py b/teams/models.py index 71a8362..d733c20 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,3 +1,33 @@ +from django.conf import settings from django.db import models -# Create your models here. + +class Team(models.Model): + name = models.CharField(max_length=100) + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="created_teams") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return self.name + + +class TeamMember(models.Model): + class Role(models.TextChoices): + LEADER = "LEADER", "LEADER" + MEMBER = "MEMBER", "MEMBER" + + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="members") + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="team_memberships") + role = models.CharField(max_length=10, choices=Role.choices, default=Role.MEMBER) + joined_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["team", "user"], name="uq_team_members"), + ] + indexes = [ + models.Index(fields=["user"]), + ] + + def __str__(self) -> str: + return f"{self.team_id}:{self.user_id}({self.role})" From 76efdf375b1c139bf94899ede03073f89efa9327 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 14:04:34 +0900 Subject: [PATCH 006/380] =?UTF-8?q?chore:=20API=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B5=AC=EC=A1=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/api_urls.py | 4 ++++ config/api_urls.py | 9 +++++++++ config/urls.py | 24 +++++++----------------- learning/api_urls.py | 4 ++++ reflections/api_urls.py | 4 ++++ roadmaps/api_urls.py | 4 ++++ teams/api_urls.py | 4 ++++ 7 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 accounts/api_urls.py create mode 100644 config/api_urls.py create mode 100644 learning/api_urls.py create mode 100644 reflections/api_urls.py create mode 100644 roadmaps/api_urls.py create mode 100644 teams/api_urls.py diff --git a/accounts/api_urls.py b/accounts/api_urls.py new file mode 100644 index 0000000..690b131 --- /dev/null +++ b/accounts/api_urls.py @@ -0,0 +1,4 @@ +from django.urls import path + +urlpatterns = [ +] diff --git a/config/api_urls.py b/config/api_urls.py new file mode 100644 index 0000000..6ed40d5 --- /dev/null +++ b/config/api_urls.py @@ -0,0 +1,9 @@ +from django.urls import path, include + +urlpatterns = [ + path("", include("accounts.api_urls")), + path("", include("learning.api_urls")), + path("", include("roadmaps.api_urls")), + path("", include("reflections.api_urls")), + path("", include("teams.api_urls")), +] diff --git a/config/urls.py b/config/urls.py index cbd63dc..7c3a4d6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,22 +1,12 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), + + # allauth (로그인/소셜로그인) + path("accounts/", include("allauth.urls")), + + # API + path("api/", include("config.api_urls")), ] diff --git a/learning/api_urls.py b/learning/api_urls.py new file mode 100644 index 0000000..690b131 --- /dev/null +++ b/learning/api_urls.py @@ -0,0 +1,4 @@ +from django.urls import path + +urlpatterns = [ +] diff --git a/reflections/api_urls.py b/reflections/api_urls.py new file mode 100644 index 0000000..690b131 --- /dev/null +++ b/reflections/api_urls.py @@ -0,0 +1,4 @@ +from django.urls import path + +urlpatterns = [ +] diff --git a/roadmaps/api_urls.py b/roadmaps/api_urls.py new file mode 100644 index 0000000..690b131 --- /dev/null +++ b/roadmaps/api_urls.py @@ -0,0 +1,4 @@ +from django.urls import path + +urlpatterns = [ +] diff --git a/teams/api_urls.py b/teams/api_urls.py new file mode 100644 index 0000000..690b131 --- /dev/null +++ b/teams/api_urls.py @@ -0,0 +1,4 @@ +from django.urls import path + +urlpatterns = [ +] From d441750b2619a09ba7045018332972438765196a Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 14:10:21 +0900 Subject: [PATCH 007/380] =?UTF-8?q?chore:=20=EC=B4=88=EA=B8=B0=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 3 +++ config/views.py | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 config/views.py diff --git a/config/urls.py b/config/urls.py index 7c3a4d6..b1b899c 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,7 +1,10 @@ from django.contrib import admin from django.urls import path, include +from .views import initial_view urlpatterns = [ + + path("", initial_view.as_view(), name="initial"), path("admin/", admin.site.urls), # allauth (로그인/소셜로그인) diff --git a/config/views.py b/config/views.py new file mode 100644 index 0000000..5ce0759 --- /dev/null +++ b/config/views.py @@ -0,0 +1,4 @@ +from django.views.generic import TemplateView + +class initial_view(TemplateView): + template_name = "initial.html" From b932bf9638796e7b8ce89fe8dfc3387067378ded Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 14:26:37 +0900 Subject: [PATCH 008/380] =?UTF-8?q?fix:=20INSTALLED=5FAPPS=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/settings.py b/config/settings.py index 51fd08d..518ceb4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -39,11 +39,11 @@ "django.contrib.staticfiles", # Local apps - "accounts.apps.AccountsConfig", - "learning.apps.LearningConfig", - "reflections.apps.ReflectionsConfig", - "roadmaps.apps.RoadsConfig", - "teams.apps.TeamsConfig", + "accounts", + "learning", + "reflections", + "roadmaps", + "teams", ] MIDDLEWARE = [ From b8d403c268e1ca4586e51f86beea1eaae0fe39a9 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 14:48:59 +0900 Subject: [PATCH 009/380] =?UTF-8?q?[docs]=20=EC=95=BD=EA=B0=84=EC=9D=98=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80,=20requirements.txt=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 5 +++-- config/views.py | 1 + requirements.txt | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/urls.py b/config/urls.py index b1b899c..0102638 100644 --- a/config/urls.py +++ b/config/urls.py @@ -7,9 +7,10 @@ path("", initial_view.as_view(), name="initial"), path("admin/", admin.site.urls), - # allauth (로그인/소셜로그인) + # template views: HTML로 보여줄 주소들 + # allauth (로그인/소셜로그인) path("accounts/", include("allauth.urls")), - # API + # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), ] diff --git a/config/views.py b/config/views.py index 5ce0759..1443102 100644 --- a/config/views.py +++ b/config/views.py @@ -1,4 +1,5 @@ from django.views.generic import TemplateView +# 테스트용 기본 템플릿 class initial_view(TemplateView): template_name = "initial.html" diff --git a/requirements.txt b/requirements.txt index 51e46b1..5e67969 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ asgiref==3.11.0 Django==5.2.10 +django-allauth==65.14.0 +pillow==12.1.0 sqlparse==0.5.5 tzdata==2025.3 From 3b238ad5887fc3d986d412b4e1b6c55413c8b27d Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 15:12:29 +0900 Subject: [PATCH 010/380] =?UTF-8?q?[fix]=20allauth=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/migrations/0001_initial.py | 155 +++++++++++++++++++++++ config/settings.py | 11 ++ learning/migrations/0001_initial.py | 163 +++++++++++++++++++++++++ reflections/migrations/0001_initial.py | 82 +++++++++++++ roadmaps/migrations/0001_initial.py | 160 ++++++++++++++++++++++++ teams/migrations/0001_initial.py | 89 ++++++++++++++ 6 files changed, 660 insertions(+) create mode 100644 accounts/migrations/0001_initial.py create mode 100644 learning/migrations/0001_initial.py create mode 100644 reflections/migrations/0001_initial.py create mode 100644 roadmaps/migrations/0001_initial.py create mode 100644 teams/migrations/0001_initial.py diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..bc4511b --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,155 @@ +# Generated by Django 5.2.10 on 2026-01-27 06:02 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "nickname", + models.CharField( + blank=True, + help_text="서비스 내 표시 닉네임 (프로필 설정 시 입력)", + max_length=30, + null=True, + unique=True, + ), + ), + ( + "profile_image", + models.TextField( + blank=True, help_text="프로필 이미지 URL 또는 media 경로", null=True + ), + ), + ( + "user_level", + models.PositiveSmallIntegerField( + default=0, help_text="사용자 학습 레벨 (0~6)" + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/config/settings.py b/config/settings.py index 518ceb4..001a01d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -38,6 +38,10 @@ "django.contrib.messages", "django.contrib.staticfiles", + # Module apps + "allauth", + "allauth.account", + "allauth.socialaccount", # Local apps "accounts", "learning", @@ -52,6 +56,9 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + + "allauth.account.middleware.AccountMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] @@ -75,6 +82,10 @@ WSGI_APPLICATION = "config.wsgi.application" +# AllAuth settings +SITE_ID = 1 +AUTH_USER_MODEL = "accounts.User" + # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases diff --git a/learning/migrations/0001_initial.py b/learning/migrations/0001_initial.py new file mode 100644 index 0000000..deaaa86 --- /dev/null +++ b/learning/migrations/0001_initial.py @@ -0,0 +1,163 @@ +# Generated by Django 5.2.10 on 2026-01-27 06:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="LearningResource", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200)), + ("url", models.TextField(blank=True, null=True)), + ( + "platform", + models.CharField( + blank=True, + choices=[ + ("YOUTUBE", "YOUTUBE"), + ("INFLEARN", "INFLEARN"), + ("VELOG", "VELOG"), + ("BLOG", "BLOG"), + ("ETC", "ETC"), + ], + max_length=50, + null=True, + ), + ), + ( + "content_type", + models.CharField( + choices=[ + ("VIDEO", "VIDEO"), + ("ARTICLE", "ARTICLE"), + ("COURSE", "COURSE"), + ], + max_length=20, + ), + ), + ( + "track", + models.CharField( + choices=[ + ("WEB_FRONT", "WEB_FRONT"), + ("WEB_BACK", "WEB_BACK"), + ("APP_FRONT", "APP_FRONT"), + ("APP_BACK", "APP_BACK"), + ("GAME", "GAME"), + ], + max_length=20, + ), + ), + ("level_min", models.PositiveSmallIntegerField(default=0)), + ("level_max", models.PositiveSmallIntegerField(default=6)), + ("estimated_time", models.PositiveIntegerField(blank=True, null=True)), + ( + "learning_style", + models.CharField(blank=True, max_length=50, null=True), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50, unique=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="LearningResourceTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "resource", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="resource_tags", + to="learning.learningresource", + ), + ), + ( + "tag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="resource_tags", + to="learning.tag", + ), + ), + ], + ), + migrations.AddField( + model_name="learningresource", + name="tags", + field=models.ManyToManyField( + related_name="resources", + through="learning.LearningResourceTag", + to="learning.tag", + ), + ), + migrations.AddIndex( + model_name="learningresourcetag", + index=models.Index(fields=["tag"], name="learning_le_tag_id_1a68f8_idx"), + ), + migrations.AddConstraint( + model_name="learningresourcetag", + constraint=models.UniqueConstraint( + fields=("resource", "tag"), name="uq_learning_resource_tags" + ), + ), + migrations.AddIndex( + model_name="learningresource", + index=models.Index( + fields=["platform"], name="learning_le_platfor_ef90d6_idx" + ), + ), + migrations.AddIndex( + model_name="learningresource", + index=models.Index( + fields=["content_type"], name="learning_le_content_37c87f_idx" + ), + ), + migrations.AddIndex( + model_name="learningresource", + index=models.Index(fields=["track"], name="learning_le_track_ecc62f_idx"), + ), + migrations.AddIndex( + model_name="learningresource", + index=models.Index( + fields=["level_min", "level_max"], name="learning_le_level_m_071736_idx" + ), + ), + ] diff --git a/reflections/migrations/0001_initial.py b/reflections/migrations/0001_initial.py new file mode 100644 index 0000000..9d15628 --- /dev/null +++ b/reflections/migrations/0001_initial.py @@ -0,0 +1,82 @@ +# Generated by Django 5.2.10 on 2026-01-27 06:02 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("roadmaps", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Reflection", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "track", + models.CharField( + choices=[ + ("WEB_FRONT", "WEB_FRONT"), + ("WEB_BACK", "WEB_BACK"), + ("APP_FRONT", "APP_FRONT"), + ("APP_BACK", "APP_BACK"), + ("GAME", "GAME"), + ], + max_length=20, + ), + ), + ("content", models.TextField()), + ("starred", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "roadmap_item", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reflections", + to="roadmaps.roadmapitem", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reflections", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "indexes": [ + models.Index( + fields=["user", "created_at"], + name="reflections_user_id_471546_idx", + ), + models.Index( + fields=["user", "starred"], + name="reflections_user_id_10be5b_idx", + ), + models.Index( + fields=["track", "created_at"], + name="reflections_track_05d546_idx", + ), + ], + }, + ), + ] diff --git a/roadmaps/migrations/0001_initial.py b/roadmaps/migrations/0001_initial.py new file mode 100644 index 0000000..aa48f5c --- /dev/null +++ b/roadmaps/migrations/0001_initial.py @@ -0,0 +1,160 @@ +# Generated by Django 5.2.10 on 2026-01-27 06:02 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("learning", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Roadmap", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "track", + models.CharField( + choices=[ + ("WEB_FRONT", "WEB_FRONT"), + ("WEB_BACK", "WEB_BACK"), + ("APP_FRONT", "APP_FRONT"), + ("APP_BACK", "APP_BACK"), + ("GAME", "GAME"), + ], + max_length=20, + ), + ), + ("level", models.PositiveSmallIntegerField()), + ("current_index", models.PositiveIntegerField(default=0)), + ("is_completed", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="roadmaps", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="RoadmapTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "roadmap", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="roadmap_tags", + to="roadmaps.roadmap", + ), + ), + ( + "tag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="roadmap_tags", + to="learning.tag", + ), + ), + ], + ), + migrations.AddField( + model_name="roadmap", + name="tags", + field=models.ManyToManyField( + related_name="roadmaps", + through="roadmaps.RoadmapTag", + to="learning.tag", + ), + ), + migrations.CreateModel( + name="RoadmapItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order_no", models.PositiveIntegerField()), + ("is_completed", models.BooleanField(default=False)), + ("completed_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "resource", + models.ForeignKey( + help_text="리소스 삭제 시 과거 로드맵 기록 보호 위해 PROTECT 권장", + on_delete=django.db.models.deletion.PROTECT, + related_name="roadmap_items", + to="learning.learningresource", + ), + ), + ( + "roadmap", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="items", + to="roadmaps.roadmap", + ), + ), + ], + options={ + "indexes": [ + models.Index( + fields=["roadmap", "is_completed"], + name="roadmaps_ro_roadmap_85ed7f_idx", + ) + ], + "constraints": [ + models.UniqueConstraint( + fields=("roadmap", "order_no"), name="uq_roadmap_items_order_no" + ) + ], + }, + ), + migrations.AddIndex( + model_name="roadmaptag", + index=models.Index(fields=["tag"], name="roadmaps_ro_tag_id_d861cf_idx"), + ), + migrations.AddConstraint( + model_name="roadmaptag", + constraint=models.UniqueConstraint( + fields=("roadmap", "tag"), name="uq_roadmap_tags" + ), + ), + migrations.AddIndex( + model_name="roadmap", + index=models.Index( + fields=["user", "track", "is_completed"], + name="roadmaps_ro_user_id_1fe566_idx", + ), + ), + ] diff --git a/teams/migrations/0001_initial.py b/teams/migrations/0001_initial.py new file mode 100644 index 0000000..eb7f449 --- /dev/null +++ b/teams/migrations/0001_initial.py @@ -0,0 +1,89 @@ +# Generated by Django 5.2.10 on 2026-01-27 06:02 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Team", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="created_teams", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="TeamMember", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "role", + models.CharField( + choices=[("LEADER", "LEADER"), ("MEMBER", "MEMBER")], + default="MEMBER", + max_length=10, + ), + ), + ("joined_at", models.DateTimeField(auto_now_add=True)), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="members", + to="teams.team", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_memberships", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "indexes": [ + models.Index(fields=["user"], name="teams_teamm_user_id_08f226_idx") + ], + "constraints": [ + models.UniqueConstraint( + fields=("team", "user"), name="uq_team_members" + ) + ], + }, + ), + ] From cf71819e99117961954048f4609845123d9130cf Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 15:24:00 +0900 Subject: [PATCH 011/380] =?UTF-8?q?[fix]=20User=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20image=EB=A5=BC=20TextField=20->=20ImageFie?= =?UTF-8?q?ld=EB=A1=9C=20=EB=B3=80=EA=B2=BD,=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EA=B0=9C?= =?UTF-8?q?=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0002_alter_user_profile_image.py | 22 +++++++++++++++++++ accounts/models.py | 3 ++- config/urls.py | 8 +++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 accounts/migrations/0002_alter_user_profile_image.py diff --git a/accounts/migrations/0002_alter_user_profile_image.py b/accounts/migrations/0002_alter_user_profile_image.py new file mode 100644 index 0000000..4659158 --- /dev/null +++ b/accounts/migrations/0002_alter_user_profile_image.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.10 on 2026-01-27 06:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("accounts", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="profile_image", + field=models.ImageField( + blank=True, + help_text="프로필 이미지 URL 또는 media 경로", + null=True, + upload_to="profiles/", + ), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index d22d934..64f7cb6 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -19,7 +19,8 @@ class User(AbstractUser): help_text="서비스 내 표시 닉네임 (프로필 설정 시 입력)", ) - profile_image = models.TextField( + profile_image = models.ImageField( + upload_to="profiles/", null=True, blank=True, help_text="프로필 이미지 URL 또는 media 경로", diff --git a/config/urls.py b/config/urls.py index 0102638..2fd3b3e 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,5 +1,7 @@ from django.contrib import admin from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static from .views import initial_view urlpatterns = [ @@ -14,3 +16,9 @@ # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), ] + +if settings.DEBUG: + urlpatterns += static( + settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT, + ) \ No newline at end of file From 9d6b906b48466734d50b432b06ff0c5e5a8cf49e Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 17:21:32 +0900 Subject: [PATCH 012/380] =?UTF-8?q?feat:=20=EC=84=B8=EC=85=98=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?login/,=20logout/,=20signup/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 15 +++++++++++++++ config/urls.py | 9 +++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/config/settings.py b/config/settings.py index 001a01d..55e3835 100644 --- a/config/settings.py +++ b/config/settings.py @@ -85,6 +85,21 @@ # AllAuth settings SITE_ID = 1 AUTH_USER_MODEL = "accounts.User" +# allauth 기본 로그인 방식 설정 +ACCOUNT_LOGIN_METHODS = {"username"} +ACCOUNT_SIGNUP_FIELDS = [ + "username*", + "password1*", + "password2*", +] + +LOGIN_URL = "/accounts/login/" +LOGIN_REDIRECT_URL = "/" # 로그인 성공 후 +LOGOUT_REDIRECT_URL = "/" # 로그아웃 후 + +ACCOUNT_SIGNUP_REDIRECT_URL = "/" # 회원가입 완료 후(가능한 버전에서 동작) +SOCIALACCOUNT_LOGIN_ON_GET = True +ACCOUNT_LOGOUT_ON_GET = True # Database diff --git a/config/urls.py b/config/urls.py index 2fd3b3e..cf07a17 100644 --- a/config/urls.py +++ b/config/urls.py @@ -2,16 +2,21 @@ from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.views.generic import RedirectView from .views import initial_view urlpatterns = [ path("", initial_view.as_view(), name="initial"), path("admin/", admin.site.urls), - - # template views: HTML로 보여줄 주소들 # allauth (로그인/소셜로그인) path("accounts/", include("allauth.urls")), + # allauth 쪽으로 리다이렉트 + path("login/", RedirectView.as_view(url="/accounts/login/")), + path("logout/", RedirectView.as_view(url="/accounts/logout/")), + path("signup/", RedirectView.as_view(url="/accounts/signup/")), + # template views: HTML로 보여줄 주소들 + # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), From d9727e879769848db524aa2766be801a097d96f5 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 27 Jan 2026 17:36:28 +0900 Subject: [PATCH 013/380] =?UTF-8?q?feat:=20=EC=B5=9C=EC=B4=88=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20->=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=98=A8=EB=B3=B4=EB=94=A9=EC=9C=BC=EB=A1=9C,=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EB=B0=8F=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/forms.py | 15 +++++++++++++++ accounts/middleware.py | 19 +++++++++++++++++++ accounts/urls.py | 6 ++++++ accounts/views.py | 19 +++++++++++++++++-- config/settings.py | 1 + config/urls.py | 4 +++- templates/account/onboarding_profile.html | 17 +++++++++++++++++ 7 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 accounts/forms.py create mode 100644 accounts/middleware.py create mode 100644 accounts/urls.py create mode 100644 templates/account/onboarding_profile.html diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..7f5af51 --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,15 @@ +from django import forms +from .models import User + +class OnboardingForm(forms.ModelForm): + class Meta: + model = User + fields = ["nickname", "profile_image"] + + def clean_nickname(self): + nick = (self.cleaned_data.get("nickname") or "").strip() + if not nick: + raise forms.ValidationError("닉네임은 필수입니다.") + if User.objects.filter(nickname=nick).exclude(pk=self.instance.pk).exists(): + raise forms.ValidationError("이미 사용 중인 닉네임입니다.") + return nick diff --git a/accounts/middleware.py b/accounts/middleware.py new file mode 100644 index 0000000..5d00ff3 --- /dev/null +++ b/accounts/middleware.py @@ -0,0 +1,19 @@ +from django.shortcuts import redirect + +EXEMPT_PREFIXES = ( + "/admin/", + "/accounts/", + "/onboarding/profile/", + "/static/", + "/media/", +) + +class RequireProfileMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated and not request.user.nickname: + if not request.path.startswith(EXEMPT_PREFIXES): + return redirect("/onboarding/profile/") + return self.get_response(request) diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..02ac164 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("onboarding/profile/", views.onboarding_profile, name="onboarding_profile"), +] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..809812f 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,18 @@ -from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect +from .forms import OnboardingForm -# Create your views here. +@login_required +def onboarding_profile(request): + if request.user.nickname: # 이미 완료면 홈 + return redirect("/") + + if request.method == "POST": + form = OnboardingForm(request.POST, request.FILES, instance=request.user) + if form.is_valid(): + form.save() + return redirect("/") + else: + form = OnboardingForm(instance=request.user) + + return render(request, "account/onboarding_profile.html", {"form": form}) diff --git a/config/settings.py b/config/settings.py index 55e3835..d8d0723 100644 --- a/config/settings.py +++ b/config/settings.py @@ -58,6 +58,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "allauth.account.middleware.AccountMiddleware", + "accounts.middleware.RequireProfileMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", diff --git a/config/urls.py b/config/urls.py index cf07a17..97479b8 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,14 +9,16 @@ path("", initial_view.as_view(), name="initial"), path("admin/", admin.site.urls), + # allauth (로그인/소셜로그인) path("accounts/", include("allauth.urls")), # allauth 쪽으로 리다이렉트 path("login/", RedirectView.as_view(url="/accounts/login/")), path("logout/", RedirectView.as_view(url="/accounts/logout/")), path("signup/", RedirectView.as_view(url="/accounts/signup/")), - # template views: HTML로 보여줄 주소들 + # template views: HTML로 보여줄 주소들 + path("", include("accounts.urls")), # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html new file mode 100644 index 0000000..595822e --- /dev/null +++ b/templates/account/onboarding_profile.html @@ -0,0 +1,17 @@ +

프로필 설정

+ +
+ {% csrf_token %} + +
+ {{ form.nickname.errors }} + {{ form.nickname.label_tag }} {{ form.nickname }} +
+ +
+ {{ form.profile_image.errors }} + {{ form.profile_image.label_tag }} {{ form.profile_image }} +
+ + +
From 8f58bc1751786f21fd1ac015aabf3c5ddd2736af Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 20:00:12 +0900 Subject: [PATCH 014/380] =?UTF-8?q?chore:=20=EA=B8=B0=EB=B3=B8=20=EC=95=B1?= =?UTF-8?q?=EB=B3=84=20URL=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/urls.py b/config/urls.py index 97479b8..50fa5c0 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,7 +18,11 @@ path("signup/", RedirectView.as_view(url="/accounts/signup/")), # template views: HTML로 보여줄 주소들 - path("", include("accounts.urls")), + path("users/", include("accounts.urls")), + path("learnings/", include("learning.urls")), + path("roadmaps/", include("roadmaps.urls")), + path("reflections/", include("reflections.urls")), + path("teams/", include("teams.urls")), # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), From 97814c8d71bda582f1df5b6dca06fd4f676e8165 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 20:56:43 +0900 Subject: [PATCH 015/380] =?UTF-8?q?chore:=20=EA=B0=81=20=EC=95=B1=EB=B3=84?= =?UTF-8?q?=20URL=20=EA=B8=B0=EB=B3=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/urls.py | 2 ++ learning/urls.py | 8 ++++++++ reflections/urls.py | 8 ++++++++ roadmaps/urls.py | 8 ++++++++ teams/urls.py | 8 ++++++++ 5 files changed, 34 insertions(+) create mode 100644 learning/urls.py create mode 100644 reflections/urls.py create mode 100644 roadmaps/urls.py create mode 100644 teams/urls.py diff --git a/accounts/urls.py b/accounts/urls.py index 02ac164..07e9ae0 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,6 +1,8 @@ from django.urls import path from . import views +app_name = "accounts" + urlpatterns = [ path("onboarding/profile/", views.onboarding_profile, name="onboarding_profile"), ] diff --git a/learning/urls.py b/learning/urls.py new file mode 100644 index 0000000..b7c7c77 --- /dev/null +++ b/learning/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +app_name = "learning" + +urlpatterns = [ + # Add your URL patterns here +] diff --git a/reflections/urls.py b/reflections/urls.py new file mode 100644 index 0000000..6ad0817 --- /dev/null +++ b/reflections/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +app_name = "reflections" + +urlpatterns = [ + # Add your URL patterns here +] diff --git a/roadmaps/urls.py b/roadmaps/urls.py new file mode 100644 index 0000000..4b69bdc --- /dev/null +++ b/roadmaps/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +app_name = "roadmaps" + +urlpatterns = [ + # Add your URL patterns here +] diff --git a/teams/urls.py b/teams/urls.py new file mode 100644 index 0000000..fe0716e --- /dev/null +++ b/teams/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +app_name = "teams" + +urlpatterns = [ + # Add your URL patterns here +] From d373c6d5b23c9175fe02111816f03e897404ffb1 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 22:11:42 +0900 Subject: [PATCH 016/380] =?UTF-8?q?chore:=20PostgreSQL=20DB=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20env=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 16 +++ SETUP_GUIDE.md | 253 +++++++++++++++++++++++++++++++++++++++++++++ config/settings.py | 13 ++- requirements.txt | 2 + 4 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 SETUP_GUIDE.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cdd5d5d --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# PostgreSQL 데이터베이스 설정 +# 팀원들이 각자 자신의 PostgreSQL 사용자명과 설정에 맞게 수정해야 합니다 +DB_ENGINE=django.db.backends.postgresql +DB_NAME=startlinedev +DB_USER=your_postgres_user # 수정 필요: 자신의 PostgreSQL 사용자명으로 변경 +DB_PASSWORD= # 수정 필요: PostgreSQL 비밀번호 입력 (없으면 비워두기) +DB_HOST=localhost +DB_PORT=5432 + +# Django 설정 +SECRET_KEY=your-secret-key-here # 수정 필요: 실제 SECRET_KEY로 변경 +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 + +# Allauth 설정 +SITE_ID=1 diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..16bc47e --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,253 @@ +# 🚀 StartLine Dev 개발 환경 설정 가이드 + +## 필수 사항 + +- **Python 3.13+** +- **PostgreSQL 14+** +- **Git** + +--- + +## 📋 팀원 설정 방법 (모두 동일) + +### 1️⃣ 프로젝트 클론 + +```bash +git clone +cd StartLineDev +``` + +### 2️⃣ 가상환경 생성 및 활성화 + +```bash +# 가상환경 생성 +python3 -m venv venv + +# 가상환경 활성화 +source venv/bin/activate # macOS/Linux +# 또는 +venv\Scripts\activate # Windows +``` + +### 3️⃣ 패키지 설치 + +```bash +pip install -r requirements.txt +``` + +### 4️⃣ PostgreSQL 설정 + +#### 4-1. PostgreSQL 설치 (처음 한 번만) + +**macOS:** +```bash +# Homebrew로 설치 +brew install postgresql + +# 서비스 시작 +brew services start postgresql +``` + +**Windows:** +- [PostgreSQL 공식 설치프로그램](https://www.postgresql.org/download/windows/) 다운로드 및 설치 + +**Linux (Ubuntu):** +```bash +sudo apt-get install postgresql postgresql-contrib +sudo service postgresql start +``` + +#### 4-2. 데이터베이스 생성 + +```bash +# PostgreSQL 접속 +psql -U isujong + +# 데이터베이스 생성 +CREATE DATABASE startlinedev; + +# 확인 +\l + +# 나가기 +\q +``` + +**⚠️ 주의: 팀원들이 동일한 DB 이름과 사용자명(`isujong`)을 사용해야 합니다.** + +### 5️⃣ 환경변수 설정 + +```bash +# .env 파일 생성 (프로젝트 루트) +cp .env.example .env +``` + +`.env` 파일 수정: +```env +DB_ENGINE=django.db.backends.postgresql +DB_NAME=startlinedev +DB_USER=isujong +DB_PASSWORD= +DB_HOST=localhost +DB_PORT=5432 +DEBUG=True +``` + +### 6️⃣ 마이그레이션 및 초기 데이터 + +```bash +# 마이그레이션 적용 +python manage.py migrate + +# 관리자 계정 생성 (처음 한 번만) +python manage.py createsuperuser +``` + +### 7️⃣ 서버 실행 + +```bash +python manage.py runserver +``` + +브라우저에서 확인: +- 메인 페이지: `http://localhost:8000` +- 관리자 페이지: `http://localhost:8000/admin` + +--- + +## 🗄️ 데이터베이스 확인 + +### Django Admin (추천) +``` +http://localhost:8000/admin +``` + +### PostgreSQL 커맨드라인 +```bash +psql -d startlinedev -U isujong + +# 테이블 목록 +\dt + +# 특정 테이블 조회 +SELECT * FROM accounts_user; +SELECT * FROM learning_learningresource; + +# 나가기 +\q +``` + +### Django Shell +```bash +python manage.py shell + +from accounts.models import User +User.objects.all() +``` + +--- + +## 📦 필수 패키지 목록 + +현재 `requirements.txt`에 포함된 패키지: + +``` +Django==5.2.10 # Django 프레임워크 +django-allauth==65.14.0 # 사용자 인증 +psycopg2-binary==2.9.10 # PostgreSQL 드라이버 +pillow==12.1.0 # 이미지 처리 +``` + +--- + +## ⚠️ 일반적인 문제 해결 + +### "role 'isujong' does not exist" +```bash +# 현재 사용자 확인 +whoami + +# PostgreSQL 사용자 생성 (필요시) +createuser -s isujong +``` + +### "database 'startlinedev' does not exist" +```bash +# 데이터베이스 생성 +createdb startlinedev +``` + +### psycopg2 설치 오류 (macOS) +```bash +pip install psycopg2-binary +# 또는 +pip install --no-binary :all: psycopg2 +``` + +### 마이그레이션 충돌 +```bash +# 마이그레이션 상태 확인 +python manage.py showmigrations + +# 특정 마이그레이션 제거 (필요시) +python manage.py migrate +``` + +--- + +## 🔐 보안 주의사항 + +⚠️ **절대 커밋하지 말기:** +- `.env` 파일 (`.gitignore`에 포함) +- `db.sqlite3` +- `local_settings.py` +- `venv/` 디렉토리 + +✅ **공유할 파일:** +- `requirements.txt` (패키지 목록) +- `.env.example` (샘플 설정) +- `SETUP_GUIDE.md` (이 파일) + +--- + +## 🤝 협업 가이드 + +### 새로운 패키지 추가 시 +```bash +pip install +pip freeze > requirements.txt +git add requirements.txt +git commit -m "Add: " +``` + +### 마이그레이션 생성 시 +```bash +python manage.py makemigrations +python manage.py migrate + +# 커밋 +git add /migrations/ +git commit -m "Migration: " +``` + +### Pull 받은 후 +```bash +git pull +source venv/bin/activate +pip install -r requirements.txt +python manage.py migrate +``` + +--- + +## 📞 문제 발생 시 + +문제 발생 시 다음 정보를 함께 공유해주세요: +- OS 및 Python 버전: `python --version` +- PostgreSQL 버전: `psql --version` +- 전체 에러 메시지 +- 수행한 명령어 + +--- + +**행운을 빕니다! 🚀** diff --git a/config/settings.py b/config/settings.py index d8d0723..9d0dab7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -11,6 +11,11 @@ """ from pathlib import Path +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -108,8 +113,12 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": os.getenv("DB_ENGINE", "django.db.backends.postgresql"), + "NAME": os.getenv("DB_NAME", "startlinedev"), + "USER": os.getenv("DB_USER", "isujong"), + "PASSWORD": os.getenv("DB_PASSWORD", ""), + "HOST": os.getenv("DB_HOST", "localhost"), + "PORT": os.getenv("DB_PORT", "5432"), } } diff --git a/requirements.txt b/requirements.txt index 5e67969..eb6722b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ asgiref==3.11.0 Django==5.2.10 django-allauth==65.14.0 +python-dotenv==1.0.0 +psycopg2-binary==2.9.10 pillow==12.1.0 sqlparse==0.5.5 tzdata==2025.3 From a933424f44d39ff6fdffb86a757d81c27e73c906 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 22:28:18 +0900 Subject: [PATCH 017/380] =?UTF-8?q?docs:=20PostgreSQL=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SETUP_GUIDE.md | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md index 16bc47e..4278935 100644 --- a/SETUP_GUIDE.md +++ b/SETUP_GUIDE.md @@ -60,10 +60,14 @@ sudo service postgresql start #### 4-2. 데이터베이스 생성 ```bash -# PostgreSQL 접속 -psql -U isujong +# PostgreSQL 접속 (자신의 PostgreSQL 사용자명으로) +# macOS 사용자 예시: +psql -U $(whoami) -# 데이터베이스 생성 +# Windows 사용자 예시: +psql -U postgres + +# 데이터베이스 생성 (모두 동일) CREATE DATABASE startlinedev; # 확인 @@ -73,7 +77,10 @@ CREATE DATABASE startlinedev; \q ``` -**⚠️ 주의: 팀원들이 동일한 DB 이름과 사용자명(`isujong`)을 사용해야 합니다.** +**⚠️ 중요: 각 팀원이 자신의 PostgreSQL 사용자명을 사용해야 합니다.** +- macOS: 기본값은 설치된 맥 사용자명 (예: `isujong`, `john` 등) +- Windows: 기본값은 `postgres` (설치 중 설정한 비밀번호 필요) +- Linux: 기본값은 `postgres` ### 5️⃣ 환경변수 설정 @@ -82,17 +89,25 @@ CREATE DATABASE startlinedev; cp .env.example .env ``` -`.env` 파일 수정: +`.env` 파일 수정 (각 팀원이 자신의 정보로 수정): ```env +# 각자 자신의 PostgreSQL 사용자명 입력 DB_ENGINE=django.db.backends.postgresql -DB_NAME=startlinedev -DB_USER=isujong -DB_PASSWORD= +DB_NAME=startlinedev # 모두 동일 +DB_USER=your_postgres_user # 자신의 PostgreSQL 사용자명으로 변경 +DB_PASSWORD=your_password # 자신의 PostgreSQL 비밀번호 DB_HOST=localhost DB_PORT=5432 + +SECRET_KEY=your-secret-key-here DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 ``` +**예시:** +- macOS 사용자 (isujong): `DB_USER=isujong` +- Windows 사용자: `DB_USER=postgres` + ### 6️⃣ 마이그레이션 및 초기 데이터 ```bash @@ -116,7 +131,8 @@ python manage.py runserver --- ## 🗄️ 데이터베이스 확인 - +# 자신의 PostgreSQL 사용자명으로 접속 +psql -d startlinedev -U your_postgres_user ### Django Admin (추천) ``` http://localhost:8000/admin @@ -247,7 +263,3 @@ python manage.py migrate - PostgreSQL 버전: `psql --version` - 전체 에러 메시지 - 수행한 명령어 - ---- - -**행운을 빕니다! 🚀** From 0134fbd3bd20d1e90487ed2bfa94f3632a8aecef Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 27 Jan 2026 23:44:48 +0900 Subject: [PATCH 018/380] =?UTF-8?q?chore:=20swagger=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 26 +++++++++++++++++++++++++- config/urls.py | 6 ++++++ requirements.txt | 2 ++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index 9d0dab7..26a6c42 100644 --- a/config/settings.py +++ b/config/settings.py @@ -43,10 +43,13 @@ "django.contrib.messages", "django.contrib.staticfiles", - # Module apps + # Third-party apps + "rest_framework", + "drf_spectacular", "allauth", "allauth.account", "allauth.socialaccount", + # Local apps "accounts", "learning", @@ -169,3 +172,24 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +# Django REST Framework 설정 +# https://www.django-rest-framework.org/ + +REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, +} + +# drf-spectacular 설정 (Swagger/OpenAPI) +SPECTACULAR_SETTINGS = { + "TITLE": "StartLine Dev API", + "DESCRIPTION": "StartLine Dev 프로젝트 API 문서", + "VERSION": "1.0.0", + "SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"], + "SERVERS": [ + {"url": "http://localhost:8000", "description": "Development"}, + ], +} diff --git a/config/urls.py b/config/urls.py index 50fa5c0..2460c39 100644 --- a/config/urls.py +++ b/config/urls.py @@ -4,6 +4,7 @@ from django.conf.urls.static import static from django.views.generic import RedirectView from .views import initial_view +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView urlpatterns = [ @@ -26,6 +27,11 @@ # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), + + # Swagger & API Schema + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), + path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), ] if settings.DEBUG: diff --git a/requirements.txt b/requirements.txt index eb6722b..ab9e76c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ asgiref==3.11.0 Django==5.2.10 django-allauth==65.14.0 +django-rest-framework==3.14.0 +drf-spectacular==0.27.0 python-dotenv==1.0.0 psycopg2-binary==2.9.10 pillow==12.1.0 From 032adbda4a99b235bf1f507666e06cab15483862 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 28 Jan 2026 15:04:41 +0900 Subject: [PATCH 019/380] =?UTF-8?q?chore:=20swagger=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EC=97=86=EC=9D=B4=20=EC=A0=91=EA=B7=BC=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/middleware.py | 1 + config/settings.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/accounts/middleware.py b/accounts/middleware.py index 5d00ff3..038df4e 100644 --- a/accounts/middleware.py +++ b/accounts/middleware.py @@ -6,6 +6,7 @@ "/onboarding/profile/", "/static/", "/media/", + "/api/", # Swagger 및 API 테스트용 ) class RequireProfileMiddleware: diff --git a/config/settings.py b/config/settings.py index 26a6c42..de596f2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -181,6 +181,11 @@ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, + # 개발 중에는 인증 없이 API 테스트 가능하도록 설정 + "DEFAULT_AUTHENTICATION_CLASSES": [], + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.AllowAny", + ], } # drf-spectacular 설정 (Swagger/OpenAPI) From 2c4be642c62559dc1a7572e23ebd781a5d730325 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 28 Jan 2026 16:11:21 +0900 Subject: [PATCH 020/380] =?UTF-8?q?chore:=20url=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/middleware.py b/accounts/middleware.py index 038df4e..0883605 100644 --- a/accounts/middleware.py +++ b/accounts/middleware.py @@ -3,7 +3,7 @@ EXEMPT_PREFIXES = ( "/admin/", "/accounts/", - "/onboarding/profile/", + "/users/onboarding/", "/static/", "/media/", "/api/", # Swagger 및 API 테스트용 @@ -16,5 +16,5 @@ def __init__(self, get_response): def __call__(self, request): if request.user.is_authenticated and not request.user.nickname: if not request.path.startswith(EXEMPT_PREFIXES): - return redirect("/onboarding/profile/") + return redirect("/users/onboarding/profile/") return self.get_response(request) From a09fe76238c124c2b5192f25988ef39dfe5ac8a8 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 28 Jan 2026 17:03:02 +0900 Subject: [PATCH 021/380] =?UTF-8?q?feat:=20roadmaps=20swagger=EC=9A=A9=20a?= =?UTF-8?q?pi=20crud=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmaps/api_urls.py | 9 ++++- roadmaps/serializers.py | 48 ++++++++++++++++++++++++ roadmaps/views.py | 82 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 roadmaps/serializers.py diff --git a/roadmaps/api_urls.py b/roadmaps/api_urls.py index 690b131..efa2cf0 100644 --- a/roadmaps/api_urls.py +++ b/roadmaps/api_urls.py @@ -1,4 +1,11 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import RoadmapViewSet, RoadmapItemViewSet + +router = DefaultRouter() +router.register(r'roadmaps', RoadmapViewSet, basename='roadmap') +router.register(r'roadmap-items', RoadmapItemViewSet, basename='roadmap-item') urlpatterns = [ + path('', include(router.urls)), ] diff --git a/roadmaps/serializers.py b/roadmaps/serializers.py new file mode 100644 index 0000000..fc7f32c --- /dev/null +++ b/roadmaps/serializers.py @@ -0,0 +1,48 @@ +from rest_framework import serializers +from .models import Roadmap, RoadmapItem, RoadmapTag + + +class RoadmapTagSerializer(serializers.ModelSerializer): + """로드맵 태그 Serializer""" + tag_name = serializers.CharField(source='tag.name', read_only=True) + + class Meta: + model = RoadmapTag + fields = ['id', 'roadmap', 'tag', 'tag_name'] + read_only_fields = ['id'] + + +class RoadmapItemSerializer(serializers.ModelSerializer): + """로드맵 아이템 Serializer""" + resource_title = serializers.CharField(source='resource.title', read_only=True) + + class Meta: + model = RoadmapItem + fields = [ + 'id', 'roadmap', 'resource', 'resource_title', + 'order_no', 'is_completed', 'completed_at', 'created_at' + ] + read_only_fields = ['id', 'created_at'] + + +class RoadmapSerializer(serializers.ModelSerializer): + """로드맵 기본 Serializer""" + items = RoadmapItemSerializer(many=True, read_only=True) + track_display = serializers.CharField(source='get_track_display', read_only=True) + + class Meta: + model = Roadmap + fields = [ + 'id', 'user', 'track', 'track_display', 'level', + 'current_index', 'is_completed', 'created_at', 'items' + ] + read_only_fields = ['id', 'created_at'] + + +class RoadmapCreateSerializer(serializers.ModelSerializer): + """로드맵 생성용 Serializer (간소화)""" + + class Meta: + model = Roadmap + fields = ['id', 'user', 'track', 'level'] + read_only_fields = ['id'] diff --git a/roadmaps/views.py b/roadmaps/views.py index 91ea44a..3f655d0 100644 --- a/roadmaps/views.py +++ b/roadmaps/views.py @@ -1,3 +1,85 @@ from django.shortcuts import render +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import action +from drf_spectacular.utils import extend_schema, extend_schema_view + +from .models import Roadmap, RoadmapItem, RoadmapTag +from .serializers import ( + RoadmapSerializer, + RoadmapCreateSerializer, + RoadmapItemSerializer, + RoadmapTagSerializer, +) + + +# ============================================ +# API ViewSets (Swagger 테스트용) +# ============================================ + +@extend_schema_view( + list=extend_schema(summary="로드맵 목록 조회", tags=["Roadmaps"]), + retrieve=extend_schema(summary="로드맵 상세 조회", tags=["Roadmaps"]), + create=extend_schema(summary="로드맵 생성", tags=["Roadmaps"]), + update=extend_schema(summary="로드맵 전체 수정", tags=["Roadmaps"]), + partial_update=extend_schema(summary="로드맵 부분 수정", tags=["Roadmaps"]), + destroy=extend_schema(summary="로드맵 삭제", tags=["Roadmaps"]), +) +class RoadmapViewSet(viewsets.ModelViewSet): + """ + 로드맵 CRUD API + + - GET /api/roadmaps/ : 목록 조회 + - POST /api/roadmaps/ : 생성 + - GET /api/roadmaps/{id}/ : 상세 조회 + - PUT /api/roadmaps/{id}/ : 전체 수정 + - PATCH /api/roadmaps/{id}/ : 부분 수정 + - DELETE /api/roadmaps/{id}/ : 삭제 + """ + queryset = Roadmap.objects.all().prefetch_related('items', 'tags') + + def get_serializer_class(self): + if self.action == 'create': + return RoadmapCreateSerializer + return RoadmapSerializer + + +@extend_schema_view( + list=extend_schema(summary="로드맵 아이템 목록 조회", tags=["Roadmap Items"]), + retrieve=extend_schema(summary="로드맵 아이템 상세 조회", tags=["Roadmap Items"]), + create=extend_schema(summary="로드맵 아이템 생성", tags=["Roadmap Items"]), + update=extend_schema(summary="로드맵 아이템 전체 수정", tags=["Roadmap Items"]), + partial_update=extend_schema(summary="로드맵 아이템 부분 수정", tags=["Roadmap Items"]), + destroy=extend_schema(summary="로드맵 아이템 삭제", tags=["Roadmap Items"]), +) +class RoadmapItemViewSet(viewsets.ModelViewSet): + """ + 로드맵 아이템 CRUD API + + - GET /api/roadmap-items/ : 목록 조회 + - POST /api/roadmap-items/ : 생성 + - GET /api/roadmap-items/{id}/ : 상세 조회 + - PUT /api/roadmap-items/{id}/ : 전체 수정 + - PATCH /api/roadmap-items/{id}/ : 부분 수정 + - DELETE /api/roadmap-items/{id}/ : 삭제 + """ + queryset = RoadmapItem.objects.all().select_related('roadmap', 'resource') + serializer_class = RoadmapItemSerializer + + @extend_schema(summary="아이템 완료 처리", tags=["Roadmap Items"]) + @action(detail=True, methods=['post']) + def complete(self, request, pk=None): + """아이템을 완료 상태로 변경""" + from django.utils import timezone + item = self.get_object() + item.is_completed = True + item.completed_at = timezone.now() + item.save() + return Response(RoadmapItemSerializer(item).data) + + +# ============================================ +# Template Views (HTML 렌더링용 - 나중에 추가) +# ============================================ # Create your views here. From 152ad972445d0375d40a5e7f45e79df035ef9567 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 28 Jan 2026 17:03:33 +0900 Subject: [PATCH 022/380] =?UTF-8?q?feat:=20teams=20swagger=EC=9A=A9=20api?= =?UTF-8?q?=20crud=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- teams/api_urls.py | 9 +++++- teams/serializers.py | 40 +++++++++++++++++++++++ teams/views.py | 75 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 teams/serializers.py diff --git a/teams/api_urls.py b/teams/api_urls.py index 690b131..641cd09 100644 --- a/teams/api_urls.py +++ b/teams/api_urls.py @@ -1,4 +1,11 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import TeamViewSet, TeamMemberViewSet + +router = DefaultRouter() +router.register(r'teams', TeamViewSet, basename='team') +router.register(r'team-members', TeamMemberViewSet, basename='team-member') urlpatterns = [ + path('', include(router.urls)), ] diff --git a/teams/serializers.py b/teams/serializers.py new file mode 100644 index 0000000..3915fc6 --- /dev/null +++ b/teams/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers +from .models import Team, TeamMember + + +class TeamMemberSerializer(serializers.ModelSerializer): + """팀 멤버 Serializer""" + username = serializers.CharField(source='user.username', read_only=True) + role_display = serializers.CharField(source='get_role_display', read_only=True) + + class Meta: + model = TeamMember + fields = ['id', 'team', 'user', 'username', 'role', 'role_display', 'joined_at'] + read_only_fields = ['id', 'joined_at'] + + +class TeamSerializer(serializers.ModelSerializer): + """팀 기본 Serializer""" + members = TeamMemberSerializer(many=True, read_only=True) + created_by_username = serializers.CharField(source='created_by.username', read_only=True) + member_count = serializers.SerializerMethodField() + + class Meta: + model = Team + fields = [ + 'id', 'name', 'created_by', 'created_by_username', + 'created_at', 'members', 'member_count' + ] + read_only_fields = ['id', 'created_at'] + + def get_member_count(self, obj): + return obj.members.count() + + +class TeamCreateSerializer(serializers.ModelSerializer): + """팀 생성용 Serializer (간소화)""" + + class Meta: + model = Team + fields = ['id', 'name', 'created_by'] + read_only_fields = ['id'] diff --git a/teams/views.py b/teams/views.py index 91ea44a..1fce967 100644 --- a/teams/views.py +++ b/teams/views.py @@ -1,3 +1,78 @@ from django.shortcuts import render +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import action +from drf_spectacular.utils import extend_schema, extend_schema_view + +from .models import Team, TeamMember +from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer + + +# ============================================ +# API ViewSets (Swagger 테스트용) +# ============================================ + +@extend_schema_view( + list=extend_schema(summary="팀 목록 조회", tags=["Teams"]), + retrieve=extend_schema(summary="팀 상세 조회", tags=["Teams"]), + create=extend_schema(summary="팀 생성", tags=["Teams"]), + update=extend_schema(summary="팀 전체 수정", tags=["Teams"]), + partial_update=extend_schema(summary="팀 부분 수정", tags=["Teams"]), + destroy=extend_schema(summary="팀 삭제", tags=["Teams"]), +) +class TeamViewSet(viewsets.ModelViewSet): + """ + 팀 CRUD API + + - GET /api/teams/ : 목록 조회 + - POST /api/teams/ : 생성 + - GET /api/teams/{id}/ : 상세 조회 + - PUT /api/teams/{id}/ : 전체 수정 + - PATCH /api/teams/{id}/ : 부분 수정 + - DELETE /api/teams/{id}/ : 삭제 + """ + queryset = Team.objects.all().prefetch_related('members') + + def get_serializer_class(self): + if self.action == 'create': + return TeamCreateSerializer + return TeamSerializer + + +@extend_schema_view( + list=extend_schema(summary="팀 멤버 목록 조회", tags=["Team Members"]), + retrieve=extend_schema(summary="팀 멤버 상세 조회", tags=["Team Members"]), + create=extend_schema(summary="팀 멤버 추가", tags=["Team Members"]), + update=extend_schema(summary="팀 멤버 전체 수정", tags=["Team Members"]), + partial_update=extend_schema(summary="팀 멤버 부분 수정", tags=["Team Members"]), + destroy=extend_schema(summary="팀 멤버 삭제", tags=["Team Members"]), +) +class TeamMemberViewSet(viewsets.ModelViewSet): + """ + 팀 멤버 CRUD API + + - GET /api/team-members/ : 목록 조회 + - POST /api/team-members/ : 생성 (팀에 멤버 추가) + - GET /api/team-members/{id}/ : 상세 조회 + - PUT /api/team-members/{id}/ : 전체 수정 + - PATCH /api/team-members/{id}/ : 부분 수정 (역할 변경 등) + - DELETE /api/team-members/{id}/ : 삭제 (팀에서 멤버 제거) + """ + queryset = TeamMember.objects.all().select_related('team', 'user') + serializer_class = TeamMemberSerializer + + @extend_schema(summary="리더로 승급", tags=["Team Members"]) + @action(detail=True, methods=['post']) + def promote_to_leader(self, request, pk=None): + """멤버를 리더로 승급""" + member = self.get_object() + member.role = TeamMember.Role.LEADER + member.save() + return Response(TeamMemberSerializer(member).data) + + +# ============================================ +# Template Views (HTML 렌더링용 - 나중에 추가) +# ============================================ # Create your views here. From c7a68bffc656f93f5db05e5a78aaab6959dddadd Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 17:19:17 +0900 Subject: [PATCH 023/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 같은 users/onboarding/profile 주소로 접근하면, 유저 닉네임 수정 가능 - requirements.txt 수정: 이미지 렌더를 위한 pillow 추가, 잘못된 모듈 설치명 수정 (django-rest-framework) --- accounts/middleware.py | 5 +++-- accounts/views.py | 13 ++++++++----- requirements.txt | 16 +++++++++++++--- templates/account/onboarding_profile.html | 6 ++++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/accounts/middleware.py b/accounts/middleware.py index 038df4e..ec21e49 100644 --- a/accounts/middleware.py +++ b/accounts/middleware.py @@ -3,7 +3,8 @@ EXEMPT_PREFIXES = ( "/admin/", "/accounts/", - "/onboarding/profile/", + "/logout/", + "/users/onboarding/", "/static/", "/media/", "/api/", # Swagger 및 API 테스트용 @@ -16,5 +17,5 @@ def __init__(self, get_response): def __call__(self, request): if request.user.is_authenticated and not request.user.nickname: if not request.path.startswith(EXEMPT_PREFIXES): - return redirect("/onboarding/profile/") + return redirect("/users/onboarding/profile/") return self.get_response(request) diff --git a/accounts/views.py b/accounts/views.py index 809812f..212d96b 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -4,15 +4,18 @@ @login_required def onboarding_profile(request): - if request.user.nickname: # 이미 완료면 홈 - return redirect("/") - if request.method == "POST": - form = OnboardingForm(request.POST, request.FILES, instance=request.user) + form = OnboardingForm( + request.POST, + request.FILES, + instance=request.user, + ) if form.is_valid(): form.save() return redirect("/") else: form = OnboardingForm(instance=request.user) - return render(request, "account/onboarding_profile.html", {"form": form}) + context = {"form": form} + + return render(request, "account/onboarding_profile.html", context) diff --git a/requirements.txt b/requirements.txt index ab9e76c..4ba6c29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,20 @@ asgiref==3.11.0 +attrs==25.4.0 Django==5.2.10 django-allauth==65.14.0 -django-rest-framework==3.14.0 +djangorestframework==3.14.0 drf-spectacular==0.27.0 -python-dotenv==1.0.0 -psycopg2-binary==2.9.10 +inflection==0.5.1 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 pillow==12.1.0 +psycopg2-binary==2.9.10 +python-dotenv==1.0.0 +pytz==2025.2 +PyYAML==6.0.3 +referencing==0.37.0 +rpds-py==0.30.0 sqlparse==0.5.5 +typing_extensions==4.15.0 tzdata==2025.3 +uritemplate==4.2.0 diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html index 595822e..5c2a253 100644 --- a/templates/account/onboarding_profile.html +++ b/templates/account/onboarding_profile.html @@ -1,4 +1,4 @@ -

프로필 설정

+

{% if user.nickname %}프로필 수정{% else %}프로필 설정{% endif %}

{% csrf_token %} @@ -13,5 +13,7 @@

프로필 설정

{{ form.profile_image.label_tag }} {{ form.profile_image }} - +
From bbd5c598c4fd66c56304f2569892454c0a7eae93 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 17:22:39 +0900 Subject: [PATCH 024/380] =?UTF-8?q?chore:=20=EC=95=B1=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - accounts, learning, reflections, roadmaps, teams 모두 apps 폴더 하위로 변경 --- {accounts => apps/accounts}/__init__.py | 0 {accounts => apps/accounts}/admin.py | 0 {accounts => apps/accounts}/api_urls.py | 0 {accounts => apps/accounts}/apps.py | 0 {accounts => apps/accounts}/forms.py | 0 {accounts => apps/accounts}/middleware.py | 0 {accounts => apps/accounts}/migrations/0001_initial.py | 0 .../accounts}/migrations/0002_alter_user_profile_image.py | 0 {accounts => apps/accounts}/migrations/__init__.py | 0 {accounts => apps/accounts}/models.py | 0 {accounts => apps/accounts}/tests.py | 0 {accounts => apps/accounts}/urls.py | 0 {accounts => apps/accounts}/views.py | 0 {learning => apps/learning}/__init__.py | 0 {learning => apps/learning}/admin.py | 0 {learning => apps/learning}/api_urls.py | 0 {learning => apps/learning}/apps.py | 0 {learning => apps/learning}/migrations/0001_initial.py | 0 {learning => apps/learning}/migrations/__init__.py | 0 {learning => apps/learning}/models.py | 0 {learning => apps/learning}/tests.py | 0 {learning => apps/learning}/urls.py | 0 {learning => apps/learning}/views.py | 0 {reflections => apps/reflections}/__init__.py | 0 {reflections => apps/reflections}/admin.py | 0 {reflections => apps/reflections}/api_urls.py | 0 {reflections => apps/reflections}/apps.py | 0 {reflections => apps/reflections}/migrations/0001_initial.py | 0 {reflections => apps/reflections}/migrations/__init__.py | 0 {reflections => apps/reflections}/models.py | 0 {reflections => apps/reflections}/tests.py | 0 {reflections => apps/reflections}/urls.py | 0 {reflections => apps/reflections}/views.py | 0 {roadmaps => apps/roadmaps}/__init__.py | 0 {roadmaps => apps/roadmaps}/admin.py | 0 {roadmaps => apps/roadmaps}/api_urls.py | 0 {roadmaps => apps/roadmaps}/apps.py | 0 {roadmaps => apps/roadmaps}/migrations/0001_initial.py | 0 {roadmaps => apps/roadmaps}/migrations/__init__.py | 0 {roadmaps => apps/roadmaps}/models.py | 0 {roadmaps => apps/roadmaps}/tests.py | 0 {roadmaps => apps/roadmaps}/urls.py | 0 {roadmaps => apps/roadmaps}/views.py | 0 {teams => apps/teams}/__init__.py | 0 {teams => apps/teams}/admin.py | 0 {teams => apps/teams}/api_urls.py | 0 {teams => apps/teams}/apps.py | 0 {teams => apps/teams}/migrations/0001_initial.py | 0 {teams => apps/teams}/migrations/__init__.py | 0 {teams => apps/teams}/models.py | 0 {teams => apps/teams}/tests.py | 0 {teams => apps/teams}/urls.py | 0 {teams => apps/teams}/views.py | 0 53 files changed, 0 insertions(+), 0 deletions(-) rename {accounts => apps/accounts}/__init__.py (100%) rename {accounts => apps/accounts}/admin.py (100%) rename {accounts => apps/accounts}/api_urls.py (100%) rename {accounts => apps/accounts}/apps.py (100%) rename {accounts => apps/accounts}/forms.py (100%) rename {accounts => apps/accounts}/middleware.py (100%) rename {accounts => apps/accounts}/migrations/0001_initial.py (100%) rename {accounts => apps/accounts}/migrations/0002_alter_user_profile_image.py (100%) rename {accounts => apps/accounts}/migrations/__init__.py (100%) rename {accounts => apps/accounts}/models.py (100%) rename {accounts => apps/accounts}/tests.py (100%) rename {accounts => apps/accounts}/urls.py (100%) rename {accounts => apps/accounts}/views.py (100%) rename {learning => apps/learning}/__init__.py (100%) rename {learning => apps/learning}/admin.py (100%) rename {learning => apps/learning}/api_urls.py (100%) rename {learning => apps/learning}/apps.py (100%) rename {learning => apps/learning}/migrations/0001_initial.py (100%) rename {learning => apps/learning}/migrations/__init__.py (100%) rename {learning => apps/learning}/models.py (100%) rename {learning => apps/learning}/tests.py (100%) rename {learning => apps/learning}/urls.py (100%) rename {learning => apps/learning}/views.py (100%) rename {reflections => apps/reflections}/__init__.py (100%) rename {reflections => apps/reflections}/admin.py (100%) rename {reflections => apps/reflections}/api_urls.py (100%) rename {reflections => apps/reflections}/apps.py (100%) rename {reflections => apps/reflections}/migrations/0001_initial.py (100%) rename {reflections => apps/reflections}/migrations/__init__.py (100%) rename {reflections => apps/reflections}/models.py (100%) rename {reflections => apps/reflections}/tests.py (100%) rename {reflections => apps/reflections}/urls.py (100%) rename {reflections => apps/reflections}/views.py (100%) rename {roadmaps => apps/roadmaps}/__init__.py (100%) rename {roadmaps => apps/roadmaps}/admin.py (100%) rename {roadmaps => apps/roadmaps}/api_urls.py (100%) rename {roadmaps => apps/roadmaps}/apps.py (100%) rename {roadmaps => apps/roadmaps}/migrations/0001_initial.py (100%) rename {roadmaps => apps/roadmaps}/migrations/__init__.py (100%) rename {roadmaps => apps/roadmaps}/models.py (100%) rename {roadmaps => apps/roadmaps}/tests.py (100%) rename {roadmaps => apps/roadmaps}/urls.py (100%) rename {roadmaps => apps/roadmaps}/views.py (100%) rename {teams => apps/teams}/__init__.py (100%) rename {teams => apps/teams}/admin.py (100%) rename {teams => apps/teams}/api_urls.py (100%) rename {teams => apps/teams}/apps.py (100%) rename {teams => apps/teams}/migrations/0001_initial.py (100%) rename {teams => apps/teams}/migrations/__init__.py (100%) rename {teams => apps/teams}/models.py (100%) rename {teams => apps/teams}/tests.py (100%) rename {teams => apps/teams}/urls.py (100%) rename {teams => apps/teams}/views.py (100%) diff --git a/accounts/__init__.py b/apps/accounts/__init__.py similarity index 100% rename from accounts/__init__.py rename to apps/accounts/__init__.py diff --git a/accounts/admin.py b/apps/accounts/admin.py similarity index 100% rename from accounts/admin.py rename to apps/accounts/admin.py diff --git a/accounts/api_urls.py b/apps/accounts/api_urls.py similarity index 100% rename from accounts/api_urls.py rename to apps/accounts/api_urls.py diff --git a/accounts/apps.py b/apps/accounts/apps.py similarity index 100% rename from accounts/apps.py rename to apps/accounts/apps.py diff --git a/accounts/forms.py b/apps/accounts/forms.py similarity index 100% rename from accounts/forms.py rename to apps/accounts/forms.py diff --git a/accounts/middleware.py b/apps/accounts/middleware.py similarity index 100% rename from accounts/middleware.py rename to apps/accounts/middleware.py diff --git a/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py similarity index 100% rename from accounts/migrations/0001_initial.py rename to apps/accounts/migrations/0001_initial.py diff --git a/accounts/migrations/0002_alter_user_profile_image.py b/apps/accounts/migrations/0002_alter_user_profile_image.py similarity index 100% rename from accounts/migrations/0002_alter_user_profile_image.py rename to apps/accounts/migrations/0002_alter_user_profile_image.py diff --git a/accounts/migrations/__init__.py b/apps/accounts/migrations/__init__.py similarity index 100% rename from accounts/migrations/__init__.py rename to apps/accounts/migrations/__init__.py diff --git a/accounts/models.py b/apps/accounts/models.py similarity index 100% rename from accounts/models.py rename to apps/accounts/models.py diff --git a/accounts/tests.py b/apps/accounts/tests.py similarity index 100% rename from accounts/tests.py rename to apps/accounts/tests.py diff --git a/accounts/urls.py b/apps/accounts/urls.py similarity index 100% rename from accounts/urls.py rename to apps/accounts/urls.py diff --git a/accounts/views.py b/apps/accounts/views.py similarity index 100% rename from accounts/views.py rename to apps/accounts/views.py diff --git a/learning/__init__.py b/apps/learning/__init__.py similarity index 100% rename from learning/__init__.py rename to apps/learning/__init__.py diff --git a/learning/admin.py b/apps/learning/admin.py similarity index 100% rename from learning/admin.py rename to apps/learning/admin.py diff --git a/learning/api_urls.py b/apps/learning/api_urls.py similarity index 100% rename from learning/api_urls.py rename to apps/learning/api_urls.py diff --git a/learning/apps.py b/apps/learning/apps.py similarity index 100% rename from learning/apps.py rename to apps/learning/apps.py diff --git a/learning/migrations/0001_initial.py b/apps/learning/migrations/0001_initial.py similarity index 100% rename from learning/migrations/0001_initial.py rename to apps/learning/migrations/0001_initial.py diff --git a/learning/migrations/__init__.py b/apps/learning/migrations/__init__.py similarity index 100% rename from learning/migrations/__init__.py rename to apps/learning/migrations/__init__.py diff --git a/learning/models.py b/apps/learning/models.py similarity index 100% rename from learning/models.py rename to apps/learning/models.py diff --git a/learning/tests.py b/apps/learning/tests.py similarity index 100% rename from learning/tests.py rename to apps/learning/tests.py diff --git a/learning/urls.py b/apps/learning/urls.py similarity index 100% rename from learning/urls.py rename to apps/learning/urls.py diff --git a/learning/views.py b/apps/learning/views.py similarity index 100% rename from learning/views.py rename to apps/learning/views.py diff --git a/reflections/__init__.py b/apps/reflections/__init__.py similarity index 100% rename from reflections/__init__.py rename to apps/reflections/__init__.py diff --git a/reflections/admin.py b/apps/reflections/admin.py similarity index 100% rename from reflections/admin.py rename to apps/reflections/admin.py diff --git a/reflections/api_urls.py b/apps/reflections/api_urls.py similarity index 100% rename from reflections/api_urls.py rename to apps/reflections/api_urls.py diff --git a/reflections/apps.py b/apps/reflections/apps.py similarity index 100% rename from reflections/apps.py rename to apps/reflections/apps.py diff --git a/reflections/migrations/0001_initial.py b/apps/reflections/migrations/0001_initial.py similarity index 100% rename from reflections/migrations/0001_initial.py rename to apps/reflections/migrations/0001_initial.py diff --git a/reflections/migrations/__init__.py b/apps/reflections/migrations/__init__.py similarity index 100% rename from reflections/migrations/__init__.py rename to apps/reflections/migrations/__init__.py diff --git a/reflections/models.py b/apps/reflections/models.py similarity index 100% rename from reflections/models.py rename to apps/reflections/models.py diff --git a/reflections/tests.py b/apps/reflections/tests.py similarity index 100% rename from reflections/tests.py rename to apps/reflections/tests.py diff --git a/reflections/urls.py b/apps/reflections/urls.py similarity index 100% rename from reflections/urls.py rename to apps/reflections/urls.py diff --git a/reflections/views.py b/apps/reflections/views.py similarity index 100% rename from reflections/views.py rename to apps/reflections/views.py diff --git a/roadmaps/__init__.py b/apps/roadmaps/__init__.py similarity index 100% rename from roadmaps/__init__.py rename to apps/roadmaps/__init__.py diff --git a/roadmaps/admin.py b/apps/roadmaps/admin.py similarity index 100% rename from roadmaps/admin.py rename to apps/roadmaps/admin.py diff --git a/roadmaps/api_urls.py b/apps/roadmaps/api_urls.py similarity index 100% rename from roadmaps/api_urls.py rename to apps/roadmaps/api_urls.py diff --git a/roadmaps/apps.py b/apps/roadmaps/apps.py similarity index 100% rename from roadmaps/apps.py rename to apps/roadmaps/apps.py diff --git a/roadmaps/migrations/0001_initial.py b/apps/roadmaps/migrations/0001_initial.py similarity index 100% rename from roadmaps/migrations/0001_initial.py rename to apps/roadmaps/migrations/0001_initial.py diff --git a/roadmaps/migrations/__init__.py b/apps/roadmaps/migrations/__init__.py similarity index 100% rename from roadmaps/migrations/__init__.py rename to apps/roadmaps/migrations/__init__.py diff --git a/roadmaps/models.py b/apps/roadmaps/models.py similarity index 100% rename from roadmaps/models.py rename to apps/roadmaps/models.py diff --git a/roadmaps/tests.py b/apps/roadmaps/tests.py similarity index 100% rename from roadmaps/tests.py rename to apps/roadmaps/tests.py diff --git a/roadmaps/urls.py b/apps/roadmaps/urls.py similarity index 100% rename from roadmaps/urls.py rename to apps/roadmaps/urls.py diff --git a/roadmaps/views.py b/apps/roadmaps/views.py similarity index 100% rename from roadmaps/views.py rename to apps/roadmaps/views.py diff --git a/teams/__init__.py b/apps/teams/__init__.py similarity index 100% rename from teams/__init__.py rename to apps/teams/__init__.py diff --git a/teams/admin.py b/apps/teams/admin.py similarity index 100% rename from teams/admin.py rename to apps/teams/admin.py diff --git a/teams/api_urls.py b/apps/teams/api_urls.py similarity index 100% rename from teams/api_urls.py rename to apps/teams/api_urls.py diff --git a/teams/apps.py b/apps/teams/apps.py similarity index 100% rename from teams/apps.py rename to apps/teams/apps.py diff --git a/teams/migrations/0001_initial.py b/apps/teams/migrations/0001_initial.py similarity index 100% rename from teams/migrations/0001_initial.py rename to apps/teams/migrations/0001_initial.py diff --git a/teams/migrations/__init__.py b/apps/teams/migrations/__init__.py similarity index 100% rename from teams/migrations/__init__.py rename to apps/teams/migrations/__init__.py diff --git a/teams/models.py b/apps/teams/models.py similarity index 100% rename from teams/models.py rename to apps/teams/models.py diff --git a/teams/tests.py b/apps/teams/tests.py similarity index 100% rename from teams/tests.py rename to apps/teams/tests.py diff --git a/teams/urls.py b/apps/teams/urls.py similarity index 100% rename from teams/urls.py rename to apps/teams/urls.py diff --git a/teams/views.py b/apps/teams/views.py similarity index 100% rename from teams/views.py rename to apps/teams/views.py From 7def922a8a8a34265471d5545905fe24b034ab60 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 17:50:59 +0900 Subject: [PATCH 025/380] =?UTF-8?q?fix:=20=EC=95=B1=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20=EC=9D=B4=ED=9B=84=EC=97=90=20=EC=83=9D?= =?UTF-8?q?=EA=B8=B4=20=EB=AC=B8=EC=A0=9C=EB=93=A4=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하위호환성 위해 모든 앱에 apps.py 수정, 라벨 추가 - settings.py, urls.py, api_urls.py 수정 --- apps/__init__.py | 0 apps/accounts/apps.py | 3 ++- apps/learning/apps.py | 3 ++- apps/reflections/apps.py | 3 ++- apps/roadmaps/apps.py | 3 ++- apps/teams/apps.py | 3 ++- config/api_urls.py | 10 +++++----- config/settings.py | 12 ++++++------ config/urls.py | 10 +++++----- 9 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 apps/__init__.py diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/accounts/apps.py b/apps/accounts/apps.py index 0cb51e6..7c8c8c0 100644 --- a/apps/accounts/apps.py +++ b/apps/accounts/apps.py @@ -3,4 +3,5 @@ class AccountsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "accounts" + name = "apps.accounts" # 파이썬 경로 + label = "accounts" # app_label diff --git a/apps/learning/apps.py b/apps/learning/apps.py index b212832..529ce0e 100644 --- a/apps/learning/apps.py +++ b/apps/learning/apps.py @@ -3,4 +3,5 @@ class LearningConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "learning" + name = "apps.learning" + label = "learning" \ No newline at end of file diff --git a/apps/reflections/apps.py b/apps/reflections/apps.py index f4f277f..5595539 100644 --- a/apps/reflections/apps.py +++ b/apps/reflections/apps.py @@ -3,4 +3,5 @@ class ReflectionsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "reflections" + name = "apps.reflections" + label = "reflections" diff --git a/apps/roadmaps/apps.py b/apps/roadmaps/apps.py index 8bae0cc..27f1dac 100644 --- a/apps/roadmaps/apps.py +++ b/apps/roadmaps/apps.py @@ -3,4 +3,5 @@ class RoadmapsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "roadmaps" + name = "apps.roadmaps" + label = "roadmaps" diff --git a/apps/teams/apps.py b/apps/teams/apps.py index be7aa05..c56e927 100644 --- a/apps/teams/apps.py +++ b/apps/teams/apps.py @@ -3,4 +3,5 @@ class TeamsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "teams" + name = "apps.teams" + label = "teams" diff --git a/config/api_urls.py b/config/api_urls.py index 6ed40d5..6031826 100644 --- a/config/api_urls.py +++ b/config/api_urls.py @@ -1,9 +1,9 @@ from django.urls import path, include urlpatterns = [ - path("", include("accounts.api_urls")), - path("", include("learning.api_urls")), - path("", include("roadmaps.api_urls")), - path("", include("reflections.api_urls")), - path("", include("teams.api_urls")), + path("", include("apps.accounts.api_urls")), + path("", include("apps.learning.api_urls")), + path("", include("apps.roadmaps.api_urls")), + path("", include("apps.reflections.api_urls")), + path("", include("apps.teams.api_urls")), ] diff --git a/config/settings.py b/config/settings.py index de596f2..300e88c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -51,11 +51,11 @@ "allauth.socialaccount", # Local apps - "accounts", - "learning", - "reflections", - "roadmaps", - "teams", + "apps.accounts.apps.AccountsConfig", + "apps.learning.apps.LearningConfig", + "apps.reflections.apps.ReflectionsConfig", + "apps.roadmaps.apps.RoadmapsConfig", + "apps.teams.apps.TeamsConfig", ] MIDDLEWARE = [ @@ -66,7 +66,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "allauth.account.middleware.AccountMiddleware", - "accounts.middleware.RequireProfileMiddleware", + "apps.accounts.middleware.RequireProfileMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", diff --git a/config/urls.py b/config/urls.py index 2460c39..bc02f79 100644 --- a/config/urls.py +++ b/config/urls.py @@ -19,11 +19,11 @@ path("signup/", RedirectView.as_view(url="/accounts/signup/")), # template views: HTML로 보여줄 주소들 - path("users/", include("accounts.urls")), - path("learnings/", include("learning.urls")), - path("roadmaps/", include("roadmaps.urls")), - path("reflections/", include("reflections.urls")), - path("teams/", include("teams.urls")), + path("users/", include("apps.accounts.urls")), + path("learnings/", include("apps.learning.urls")), + path("roadmaps/", include("apps.roadmaps.urls")), + path("reflections/", include("apps.reflections.urls")), + path("teams/", include("apps.teams.urls")), # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), From 8dffc2604daae7332ea54484d54554146a8c91e9 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 18:05:38 +0900 Subject: [PATCH 026/380] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 19 +++++++++++++++++++ requirements.txt | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/config/settings.py b/config/settings.py index 300e88c..d69335d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -49,6 +49,7 @@ "allauth", "allauth.account", "allauth.socialaccount", + "allauth.socialaccount.providers.google", # Local apps "apps.accounts.apps.AccountsConfig", @@ -110,6 +111,24 @@ SOCIALACCOUNT_LOGIN_ON_GET = True ACCOUNT_LOGOUT_ON_GET = True +GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") + +SOCIALACCOUNT_PROVIDERS = { + "google": { + "APPS": [ + { + "client_id": GOOGLE_CLIENT_ID, + "secret": GOOGLE_CLIENT_SECRET, + "key": "", + } + ], + "SCOPE": ["profile", "email"], + "AUTH_PARAMS": {"access_type": "online"}, + "OAUTH_PKCE_ENABLED": True, + } +} + # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases diff --git a/requirements.txt b/requirements.txt index 4ba6c29..6e85486 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,29 @@ asgiref==3.11.0 attrs==25.4.0 +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +cryptography==46.0.4 Django==5.2.10 django-allauth==65.14.0 djangorestframework==3.14.0 drf-spectacular==0.27.0 +idna==3.11 inflection==0.5.1 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 pillow==12.1.0 psycopg2-binary==2.9.10 +pycparser==3.0 +PyJWT==2.10.1 python-dotenv==1.0.0 pytz==2025.2 PyYAML==6.0.3 referencing==0.37.0 +requests==2.32.5 rpds-py==0.30.0 sqlparse==0.5.5 typing_extensions==4.15.0 tzdata==2025.3 uritemplate==4.2.0 +urllib3==2.6.3 From 25343c690f077791b4ecd3135a5e28ee521cabdc Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 18:43:19 +0900 Subject: [PATCH 027/380] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/settings.py b/config/settings.py index d69335d..bedb818 100644 --- a/config/settings.py +++ b/config/settings.py @@ -50,6 +50,7 @@ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.google", + "allauth.socialaccount.providers.kakao", # Local apps "apps.accounts.apps.AccountsConfig", @@ -126,6 +127,15 @@ "SCOPE": ["profile", "email"], "AUTH_PARAMS": {"access_type": "online"}, "OAUTH_PKCE_ENABLED": True, + }, + "kakao": { + "APPS": [ + { + "client_id": os.getenv("KAKAO_CLIENT_ID"), + "secret": os.getenv("KAKAO_CLIENT_SECRET"), + "key": "", + } + ] } } From 3c5e72348ba65004c94d1935312ec6528fce188b Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 18:54:21 +0900 Subject: [PATCH 028/380] =?UTF-8?q?feat:=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index bedb818..d6f9bb3 100644 --- a/config/settings.py +++ b/config/settings.py @@ -51,6 +51,7 @@ "allauth.socialaccount", "allauth.socialaccount.providers.google", "allauth.socialaccount.providers.kakao", + "allauth.socialaccount.providers.github", # Local apps "apps.accounts.apps.AccountsConfig", @@ -136,7 +137,17 @@ "key": "", } ] - } + }, + "github": { + "APPS": [ + { + "client_id": os.getenv("GITHUB_CLIENT_ID"), + "secret": os.getenv("GITHUB_CLIENT_SECRET"), + "key": "", + } + ], + "SCOPE": ["user:email"], # 이메일 가져오려면 이거 필수급 + }, } From 92fec0ceb6194dac52e42f534580369575601eed Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 28 Jan 2026 19:01:55 +0900 Subject: [PATCH 029/380] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/settings.py b/config/settings.py index d6f9bb3..d3afac5 100644 --- a/config/settings.py +++ b/config/settings.py @@ -51,6 +51,7 @@ "allauth.socialaccount", "allauth.socialaccount.providers.google", "allauth.socialaccount.providers.kakao", + "allauth.socialaccount.providers.naver", "allauth.socialaccount.providers.github", # Local apps @@ -138,6 +139,15 @@ } ] }, + "naver": { + "APPS": [ + { + "client_id": os.getenv("NAVER_CLIENT_ID"), + "secret": os.getenv("NAVER_CLIENT_SECRET"), + "key": "", + } + ], + }, "github": { "APPS": [ { From 17323bb8d43d6d745df0743095119e77a85f0f23 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 28 Jan 2026 19:51:05 +0900 Subject: [PATCH 030/380] =?UTF-8?q?chore:=20merge=20=ED=9B=84=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/learning/api_urls.py | 9 +-- apps/learning/views.py | 82 ---------------------- apps/reflections/api_urls.py | 9 +-- apps/reflections/views.py | 75 -------------------- apps/roadmaps/api_urls.py | 9 ++- {roadmaps => apps/roadmaps}/serializers.py | 0 apps/roadmaps/views.py | 55 +++++++++++++++ apps/teams/api_urls.py | 9 ++- {teams => apps/teams}/serializers.py | 2 +- apps/teams/views.py | 49 +++++++++++++ 10 files changed, 123 insertions(+), 176 deletions(-) rename {roadmaps => apps/roadmaps}/serializers.py (100%) rename {teams => apps/teams}/serializers.py (96%) diff --git a/apps/learning/api_urls.py b/apps/learning/api_urls.py index efa2cf0..690b131 100644 --- a/apps/learning/api_urls.py +++ b/apps/learning/api_urls.py @@ -1,11 +1,4 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter -from .views import RoadmapViewSet, RoadmapItemViewSet - -router = DefaultRouter() -router.register(r'roadmaps', RoadmapViewSet, basename='roadmap') -router.register(r'roadmap-items', RoadmapItemViewSet, basename='roadmap-item') +from django.urls import path urlpatterns = [ - path('', include(router.urls)), ] diff --git a/apps/learning/views.py b/apps/learning/views.py index 3f655d0..91ea44a 100644 --- a/apps/learning/views.py +++ b/apps/learning/views.py @@ -1,85 +1,3 @@ from django.shortcuts import render -from rest_framework import viewsets, status -from rest_framework.response import Response -from rest_framework.decorators import action -from drf_spectacular.utils import extend_schema, extend_schema_view - -from .models import Roadmap, RoadmapItem, RoadmapTag -from .serializers import ( - RoadmapSerializer, - RoadmapCreateSerializer, - RoadmapItemSerializer, - RoadmapTagSerializer, -) - - -# ============================================ -# API ViewSets (Swagger 테스트용) -# ============================================ - -@extend_schema_view( - list=extend_schema(summary="로드맵 목록 조회", tags=["Roadmaps"]), - retrieve=extend_schema(summary="로드맵 상세 조회", tags=["Roadmaps"]), - create=extend_schema(summary="로드맵 생성", tags=["Roadmaps"]), - update=extend_schema(summary="로드맵 전체 수정", tags=["Roadmaps"]), - partial_update=extend_schema(summary="로드맵 부분 수정", tags=["Roadmaps"]), - destroy=extend_schema(summary="로드맵 삭제", tags=["Roadmaps"]), -) -class RoadmapViewSet(viewsets.ModelViewSet): - """ - 로드맵 CRUD API - - - GET /api/roadmaps/ : 목록 조회 - - POST /api/roadmaps/ : 생성 - - GET /api/roadmaps/{id}/ : 상세 조회 - - PUT /api/roadmaps/{id}/ : 전체 수정 - - PATCH /api/roadmaps/{id}/ : 부분 수정 - - DELETE /api/roadmaps/{id}/ : 삭제 - """ - queryset = Roadmap.objects.all().prefetch_related('items', 'tags') - - def get_serializer_class(self): - if self.action == 'create': - return RoadmapCreateSerializer - return RoadmapSerializer - - -@extend_schema_view( - list=extend_schema(summary="로드맵 아이템 목록 조회", tags=["Roadmap Items"]), - retrieve=extend_schema(summary="로드맵 아이템 상세 조회", tags=["Roadmap Items"]), - create=extend_schema(summary="로드맵 아이템 생성", tags=["Roadmap Items"]), - update=extend_schema(summary="로드맵 아이템 전체 수정", tags=["Roadmap Items"]), - partial_update=extend_schema(summary="로드맵 아이템 부분 수정", tags=["Roadmap Items"]), - destroy=extend_schema(summary="로드맵 아이템 삭제", tags=["Roadmap Items"]), -) -class RoadmapItemViewSet(viewsets.ModelViewSet): - """ - 로드맵 아이템 CRUD API - - - GET /api/roadmap-items/ : 목록 조회 - - POST /api/roadmap-items/ : 생성 - - GET /api/roadmap-items/{id}/ : 상세 조회 - - PUT /api/roadmap-items/{id}/ : 전체 수정 - - PATCH /api/roadmap-items/{id}/ : 부분 수정 - - DELETE /api/roadmap-items/{id}/ : 삭제 - """ - queryset = RoadmapItem.objects.all().select_related('roadmap', 'resource') - serializer_class = RoadmapItemSerializer - - @extend_schema(summary="아이템 완료 처리", tags=["Roadmap Items"]) - @action(detail=True, methods=['post']) - def complete(self, request, pk=None): - """아이템을 완료 상태로 변경""" - from django.utils import timezone - item = self.get_object() - item.is_completed = True - item.completed_at = timezone.now() - item.save() - return Response(RoadmapItemSerializer(item).data) - - -# ============================================ -# Template Views (HTML 렌더링용 - 나중에 추가) -# ============================================ # Create your views here. diff --git a/apps/reflections/api_urls.py b/apps/reflections/api_urls.py index 641cd09..690b131 100644 --- a/apps/reflections/api_urls.py +++ b/apps/reflections/api_urls.py @@ -1,11 +1,4 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter -from .views import TeamViewSet, TeamMemberViewSet - -router = DefaultRouter() -router.register(r'teams', TeamViewSet, basename='team') -router.register(r'team-members', TeamMemberViewSet, basename='team-member') +from django.urls import path urlpatterns = [ - path('', include(router.urls)), ] diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 1fce967..91ea44a 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -1,78 +1,3 @@ from django.shortcuts import render -from rest_framework import viewsets, status -from rest_framework.response import Response -from rest_framework.decorators import action -from drf_spectacular.utils import extend_schema, extend_schema_view - -from .models import Team, TeamMember -from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer - - -# ============================================ -# API ViewSets (Swagger 테스트용) -# ============================================ - -@extend_schema_view( - list=extend_schema(summary="팀 목록 조회", tags=["Teams"]), - retrieve=extend_schema(summary="팀 상세 조회", tags=["Teams"]), - create=extend_schema(summary="팀 생성", tags=["Teams"]), - update=extend_schema(summary="팀 전체 수정", tags=["Teams"]), - partial_update=extend_schema(summary="팀 부분 수정", tags=["Teams"]), - destroy=extend_schema(summary="팀 삭제", tags=["Teams"]), -) -class TeamViewSet(viewsets.ModelViewSet): - """ - 팀 CRUD API - - - GET /api/teams/ : 목록 조회 - - POST /api/teams/ : 생성 - - GET /api/teams/{id}/ : 상세 조회 - - PUT /api/teams/{id}/ : 전체 수정 - - PATCH /api/teams/{id}/ : 부분 수정 - - DELETE /api/teams/{id}/ : 삭제 - """ - queryset = Team.objects.all().prefetch_related('members') - - def get_serializer_class(self): - if self.action == 'create': - return TeamCreateSerializer - return TeamSerializer - - -@extend_schema_view( - list=extend_schema(summary="팀 멤버 목록 조회", tags=["Team Members"]), - retrieve=extend_schema(summary="팀 멤버 상세 조회", tags=["Team Members"]), - create=extend_schema(summary="팀 멤버 추가", tags=["Team Members"]), - update=extend_schema(summary="팀 멤버 전체 수정", tags=["Team Members"]), - partial_update=extend_schema(summary="팀 멤버 부분 수정", tags=["Team Members"]), - destroy=extend_schema(summary="팀 멤버 삭제", tags=["Team Members"]), -) -class TeamMemberViewSet(viewsets.ModelViewSet): - """ - 팀 멤버 CRUD API - - - GET /api/team-members/ : 목록 조회 - - POST /api/team-members/ : 생성 (팀에 멤버 추가) - - GET /api/team-members/{id}/ : 상세 조회 - - PUT /api/team-members/{id}/ : 전체 수정 - - PATCH /api/team-members/{id}/ : 부분 수정 (역할 변경 등) - - DELETE /api/team-members/{id}/ : 삭제 (팀에서 멤버 제거) - """ - queryset = TeamMember.objects.all().select_related('team', 'user') - serializer_class = TeamMemberSerializer - - @extend_schema(summary="리더로 승급", tags=["Team Members"]) - @action(detail=True, methods=['post']) - def promote_to_leader(self, request, pk=None): - """멤버를 리더로 승급""" - member = self.get_object() - member.role = TeamMember.Role.LEADER - member.save() - return Response(TeamMemberSerializer(member).data) - - -# ============================================ -# Template Views (HTML 렌더링용 - 나중에 추가) -# ============================================ # Create your views here. diff --git a/apps/roadmaps/api_urls.py b/apps/roadmaps/api_urls.py index 690b131..efa2cf0 100644 --- a/apps/roadmaps/api_urls.py +++ b/apps/roadmaps/api_urls.py @@ -1,4 +1,11 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import RoadmapViewSet, RoadmapItemViewSet + +router = DefaultRouter() +router.register(r'roadmaps', RoadmapViewSet, basename='roadmap') +router.register(r'roadmap-items', RoadmapItemViewSet, basename='roadmap-item') urlpatterns = [ + path('', include(router.urls)), ] diff --git a/roadmaps/serializers.py b/apps/roadmaps/serializers.py similarity index 100% rename from roadmaps/serializers.py rename to apps/roadmaps/serializers.py diff --git a/apps/roadmaps/views.py b/apps/roadmaps/views.py index 91ea44a..aab6ede 100644 --- a/apps/roadmaps/views.py +++ b/apps/roadmaps/views.py @@ -1,3 +1,58 @@ from django.shortcuts import render +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.decorators import action +from drf_spectacular.utils import extend_schema, extend_schema_view + +from .models import Roadmap, RoadmapItem +from .serializers import ( + RoadmapSerializer, + RoadmapCreateSerializer, + RoadmapItemSerializer, +) + + +@extend_schema_view( + list=extend_schema(summary="로드맵 목록 조회", tags=["Roadmaps"]), + retrieve=extend_schema(summary="로드맵 상세 조회", tags=["Roadmaps"]), + create=extend_schema(summary="로드맵 생성", tags=["Roadmaps"]), + update=extend_schema(summary="로드맵 전체 수정", tags=["Roadmaps"]), + partial_update=extend_schema(summary="로드맵 부분 수정", tags=["Roadmaps"]), + destroy=extend_schema(summary="로드맵 삭제", tags=["Roadmaps"]), +) +class RoadmapViewSet(viewsets.ModelViewSet): + """로드맵 CRUD API""" + queryset = Roadmap.objects.all().prefetch_related('items', 'tags') + + def get_serializer_class(self): + if self.action == 'create': + return RoadmapCreateSerializer + return RoadmapSerializer + + +@extend_schema_view( + list=extend_schema(summary="로드맵 아이템 목록 조회", tags=["Roadmap Items"]), + retrieve=extend_schema(summary="로드맵 아이템 상세 조회", tags=["Roadmap Items"]), + create=extend_schema(summary="로드맵 아이템 생성", tags=["Roadmap Items"]), + update=extend_schema(summary="로드맵 아이템 전체 수정", tags=["Roadmap Items"]), + partial_update=extend_schema(summary="로드맵 아이템 부분 수정", tags=["Roadmap Items"]), + destroy=extend_schema(summary="로드맵 아이템 삭제", tags=["Roadmap Items"]), +) +class RoadmapItemViewSet(viewsets.ModelViewSet): + """로드맵 아이템 CRUD API""" + queryset = RoadmapItem.objects.all().select_related('roadmap', 'resource') + serializer_class = RoadmapItemSerializer + + @extend_schema(summary="아이템 완료 처리", tags=["Roadmap Items"]) + @action(detail=True, methods=['post']) + def complete(self, request, pk=None): + """아이템을 완료 상태로 변경""" + from django.utils import timezone + item = self.get_object() + item.is_completed = True + item.completed_at = timezone.now() + item.save() + return Response(RoadmapItemSerializer(item).data) + # Create your views here. diff --git a/apps/teams/api_urls.py b/apps/teams/api_urls.py index 690b131..641cd09 100644 --- a/apps/teams/api_urls.py +++ b/apps/teams/api_urls.py @@ -1,4 +1,11 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import TeamViewSet, TeamMemberViewSet + +router = DefaultRouter() +router.register(r'teams', TeamViewSet, basename='team') +router.register(r'team-members', TeamMemberViewSet, basename='team-member') urlpatterns = [ + path('', include(router.urls)), ] diff --git a/teams/serializers.py b/apps/teams/serializers.py similarity index 96% rename from teams/serializers.py rename to apps/teams/serializers.py index 3915fc6..866da34 100644 --- a/teams/serializers.py +++ b/apps/teams/serializers.py @@ -27,7 +27,7 @@ class Meta: ] read_only_fields = ['id', 'created_at'] - def get_member_count(self, obj): + def get_member_count(self, obj) -> int: return obj.members.count() diff --git a/apps/teams/views.py b/apps/teams/views.py index 91ea44a..98d68bd 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -1,3 +1,52 @@ from django.shortcuts import render +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.decorators import action +from drf_spectacular.utils import extend_schema, extend_schema_view + +from .models import Team, TeamMember +from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer + + +@extend_schema_view( + list=extend_schema(summary="팀 목록 조회", tags=["Teams"]), + retrieve=extend_schema(summary="팀 상세 조회", tags=["Teams"]), + create=extend_schema(summary="팀 생성", tags=["Teams"]), + update=extend_schema(summary="팀 전체 수정", tags=["Teams"]), + partial_update=extend_schema(summary="팀 부분 수정", tags=["Teams"]), + destroy=extend_schema(summary="팀 삭제", tags=["Teams"]), +) +class TeamViewSet(viewsets.ModelViewSet): + """팀 CRUD API""" + queryset = Team.objects.all().prefetch_related('members') + + def get_serializer_class(self): + if self.action == 'create': + return TeamCreateSerializer + return TeamSerializer + + +@extend_schema_view( + list=extend_schema(summary="팀 멤버 목록 조회", tags=["Team Members"]), + retrieve=extend_schema(summary="팀 멤버 상세 조회", tags=["Team Members"]), + create=extend_schema(summary="팀 멤버 추가", tags=["Team Members"]), + update=extend_schema(summary="팀 멤버 전체 수정", tags=["Team Members"]), + partial_update=extend_schema(summary="팀 멤버 부분 수정", tags=["Team Members"]), + destroy=extend_schema(summary="팀 멤버 삭제", tags=["Team Members"]), +) +class TeamMemberViewSet(viewsets.ModelViewSet): + """팀 멤버 CRUD API""" + queryset = TeamMember.objects.all().select_related('team', 'user') + serializer_class = TeamMemberSerializer + + @extend_schema(summary="리더로 승급", tags=["Team Members"]) + @action(detail=True, methods=['post']) + def promote_to_leader(self, request, pk=None): + """멤버를 리더로 승급""" + member = self.get_object() + member.role = TeamMember.Role.LEADER + member.save() + return Response(TeamMemberSerializer(member).data) + # Create your views here. From fc55866ab6a95d518bcc8a9b22d701f2e3546345 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 28 Jan 2026 22:49:49 +0900 Subject: [PATCH 031/380] =?UTF-8?q?feat:=20UserTrackLevel=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=B6=94=EA=B0=80=20(=ED=8A=B8=EB=9E=99=EB=B3=84?= =?UTF-8?q?=20=EB=A0=88=EB=B2=A8=20=EA=B4=80=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...3_remove_user_user_level_usertracklevel.py | 34 ++++++++++++ apps/accounts/models.py | 53 ++++++++++++++++--- .../migrations/0002_alter_roadmap_track.py | 18 +++++++ apps/roadmaps/models.py | 9 +--- 4 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py create mode 100644 apps/roadmaps/migrations/0002_alter_roadmap_track.py diff --git a/apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py b/apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py new file mode 100644 index 0000000..d5dd167 --- /dev/null +++ b/apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.10 on 2026-01-28 13:48 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_alter_user_profile_image'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='user_level', + ), + migrations.CreateModel( + name='UserTrackLevel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('track', models.CharField(choices=[('WEB_FRONT', '웹 프론트엔드'), ('WEB_BACK', '웹 백엔드'), ('APP_FRONT', '앱 프론트엔드'), ('APP_BACK', '앱 백엔드'), ('GAME', '게임 개발')], max_length=20)), + ('level', models.PositiveSmallIntegerField(default=0, help_text='해당 트랙의 레벨 (0~6)')), + ('diagnosed_at', models.DateTimeField(auto_now_add=True, help_text='레벨 진단 일시')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='track_levels', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'indexes': [models.Index(fields=['user', 'track'], name='accounts_us_user_id_0de7d8_idx')], + 'constraints': [models.UniqueConstraint(fields=('user', 'track'), name='uq_user_track_level')], + }, + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 64f7cb6..d2d1e45 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -2,6 +2,15 @@ from django.db import models +class Track(models.TextChoices): + """트랙 선택지 (accounts, roadmaps에서 공통 사용)""" + WEB_FRONT = "WEB_FRONT", "웹 프론트엔드" + WEB_BACK = "WEB_BACK", "웹 백엔드" + APP_FRONT = "APP_FRONT", "앱 프론트엔드" + APP_BACK = "APP_BACK", "앱 백엔드" + GAME = "GAME", "게임 개발" + + class User(AbstractUser): """ Custom User for StartLine.dev @@ -26,12 +35,6 @@ class User(AbstractUser): help_text="프로필 이미지 URL 또는 media 경로", ) - # 학습 레벨 - user_level = models.PositiveSmallIntegerField( - default=0, - help_text="사용자 학습 레벨 (0~6)", - ) - # 공통 타임스탬프 created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -44,5 +47,43 @@ def is_profile_completed(self) -> bool: """ return bool(self.nickname) + def get_track_level(self, track: str) -> int: + """특정 트랙의 레벨 조회""" + track_level = self.track_levels.filter(track=track).first() + return track_level.level if track_level else 0 + def __str__(self) -> str: return self.nickname or self.username + + +class UserTrackLevel(models.Model): + """유저별 트랙 레벨 (트랙별로 다른 레벨 관리)""" + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="track_levels", + ) + track = models.CharField( + max_length=20, + choices=Track.choices, + ) + level = models.PositiveSmallIntegerField( + default=0, + help_text="해당 트랙의 레벨 (0~6)", + ) + diagnosed_at = models.DateTimeField( + auto_now_add=True, + help_text="레벨 진단 일시", + ) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["user", "track"], name="uq_user_track_level"), + ] + indexes = [ + models.Index(fields=["user", "track"]), + ] + + def __str__(self) -> str: + return f"{self.user}:{self.track}=Lv.{self.level}" diff --git a/apps/roadmaps/migrations/0002_alter_roadmap_track.py b/apps/roadmaps/migrations/0002_alter_roadmap_track.py new file mode 100644 index 0000000..dbcde23 --- /dev/null +++ b/apps/roadmaps/migrations/0002_alter_roadmap_track.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-01-28 13:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('roadmaps', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='roadmap', + name='track', + field=models.CharField(choices=[('WEB_FRONT', '웹 프론트엔드'), ('WEB_BACK', '웹 백엔드'), ('APP_FRONT', '앱 프론트엔드'), ('APP_BACK', '앱 백엔드'), ('GAME', '게임 개발')], max_length=20), + ), + ] diff --git a/apps/roadmaps/models.py b/apps/roadmaps/models.py index a9d346a..42e2630 100644 --- a/apps/roadmaps/models.py +++ b/apps/roadmaps/models.py @@ -1,15 +1,10 @@ from django.conf import settings from django.db import models +from apps.accounts.models import Track -class Roadmap(models.Model): - class Track(models.TextChoices): - WEB_FRONT = "WEB_FRONT", "WEB_FRONT" - WEB_BACK = "WEB_BACK", "WEB_BACK" - APP_FRONT = "APP_FRONT", "APP_FRONT" - APP_BACK = "APP_BACK", "APP_BACK" - GAME = "GAME", "GAME" +class Roadmap(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="roadmaps") track = models.CharField(max_length=20, choices=Track.choices) From c6af139fcaca28eae2dc78939c82c9f251571ea3 Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 29 Jan 2026 00:14:08 +0900 Subject: [PATCH 032/380] =?UTF-8?q?chore:=20=EA=B0=81=20=EC=95=B1=EB=B3=84?= =?UTF-8?q?=20URL=20=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/urls.py | 10 ++++++++++ apps/learning/urls.py | 12 +++++++++++- apps/reflections/urls.py | 15 ++++++++++++++- apps/roadmaps/urls.py | 6 +++++- apps/teams/urls.py | 15 ++++++++++++++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 07e9ae0..b978215 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -4,5 +4,15 @@ app_name = "accounts" urlpatterns = [ + # 온보딩 path("onboarding/profile/", views.onboarding_profile, name="onboarding_profile"), + + # 마이페이지 + path("mypage/", views.mypage, name="mypage"), + + # 프로필 수정 + path("profile/", views.profile_update, name="profile_update"), + + # 회원 탈퇴 + path("withdraw/", views.withdraw, name="withdraw"), ] diff --git a/apps/learning/urls.py b/apps/learning/urls.py index b7c7c77..d1df4b0 100644 --- a/apps/learning/urls.py +++ b/apps/learning/urls.py @@ -4,5 +4,15 @@ app_name = "learning" urlpatterns = [ - # Add your URL patterns here + # 레벨 진단 테스트 + path("test/", views.level_test, name="level_test"), + + # 진단 결과 + path("test/result/", views.test_result, name="test_result"), + + # 학습 페이지 + path("study/", views.study, name="study"), + + # 챗봇 (비동기) + path("chatbot/", views.chatbot, name="chatbot"), ] diff --git a/apps/reflections/urls.py b/apps/reflections/urls.py index 6ad0817..cb8ecf4 100644 --- a/apps/reflections/urls.py +++ b/apps/reflections/urls.py @@ -4,5 +4,18 @@ app_name = "reflections" urlpatterns = [ - # Add your URL patterns here + # 노트 목록 (List) + path("", views.note_list, name="note_list"), + + # 노트 작성 (Create) + path("create/", views.note_create, name="note_create"), + + # 노트 상세 (Read) + path("/", views.note_detail, name="note_detail"), + + # 노트 수정 (Update) + path("/update/", views.note_update, name="note_update"), + + # 노트 삭제 (Delete) + path("/delete/", views.note_delete, name="note_delete"), ] diff --git a/apps/roadmaps/urls.py b/apps/roadmaps/urls.py index 4b69bdc..f66eb5d 100644 --- a/apps/roadmaps/urls.py +++ b/apps/roadmaps/urls.py @@ -4,5 +4,9 @@ app_name = "roadmaps" urlpatterns = [ - # Add your URL patterns here + # 로드맵 목록 + path("", views.roadmap_list, name="roadmap_list"), + + # 로드맵 상세 + path("/", views.roadmap_detail, name="roadmap_detail"), ] diff --git a/apps/teams/urls.py b/apps/teams/urls.py index fe0716e..236863e 100644 --- a/apps/teams/urls.py +++ b/apps/teams/urls.py @@ -4,5 +4,18 @@ app_name = "teams" urlpatterns = [ - # Add your URL patterns here + # 팀 목록 (매칭 페이지) + path("", views.team_list, name="team_list"), + + # 팀 생성 + path("create/", views.team_create, name="team_create"), + + # 팀 상세 + path("/", views.team_detail, name="team_detail"), + + # 팀 가입 + path("/join/", views.team_join, name="team_join"), + + # 팀 탈퇴 + path("/leave/", views.team_leave, name="team_leave"), ] From a28cd45b84a12796634f945dcf7dec38e8932c5d Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 30 Jan 2026 14:12:36 +0900 Subject: [PATCH 033/380] =?UTF-8?q?refactor:=20=EB=B3=80=EA=B2=BD=EB=90=9C?= =?UTF-8?q?=20=EA=B8=B0=ED=9A=8D=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=9E=AC=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/admin.py | 38 +++- apps/accounts/forms.py | 23 +- apps/accounts/management/__init__.py | 0 apps/accounts/management/commands/__init__.py | 0 .../management/commands/seed_roles.py | 33 +++ apps/accounts/migrations/0001_initial.py | 190 +++++----------- .../0002_alter_user_profile_image.py | 22 -- ...3_remove_user_user_level_usertracklevel.py | 34 --- apps/accounts/models.py | 134 ++++++++---- apps/accounts/views.py | 56 ++++- apps/guides/__init__.py | 0 apps/guides/admin.py | 49 +++++ apps/guides/api_urls.py | 5 + apps/guides/apps.py | 7 + apps/guides/migrations/0001_initial.py | 80 +++++++ apps/guides/migrations/0002_initial.py | 54 +++++ apps/guides/migrations/__init__.py | 1 + apps/guides/models.py | 203 ++++++++++++++++++ apps/guides/tests.py | 3 + apps/guides/urls.py | 5 + apps/guides/views.py | 1 + apps/learning/migrations/0001_initial.py | 163 -------------- apps/projects/__init__.py | 0 apps/projects/admin.py | 19 ++ apps/projects/api_urls.py | 5 + apps/projects/apps.py | 7 + apps/projects/migrations/0001_initial.py | 85 ++++++++ apps/projects/migrations/__init__.py | 1 + apps/projects/models.py | 166 ++++++++++++++ apps/projects/tests.py | 3 + apps/projects/urls.py | 5 + apps/projects/views.py | 1 + apps/reflections/admin.py | 10 +- apps/reflections/migrations/0001_initial.py | 74 ++----- apps/reflections/models.py | 51 +++-- apps/reflections/urls.py | 16 +- apps/roadmaps/migrations/0001_initial.py | 160 -------------- .../migrations/0002_alter_roadmap_track.py | 18 -- apps/teams/admin.py | 25 ++- apps/teams/migrations/0001_initial.py | 87 ++------ apps/teams/models.py | 83 ++++++- apps/teams/urls.py | 16 +- config/api_urls.py | 6 +- config/settings.py | 6 +- config/urls.py | 6 +- 45 files changed, 1174 insertions(+), 777 deletions(-) create mode 100644 apps/accounts/management/__init__.py create mode 100644 apps/accounts/management/commands/__init__.py create mode 100644 apps/accounts/management/commands/seed_roles.py delete mode 100644 apps/accounts/migrations/0002_alter_user_profile_image.py delete mode 100644 apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py create mode 100644 apps/guides/__init__.py create mode 100644 apps/guides/admin.py create mode 100644 apps/guides/api_urls.py create mode 100644 apps/guides/apps.py create mode 100644 apps/guides/migrations/0001_initial.py create mode 100644 apps/guides/migrations/0002_initial.py create mode 100644 apps/guides/migrations/__init__.py create mode 100644 apps/guides/models.py create mode 100644 apps/guides/tests.py create mode 100644 apps/guides/urls.py create mode 100644 apps/guides/views.py delete mode 100644 apps/learning/migrations/0001_initial.py create mode 100644 apps/projects/__init__.py create mode 100644 apps/projects/admin.py create mode 100644 apps/projects/api_urls.py create mode 100644 apps/projects/apps.py create mode 100644 apps/projects/migrations/0001_initial.py create mode 100644 apps/projects/migrations/__init__.py create mode 100644 apps/projects/models.py create mode 100644 apps/projects/tests.py create mode 100644 apps/projects/urls.py create mode 100644 apps/projects/views.py delete mode 100644 apps/roadmaps/migrations/0001_initial.py delete mode 100644 apps/roadmaps/migrations/0002_alter_roadmap_track.py diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index 8c38f3f..fe3605b 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -1,3 +1,39 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -# Register your models here. +from .models import User, Role, UserRoleLevel + + +class UserRoleLevelInline(admin.TabularInline): + model = UserRoleLevel + extra = 0 + fields = ["role", "level", "last_diagnosed_at"] + readonly_fields = ["last_diagnosed_at"] + + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + list_display = ["id", "username", "nickname", "email", "is_staff", "created_at"] + list_filter = ["is_staff", "is_active", "created_at"] + search_fields = ["username", "nickname", "email"] + ordering = ["-created_at"] + inlines = [UserRoleLevelInline] + + fieldsets = BaseUserAdmin.fieldsets + ( + ("프로필 정보", {"fields": ("nickname", "profile_image_url", "bio")}), + ) + + +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + list_display = ["id", "code", "name", "created_at"] + search_fields = ["code", "name"] + ordering = ["code"] + + +@admin.register(UserRoleLevel) +class UserRoleLevelAdmin(admin.ModelAdmin): + list_display = ["id", "user", "role", "level", "last_diagnosed_at", "updated_at"] + list_filter = ["role", "level"] + search_fields = ["user__nickname", "user__username"] + ordering = ["-updated_at"] diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 7f5af51..9cd4064 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -1,10 +1,31 @@ from django import forms from .models import User + class OnboardingForm(forms.ModelForm): class Meta: model = User - fields = ["nickname", "profile_image"] + fields = ["nickname", "profile_image_url", "bio"] + widgets = { + "bio": forms.Textarea(attrs={"rows": 3}), + } + + def clean_nickname(self): + nick = (self.cleaned_data.get("nickname") or "").strip() + if not nick: + raise forms.ValidationError("닉네임은 필수입니다.") + if User.objects.filter(nickname=nick).exclude(pk=self.instance.pk).exists(): + raise forms.ValidationError("이미 사용 중인 닉네임입니다.") + return nick + + +class ProfileUpdateForm(forms.ModelForm): + class Meta: + model = User + fields = ["nickname", "profile_image_url", "bio"] + widgets = { + "bio": forms.Textarea(attrs={"rows": 3}), + } def clean_nickname(self): nick = (self.cleaned_data.get("nickname") or "").strip() diff --git a/apps/accounts/management/__init__.py b/apps/accounts/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/accounts/management/commands/__init__.py b/apps/accounts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/accounts/management/commands/seed_roles.py b/apps/accounts/management/commands/seed_roles.py new file mode 100644 index 0000000..992f5d1 --- /dev/null +++ b/apps/accounts/management/commands/seed_roles.py @@ -0,0 +1,33 @@ +from django.core.management.base import BaseCommand +from apps.accounts.models import Role + + +class Command(BaseCommand): + help = "Role 시드 데이터 생성 (PM, FRONTEND, BACKEND)" + + def handle(self, *args, **options): + roles = [ + {"code": "PM", "name": "PM(기획)"}, + {"code": "FRONTEND", "name": "프론트엔드"}, + {"code": "BACKEND", "name": "백엔드"}, + ] + + created_count = 0 + for role_data in roles: + role, created = Role.objects.get_or_create( + code=role_data["code"], + defaults={"name": role_data["name"]}, + ) + if created: + created_count += 1 + self.stdout.write( + self.style.SUCCESS(f" ✓ Role '{role.code}' 생성됨") + ) + else: + self.stdout.write( + self.style.WARNING(f" - Role '{role.code}' 이미 존재함") + ) + + self.stdout.write( + self.style.SUCCESS(f"\n총 {created_count}개의 Role이 생성되었습니다.") + ) diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py index bc4511b..343c780 100644 --- a/apps/accounts/migrations/0001_initial.py +++ b/apps/accounts/migrations/0001_initial.py @@ -1,155 +1,81 @@ -# Generated by Django 5.2.10 on 2026-01-27 06:02 +# Generated by Django 5.2.10 on 2026-01-30 05:10 import django.contrib.auth.models import django.contrib.auth.validators +import django.core.validators +import django.db.models.deletion import django.utils.timezone +from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name="User", + name='Role', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(choices=[('PM', 'PM(기획)'), ('FRONTEND', '프론트엔드'), ('BACKEND', '백엔드')], help_text='역할 코드 (PM/FRONTEND/BACKEND)', max_length=20, unique=True)), + ('name', models.CharField(help_text='역할 표시명', max_length=30)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'roles', + }, + ), + migrations.CreateModel( + name='User', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("password", models.CharField(max_length=128, verbose_name="password")), - ( - "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), - ), - ( - "is_superuser", - models.BooleanField( - default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", - verbose_name="superuser status", - ), - ), - ( - "username", - models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, - help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", - max_length=150, - unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], - verbose_name="username", - ), - ), - ( - "first_name", - models.CharField( - blank=True, max_length=150, verbose_name="first name" - ), - ), - ( - "last_name", - models.CharField( - blank=True, max_length=150, verbose_name="last name" - ), - ), - ( - "email", - models.EmailField( - blank=True, max_length=254, verbose_name="email address" - ), - ), - ( - "is_staff", - models.BooleanField( - default=False, - help_text="Designates whether the user can log into this admin site.", - verbose_name="staff status", - ), - ), - ( - "is_active", - models.BooleanField( - default=True, - help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", - verbose_name="active", - ), - ), - ( - "date_joined", - models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" - ), - ), - ( - "nickname", - models.CharField( - blank=True, - help_text="서비스 내 표시 닉네임 (프로필 설정 시 입력)", - max_length=30, - null=True, - unique=True, - ), - ), - ( - "profile_image", - models.TextField( - blank=True, help_text="프로필 이미지 URL 또는 media 경로", null=True - ), - ), - ( - "user_level", - models.PositiveSmallIntegerField( - default=0, help_text="사용자 학습 레벨 (0~6)" - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "groups", - models.ManyToManyField( - blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", - related_name="user_set", - related_query_name="user", - to="auth.group", - verbose_name="groups", - ), - ), - ( - "user_permissions", - models.ManyToManyField( - blank=True, - help_text="Specific permissions for this user.", - related_name="user_set", - related_query_name="user", - to="auth.permission", - verbose_name="user permissions", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('nickname', models.CharField(blank=True, help_text='서비스 내 표시 닉네임', max_length=50, null=True, unique=True)), + ('profile_image_url', models.TextField(blank=True, help_text='프로필 이미지 URL', null=True)), + ('bio', models.TextField(blank=True, help_text='자기소개', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ - "verbose_name": "user", - "verbose_name_plural": "users", - "abstract": False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, }, managers=[ - ("objects", django.contrib.auth.models.UserManager()), + ('objects', django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + name='UserRoleLevel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('level', models.SmallIntegerField(help_text='실력 레벨 (1~4)', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4)])), + ('last_diagnosed_at', models.DateTimeField(blank=True, help_text='마지막 진단 일시', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_levels', to='accounts.role')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_levels', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'user_role_levels', + 'indexes': [models.Index(fields=['role', 'level'], name='user_role_l_role_id_b06a10_idx')], + 'constraints': [models.UniqueConstraint(fields=('user', 'role'), name='uq_user_role_level'), models.CheckConstraint(condition=models.Q(('level__gte', 1), ('level__lte', 4)), name='ck_user_role_level_range')], + }, + ), ] diff --git a/apps/accounts/migrations/0002_alter_user_profile_image.py b/apps/accounts/migrations/0002_alter_user_profile_image.py deleted file mode 100644 index 4659158..0000000 --- a/apps/accounts/migrations/0002_alter_user_profile_image.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-27 06:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("accounts", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="user", - name="profile_image", - field=models.ImageField( - blank=True, - help_text="프로필 이미지 URL 또는 media 경로", - null=True, - upload_to="profiles/", - ), - ), - ] diff --git a/apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py b/apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py deleted file mode 100644 index d5dd167..0000000 --- a/apps/accounts/migrations/0003_remove_user_user_level_usertracklevel.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-28 13:48 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_alter_user_profile_image'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='user_level', - ), - migrations.CreateModel( - name='UserTrackLevel', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('track', models.CharField(choices=[('WEB_FRONT', '웹 프론트엔드'), ('WEB_BACK', '웹 백엔드'), ('APP_FRONT', '앱 프론트엔드'), ('APP_BACK', '앱 백엔드'), ('GAME', '게임 개발')], max_length=20)), - ('level', models.PositiveSmallIntegerField(default=0, help_text='해당 트랙의 레벨 (0~6)')), - ('diagnosed_at', models.DateTimeField(auto_now_add=True, help_text='레벨 진단 일시')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='track_levels', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'indexes': [models.Index(fields=['user', 'track'], name='accounts_us_user_id_0de7d8_idx')], - 'constraints': [models.UniqueConstraint(fields=('user', 'track'), name='uq_user_track_level')], - }, - ), - ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index d2d1e45..300a8e0 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -1,89 +1,137 @@ from django.contrib.auth.models import AbstractUser from django.db import models - - -class Track(models.TextChoices): - """트랙 선택지 (accounts, roadmaps에서 공통 사용)""" - WEB_FRONT = "WEB_FRONT", "웹 프론트엔드" - WEB_BACK = "WEB_BACK", "웹 백엔드" - APP_FRONT = "APP_FRONT", "앱 프론트엔드" - APP_BACK = "APP_BACK", "앱 백엔드" - GAME = "GAME", "게임 개발" +from django.core.validators import MinValueValidator, MaxValueValidator class User(AbstractUser): """ Custom User for StartLine.dev - - - allauth 사용 - - 로그인 후 프로필 설정 화면에서 nickname / profile_image 입력 + - allauth 사용 (password_hash는 Django가 내부적으로 관리) + - 로그인 후 프로필 설정 화면에서 nickname 입력 """ - # 프로필 정보 (로그인 직후에는 비어 있을 수 있음) nickname = models.CharField( - max_length=30, + max_length=50, unique=True, null=True, blank=True, - help_text="서비스 내 표시 닉네임 (프로필 설정 시 입력)", + help_text="서비스 내 표시 닉네임", + ) + + profile_image_url = models.TextField( + null=True, + blank=True, + help_text="프로필 이미지 URL", ) - profile_image = models.ImageField( - upload_to="profiles/", + bio = models.TextField( null=True, blank=True, - help_text="프로필 이미지 URL 또는 media 경로", + help_text="자기소개", ) - # 공통 타임스탬프 created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def is_profile_completed(self) -> bool: - """ - 프로필 설정 완료 여부 - - 프론트에서 /me 응답 보고 판단해도 되고 - - 백엔드에서도 재사용 가능 - """ + """프로필 설정 완료 여부""" return bool(self.nickname) - def get_track_level(self, track: str) -> int: - """특정 트랙의 레벨 조회""" - track_level = self.track_levels.filter(track=track).first() - return track_level.level if track_level else 0 + def get_role_level(self, role_code: str) -> int: + """특정 역할의 레벨 조회 (1~4, 없으면 0)""" + try: + role = Role.objects.get(code=role_code) + user_level = self.role_levels.filter(role=role).first() + return user_level.level if user_level else 0 + except Role.DoesNotExist: + return 0 def __str__(self) -> str: return self.nickname or self.username -class UserTrackLevel(models.Model): - """유저별 트랙 레벨 (트랙별로 다른 레벨 관리)""" +class Role(models.Model): + """ + 역할 (시드 데이터, 고정) + - PM: 기획 + - FRONTEND: 프론트엔드 + - BACKEND: 백엔드 + """ + + class RoleCode(models.TextChoices): + PM = "PM", "PM(기획)" + FRONTEND = "FRONTEND", "프론트엔드" + BACKEND = "BACKEND", "백엔드" + + code = models.CharField( + max_length=20, + unique=True, + choices=RoleCode.choices, + help_text="역할 코드 (PM/FRONTEND/BACKEND)", + ) + + name = models.CharField( + max_length=30, + help_text="역할 표시명", + ) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "roles" + + def __str__(self) -> str: + return self.name + + +class UserRoleLevel(models.Model): + """ + 사용자별 역할 레벨 (1~4) + - 역할별로 다른 레벨 관리 + - 설문 기반 진단, 재진단 가능 + """ + user = models.ForeignKey( User, on_delete=models.CASCADE, - related_name="track_levels", + related_name="role_levels", ) - track = models.CharField( - max_length=20, - choices=Track.choices, + + role = models.ForeignKey( + Role, + on_delete=models.CASCADE, + related_name="user_levels", ) - level = models.PositiveSmallIntegerField( - default=0, - help_text="해당 트랙의 레벨 (0~6)", + + level = models.SmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(4)], + help_text="실력 레벨 (1~4)", ) - diagnosed_at = models.DateTimeField( - auto_now_add=True, - help_text="레벨 진단 일시", + + last_diagnosed_at = models.DateTimeField( + null=True, + blank=True, + help_text="마지막 진단 일시", ) + + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: + db_table = "user_role_levels" constraints = [ - models.UniqueConstraint(fields=["user", "track"], name="uq_user_track_level"), + models.UniqueConstraint( + fields=["user", "role"], + name="uq_user_role_level", + ), + models.CheckConstraint( + check=models.Q(level__gte=1, level__lte=4), + name="ck_user_role_level_range", + ), ] indexes = [ - models.Index(fields=["user", "track"]), + models.Index(fields=["role", "level"]), ] def __str__(self) -> str: - return f"{self.user}:{self.track}=Lv.{self.level}" + return f"{self.user}:{self.role.code}=Lv.{self.level}" diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 212d96b..da3cf3e 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,14 +1,18 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect -from .forms import OnboardingForm +from django.contrib.auth import logout +from django.contrib import messages + +from .forms import OnboardingForm, ProfileUpdateForm + @login_required def onboarding_profile(request): + """온보딩: 최초 프로필 설정""" if request.method == "POST": form = OnboardingForm( request.POST, - request.FILES, - instance=request.user, + instance=request.user, ) if form.is_valid(): form.save() @@ -17,5 +21,49 @@ def onboarding_profile(request): form = OnboardingForm(instance=request.user) context = {"form": form} - return render(request, "account/onboarding_profile.html", context) + + +@login_required +def mypage(request): + """마이페이지""" + user = request.user + role_levels = user.role_levels.select_related("role").all() + + context = { + "user": user, + "role_levels": role_levels, + } + return render(request, "account/mypage.html", context) + + +@login_required +def profile_update(request): + """프로필 수정""" + if request.method == "POST": + form = ProfileUpdateForm( + request.POST, + instance=request.user, + ) + if form.is_valid(): + form.save() + messages.success(request, "프로필이 수정되었습니다.") + return redirect("accounts:mypage") + else: + form = ProfileUpdateForm(instance=request.user) + + context = {"form": form} + return render(request, "account/profile_update.html", context) + + +@login_required +def withdraw(request): + """회원 탈퇴""" + if request.method == "POST": + user = request.user + logout(request) + user.delete() + messages.success(request, "회원 탈퇴가 완료되었습니다.") + return redirect("/") + + return render(request, "account/withdraw.html") diff --git a/apps/guides/__init__.py b/apps/guides/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/guides/admin.py b/apps/guides/admin.py new file mode 100644 index 0000000..8d66705 --- /dev/null +++ b/apps/guides/admin.py @@ -0,0 +1,49 @@ +from django.contrib import admin + +from .models import GuideStage, GuideCard, GuideTask, GuideTaskProgress + + +class GuideCardInline(admin.TabularInline): + model = GuideCard + extra = 0 + fields = ["role", "title", "order_no", "is_active"] + + +class GuideTaskInline(admin.TabularInline): + model = GuideTask + extra = 0 + fields = ["title", "order_no", "is_required"] + + +@admin.register(GuideStage) +class GuideStageAdmin(admin.ModelAdmin): + list_display = ["id", "code", "title", "order_no", "is_active", "created_at"] + list_filter = ["is_active"] + search_fields = ["code", "title"] + ordering = ["order_no"] + inlines = [GuideCardInline] + + +@admin.register(GuideCard) +class GuideCardAdmin(admin.ModelAdmin): + list_display = ["id", "stage", "role", "title", "order_no", "is_active"] + list_filter = ["stage", "role", "is_active"] + search_fields = ["title"] + ordering = ["stage__order_no", "role", "order_no"] + inlines = [GuideTaskInline] + + +@admin.register(GuideTask) +class GuideTaskAdmin(admin.ModelAdmin): + list_display = ["id", "card", "title", "order_no", "is_required"] + list_filter = ["is_required", "card__stage"] + search_fields = ["title"] + ordering = ["card__stage__order_no", "card__order_no", "order_no"] + + +@admin.register(GuideTaskProgress) +class GuideTaskProgressAdmin(admin.ModelAdmin): + list_display = ["id", "task", "project", "user", "is_completed", "completed_at"] + list_filter = ["is_completed", "project"] + search_fields = ["task__title", "user__nickname"] + ordering = ["-updated_at"] diff --git a/apps/guides/api_urls.py b/apps/guides/api_urls.py new file mode 100644 index 0000000..e2d5eea --- /dev/null +++ b/apps/guides/api_urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + # 가이드 API URL은 추후 추가 +] diff --git a/apps/guides/apps.py b/apps/guides/apps.py new file mode 100644 index 0000000..6ed821d --- /dev/null +++ b/apps/guides/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class GuidesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.guides" + verbose_name = "가이드" diff --git a/apps/guides/migrations/0001_initial.py b/apps/guides/migrations/0001_initial.py new file mode 100644 index 0000000..23bcb22 --- /dev/null +++ b/apps/guides/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# Generated by Django 5.2.10 on 2026-01-30 05:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='GuideTaskProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_completed', models.BooleanField(default=False)), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'guide_task_progress', + }, + ), + migrations.CreateModel( + name='GuideStage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(help_text='단계 코드 (예: S01_KICKOFF, S02_ERD)', max_length=40, unique=True)), + ('title', models.CharField(help_text='단계 제목', max_length=120)), + ('description', models.TextField(blank=True, help_text='단계 설명', null=True)), + ('order_no', models.IntegerField(default=0, help_text='정렬 순서')), + ('is_active', models.BooleanField(default=True, help_text='활성화 여부')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'guide_stages', + 'ordering': ['order_no'], + 'indexes': [models.Index(fields=['order_no'], name='guide_stage_order_n_5f6b90_idx'), models.Index(fields=['is_active'], name='guide_stage_is_acti_27d912_idx')], + }, + ), + migrations.CreateModel( + name='GuideCard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(help_text='카드 제목', max_length=120)), + ('content_md', models.TextField(help_text='상세 가이드 내용 (마크다운)')), + ('order_no', models.IntegerField(default=0, help_text='정렬 순서')), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('role', models.ForeignKey(help_text='대상 역할 (PM/FRONTEND/BACKEND)', on_delete=django.db.models.deletion.CASCADE, related_name='guide_cards', to='accounts.role')), + ('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cards', to='guides.guidestage')), + ], + options={ + 'db_table': 'guide_cards', + 'ordering': ['stage', 'role', 'order_no'], + }, + ), + migrations.CreateModel( + name='GuideTask', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(help_text='태스크 제목', max_length=140)), + ('description', models.TextField(blank=True, help_text='태스크 상세 설명', null=True)), + ('order_no', models.IntegerField(default=0, help_text='정렬 순서')), + ('is_required', models.BooleanField(default=True, help_text='필수 여부')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='guides.guidecard')), + ], + options={ + 'db_table': 'guide_tasks', + 'ordering': ['card', 'order_no'], + }, + ), + ] diff --git a/apps/guides/migrations/0002_initial.py b/apps/guides/migrations/0002_initial.py new file mode 100644 index 0000000..0428089 --- /dev/null +++ b/apps/guides/migrations/0002_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.10 on 2026-01-30 05:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('guides', '0001_initial'), + ('projects', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='guidetaskprogress', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='task_progress', to='projects.project'), + ), + migrations.AddField( + model_name='guidetaskprogress', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress', to='guides.guidetask'), + ), + migrations.AddField( + model_name='guidetaskprogress', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='task_progress', to=settings.AUTH_USER_MODEL), + ), + migrations.AddIndex( + model_name='guidecard', + index=models.Index(fields=['stage', 'role', 'order_no'], name='guide_cards_stage_i_132d3c_idx'), + ), + migrations.AddIndex( + model_name='guidecard', + index=models.Index(fields=['is_active'], name='guide_cards_is_acti_e632b4_idx'), + ), + migrations.AddIndex( + model_name='guidetask', + index=models.Index(fields=['card', 'order_no'], name='guide_tasks_card_id_17746d_idx'), + ), + migrations.AddIndex( + model_name='guidetaskprogress', + index=models.Index(fields=['project', 'user'], name='guide_task__project_5c19f5_idx'), + ), + migrations.AddConstraint( + model_name='guidetaskprogress', + constraint=models.UniqueConstraint(fields=('task', 'project', 'user'), name='uq_task_project_user'), + ), + ] diff --git a/apps/guides/migrations/__init__.py b/apps/guides/migrations/__init__.py new file mode 100644 index 0000000..8392b3c --- /dev/null +++ b/apps/guides/migrations/__init__.py @@ -0,0 +1 @@ +# Generated by Django - will be auto-generated diff --git a/apps/guides/models.py b/apps/guides/models.py new file mode 100644 index 0000000..a5f22b1 --- /dev/null +++ b/apps/guides/models.py @@ -0,0 +1,203 @@ +from django.conf import settings +from django.db import models + + +class GuideStage(models.Model): + """ + 가이드 단계 + - 팀플 진행 순서를 단계별로 정의 + - code는 시드/운영용 안정적 식별자 + """ + + code = models.CharField( + max_length=40, + unique=True, + help_text="단계 코드 (예: S01_KICKOFF, S02_ERD)", + ) + + title = models.CharField( + max_length=120, + help_text="단계 제목", + ) + + description = models.TextField( + null=True, + blank=True, + help_text="단계 설명", + ) + + order_no = models.IntegerField( + default=0, + help_text="정렬 순서", + ) + + is_active = models.BooleanField( + default=True, + help_text="활성화 여부", + ) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "guide_stages" + ordering = ["order_no"] + indexes = [ + models.Index(fields=["order_no"]), + models.Index(fields=["is_active"]), + ] + + def __str__(self) -> str: + return f"[{self.order_no}] {self.title}" + + +class GuideCard(models.Model): + """ + 가이드 카드 + - 각 단계(stage)에서 역할별로 제공되는 상세 가이드 + - 마크다운 형식 콘텐츠 + """ + + stage = models.ForeignKey( + GuideStage, + on_delete=models.CASCADE, + related_name="cards", + ) + + role = models.ForeignKey( + "accounts.Role", + on_delete=models.CASCADE, + related_name="guide_cards", + help_text="대상 역할 (PM/FRONTEND/BACKEND)", + ) + + title = models.CharField( + max_length=120, + help_text="카드 제목", + ) + + content_md = models.TextField( + help_text="상세 가이드 내용 (마크다운)", + ) + + order_no = models.IntegerField( + default=0, + help_text="정렬 순서", + ) + + is_active = models.BooleanField( + default=True, + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "guide_cards" + ordering = ["stage", "role", "order_no"] + indexes = [ + models.Index(fields=["stage", "role", "order_no"]), + models.Index(fields=["is_active"]), + ] + + def __str__(self) -> str: + return f"{self.stage.code} - {self.role.code}: {self.title}" + + +class GuideTask(models.Model): + """ + 가이드 태스크 (체크리스트 항목) + - 각 카드에 포함된 세부 할 일 + - 퀘스트 형식으로 진행 + """ + + card = models.ForeignKey( + GuideCard, + on_delete=models.CASCADE, + related_name="tasks", + ) + + title = models.CharField( + max_length=140, + help_text="태스크 제목", + ) + + description = models.TextField( + null=True, + blank=True, + help_text="태스크 상세 설명", + ) + + order_no = models.IntegerField( + default=0, + help_text="정렬 순서", + ) + + is_required = models.BooleanField( + default=True, + help_text="필수 여부", + ) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "guide_tasks" + ordering = ["card", "order_no"] + indexes = [ + models.Index(fields=["card", "order_no"]), + ] + + def __str__(self) -> str: + return f"{self.card.title} - {self.title}" + + +class GuideTaskProgress(models.Model): + """ + 가이드 태스크 진행 상황 + - 프로젝트 × 사용자 × 태스크 별 완료 여부 + """ + + task = models.ForeignKey( + GuideTask, + on_delete=models.CASCADE, + related_name="progress", + ) + + project = models.ForeignKey( + "projects.Project", + on_delete=models.CASCADE, + related_name="task_progress", + ) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="task_progress", + ) + + is_completed = models.BooleanField( + default=False, + ) + + completed_at = models.DateTimeField( + null=True, + blank=True, + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "guide_task_progress" + constraints = [ + models.UniqueConstraint( + fields=["task", "project", "user"], + name="uq_task_project_user", + ), + ] + indexes = [ + models.Index(fields=["project", "user"]), + ] + + def __str__(self) -> str: + status = "✓" if self.is_completed else "○" + return f"{status} {self.task.title} ({self.user})" diff --git a/apps/guides/tests.py b/apps/guides/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/guides/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/guides/urls.py b/apps/guides/urls.py new file mode 100644 index 0000000..c339279 --- /dev/null +++ b/apps/guides/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + # 가이드 관련 URL은 추후 추가 +] diff --git a/apps/guides/views.py b/apps/guides/views.py new file mode 100644 index 0000000..a0d51a6 --- /dev/null +++ b/apps/guides/views.py @@ -0,0 +1 @@ +# Guide views will be implemented here diff --git a/apps/learning/migrations/0001_initial.py b/apps/learning/migrations/0001_initial.py deleted file mode 100644 index deaaa86..0000000 --- a/apps/learning/migrations/0001_initial.py +++ /dev/null @@ -1,163 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-27 06:02 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="LearningResource", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("title", models.CharField(max_length=200)), - ("url", models.TextField(blank=True, null=True)), - ( - "platform", - models.CharField( - blank=True, - choices=[ - ("YOUTUBE", "YOUTUBE"), - ("INFLEARN", "INFLEARN"), - ("VELOG", "VELOG"), - ("BLOG", "BLOG"), - ("ETC", "ETC"), - ], - max_length=50, - null=True, - ), - ), - ( - "content_type", - models.CharField( - choices=[ - ("VIDEO", "VIDEO"), - ("ARTICLE", "ARTICLE"), - ("COURSE", "COURSE"), - ], - max_length=20, - ), - ), - ( - "track", - models.CharField( - choices=[ - ("WEB_FRONT", "WEB_FRONT"), - ("WEB_BACK", "WEB_BACK"), - ("APP_FRONT", "APP_FRONT"), - ("APP_BACK", "APP_BACK"), - ("GAME", "GAME"), - ], - max_length=20, - ), - ), - ("level_min", models.PositiveSmallIntegerField(default=0)), - ("level_max", models.PositiveSmallIntegerField(default=6)), - ("estimated_time", models.PositiveIntegerField(blank=True, null=True)), - ( - "learning_style", - models.CharField(blank=True, max_length=50, null=True), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name="Tag", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=50, unique=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name="LearningResourceTag", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "resource", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="resource_tags", - to="learning.learningresource", - ), - ), - ( - "tag", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="resource_tags", - to="learning.tag", - ), - ), - ], - ), - migrations.AddField( - model_name="learningresource", - name="tags", - field=models.ManyToManyField( - related_name="resources", - through="learning.LearningResourceTag", - to="learning.tag", - ), - ), - migrations.AddIndex( - model_name="learningresourcetag", - index=models.Index(fields=["tag"], name="learning_le_tag_id_1a68f8_idx"), - ), - migrations.AddConstraint( - model_name="learningresourcetag", - constraint=models.UniqueConstraint( - fields=("resource", "tag"), name="uq_learning_resource_tags" - ), - ), - migrations.AddIndex( - model_name="learningresource", - index=models.Index( - fields=["platform"], name="learning_le_platfor_ef90d6_idx" - ), - ), - migrations.AddIndex( - model_name="learningresource", - index=models.Index( - fields=["content_type"], name="learning_le_content_37c87f_idx" - ), - ), - migrations.AddIndex( - model_name="learningresource", - index=models.Index(fields=["track"], name="learning_le_track_ecc62f_idx"), - ), - migrations.AddIndex( - model_name="learningresource", - index=models.Index( - fields=["level_min", "level_max"], name="learning_le_level_m_071736_idx" - ), - ), - ] diff --git a/apps/projects/__init__.py b/apps/projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/projects/admin.py b/apps/projects/admin.py new file mode 100644 index 0000000..f28d27e --- /dev/null +++ b/apps/projects/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin + +from .models import Project, ProjectApplication + + +@admin.register(Project) +class ProjectAdmin(admin.ModelAdmin): + list_display = ["id", "title", "owner", "status", "duration_weeks", "created_at"] + list_filter = ["status", "created_at"] + search_fields = ["title", "description"] + ordering = ["-created_at"] + + +@admin.register(ProjectApplication) +class ProjectApplicationAdmin(admin.ModelAdmin): + list_display = ["id", "project", "user", "role", "passion_level", "status", "applied_at"] + list_filter = ["status", "role", "passion_level"] + search_fields = ["project__title", "user__nickname"] + ordering = ["-applied_at"] diff --git a/apps/projects/api_urls.py b/apps/projects/api_urls.py new file mode 100644 index 0000000..8dadb0d --- /dev/null +++ b/apps/projects/api_urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + # 프로젝트 API URL은 추후 추가 +] diff --git a/apps/projects/apps.py b/apps/projects/apps.py new file mode 100644 index 0000000..e55710a --- /dev/null +++ b/apps/projects/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ProjectsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.projects" + verbose_name = "프로젝트" diff --git a/apps/projects/migrations/0001_initial.py b/apps/projects/migrations/0001_initial.py new file mode 100644 index 0000000..576b701 --- /dev/null +++ b/apps/projects/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 5.2.10 on 2026-01-30 05:10 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0001_initial'), + ('guides', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(help_text='프로젝트 제목', max_length=120)), + ('description', models.TextField(blank=True, help_text='프로젝트 설명', null=True)), + ('duration_weeks', models.SmallIntegerField(default=6, help_text='프로젝트 기간 (주)', validators=[django.core.validators.MinValueValidator(1)])), + ('target_team_size', models.SmallIntegerField(default=5, help_text='목표 팀 인원 (PM1/FE2/BE2 = 5명)')), + ('status', models.CharField(choices=[('DRAFT', '임시저장'), ('OPEN', '모집중'), ('MATCHED', '매칭완료'), ('IN_PROGRESS', '진행중'), ('COMPLETED', '완료'), ('ARCHIVED', '보관됨')], default='OPEN', help_text='프로젝트 상태', max_length=20)), + ('starts_at', models.DateField(blank=True, help_text='시작 예정일', null=True)), + ('ends_at', models.DateField(blank=True, help_text='종료 예정일', null=True)), + ('region', models.CharField(blank=True, help_text='활동 지역 (오프라인 시)', max_length=80, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('current_stage', models.ForeignKey(blank=True, help_text='현재 진행 중인 가이드 단계', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='guides.guidestage')), + ('owner', models.ForeignKey(blank=True, help_text='프로젝트 생성자', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_projects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'projects', + }, + ), + migrations.CreateModel( + name='ProjectApplication', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('passion_level', models.SmallIntegerField(help_text='열정 레벨 (1~4, 설문 결과)', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4)])), + ('status', models.CharField(choices=[('APPLIED', '지원됨'), ('CANCELLED', '취소됨'), ('MATCHED', '매칭됨'), ('REJECTED', '거절됨')], default='APPLIED', max_length=20)), + ('applied_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='projects.project')), + ('role', models.ForeignKey(help_text='지원 역할 (PM/FRONTEND/BACKEND)', on_delete=django.db.models.deletion.PROTECT, related_name='applications', to='accounts.role')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'project_applications', + }, + ), + migrations.AddIndex( + model_name='project', + index=models.Index(fields=['status'], name='projects_status_6303d7_idx'), + ), + migrations.AddIndex( + model_name='project', + index=models.Index(fields=['created_at'], name='projects_created_40bcd1_idx'), + ), + migrations.AddIndex( + model_name='projectapplication', + index=models.Index(fields=['project', 'role', 'status'], name='project_app_project_6eb18d_idx'), + ), + migrations.AddIndex( + model_name='projectapplication', + index=models.Index(fields=['user', 'status'], name='project_app_user_id_49508c_idx'), + ), + migrations.AddIndex( + model_name='projectapplication', + index=models.Index(fields=['passion_level'], name='project_app_passion_afa48b_idx'), + ), + migrations.AddConstraint( + model_name='projectapplication', + constraint=models.UniqueConstraint(fields=('project', 'user'), name='uq_project_application'), + ), + migrations.AddConstraint( + model_name='projectapplication', + constraint=models.CheckConstraint(condition=models.Q(('passion_level__gte', 1), ('passion_level__lte', 4)), name='ck_passion_level_range'), + ), + ] diff --git a/apps/projects/migrations/__init__.py b/apps/projects/migrations/__init__.py new file mode 100644 index 0000000..8392b3c --- /dev/null +++ b/apps/projects/migrations/__init__.py @@ -0,0 +1 @@ +# Generated by Django - will be auto-generated diff --git a/apps/projects/models.py b/apps/projects/models.py new file mode 100644 index 0000000..48188c0 --- /dev/null +++ b/apps/projects/models.py @@ -0,0 +1,166 @@ +from django.conf import settings +from django.db import models +from django.core.validators import MinValueValidator, MaxValueValidator + + +class Project(models.Model): + """ + 프로젝트 + - 1 project = 1 team (1:1 관계) + - 팀 구성: PM 1명 / FE 2명 / BE 2명 (총 5명) + """ + + class Status(models.TextChoices): + DRAFT = "DRAFT", "임시저장" + OPEN = "OPEN", "모집중" + MATCHED = "MATCHED", "매칭완료" + IN_PROGRESS = "IN_PROGRESS", "진행중" + COMPLETED = "COMPLETED", "완료" + ARCHIVED = "ARCHIVED", "보관됨" + + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="owned_projects", + help_text="프로젝트 생성자", + ) + + title = models.CharField( + max_length=120, + help_text="프로젝트 제목", + ) + + description = models.TextField( + null=True, + blank=True, + help_text="프로젝트 설명", + ) + + duration_weeks = models.SmallIntegerField( + default=6, + validators=[MinValueValidator(1)], + help_text="프로젝트 기간 (주)", + ) + + target_team_size = models.SmallIntegerField( + default=5, + help_text="목표 팀 인원 (PM1/FE2/BE2 = 5명)", + ) + + status = models.CharField( + max_length=20, + choices=Status.choices, + default=Status.OPEN, + help_text="프로젝트 상태", + ) + + starts_at = models.DateField( + null=True, + blank=True, + help_text="시작 예정일", + ) + + ends_at = models.DateField( + null=True, + blank=True, + help_text="종료 예정일", + ) + + region = models.CharField( + max_length=80, + null=True, + blank=True, + help_text="활동 지역 (오프라인 시)", + ) + + current_stage = models.ForeignKey( + "guides.GuideStage", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="projects", + help_text="현재 진행 중인 가이드 단계", + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "projects" + indexes = [ + models.Index(fields=["status"]), + models.Index(fields=["created_at"]), + ] + + def __str__(self) -> str: + return self.title + + +class ProjectApplication(models.Model): + """ + 프로젝트 지원 + - 열정 레벨 (1~4) 저장 + - 지원 역할 선택 + """ + + class Status(models.TextChoices): + APPLIED = "APPLIED", "지원됨" + CANCELLED = "CANCELLED", "취소됨" + MATCHED = "MATCHED", "매칭됨" + REJECTED = "REJECTED", "거절됨" + + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + related_name="applications", + ) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="applications", + ) + + role = models.ForeignKey( + "accounts.Role", + on_delete=models.PROTECT, + related_name="applications", + help_text="지원 역할 (PM/FRONTEND/BACKEND)", + ) + + passion_level = models.SmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(4)], + help_text="열정 레벨 (1~4, 설문 결과)", + ) + + status = models.CharField( + max_length=20, + choices=Status.choices, + default=Status.APPLIED, + ) + + applied_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "project_applications" + constraints = [ + models.UniqueConstraint( + fields=["project", "user"], + name="uq_project_application", + ), + models.CheckConstraint( + check=models.Q(passion_level__gte=1, passion_level__lte=4), + name="ck_passion_level_range", + ), + ] + indexes = [ + models.Index(fields=["project", "role", "status"]), + models.Index(fields=["user", "status"]), + models.Index(fields=["passion_level"]), + ] + + def __str__(self) -> str: + return f"{self.user} → {self.project} ({self.role.code})" diff --git a/apps/projects/tests.py b/apps/projects/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/projects/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/projects/urls.py b/apps/projects/urls.py new file mode 100644 index 0000000..b94ccae --- /dev/null +++ b/apps/projects/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + # 프로젝트 관련 URL은 추후 추가 +] diff --git a/apps/projects/views.py b/apps/projects/views.py new file mode 100644 index 0000000..58bade1 --- /dev/null +++ b/apps/projects/views.py @@ -0,0 +1 @@ +# Project views will be implemented here diff --git a/apps/reflections/admin.py b/apps/reflections/admin.py index 8c38f3f..d25c358 100644 --- a/apps/reflections/admin.py +++ b/apps/reflections/admin.py @@ -1,3 +1,11 @@ from django.contrib import admin -# Register your models here. +from .models import Retrospective + + +@admin.register(Retrospective) +class RetrospectiveAdmin(admin.ModelAdmin): + list_display = ["id", "project", "user", "title", "created_at", "updated_at"] + list_filter = ["created_at", "project"] + search_fields = ["title", "user__nickname", "project__title"] + ordering = ["-created_at"] diff --git a/apps/reflections/migrations/0001_initial.py b/apps/reflections/migrations/0001_initial.py index 9d15628..7267f49 100644 --- a/apps/reflections/migrations/0001_initial.py +++ b/apps/reflections/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.10 on 2026-01-27 06:02 +# Generated by Django 5.2.10 on 2026-01-30 05:10 import django.db.models.deletion from django.conf import settings @@ -6,77 +6,29 @@ class Migration(migrations.Migration): + initial = True dependencies = [ - ("roadmaps", "0001_initial"), + ('projects', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name="Reflection", + name='Retrospective', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "track", - models.CharField( - choices=[ - ("WEB_FRONT", "WEB_FRONT"), - ("WEB_BACK", "WEB_BACK"), - ("APP_FRONT", "APP_FRONT"), - ("APP_BACK", "APP_BACK"), - ("GAME", "GAME"), - ], - max_length=20, - ), - ), - ("content", models.TextField()), - ("starred", models.BooleanField(default=False)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "roadmap_item", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="reflections", - to="roadmaps.roadmapitem", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="reflections", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, help_text='회고 제목', max_length=120, null=True)), + ('content_md', models.TextField(help_text='회고 내용 (마크다운)')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='retrospectives', to='projects.project')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='retrospectives', to=settings.AUTH_USER_MODEL)), ], options={ - "indexes": [ - models.Index( - fields=["user", "created_at"], - name="reflections_user_id_471546_idx", - ), - models.Index( - fields=["user", "starred"], - name="reflections_user_id_10be5b_idx", - ), - models.Index( - fields=["track", "created_at"], - name="reflections_track_05d546_idx", - ), - ], + 'db_table': 'retrospectives', + 'indexes': [models.Index(fields=['project', 'user'], name='retrospecti_project_1604fb_idx'), models.Index(fields=['created_at'], name='retrospecti_created_521842_idx')], }, ), ] diff --git a/apps/reflections/models.py b/apps/reflections/models.py index c2d3585..c2702f9 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -2,38 +2,45 @@ from django.db import models -class Reflection(models.Model): - class Track(models.TextChoices): - WEB_FRONT = "WEB_FRONT", "WEB_FRONT" - WEB_BACK = "WEB_BACK", "WEB_BACK" - APP_FRONT = "APP_FRONT", "APP_FRONT" - APP_BACK = "APP_BACK", "APP_BACK" - GAME = "GAME", "GAME" - - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reflections") - - # nullable 허용(아이템과 느슨 연결) - roadmap_item = models.ForeignKey( - "roadmaps.RoadmapItem", - on_delete=models.SET_NULL, +class Retrospective(models.Model): + """ + 회고 + - 프로젝트별 개인 회고 작성 + - 마크다운 형식 + """ + + project = models.ForeignKey( + "projects.Project", + on_delete=models.CASCADE, + related_name="retrospectives", + ) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="retrospectives", + ) + + title = models.CharField( + max_length=120, null=True, blank=True, - related_name="reflections", + help_text="회고 제목", ) - track = models.CharField(max_length=20, choices=Track.choices) - content = models.TextField() - starred = models.BooleanField(default=False) + content_md = models.TextField( + help_text="회고 내용 (마크다운)", + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: + db_table = "retrospectives" indexes = [ - models.Index(fields=["user", "created_at"]), - models.Index(fields=["user", "starred"]), - models.Index(fields=["track", "created_at"]), + models.Index(fields=["project", "user"]), + models.Index(fields=["created_at"]), ] def __str__(self) -> str: - return f"Reflection({self.user_id}, {self.track})" + return f"{self.user} - {self.project}: {self.title or '회고'}" diff --git a/apps/reflections/urls.py b/apps/reflections/urls.py index cb8ecf4..2131fbf 100644 --- a/apps/reflections/urls.py +++ b/apps/reflections/urls.py @@ -1,21 +1,7 @@ from django.urls import path -from . import views app_name = "reflections" urlpatterns = [ - # 노트 목록 (List) - path("", views.note_list, name="note_list"), - - # 노트 작성 (Create) - path("create/", views.note_create, name="note_create"), - - # 노트 상세 (Read) - path("/", views.note_detail, name="note_detail"), - - # 노트 수정 (Update) - path("/update/", views.note_update, name="note_update"), - - # 노트 삭제 (Delete) - path("/delete/", views.note_delete, name="note_delete"), + # 회고 관련 URL은 추후 추가 ] diff --git a/apps/roadmaps/migrations/0001_initial.py b/apps/roadmaps/migrations/0001_initial.py deleted file mode 100644 index aa48f5c..0000000 --- a/apps/roadmaps/migrations/0001_initial.py +++ /dev/null @@ -1,160 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-27 06:02 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("learning", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="Roadmap", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "track", - models.CharField( - choices=[ - ("WEB_FRONT", "WEB_FRONT"), - ("WEB_BACK", "WEB_BACK"), - ("APP_FRONT", "APP_FRONT"), - ("APP_BACK", "APP_BACK"), - ("GAME", "GAME"), - ], - max_length=20, - ), - ), - ("level", models.PositiveSmallIntegerField()), - ("current_index", models.PositiveIntegerField(default=0)), - ("is_completed", models.BooleanField(default=False)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="roadmaps", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name="RoadmapTag", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "roadmap", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="roadmap_tags", - to="roadmaps.roadmap", - ), - ), - ( - "tag", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="roadmap_tags", - to="learning.tag", - ), - ), - ], - ), - migrations.AddField( - model_name="roadmap", - name="tags", - field=models.ManyToManyField( - related_name="roadmaps", - through="roadmaps.RoadmapTag", - to="learning.tag", - ), - ), - migrations.CreateModel( - name="RoadmapItem", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("order_no", models.PositiveIntegerField()), - ("is_completed", models.BooleanField(default=False)), - ("completed_at", models.DateTimeField(blank=True, null=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "resource", - models.ForeignKey( - help_text="리소스 삭제 시 과거 로드맵 기록 보호 위해 PROTECT 권장", - on_delete=django.db.models.deletion.PROTECT, - related_name="roadmap_items", - to="learning.learningresource", - ), - ), - ( - "roadmap", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="items", - to="roadmaps.roadmap", - ), - ), - ], - options={ - "indexes": [ - models.Index( - fields=["roadmap", "is_completed"], - name="roadmaps_ro_roadmap_85ed7f_idx", - ) - ], - "constraints": [ - models.UniqueConstraint( - fields=("roadmap", "order_no"), name="uq_roadmap_items_order_no" - ) - ], - }, - ), - migrations.AddIndex( - model_name="roadmaptag", - index=models.Index(fields=["tag"], name="roadmaps_ro_tag_id_d861cf_idx"), - ), - migrations.AddConstraint( - model_name="roadmaptag", - constraint=models.UniqueConstraint( - fields=("roadmap", "tag"), name="uq_roadmap_tags" - ), - ), - migrations.AddIndex( - model_name="roadmap", - index=models.Index( - fields=["user", "track", "is_completed"], - name="roadmaps_ro_user_id_1fe566_idx", - ), - ), - ] diff --git a/apps/roadmaps/migrations/0002_alter_roadmap_track.py b/apps/roadmaps/migrations/0002_alter_roadmap_track.py deleted file mode 100644 index dbcde23..0000000 --- a/apps/roadmaps/migrations/0002_alter_roadmap_track.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.10 on 2026-01-28 13:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('roadmaps', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='roadmap', - name='track', - field=models.CharField(choices=[('WEB_FRONT', '웹 프론트엔드'), ('WEB_BACK', '웹 백엔드'), ('APP_FRONT', '앱 프론트엔드'), ('APP_BACK', '앱 백엔드'), ('GAME', '게임 개발')], max_length=20), - ), - ] diff --git a/apps/teams/admin.py b/apps/teams/admin.py index 8c38f3f..a8b34e7 100644 --- a/apps/teams/admin.py +++ b/apps/teams/admin.py @@ -1,3 +1,26 @@ from django.contrib import admin -# Register your models here. +from .models import Team, TeamMember + + +class TeamMemberInline(admin.TabularInline): + model = TeamMember + extra = 0 + fields = ["user", "role", "is_active", "joined_at", "left_at"] + readonly_fields = ["joined_at"] + + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + list_display = ["id", "name", "project", "created_at"] + search_fields = ["name", "project__title"] + ordering = ["-created_at"] + inlines = [TeamMemberInline] + + +@admin.register(TeamMember) +class TeamMemberAdmin(admin.ModelAdmin): + list_display = ["id", "team", "user", "role", "is_active", "joined_at"] + list_filter = ["role", "is_active"] + search_fields = ["user__nickname", "team__name"] + ordering = ["-joined_at"] diff --git a/apps/teams/migrations/0001_initial.py b/apps/teams/migrations/0001_initial.py index eb7f449..b56768a 100644 --- a/apps/teams/migrations/0001_initial.py +++ b/apps/teams/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.10 on 2026-01-27 06:02 +# Generated by Django 5.2.10 on 2026-01-30 05:10 import django.db.models.deletion from django.conf import settings @@ -6,84 +6,43 @@ class Migration(migrations.Migration): + initial = True dependencies = [ + ('accounts', '0001_initial'), + ('projects', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name="Team", + name='Team', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=100)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "created_by", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="created_teams", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, help_text='팀 이름', max_length=80, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('project', models.OneToOneField(help_text='연결된 프로젝트 (1:1)', on_delete=django.db.models.deletion.CASCADE, related_name='team', to='projects.project')), ], + options={ + 'db_table': 'teams', + }, ), migrations.CreateModel( - name="TeamMember", + name='TeamMember', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "role", - models.CharField( - choices=[("LEADER", "LEADER"), ("MEMBER", "MEMBER")], - default="MEMBER", - max_length=10, - ), - ), - ("joined_at", models.DateTimeField(auto_now_add=True)), - ( - "team", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="members", - to="teams.team", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="team_memberships", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_active', models.BooleanField(default=True, help_text='활성 상태 (탈퇴 시 False)')), + ('joined_at', models.DateTimeField(auto_now_add=True)), + ('left_at', models.DateTimeField(blank=True, help_text='탈퇴 일시', null=True)), + ('role', models.ForeignKey(help_text='배정된 역할', on_delete=django.db.models.deletion.PROTECT, related_name='team_members', to='accounts.role')), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='teams.team')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to=settings.AUTH_USER_MODEL)), ], options={ - "indexes": [ - models.Index(fields=["user"], name="teams_teamm_user_id_08f226_idx") - ], - "constraints": [ - models.UniqueConstraint( - fields=("team", "user"), name="uq_team_members" - ) - ], + 'db_table': 'team_members', + 'indexes': [models.Index(fields=['team', 'role'], name='team_member_team_id_a23ddb_idx'), models.Index(fields=['user'], name='team_member_user_id_906d63_idx')], + 'constraints': [models.UniqueConstraint(fields=('team', 'user'), name='uq_team_member')], }, ), ] diff --git a/apps/teams/models.py b/apps/teams/models.py index d733c20..6d4e8ea 100644 --- a/apps/teams/models.py +++ b/apps/teams/models.py @@ -3,31 +3,92 @@ class Team(models.Model): - name = models.CharField(max_length=100) - created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="created_teams") + """ + 팀 + - 1 project = 1 team (1:1 관계) + - 팀 구성: PM 1명 / FE 2명 / BE 2명 (총 5명) + """ + + project = models.OneToOneField( + "projects.Project", + on_delete=models.CASCADE, + related_name="team", + help_text="연결된 프로젝트 (1:1)", + ) + + name = models.CharField( + max_length=80, + null=True, + blank=True, + help_text="팀 이름", + ) + created_at = models.DateTimeField(auto_now_add=True) + class Meta: + db_table = "teams" + def __str__(self) -> str: - return self.name + return self.name or f"Team#{self.id}" + + def get_member_count_by_role(self) -> dict: + """역할별 현재 멤버 수 반환""" + from django.db.models import Count + counts = self.members.filter(is_active=True).values("role__code").annotate(count=Count("id")) + return {item["role__code"]: item["count"] for item in counts} class TeamMember(models.Model): - class Role(models.TextChoices): - LEADER = "LEADER", "LEADER" - MEMBER = "MEMBER", "MEMBER" + """ + 팀 멤버 + - 역할별로 배정 + - PM 1명 / FE 2명 / BE 2명 구성은 서비스 로직에서 검증 + """ + + team = models.ForeignKey( + Team, + on_delete=models.CASCADE, + related_name="members", + ) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="team_memberships", + ) + + role = models.ForeignKey( + "accounts.Role", + on_delete=models.PROTECT, + related_name="team_members", + help_text="배정된 역할", + ) + + is_active = models.BooleanField( + default=True, + help_text="활성 상태 (탈퇴 시 False)", + ) - team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="members") - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="team_memberships") - role = models.CharField(max_length=10, choices=Role.choices, default=Role.MEMBER) joined_at = models.DateTimeField(auto_now_add=True) + left_at = models.DateTimeField( + null=True, + blank=True, + help_text="탈퇴 일시", + ) + class Meta: + db_table = "team_members" constraints = [ - models.UniqueConstraint(fields=["team", "user"], name="uq_team_members"), + models.UniqueConstraint( + fields=["team", "user"], + name="uq_team_member", + ), ] indexes = [ + models.Index(fields=["team", "role"]), models.Index(fields=["user"]), ] def __str__(self) -> str: - return f"{self.team_id}:{self.user_id}({self.role})" + return f"{self.user} @ {self.team} ({self.role.code})" diff --git a/apps/teams/urls.py b/apps/teams/urls.py index 236863e..da9535a 100644 --- a/apps/teams/urls.py +++ b/apps/teams/urls.py @@ -1,21 +1,7 @@ from django.urls import path -from . import views app_name = "teams" urlpatterns = [ - # 팀 목록 (매칭 페이지) - path("", views.team_list, name="team_list"), - - # 팀 생성 - path("create/", views.team_create, name="team_create"), - - # 팀 상세 - path("/", views.team_detail, name="team_detail"), - - # 팀 가입 - path("/join/", views.team_join, name="team_join"), - - # 팀 탈퇴 - path("/leave/", views.team_leave, name="team_leave"), + # 팀 관련 URL은 추후 추가 ] diff --git a/config/api_urls.py b/config/api_urls.py index 6031826..6ff0bc4 100644 --- a/config/api_urls.py +++ b/config/api_urls.py @@ -2,8 +2,8 @@ urlpatterns = [ path("", include("apps.accounts.api_urls")), - path("", include("apps.learning.api_urls")), - path("", include("apps.roadmaps.api_urls")), - path("", include("apps.reflections.api_urls")), + path("", include("apps.projects.api_urls")), path("", include("apps.teams.api_urls")), + path("", include("apps.guides.api_urls")), + path("", include("apps.reflections.api_urls")), ] diff --git a/config/settings.py b/config/settings.py index d3afac5..b81d8bc 100644 --- a/config/settings.py +++ b/config/settings.py @@ -56,10 +56,10 @@ # Local apps "apps.accounts.apps.AccountsConfig", - "apps.learning.apps.LearningConfig", - "apps.reflections.apps.ReflectionsConfig", - "apps.roadmaps.apps.RoadmapsConfig", + "apps.projects.apps.ProjectsConfig", "apps.teams.apps.TeamsConfig", + "apps.guides.apps.GuidesConfig", + "apps.reflections.apps.ReflectionsConfig", ] MIDDLEWARE = [ diff --git a/config/urls.py b/config/urls.py index bc02f79..272c8ae 100644 --- a/config/urls.py +++ b/config/urls.py @@ -20,10 +20,10 @@ # template views: HTML로 보여줄 주소들 path("users/", include("apps.accounts.urls")), - path("learnings/", include("apps.learning.urls")), - path("roadmaps/", include("apps.roadmaps.urls")), - path("reflections/", include("apps.reflections.urls")), + path("projects/", include("apps.projects.urls")), path("teams/", include("apps.teams.urls")), + path("guides/", include("apps.guides.urls")), + path("reflections/", include("apps.reflections.urls")), # API views: Swagger로 테스트할 주소들 path("api/", include("config.api_urls")), From 3732bffef846f4086ba19d48a1f41f877c2f691f Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 30 Jan 2026 16:29:36 +0900 Subject: [PATCH 034/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/admin.py | 2 +- apps/accounts/forms.py | 4 ++-- ...er_profile_image_url_user_profile_image.py | 22 +++++++++++++++++++ apps/accounts/models.py | 5 +++-- apps/accounts/views.py | 2 ++ 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 apps/accounts/migrations/0002_remove_user_profile_image_url_user_profile_image.py diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index fe3605b..5ca41f9 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -20,7 +20,7 @@ class UserAdmin(BaseUserAdmin): inlines = [UserRoleLevelInline] fieldsets = BaseUserAdmin.fieldsets + ( - ("프로필 정보", {"fields": ("nickname", "profile_image_url", "bio")}), + ("프로필 정보", {"fields": ("nickname", "profile_image", "bio")}), ) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 9cd4064..21825d1 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -5,7 +5,7 @@ class OnboardingForm(forms.ModelForm): class Meta: model = User - fields = ["nickname", "profile_image_url", "bio"] + fields = ["nickname", "profile_image", "bio"] widgets = { "bio": forms.Textarea(attrs={"rows": 3}), } @@ -22,7 +22,7 @@ def clean_nickname(self): class ProfileUpdateForm(forms.ModelForm): class Meta: model = User - fields = ["nickname", "profile_image_url", "bio"] + fields = ["nickname", "profile_image", "bio"] widgets = { "bio": forms.Textarea(attrs={"rows": 3}), } diff --git a/apps/accounts/migrations/0002_remove_user_profile_image_url_user_profile_image.py b/apps/accounts/migrations/0002_remove_user_profile_image_url_user_profile_image.py new file mode 100644 index 0000000..12bfcc5 --- /dev/null +++ b/apps/accounts/migrations/0002_remove_user_profile_image_url_user_profile_image.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.10 on 2026-01-30 07:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='profile_image_url', + ), + migrations.AddField( + model_name='user', + name='profile_image', + field=models.ImageField(blank=True, help_text='프로필 이미지', null=True, upload_to='profiles/'), + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 300a8e0..76bae99 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -18,10 +18,11 @@ class User(AbstractUser): help_text="서비스 내 표시 닉네임", ) - profile_image_url = models.TextField( + profile_image = models.ImageField( + upload_to="profiles/", null=True, blank=True, - help_text="프로필 이미지 URL", + help_text="프로필 이미지", ) bio = models.TextField( diff --git a/apps/accounts/views.py b/apps/accounts/views.py index da3cf3e..81c7599 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -12,6 +12,7 @@ def onboarding_profile(request): if request.method == "POST": form = OnboardingForm( request.POST, + request.FILES, instance=request.user, ) if form.is_valid(): @@ -43,6 +44,7 @@ def profile_update(request): if request.method == "POST": form = ProfileUpdateForm( request.POST, + request.FILES, instance=request.user, ) if form.is_valid(): From e0667d54409e4335922d0eebf016e1b8b605f5ae Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 30 Jan 2026 16:34:39 +0900 Subject: [PATCH 035/380] =?UTF-8?q?fix:=20swagger=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/serializers.py | 17 +++++++++-------- apps/teams/views.py | 17 ++--------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/apps/teams/serializers.py b/apps/teams/serializers.py index 866da34..cfb85de 100644 --- a/apps/teams/serializers.py +++ b/apps/teams/serializers.py @@ -4,37 +4,38 @@ class TeamMemberSerializer(serializers.ModelSerializer): """팀 멤버 Serializer""" - username = serializers.CharField(source='user.username', read_only=True) - role_display = serializers.CharField(source='get_role_display', read_only=True) + username = serializers.CharField(source='user.nickname', read_only=True) + role_code = serializers.CharField(source='role.code', read_only=True) + role_name = serializers.CharField(source='role.name', read_only=True) class Meta: model = TeamMember - fields = ['id', 'team', 'user', 'username', 'role', 'role_display', 'joined_at'] + fields = ['id', 'team', 'user', 'username', 'role', 'role_code', 'role_name', 'is_active', 'joined_at', 'left_at'] read_only_fields = ['id', 'joined_at'] class TeamSerializer(serializers.ModelSerializer): """팀 기본 Serializer""" members = TeamMemberSerializer(many=True, read_only=True) - created_by_username = serializers.CharField(source='created_by.username', read_only=True) + project_title = serializers.CharField(source='project.title', read_only=True) member_count = serializers.SerializerMethodField() class Meta: model = Team fields = [ - 'id', 'name', 'created_by', 'created_by_username', + 'id', 'name', 'project', 'project_title', 'created_at', 'members', 'member_count' ] read_only_fields = ['id', 'created_at'] def get_member_count(self, obj) -> int: - return obj.members.count() + return obj.members.filter(is_active=True).count() class TeamCreateSerializer(serializers.ModelSerializer): - """팀 생성용 Serializer (간소화)""" + """팀 생성용 Serializer""" class Meta: model = Team - fields = ['id', 'name', 'created_by'] + fields = ['id', 'name', 'project'] read_only_fields = ['id'] diff --git a/apps/teams/views.py b/apps/teams/views.py index 98d68bd..ccff7fb 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -1,4 +1,3 @@ -from django.shortcuts import render from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import action @@ -18,7 +17,7 @@ ) class TeamViewSet(viewsets.ModelViewSet): """팀 CRUD API""" - queryset = Team.objects.all().prefetch_related('members') + queryset = Team.objects.all().prefetch_related('members__user', 'members__role') def get_serializer_class(self): if self.action == 'create': @@ -36,17 +35,5 @@ def get_serializer_class(self): ) class TeamMemberViewSet(viewsets.ModelViewSet): """팀 멤버 CRUD API""" - queryset = TeamMember.objects.all().select_related('team', 'user') + queryset = TeamMember.objects.all().select_related('team', 'user', 'role') serializer_class = TeamMemberSerializer - - @extend_schema(summary="리더로 승급", tags=["Team Members"]) - @action(detail=True, methods=['post']) - def promote_to_leader(self, request, pk=None): - """멤버를 리더로 승급""" - member = self.get_object() - member.role = TeamMember.Role.LEADER - member.save() - return Response(TeamMemberSerializer(member).data) - - -# Create your views here. From 9d462859dccf02d0654e634534ff4144a6f1031a Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 30 Jan 2026 19:16:09 +0900 Subject: [PATCH 036/380] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=95=B1=20=ED=8F=B4=EB=8D=94=20=EC=82=AD=EC=A0=9C=20(learning?= =?UTF-8?q?,=20roadmaps)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/learning/__init__.py | 0 apps/learning/admin.py | 3 -- apps/learning/api_urls.py | 4 -- apps/learning/apps.py | 7 --- apps/learning/migrations/__init__.py | 0 apps/learning/models.py | 70 ---------------------------- apps/learning/tests.py | 3 -- apps/learning/urls.py | 18 ------- apps/learning/views.py | 3 -- apps/roadmaps/__init__.py | 0 apps/roadmaps/admin.py | 3 -- apps/roadmaps/api_urls.py | 11 ----- apps/roadmaps/apps.py | 7 --- apps/roadmaps/migrations/__init__.py | 0 apps/roadmaps/models.py | 65 -------------------------- apps/roadmaps/serializers.py | 48 ------------------- apps/roadmaps/tests.py | 3 -- apps/roadmaps/urls.py | 12 ----- apps/roadmaps/views.py | 58 ----------------------- 19 files changed, 315 deletions(-) delete mode 100644 apps/learning/__init__.py delete mode 100644 apps/learning/admin.py delete mode 100644 apps/learning/api_urls.py delete mode 100644 apps/learning/apps.py delete mode 100644 apps/learning/migrations/__init__.py delete mode 100644 apps/learning/models.py delete mode 100644 apps/learning/tests.py delete mode 100644 apps/learning/urls.py delete mode 100644 apps/learning/views.py delete mode 100644 apps/roadmaps/__init__.py delete mode 100644 apps/roadmaps/admin.py delete mode 100644 apps/roadmaps/api_urls.py delete mode 100644 apps/roadmaps/apps.py delete mode 100644 apps/roadmaps/migrations/__init__.py delete mode 100644 apps/roadmaps/models.py delete mode 100644 apps/roadmaps/serializers.py delete mode 100644 apps/roadmaps/tests.py delete mode 100644 apps/roadmaps/urls.py delete mode 100644 apps/roadmaps/views.py diff --git a/apps/learning/__init__.py b/apps/learning/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/learning/admin.py b/apps/learning/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/apps/learning/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/learning/api_urls.py b/apps/learning/api_urls.py deleted file mode 100644 index 690b131..0000000 --- a/apps/learning/api_urls.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.urls import path - -urlpatterns = [ -] diff --git a/apps/learning/apps.py b/apps/learning/apps.py deleted file mode 100644 index 529ce0e..0000000 --- a/apps/learning/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class LearningConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.learning" - label = "learning" \ No newline at end of file diff --git a/apps/learning/migrations/__init__.py b/apps/learning/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/learning/models.py b/apps/learning/models.py deleted file mode 100644 index 776419b..0000000 --- a/apps/learning/models.py +++ /dev/null @@ -1,70 +0,0 @@ -from django.db import models - - -class Tag(models.Model): - name = models.CharField(max_length=50, unique=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self) -> str: - return self.name - - -class LearningResource(models.Model): - class Platform(models.TextChoices): - YOUTUBE = "YOUTUBE", "YOUTUBE" - INFLEARN = "INFLEARN", "INFLEARN" - VELOG = "VELOG", "VELOG" - BLOG = "BLOG", "BLOG" - ETC = "ETC", "ETC" - - class ContentType(models.TextChoices): - VIDEO = "VIDEO", "VIDEO" - ARTICLE = "ARTICLE", "ARTICLE" - COURSE = "COURSE", "COURSE" - - class Track(models.TextChoices): - WEB_FRONT = "WEB_FRONT", "WEB_FRONT" - WEB_BACK = "WEB_BACK", "WEB_BACK" - APP_FRONT = "APP_FRONT", "APP_FRONT" - APP_BACK = "APP_BACK", "APP_BACK" - GAME = "GAME", "GAME" - - title = models.CharField(max_length=200) - url = models.TextField(null=True, blank=True) - - platform = models.CharField(max_length=50, choices=Platform.choices, null=True, blank=True) - content_type = models.CharField(max_length=20, choices=ContentType.choices) - track = models.CharField(max_length=20, choices=Track.choices) - - level_min = models.PositiveSmallIntegerField(default=0) - level_max = models.PositiveSmallIntegerField(default=6) - - estimated_time = models.PositiveIntegerField(null=True, blank=True) - learning_style = models.CharField(max_length=50, null=True, blank=True) - - tags = models.ManyToManyField(Tag, through="LearningResourceTag", related_name="resources") - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - indexes = [ - models.Index(fields=["platform"]), - models.Index(fields=["content_type"]), - models.Index(fields=["track"]), - models.Index(fields=["level_min", "level_max"]), - ] - - def __str__(self) -> str: - return self.title - - -class LearningResourceTag(models.Model): - resource = models.ForeignKey(LearningResource, on_delete=models.CASCADE, related_name="resource_tags") - tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="resource_tags") - - class Meta: - constraints = [ - models.UniqueConstraint(fields=["resource", "tag"], name="uq_learning_resource_tags"), - ] - indexes = [ - models.Index(fields=["tag"]), - ] diff --git a/apps/learning/tests.py b/apps/learning/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/apps/learning/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/learning/urls.py b/apps/learning/urls.py deleted file mode 100644 index d1df4b0..0000000 --- a/apps/learning/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.urls import path -from . import views - -app_name = "learning" - -urlpatterns = [ - # 레벨 진단 테스트 - path("test/", views.level_test, name="level_test"), - - # 진단 결과 - path("test/result/", views.test_result, name="test_result"), - - # 학습 페이지 - path("study/", views.study, name="study"), - - # 챗봇 (비동기) - path("chatbot/", views.chatbot, name="chatbot"), -] diff --git a/apps/learning/views.py b/apps/learning/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/apps/learning/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/apps/roadmaps/__init__.py b/apps/roadmaps/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/roadmaps/admin.py b/apps/roadmaps/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/apps/roadmaps/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/roadmaps/api_urls.py b/apps/roadmaps/api_urls.py deleted file mode 100644 index efa2cf0..0000000 --- a/apps/roadmaps/api_urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter -from .views import RoadmapViewSet, RoadmapItemViewSet - -router = DefaultRouter() -router.register(r'roadmaps', RoadmapViewSet, basename='roadmap') -router.register(r'roadmap-items', RoadmapItemViewSet, basename='roadmap-item') - -urlpatterns = [ - path('', include(router.urls)), -] diff --git a/apps/roadmaps/apps.py b/apps/roadmaps/apps.py deleted file mode 100644 index 27f1dac..0000000 --- a/apps/roadmaps/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class RoadmapsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.roadmaps" - label = "roadmaps" diff --git a/apps/roadmaps/migrations/__init__.py b/apps/roadmaps/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/roadmaps/models.py b/apps/roadmaps/models.py deleted file mode 100644 index 42e2630..0000000 --- a/apps/roadmaps/models.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.conf import settings -from django.db import models - -from apps.accounts.models import Track - - -class Roadmap(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="roadmaps") - track = models.CharField(max_length=20, choices=Track.choices) - - level = models.PositiveSmallIntegerField() - current_index = models.PositiveIntegerField(default=0) - - is_completed = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) - - # include-only 태그(선호/방향) - tags = models.ManyToManyField("learning.Tag", through="RoadmapTag", related_name="roadmaps") - - class Meta: - indexes = [ - models.Index(fields=["user", "track", "is_completed"]), - ] - - def __str__(self) -> str: - return f"Roadmap({self.user_id}, {self.track})" - - -class RoadmapTag(models.Model): - roadmap = models.ForeignKey(Roadmap, on_delete=models.CASCADE, related_name="roadmap_tags") - tag = models.ForeignKey("learning.Tag", on_delete=models.CASCADE, related_name="roadmap_tags") - - class Meta: - constraints = [ - models.UniqueConstraint(fields=["roadmap", "tag"], name="uq_roadmap_tags"), - ] - indexes = [ - models.Index(fields=["tag"]), - ] - - -class RoadmapItem(models.Model): - roadmap = models.ForeignKey(Roadmap, on_delete=models.CASCADE, related_name="items") - resource = models.ForeignKey( - "learning.LearningResource", - on_delete=models.PROTECT, - related_name="roadmap_items", - help_text="리소스 삭제 시 과거 로드맵 기록 보호 위해 PROTECT 권장", - ) - - order_no = models.PositiveIntegerField() - is_completed = models.BooleanField(default=False) - completed_at = models.DateTimeField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=["roadmap", "order_no"], name="uq_roadmap_items_order_no"), - ] - indexes = [ - models.Index(fields=["roadmap", "is_completed"]), - ] - - def __str__(self) -> str: - return f"RoadmapItem({self.roadmap_id}, #{self.order_no})" diff --git a/apps/roadmaps/serializers.py b/apps/roadmaps/serializers.py deleted file mode 100644 index fc7f32c..0000000 --- a/apps/roadmaps/serializers.py +++ /dev/null @@ -1,48 +0,0 @@ -from rest_framework import serializers -from .models import Roadmap, RoadmapItem, RoadmapTag - - -class RoadmapTagSerializer(serializers.ModelSerializer): - """로드맵 태그 Serializer""" - tag_name = serializers.CharField(source='tag.name', read_only=True) - - class Meta: - model = RoadmapTag - fields = ['id', 'roadmap', 'tag', 'tag_name'] - read_only_fields = ['id'] - - -class RoadmapItemSerializer(serializers.ModelSerializer): - """로드맵 아이템 Serializer""" - resource_title = serializers.CharField(source='resource.title', read_only=True) - - class Meta: - model = RoadmapItem - fields = [ - 'id', 'roadmap', 'resource', 'resource_title', - 'order_no', 'is_completed', 'completed_at', 'created_at' - ] - read_only_fields = ['id', 'created_at'] - - -class RoadmapSerializer(serializers.ModelSerializer): - """로드맵 기본 Serializer""" - items = RoadmapItemSerializer(many=True, read_only=True) - track_display = serializers.CharField(source='get_track_display', read_only=True) - - class Meta: - model = Roadmap - fields = [ - 'id', 'user', 'track', 'track_display', 'level', - 'current_index', 'is_completed', 'created_at', 'items' - ] - read_only_fields = ['id', 'created_at'] - - -class RoadmapCreateSerializer(serializers.ModelSerializer): - """로드맵 생성용 Serializer (간소화)""" - - class Meta: - model = Roadmap - fields = ['id', 'user', 'track', 'level'] - read_only_fields = ['id'] diff --git a/apps/roadmaps/tests.py b/apps/roadmaps/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/apps/roadmaps/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/roadmaps/urls.py b/apps/roadmaps/urls.py deleted file mode 100644 index f66eb5d..0000000 --- a/apps/roadmaps/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path -from . import views - -app_name = "roadmaps" - -urlpatterns = [ - # 로드맵 목록 - path("", views.roadmap_list, name="roadmap_list"), - - # 로드맵 상세 - path("/", views.roadmap_detail, name="roadmap_detail"), -] diff --git a/apps/roadmaps/views.py b/apps/roadmaps/views.py deleted file mode 100644 index aab6ede..0000000 --- a/apps/roadmaps/views.py +++ /dev/null @@ -1,58 +0,0 @@ -from django.shortcuts import render -from rest_framework import viewsets -from rest_framework.response import Response -from rest_framework.decorators import action -from drf_spectacular.utils import extend_schema, extend_schema_view - -from .models import Roadmap, RoadmapItem -from .serializers import ( - RoadmapSerializer, - RoadmapCreateSerializer, - RoadmapItemSerializer, -) - - -@extend_schema_view( - list=extend_schema(summary="로드맵 목록 조회", tags=["Roadmaps"]), - retrieve=extend_schema(summary="로드맵 상세 조회", tags=["Roadmaps"]), - create=extend_schema(summary="로드맵 생성", tags=["Roadmaps"]), - update=extend_schema(summary="로드맵 전체 수정", tags=["Roadmaps"]), - partial_update=extend_schema(summary="로드맵 부분 수정", tags=["Roadmaps"]), - destroy=extend_schema(summary="로드맵 삭제", tags=["Roadmaps"]), -) -class RoadmapViewSet(viewsets.ModelViewSet): - """로드맵 CRUD API""" - queryset = Roadmap.objects.all().prefetch_related('items', 'tags') - - def get_serializer_class(self): - if self.action == 'create': - return RoadmapCreateSerializer - return RoadmapSerializer - - -@extend_schema_view( - list=extend_schema(summary="로드맵 아이템 목록 조회", tags=["Roadmap Items"]), - retrieve=extend_schema(summary="로드맵 아이템 상세 조회", tags=["Roadmap Items"]), - create=extend_schema(summary="로드맵 아이템 생성", tags=["Roadmap Items"]), - update=extend_schema(summary="로드맵 아이템 전체 수정", tags=["Roadmap Items"]), - partial_update=extend_schema(summary="로드맵 아이템 부분 수정", tags=["Roadmap Items"]), - destroy=extend_schema(summary="로드맵 아이템 삭제", tags=["Roadmap Items"]), -) -class RoadmapItemViewSet(viewsets.ModelViewSet): - """로드맵 아이템 CRUD API""" - queryset = RoadmapItem.objects.all().select_related('roadmap', 'resource') - serializer_class = RoadmapItemSerializer - - @extend_schema(summary="아이템 완료 처리", tags=["Roadmap Items"]) - @action(detail=True, methods=['post']) - def complete(self, request, pk=None): - """아이템을 완료 상태로 변경""" - from django.utils import timezone - item = self.get_object() - item.is_completed = True - item.completed_at = timezone.now() - item.save() - return Response(RoadmapItemSerializer(item).data) - - -# Create your views here. From ff3fea32fea6bc361177f61b90427669cc4c8fb3 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 10:15:58 +0900 Subject: [PATCH 037/380] =?UTF-8?q?chore:=20.gitignore=EC=97=90=EC=84=9C?= =?UTF-8?q?=20media,=20static=20=ED=8F=B4=EB=8D=94=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitignore b/.gitignore index e5d8fad..e648c58 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ local_settings.py settings_local.py # Django media & static (선택) -media/ staticfiles/ ############################ @@ -35,11 +34,6 @@ env/ ENV/ pip-wheel-metadata/ -############################ -# Django collectstatic -############################ -static/ - ############################ # Docker ############################ From 375ef976e7cb5ad48ba50918a395fc67ecec0b56 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 10:28:04 +0900 Subject: [PATCH 038/380] feat : base.html&css --- static/css/base.css | 54 ++++++++++++++++++++++++++++++++++ static/images/github-icon.png | Bin 0 -> 2903 bytes static/images/google-icon.png | Bin 0 -> 2400 bytes static/images/kakao-icon.png | Bin 0 -> 3187 bytes static/images/naver-icon.jpeg | Bin 0 -> 2690 bytes static/js/set.txt | 1 + templates/base.html | 37 +++++++++++++++++++++++ 7 files changed, 92 insertions(+) create mode 100644 static/css/base.css create mode 100644 static/images/github-icon.png create mode 100644 static/images/google-icon.png create mode 100644 static/images/kakao-icon.png create mode 100644 static/images/naver-icon.jpeg create mode 100644 static/js/set.txt create mode 100644 templates/base.html diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..8317bd8 --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,54 @@ +@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css'); + +body { + font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; +} + + +* { + margin: 0; padding: 0; + box-sizing: border-box; +} + +header { + width: 100%; height: 60px; + display: flex; + align-items: center; + padding: 0 100px; + justify-content: space-between; +} + +header a { + text-decoration: none; + color: black; +} + +header a h3 { + background: #EAF0FF; color: #4272EF; + font-size: 32px; + font-weight: bolder; +} + +header .header_menu { + display: flex; +} + +header .header_menu a { + display: block; + color: #4272EF; + width: 130px; height: 40px; + margin-left: 20px; text-align: center; + border-radius: 20px; +} + +header .header_menu a:hover { + background: #4272EF; + color: #fff; + transition: 0.3s ease; +} + +header .header_menu a p { + padding: 13px auto; + font-size: 16px; font-weight: bold; + line-height: 40px; +} \ No newline at end of file diff --git a/static/images/github-icon.png b/static/images/github-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d96b8a2be92d61a4aa59875ce47f85752c881ab3 GIT binary patch literal 2903 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 z8ohR-a|`6=NEZstA%$CJ1^@s6000000000000000000000000000000P~tzZUiC^r S^{Wg3000074hEYO-TkD3cRqJ3|wJz;++CJ_7 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 Z{SWh?&*B{jFs}dr002ovPDHLkV1oEt_Lcwu literal 0 HcmV?d00001 diff --git a/static/images/naver-icon.jpeg b/static/images/naver-icon.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d1a144bcfa08576c6f333d559d5d98052960be17 GIT binary patch literal 2690 zcma)83pkY98eTK*%4S9gg(SDxBbO$3Cb}p^2-T3N35uYgfUklQmk>jXia@7@9L!%7 zpu}V(L`9bT7!+DWR7_mLD5MxGze+)I9bt>>zOXBeE^bE>EVA@4lR{Kh?_neAW7&e%)rA#=*!XzuL2sk86B~CQmlh zdUvcY_GP{FB&F1KQH;7A3lF-;`&L|tx~a5D+aSp||J=~n@s_q!fNJW_lHaKO@uI|I zaf9&=*M)h$8*4PrLT|=qh(YLYIUGiL>Dydg9W>JLn3Uh|)X?Q0%Mb!Zr(ZojJ)am| z&v5Q~J)z?_7WSAV1c{uH&RFm2@Zm4BYFppWf4U}!8#iZ@8tN-j__lF7Y?CKWnMyvr zXme_Mf^-FM_~v>bC2&_wpn%#vLu=xy3^=OZq4nq{2@15eDPd;s$%g?V2^xr&3@W

= z@T?j2kZfnkSpj8DX5ARK=EQ-pZcsRxFvRysq5d?uVnJ|oK&hVF+oO-YyR`dqSL2Z- z*OhGE=nQ~*N@rQ-Nn?Ru?#7z*y4IMot<}2&(k+|sJ=)q%BUBf6nmqrthm=7$!D$wP zc5xpMlC`@FEhCR&wlWx7iEm|ic)L}puNc@X){sd{(c}H&&-+Ja`8#xsA5D#WHG3NZ z{f_QgQ_sHq7UpRoV7)$Zc3^n?c$$>^zmOyVJc3YACFL8Ppgj?}%mX{8@9;di%ddb}v za6M8TM>Vxx<4Kno+y8u;mEv?PVz`%ipLH0xGoMWb(_gqf3>6as%B=y}MOLH>zhgz_ z_JgQjU?$RW_!*t(@=B7&)zSBi1#-F&WLNP91fJ|2qi0R?red2y&xdf8XJ_w?j2@s2 zzPp<=-90Z*wBXz77&DQ|*CLhoL@iZ*W?_$nZtS6(N>Y0jyv`vNgGB7|i-6wS=Gl1z zj-YY7ymz&Sd8GaBkv7Q@-5ofT?pIf};*dPU>$<-Wx`o9!Oy_$G%A5vNO*(ov(L^%6 z+Nd3c2BTv;{4!HgaFF`q{eqy+mT3*tq@f4HM3nnG*+$mmwN5oJO^bhtw4a!osn``? z(2sm3K&lq7Bf?byn&kS03xI@adfasn#0jxv^Or-NFDues@S%FPZ#D zNTX5buniAHS329PW1H&pAMPyKV8wHzuI(&noOCzZ7m&WvNiBIBlX1wEHnHAON4B*u z>eQ;_3i{KX)NNA>6-Bnp@lZhn!J!>jXP=*3#930Imt~kBjE-|*jMc9 zWD}gv|5h^Xr-`Hotm<4Hw27%(6Erf=cBF%oZ=L{gm)eDm5fWVt+nf*4*CN>WW8Yrk z^0ry%w`K>+qLQ=lAL#AY^7Dk<=48%AwIFwf_K)eVa}n+Da+~OOeXKjIz&!mtN;Ui9 zEj53#9_80uJwBg|XN`IL@4Wb@Pt*2$JbKZP1JKXvoi%YoPVSjcdt`NO#B2<(4Fle_ zSjki^d5uKKBIFT@s3ZAM)P4Y}1>8fddzEdWUQDSMmi(z6Rl>q~0jZS!XwG1|{d37d zy65njh2q~+K^LdZiqmmq#O`~ z-oP+m>jLnT`!L;mUk=$kolgqRZEh~-ll>_S(qK`UWJV%S#_@7xaMsaX*%PICqij82 zP7bd%Vz6u*Y43-rU<;$oteBC|>F_QxOd3snWqOxjW0QUUI{oKHe<4_;{B49bvKr8Y zzY#cx=+Mqny9|Jl$`p@Q;B1=JsHq+y-W&0;sDpIUx3rx7Dk%1TbJWmm9>uBbzP5fP zp@r!@zi^{D{cR?zC45@I@8S4G9IJP+=p#KU3(qXwR;P2vhjjn|G3Ttf?E%p-mP$t6 zwRgBefQGMz-(^~VEhKgE4`%1On2F!~n z$FU4rZWYF_fE^cTtuPhF;;8;UL~G`>mAN}DR0x6Z=t+TdXV}6qI=djE+-p*H?$Y4$ zNZwn^SCMBI944(O)G>=VV{Sn9JFDkHpcPrTrmA=K=9%!K8_WlORs1#|N(C)uy+iZw_v${9VL(eOWzs~KfFW$E@=QLXe^??5e+yH@1Oy; xCjQNB;y-T~aHp2$BLHZW24WUPW7&Rh{=X{)7M@WciY9&^3-399C$hrUe*^S}EZzVB literal 0 HcmV?d00001 diff --git a/static/js/set.txt b/static/js/set.txt new file mode 100644 index 0000000..1b5c358 --- /dev/null +++ b/static/js/set.txt @@ -0,0 +1 @@ +초기 \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..3e86df5 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,37 @@ +{% load static %} + + + + + + {% block title %}KITUP{% endblock %} + + {% block header %}{% endblock %} + + +

+ +

KITUP

+
+
+ {% if user.is_authenticated %} +

팀매칭

+

내 프로젝트

+

프로젝트 기록

+

마이페이지

+

로그아웃

+ {% else %} +

로그인

+

회원가입

+ {% endif %} +
+
+ +
+ {% block content %} + {% endblock %} +
+ +
+ + \ No newline at end of file From 8655117cc5f4a223ccc1ed12380906045076179a Mon Sep 17 00:00:00 2001 From: knana6 Date: Sat, 31 Jan 2026 10:54:16 +0900 Subject: [PATCH 039/380] signup.html --- static/css/signup.css | 112 ++++++++++++++++++++++++++++++++++++++++++ templates/signup.html | 60 ++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 static/css/signup.css create mode 100644 templates/signup.html diff --git a/static/css/signup.css b/static/css/signup.css new file mode 100644 index 0000000..178c583 --- /dev/null +++ b/static/css/signup.css @@ -0,0 +1,112 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: #f5f5f5; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; +} + +.container { + width: 100%; + max-width: 400px; +} + +.signup-card { + background: white; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 40px 30px; +} + +.title { + text-align: center; + font-size: 24px; + font-weight: 600; + color: #333; + margin-bottom: 30px; +} + +.signup-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.input-wrapper { + display: flex; + gap: 8px; +} + +.input { + flex: 1; + padding: 12px 16px; + border: none; + background-color: #f0f0f0; + border-radius: 6px; + font-size: 14px; + outline: none; + transition: background-color 0.2s; +} + +.input:focus { + background-color: #e8e8e8; +} + +.input::placeholder { + color: #999; +} + +.check-btn { + padding: 12px 20px; + background-color: #4a7cff; + color: white; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.2s; +} + +.check-btn:hover { + background-color: #3a6cef; +} + +.submit-btn { + width: 100%; + padding: 14px; + background-color: #4a7cff; + color: white; + border: none; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + margin-top: 10px; + transition: background-color 0.2s; +} + +.submit-btn:hover { + background-color: #3a6cef; +} diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..8db44d6 --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,60 @@ +{extends 'base.html' %} +{% load static %} + +{% block title %}회원가입 - KITUP{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+ + +
+{% endblock %} + From ce13b6709d33e4bd25dd0837ecb37f740c7f0dba Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 11:03:45 +0900 Subject: [PATCH 040/380] =?UTF-8?q?chore:=20=EC=95=B1=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=20url=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/urls.py | 8 ++++++-- apps/guides/urls.py | 7 ++++++- apps/projects/urls.py | 7 ++++++- apps/reflections/urls.py | 16 +++++++++++++++- apps/teams/urls.py | 10 +++++++++- config/urls.py | 4 ++-- config/views.py | 14 ++++++++++---- 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index b978215..d142bc3 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -7,11 +7,15 @@ # 온보딩 path("onboarding/profile/", views.onboarding_profile, name="onboarding_profile"), + # 레벨 진단 (Test) + path("level-test/", views.level_test, name="level_test"), # level_test.html + path("level-test/result/", views.test_result, name="test_result"), # test_result.html + # 마이페이지 - path("mypage/", views.mypage, name="mypage"), + path("mypage/", views.mypage, name="mypage"), # mypage.html # 프로필 수정 - path("profile/", views.profile_update, name="profile_update"), + path("profile/edit/", views.profile_edit, name="profile_edit"), # profile_edit.html # 회원 탈퇴 path("withdraw/", views.withdraw, name="withdraw"), diff --git a/apps/guides/urls.py b/apps/guides/urls.py index c339279..bf91e5e 100644 --- a/apps/guides/urls.py +++ b/apps/guides/urls.py @@ -1,5 +1,10 @@ from django.urls import path +from . import views + +app_name = "guides" urlpatterns = [ - # 가이드 관련 URL은 추후 추가 + # 미션/체크리스트 페이지 + path("mission/", views.mission, name="mission"), # mission.html + path("mission//", views.mission_detail, name="mission_detail"), # mission.html (특정 프로젝트) ] diff --git a/apps/projects/urls.py b/apps/projects/urls.py index b94ccae..6e11a63 100644 --- a/apps/projects/urls.py +++ b/apps/projects/urls.py @@ -1,5 +1,10 @@ from django.urls import path +from . import views + +app_name = "projects" urlpatterns = [ - # 프로젝트 관련 URL은 추후 추가 + # 프로젝트 대시보드 + path("dashboard/", views.dashboard, name="dashboard"), # dashboard.html + path("dashboard//", views.dashboard_detail, name="dashboard_detail"), # dashboard.html (특정 프로젝트) ] diff --git a/apps/reflections/urls.py b/apps/reflections/urls.py index 2131fbf..45e4f29 100644 --- a/apps/reflections/urls.py +++ b/apps/reflections/urls.py @@ -1,7 +1,21 @@ from django.urls import path +from . import views app_name = "reflections" urlpatterns = [ - # 회고 관련 URL은 추후 추가 + # 회고 목록 + path("", views.note_list, name="note_list"), # note_list.html + + # 회고 작성 + path("create/", views.note_create, name="note_create"), # note_create.html + + # 회고 상세 + path("/", views.note_detail, name="note_detail"), # note_detail.html + + # 회고 수정 + path("/edit/", views.note_update, name="note_update"), # note_update.html + + # 회고 삭제 + path("/delete/", views.note_delete, name="note_delete"), ] diff --git a/apps/teams/urls.py b/apps/teams/urls.py index da9535a..757bce3 100644 --- a/apps/teams/urls.py +++ b/apps/teams/urls.py @@ -1,7 +1,15 @@ from django.urls import path +from . import views app_name = "teams" urlpatterns = [ - # 팀 관련 URL은 추후 추가 + # 팀 매칭 신청 + path("apply/", views.team_apply, name="team_apply"), # team_apply.html + + # 열정 테스트 (팀플 신청 시) + path("passion-test/", views.passion_test, name="passion_test"), # passion_test.html + + # 팀 매칭 결과/대기 화면 + path("status/", views.team_status, name="team_status"), # team.html ] diff --git a/config/urls.py b/config/urls.py index 272c8ae..371680d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -3,12 +3,12 @@ from django.conf import settings from django.conf.urls.static import static from django.views.generic import RedirectView -from .views import initial_view +from .views import main_view from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView urlpatterns = [ - path("", initial_view.as_view(), name="initial"), + path("", main_view, name="main"), # 메인 화면 path("admin/", admin.site.urls), # allauth (로그인/소셜로그인) diff --git a/config/views.py b/config/views.py index 1443102..bd11817 100644 --- a/config/views.py +++ b/config/views.py @@ -1,5 +1,11 @@ -from django.views.generic import TemplateView +from django.shortcuts import render -# 테스트용 기본 템플릿 -class initial_view(TemplateView): - template_name = "initial.html" + +# 메인 화면 +def main_view(request): + """ + 메인 화면 (main.html) + - 비로그인: 컨텐츠들 제목 섹션 - 로그인 후 이용해보세요. + - 로그인: 각 섹션 클릭 시 해당하는 html로 이동 + """ + return render(request, "main.html") From 8fa559796e57780a5af244288a8c1bba43482a5a Mon Sep 17 00:00:00 2001 From: knana6 Date: Sat, 31 Jan 2026 11:51:39 +0900 Subject: [PATCH 041/380] DB setting --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index cdd5d5d..431a260 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ DB_ENGINE=django.db.backends.postgresql DB_NAME=startlinedev DB_USER=your_postgres_user # 수정 필요: 자신의 PostgreSQL 사용자명으로 변경 -DB_PASSWORD= # 수정 필요: PostgreSQL 비밀번호 입력 (없으면 비워두기) +DB_PASSWORD= esdel2005 DB_HOST=localhost DB_PORT=5432 From f1ab74f583af28e971deb93aa74de96d578a4716 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 11:54:22 +0900 Subject: [PATCH 042/380] =?UTF-8?q?feat:=20=EA=B0=81=20=EC=95=B1=EB=B3=84?= =?UTF-8?q?=20=EB=8D=94=EB=AF=B8=20=EB=B7=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 54 ++++++++++++++++++++++------------- apps/guides/views.py | 22 +++++++++++++- apps/projects/views.py | 23 ++++++++++++++- apps/reflections/views.py | 60 +++++++++++++++++++++++++++++++++++++-- apps/teams/views.py | 33 +++++++++++++++++++++ 5 files changed, 168 insertions(+), 24 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 81c7599..6ea89df 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -6,6 +6,40 @@ from .forms import OnboardingForm, ProfileUpdateForm +@login_required +def level_test(request): + """레벨 진단 테스트""" + # TODO: 레벨 테스트 로직 구현 + return render(request, "accounts/level_test.html") + + +@login_required +def test_result(request): + """레벨 테스트 결과""" + # TODO: 테스트 결과 로직 구현 + return render(request, "accounts/test_result.html") + + +@login_required +def profile_edit(request): + """프로필 수정""" + if request.method == "POST": + form = ProfileUpdateForm( + request.POST, + request.FILES, + instance=request.user, + ) + if form.is_valid(): + form.save() + messages.success(request, "프로필이 수정되었습니다.") + return redirect("accounts:mypage") + else: + form = ProfileUpdateForm(instance=request.user) + + context = {"form": form} + return render(request, "accounts/profile_edit.html", context) + + @login_required def onboarding_profile(request): """온보딩: 최초 프로필 설정""" @@ -38,26 +72,6 @@ def mypage(request): return render(request, "account/mypage.html", context) -@login_required -def profile_update(request): - """프로필 수정""" - if request.method == "POST": - form = ProfileUpdateForm( - request.POST, - request.FILES, - instance=request.user, - ) - if form.is_valid(): - form.save() - messages.success(request, "프로필이 수정되었습니다.") - return redirect("accounts:mypage") - else: - form = ProfileUpdateForm(instance=request.user) - - context = {"form": form} - return render(request, "account/profile_update.html", context) - - @login_required def withdraw(request): """회원 탈퇴""" diff --git a/apps/guides/views.py b/apps/guides/views.py index a0d51a6..6bbc86d 100644 --- a/apps/guides/views.py +++ b/apps/guides/views.py @@ -1 +1,21 @@ -# Guide views will be implemented here +from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.decorators import login_required + +# from .models import Guide + + +@login_required +def mission(request): + """미션/체크리스트 페이지""" + # TODO: 미션 로직 구현 + return render(request, "guides/mission.html") + + +@login_required +def mission_detail(request, project_id): + """특정 프로젝트 미션 페이지""" + # TODO: 특정 프로젝트 미션 로직 구현 + context = { + "project_id": project_id, + } + return render(request, "guides/mission.html", context) diff --git a/apps/projects/views.py b/apps/projects/views.py index 58bade1..9c35e90 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -1 +1,22 @@ -# Project views will be implemented here +from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.decorators import login_required + +# from .models import Project + + +@login_required +def dashboard(request): + """프로젝트 대시보드 (전체)""" + # TODO: 대시보드 로직 구현 + return render(request, "projects/dashboard.html") + + +@login_required +def dashboard_detail(request, project_id): + """특정 프로젝트 대시보드""" + # TODO: 특정 프로젝트 대시보드 로직 구현 + # project = get_object_or_404(Project, id=project_id) + context = { + "project_id": project_id, + } + return render(request, "projects/dashboard.html", context) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 91ea44a..f9cfd44 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -1,3 +1,59 @@ -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required +from django.contrib import messages -# Create your views here. +# from .models import Reflection + + +@login_required +def note_list(request): + """회고 목록""" + # TODO: 회고 목록 로직 구현 + return render(request, "reflections/note_list.html") + + +@login_required +def note_create(request): + """회고 작성""" + # TODO: 회고 작성 로직 구현 + if request.method == "POST": + # 폼 처리 로직 + pass + return render(request, "reflections/note_create.html") + + +@login_required +def note_detail(request, note_id): + """회고 상세""" + # TODO: 회고 상세 로직 구현 + # note = get_object_or_404(Reflection, id=note_id) + context = { + "note_id": note_id, + } + return render(request, "reflections/note_detail.html", context) + + +@login_required +def note_update(request, note_id): + """회고 수정""" + # TODO: 회고 수정 로직 구현 + # note = get_object_or_404(Reflection, id=note_id) + if request.method == "POST": + # 폼 처리 로직 + pass + context = { + "note_id": note_id, + } + return render(request, "reflections/note_update.html", context) + + +@login_required +def note_delete(request, note_id): + """회고 삭제""" + # TODO: 회고 삭제 로직 구현 + # note = get_object_or_404(Reflection, id=note_id) + if request.method == "POST": + # note.delete() + messages.success(request, "회고가 삭제되었습니다.") + return redirect("reflections:note_list") + return redirect("reflections:note_detail", note_id=note_id) diff --git a/apps/teams/views.py b/apps/teams/views.py index ccff7fb..7ab49fc 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -1,3 +1,6 @@ +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required + from rest_framework import viewsets from rest_framework.response import Response from rest_framework.decorators import action @@ -7,6 +10,36 @@ from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer +# ================================ +# Template Views (HTML 렌더링) +# ================================ + +@login_required +def team_apply(request): + """팀 매칭 신청 페이지""" + # TODO: 팀 매칭 신청 로직 구현 + return render(request, "teams/team_apply.html") + + +@login_required +def passion_test(request): + """열정 테스트 페이지""" + # TODO: 열정 테스트 로직 구현 + return render(request, "teams/passion_test.html") + + +@login_required +def team_status(request): + """팀 매칭 결과/대기 페이지""" + # TODO: 팀 매칭 상태 로직 구현 + return render(request, "teams/team.html") + + +# ================================ +# API Views (DRF ViewSets) +# ================================ + + @extend_schema_view( list=extend_schema(summary="팀 목록 조회", tags=["Teams"]), retrieve=extend_schema(summary="팀 상세 조회", tags=["Teams"]), From 9921ea08077c6a1a3ce76dbef64bcd32617110a3 Mon Sep 17 00:00:00 2001 From: knana6 Date: Sat, 31 Jan 2026 12:19:45 +0900 Subject: [PATCH 043/380] feat: password delete --- .env.example | 2 +- env | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 env diff --git a/.env.example b/.env.example index 431a260..846ec4a 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,4 @@ DEBUG=True ALLOWED_HOSTS=localhost,127.0.0.1 # Allauth 설정 -SITE_ID=1 +SITE_ID=1 \ No newline at end of file diff --git a/env b/env new file mode 100644 index 0000000..e69de29 From 9d8752b9743b4268254dc2c7ba9ab89b4460f809 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 12:26:07 +0900 Subject: [PATCH 044/380] =?UTF-8?q?refactor:=20db=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 846ec4a..1942771 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ # PostgreSQL 데이터베이스 설정 # 팀원들이 각자 자신의 PostgreSQL 사용자명과 설정에 맞게 수정해야 합니다 DB_ENGINE=django.db.backends.postgresql -DB_NAME=startlinedev +DB_NAME=kitup DB_USER=your_postgres_user # 수정 필요: 자신의 PostgreSQL 사용자명으로 변경 -DB_PASSWORD= esdel2005 +DB_PASSWORD= # 수정 필요: PostgreSQL 비밀번호 입력 (없으면 비워두기) DB_HOST=localhost DB_PORT=5432 From 83d60eaca556dab4db621010e2b549affbd06b0c Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 12:32:08 +0900 Subject: [PATCH 045/380] =?UTF-8?q?feat=20:=20html=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/account/level_test.html | 0 templates/account/login.html | 0 templates/account/mypage.html | 0 templates/account/passion_test.html | 0 templates/account/profile_edit.html | 0 templates/{ => account}/signup.html | 0 templates/account/test_result.html | 0 templates/guides/mission.html | 0 templates/initial.html | 1 + templates/main.html | 0 templates/projects/dashboard.html | 0 templates/projects/team.html | 0 templates/projects/team_apply.html | 0 templates/reflections/note_create.html | 0 templates/reflections/note_detail.html | 0 templates/reflections/note_list.html | 0 templates/reflections/note_update.html | 0 17 files changed, 1 insertion(+) create mode 100644 templates/account/level_test.html create mode 100644 templates/account/login.html create mode 100644 templates/account/mypage.html create mode 100644 templates/account/passion_test.html create mode 100644 templates/account/profile_edit.html rename templates/{ => account}/signup.html (100%) create mode 100644 templates/account/test_result.html create mode 100644 templates/guides/mission.html create mode 100644 templates/initial.html create mode 100644 templates/main.html create mode 100644 templates/projects/dashboard.html create mode 100644 templates/projects/team.html create mode 100644 templates/projects/team_apply.html create mode 100644 templates/reflections/note_create.html create mode 100644 templates/reflections/note_detail.html create mode 100644 templates/reflections/note_list.html create mode 100644 templates/reflections/note_update.html diff --git a/templates/account/level_test.html b/templates/account/level_test.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/account/login.html b/templates/account/login.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/account/mypage.html b/templates/account/mypage.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/account/passion_test.html b/templates/account/passion_test.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/signup.html b/templates/account/signup.html similarity index 100% rename from templates/signup.html rename to templates/account/signup.html diff --git a/templates/account/test_result.html b/templates/account/test_result.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/guides/mission.html b/templates/guides/mission.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/initial.html b/templates/initial.html new file mode 100644 index 0000000..924aef4 --- /dev/null +++ b/templates/initial.html @@ -0,0 +1 @@ +초기화면 \ No newline at end of file diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/projects/team.html b/templates/projects/team.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/projects/team_apply.html b/templates/projects/team_apply.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/reflections/note_create.html b/templates/reflections/note_create.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/reflections/note_detail.html b/templates/reflections/note_detail.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/reflections/note_list.html b/templates/reflections/note_list.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/reflections/note_update.html b/templates/reflections/note_update.html new file mode 100644 index 0000000..e69de29 From 64d032b4a71b0b7641dc44c89fbc79cde2714a76 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 12:34:47 +0900 Subject: [PATCH 046/380] =?UTF-8?q?docs=20:=20base.html=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index 3e86df5..259e69e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,7 +5,7 @@ {% block title %}KITUP{% endblock %} - + {% block header %}{% endblock %} From ff8d4b7d25e5c0fdd3055a108ad52819eebd1c38 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 12:40:11 +0900 Subject: [PATCH 047/380] =?UTF-8?q?refactor:=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 5 +++-- config/views.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/config/urls.py b/config/urls.py index 371680d..6a49cf4 100644 --- a/config/urls.py +++ b/config/urls.py @@ -3,12 +3,13 @@ from django.conf import settings from django.conf.urls.static import static from django.views.generic import RedirectView -from .views import main_view +from .views import initial_view, main_view from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView urlpatterns = [ - path("", main_view, name="main"), # 메인 화면 + path("", initial_view, name="initial"), # 초기 화면 (initial.html) + path("main/", main_view, name="main"), # 메인 화면 (main.html) path("admin/", admin.site.urls), # allauth (로그인/소셜로그인) diff --git a/config/views.py b/config/views.py index bd11817..b3be8ed 100644 --- a/config/views.py +++ b/config/views.py @@ -1,7 +1,16 @@ from django.shortcuts import render -# 메인 화면 +# 초기 화면 (initial.html) +def initial_view(request): + """ + 초기 화면 (initial.html) + - 랜딩 페이지 + """ + return render(request, "initial.html") + + +# 메인 화면 (main.html) def main_view(request): """ 메인 화면 (main.html) From e6d377bc5610527a96e0c99a4e70639b52f9deb4 Mon Sep 17 00:00:00 2001 From: knana6 Date: Sat, 31 Jan 2026 12:51:30 +0900 Subject: [PATCH 048/380] fix: signup block load --- templates/signup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/signup.html b/templates/signup.html index 8db44d6..35d227f 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -1,4 +1,4 @@ -{extends 'base.html' %} +{% extends 'base.html' %} {% load static %} {% block title %}회원가입 - KITUP{% endblock %} From 0a49f367987a4c97625dac93b178c2c6c5fc2284 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 13:06:20 +0900 Subject: [PATCH 049/380] =?UTF-8?q?docs:=20initial.html=20&=20base.html=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/base.css | 11 ++- static/css/initial.css | 0 static/css/login.css | 172 +++++++++++++++++++++++++++++++++++ templates/account/login.html | 58 ++++++++++++ templates/base.html | 2 +- templates/initial.html | 15 ++- templates/main.html | 14 +++ 7 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 static/css/initial.css create mode 100644 static/css/login.css diff --git a/static/css/base.css b/static/css/base.css index 8317bd8..a61130c 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -2,15 +2,19 @@ body { font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; + margin: 0; padding: 0; } - * { margin: 0; padding: 0; box-sizing: border-box; } header { + position: fixed; + top: 0; left: 0; + background: #fff; + z-index: 1000; width: 100%; height: 60px; display: flex; align-items: center; @@ -51,4 +55,9 @@ header .header_menu a p { padding: 13px auto; font-size: 16px; font-weight: bold; line-height: 40px; +} + +main { + padding-top: 60px; + min-height: calc(100vh - 60px); } \ No newline at end of file diff --git a/static/css/initial.css b/static/css/initial.css new file mode 100644 index 0000000..e69de29 diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..cdda8a5 --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,172 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Apple SD Gothic Neo', sans-serif; + background-color: #f5f5f5; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; +} + +.login-container { + position: relative; + width: 100%; + max-width: 440px; +} + +.login-box { + background: white; + border-radius: 16px; + padding: 48px 40px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.logo { + text-align: center; + margin-bottom: 32px; +} + +.logo h3 { + font-size: 32px; + color: #4285f4; +} + +.login-title { + font-size: 24px; + font-weight: 600; + text-align: center; + margin-bottom: 32px; + color: #333; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.input-group { + position: relative; +} + +.input-group input { + width: 100%; + padding: 14px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 15px; + transition: all 0.3s ease; + background-color: #fafafa; +} + +.input-group input:focus { + outline: none; + border-color: #4285f4; + background-color: white; +} + +.input-group input::placeholder { + color: #999; +} + +.login-button { + width: 100%; + padding: 14px; + background: #4285f4; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + margin-top: 8px; + transition: background 0.3s ease; +} + +.login-button:hover { + background: #3367d6; +} + +.login-button:active { + background: #2851a3; +} + +.links { + display: flex; + justify-content: space-between; + margin-top: 20px; + padding: 0 4px; +} + +.link { + font-size: 14px; + color: #4285f4; + text-decoration: none; + transition: color 0.3s ease; +} + +.link:hover { + color: #3367d6; + text-decoration: underline; +} + +.social-login { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid #e0e0e0; +} + +.social-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: white; + border: 1px solid #e0e0e0; + transition: all 0.3s ease; + cursor: pointer; +} + +.social-icon:hover { + border-color: #4285f4; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(66, 133, 244, 0.2); +} + +.social-icon img { + width: 100%; + height: 100%; + border-radius: 50%; +} + +/* 반응형 디자인 */ +@media (max-width: 480px) { + .login-box { + padding: 32px 24px; + } + + .login-title { + font-size: 20px; + } + + .social-icon { + width: 44px; + height: 44px; + } + + .social-icon img { + width: 20px; + height: 20px; + } +} \ No newline at end of file diff --git a/templates/account/login.html b/templates/account/login.html index e69de29..685a493 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -0,0 +1,58 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}KITUP - 로그인{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 259e69e..2e5be52 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,7 +15,7 @@

KITUP

{% if user.is_authenticated %} -

팀매칭

+

팀매칭

내 프로젝트

프로젝트 기록

마이페이지

diff --git a/templates/initial.html b/templates/initial.html index 924aef4..90df0c8 100644 --- a/templates/initial.html +++ b/templates/initial.html @@ -1 +1,14 @@ -초기화면 \ No newline at end of file +{% extends 'base.html' %} +{% load static %} + + +{% block header %} + +{% endblock %} + +{% block content %} +
+ +
+{% endblock %} + diff --git a/templates/main.html b/templates/main.html index e69de29..eb039f8 100644 --- a/templates/main.html +++ b/templates/main.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% load static %} + + +{% block header %} + +{% endblock %} + +{% block content %} +
+ +
+{% endblock %} + From bbd2c2b0995400ef17b1c3cfb4524144859c3db2 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 13:11:07 +0900 Subject: [PATCH 050/380] =?UTF-8?q?docs:=20initial.html=20&=20main.html=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/initial.html | 2 +- templates/main.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/initial.html b/templates/initial.html index 90df0c8..a694882 100644 --- a/templates/initial.html +++ b/templates/initial.html @@ -8,7 +8,7 @@ {% block content %}
- + 초기화면
{% endblock %} diff --git a/templates/main.html b/templates/main.html index eb039f8..2438a5c 100644 --- a/templates/main.html +++ b/templates/main.html @@ -8,7 +8,7 @@ {% block content %}
- + 메인화면
{% endblock %} From b1497ec790710c90ea66ff585aaf2334b0d12348 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 13:19:43 +0900 Subject: [PATCH 051/380] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0003_user_team_ban_count_report.py | 38 ++++++++ apps/accounts/models.py | 88 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 apps/accounts/migrations/0003_user_team_ban_count_report.py diff --git a/apps/accounts/migrations/0003_user_team_ban_count_report.py b/apps/accounts/migrations/0003_user_team_ban_count_report.py new file mode 100644 index 0000000..8f59ed1 --- /dev/null +++ b/apps/accounts/migrations/0003_user_team_ban_count_report.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.10 on 2026-01-31 04:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_remove_user_profile_image_url_user_profile_image'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='team_ban_count', + field=models.PositiveSmallIntegerField(default=0, help_text='남은 팀플 참여 금지 횟수'), + ), + migrations.CreateModel( + name='Report', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reason', models.TextField(help_text='신고 사유')), + ('status', models.CharField(choices=[('PENDING', '대기중'), ('APPROVED', '승인'), ('REJECTED', '거절')], default='PENDING', help_text='신고 처리 상태', max_length=10)), + ('admin_note', models.TextField(blank=True, help_text='운영자 메모', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('processed_at', models.DateTimeField(blank=True, help_text='처리 일시', null=True)), + ('reported_user', models.ForeignKey(help_text='피신고자', on_delete=django.db.models.deletion.CASCADE, related_name='reports_received', to=settings.AUTH_USER_MODEL)), + ('reporter', models.ForeignKey(help_text='신고자', on_delete=django.db.models.deletion.CASCADE, related_name='reports_made', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'reports', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['status', 'created_at'], name='reports_status_19e159_idx')], + }, + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 76bae99..3aa7232 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -31,9 +31,18 @@ class User(AbstractUser): help_text="자기소개", ) + team_ban_count = models.PositiveSmallIntegerField( + default=0, + help_text="남은 팀플 참여 금지 횟수", + ) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def is_banned(self) -> bool: + """팀플 참여 금지 상태 여부""" + return self.team_ban_count > 0 + def is_profile_completed(self) -> bool: """프로필 설정 완료 여부""" return bool(self.nickname) @@ -136,3 +145,82 @@ class Meta: def __str__(self) -> str: return f"{self.user}:{self.role.code}=Lv.{self.level}" + + +class Report(models.Model): + """ + 사용자 신고 + - 팀원을 신고하면 사유를 작성 + - 운영자가 승인 시 피신고자에게 팀플 2회 금지 제재 + """ + + class Status(models.TextChoices): + PENDING = "PENDING", "대기중" + APPROVED = "APPROVED", "승인" + REJECTED = "REJECTED", "거절" + + reporter = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="reports_made", + help_text="신고자", + ) + + reported_user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="reports_received", + help_text="피신고자", + ) + + reason = models.TextField( + help_text="신고 사유", + ) + + status = models.CharField( + max_length=10, + choices=Status.choices, + default=Status.PENDING, + help_text="신고 처리 상태", + ) + + admin_note = models.TextField( + null=True, + blank=True, + help_text="운영자 메모", + ) + + created_at = models.DateTimeField(auto_now_add=True) + processed_at = models.DateTimeField( + null=True, + blank=True, + help_text="처리 일시", + ) + + class Meta: + db_table = "reports" + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["status", "created_at"]), + ] + + def approve(self): + """신고 승인 - 피신고자에게 2회 팀플 금지 제재""" + from django.utils import timezone + self.status = self.Status.APPROVED + self.processed_at = timezone.now() + self.reported_user.team_ban_count += 2 + self.reported_user.save(update_fields=["team_ban_count"]) + self.save() + + def reject(self, admin_note: str = None): + """신고 거절""" + from django.utils import timezone + self.status = self.Status.REJECTED + self.processed_at = timezone.now() + if admin_note: + self.admin_note = admin_note + self.save() + + def __str__(self) -> str: + return f"{self.reporter} → {self.reported_user} ({self.get_status_display()})" From c6c45e776b24acbfd85463fd413d82a938cd4d19 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 13:47:01 +0900 Subject: [PATCH 052/380] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/api_urls.py | 3 + apps/accounts/views.py | 34 +++++++ config/api_urls.py | 10 +- config/settings.py | 22 ++++- templates/account/email_confirm.html | 47 +++++++++ templates/account/login.html | 62 +++++++++--- templates/account/signup.html | 119 ++++++++++++++++++++--- templates/account/verification_sent.html | 53 ++++++++++ 8 files changed, 313 insertions(+), 37 deletions(-) create mode 100644 templates/account/email_confirm.html create mode 100644 templates/account/verification_sent.html diff --git a/apps/accounts/api_urls.py b/apps/accounts/api_urls.py index 690b131..b683fdd 100644 --- a/apps/accounts/api_urls.py +++ b/apps/accounts/api_urls.py @@ -1,4 +1,7 @@ from django.urls import path +from . import views urlpatterns = [ + path("check-username/", views.check_username, name="check_username"), + path("check-email/", views.check_email, name="check_email"), ] diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 6ea89df..d4a040f 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -2,8 +2,42 @@ from django.shortcuts import render, redirect from django.contrib.auth import logout from django.contrib import messages +from django.http import JsonResponse +from django.views.decorators.http import require_GET from .forms import OnboardingForm, ProfileUpdateForm +from .models import User + + +@require_GET +def check_username(request): + """아이디 중복 확인 API""" + username = request.GET.get("username", "").strip() + + if not username: + return JsonResponse({"available": False, "message": "아이디를 입력해주세요."}) + + if len(username) < 4: + return JsonResponse({"available": False, "message": "아이디는 4자 이상이어야 합니다."}) + + if User.objects.filter(username=username).exists(): + return JsonResponse({"available": False, "message": "이미 사용 중인 아이디입니다."}) + + return JsonResponse({"available": True, "message": "사용 가능한 아이디입니다."}) + + +@require_GET +def check_email(request): + """이메일 중복 확인 API""" + email = request.GET.get("email", "").strip() + + if not email: + return JsonResponse({"available": False, "message": "이메일을 입력해주세요."}) + + if User.objects.filter(email=email).exists(): + return JsonResponse({"available": False, "message": "이미 사용 중인 이메일입니다."}) + + return JsonResponse({"available": True, "message": "사용 가능한 이메일입니다."}) @login_required diff --git a/config/api_urls.py b/config/api_urls.py index 6ff0bc4..a7b4874 100644 --- a/config/api_urls.py +++ b/config/api_urls.py @@ -1,9 +1,9 @@ from django.urls import path, include urlpatterns = [ - path("", include("apps.accounts.api_urls")), - path("", include("apps.projects.api_urls")), - path("", include("apps.teams.api_urls")), - path("", include("apps.guides.api_urls")), - path("", include("apps.reflections.api_urls")), + path("accounts/", include("apps.accounts.api_urls")), + path("projects/", include("apps.projects.api_urls")), + path("teams/", include("apps.teams.api_urls")), + path("guides/", include("apps.guides.api_urls")), + path("reflections/", include("apps.reflections.api_urls")), ] diff --git a/config/settings.py b/config/settings.py index b81d8bc..3d594a3 100644 --- a/config/settings.py +++ b/config/settings.py @@ -98,13 +98,31 @@ # AllAuth settings SITE_ID = 1 AUTH_USER_MODEL = "accounts.User" -# allauth 기본 로그인 방식 설정 -ACCOUNT_LOGIN_METHODS = {"username"} + +# allauth 설정 +ACCOUNT_LOGIN_METHODS = {"username", "email"} # 아이디 또는 이메일로 로그인 +ACCOUNT_EMAIL_REQUIRED = True # 이메일 필수 +ACCOUNT_EMAIL_VERIFICATION = "mandatory" # 이메일 인증 필수 ACCOUNT_SIGNUP_FIELDS = [ "username*", + "email*", "password1*", "password2*", ] +ACCOUNT_CONFIRM_EMAIL_ON_GET = True # 이메일 링크 클릭만으로 인증 완료 +ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 # 인증 링크 유효기간 (일) +ACCOUNT_EMAIL_SUBJECT_PREFIX = "[KITUP] " # 이메일 제목 접두사 + +# 이메일 발송 설정 (개발용 - 콘솔 출력) +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +# 배포 시 실제 SMTP 설정으로 변경 +# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +# EMAIL_HOST = "smtp.gmail.com" +# EMAIL_PORT = 587 +# EMAIL_USE_TLS = True +# EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") +# DEFAULT_FROM_EMAIL = "KITUP " LOGIN_URL = "/accounts/login/" LOGIN_REDIRECT_URL = "/" # 로그인 성공 후 diff --git a/templates/account/email_confirm.html b/templates/account/email_confirm.html new file mode 100644 index 0000000..1fac920 --- /dev/null +++ b/templates/account/email_confirm.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}이메일 인증 완료 - KITUP{% endblock %} + +{% block content %} +
+
+ {% if confirmation %} +

✅ 이메일 인증 완료!

+

{{ confirmation.email_address.email }}

+

이메일 인증이 완료되었습니다.

+

이제 로그인하실 수 있습니다.

+ + 로그인하기 + {% else %} +

❌ 인증 실패

+

유효하지 않거나 만료된 인증 링크입니다.

+ 다시 가입하기 + {% endif %} +
+
+ + +{% endblock %} diff --git a/templates/account/login.html b/templates/account/login.html index 685a493..09f2624 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -18,11 +18,25 @@

KITUP

로그인

+ + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + -
+ + {% endblock %} \ No newline at end of file diff --git a/templates/account/signup.html b/templates/account/signup.html index 35d227f..c214eba 100644 --- a/templates/account/signup.html +++ b/templates/account/signup.html @@ -14,47 +14,138 @@

회원가입

- - + + + {% csrf_token %} +
- - + +
+
- +
- +
- -
- - -
-
- - + +
+ +
+ + + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +

{{ error }}

+ {% endfor %}
+ {% endif %} + + + + + + + {% endblock %} diff --git a/templates/account/verification_sent.html b/templates/account/verification_sent.html new file mode 100644 index 0000000..a820f96 --- /dev/null +++ b/templates/account/verification_sent.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}이메일 인증 - KITUP{% endblock %} + +{% block content %} +
+
+

📧 이메일 인증

+

입력하신 이메일 주소로 인증 메일을 발송했습니다.

+

이메일을 확인하고 인증 링크를 클릭해주세요.

+ +
+

✓ 메일이 오지 않았다면 스팸함을 확인해주세요.

+

✓ 인증 링크는 3일간 유효합니다.

+
+ + 로그인 페이지로 +
+
+ + +{% endblock %} From ade6d2197a73a1790cfc58e6b0b6e02070b40f81 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 31 Jan 2026 14:11:29 +0900 Subject: [PATCH 053/380] =?UTF-8?q?docker-compose=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +++- Dockerfile | 23 +++++++++++++++++ Dockerrun.aws.json | 12 +++++++++ config/settings.py | 24 +++++------------ docker-compose.yml | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 Dockerfile create mode 100644 Dockerrun.aws.json create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index e648c58..d876c8c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ pip-wheel-metadata/ ############################ # Docker ############################ -docker-compose.override.yml +#docker-compose.override.yml *.tar *.log @@ -81,3 +81,6 @@ htmlcov/ # 기타 ############################ .cache/ + +*.pem +kitup-key.pem \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1db7b6d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12-slim + +WORKDIR /app + +# system dependencies +RUN apt-get update && apt-get install -y \ + postgresql-client \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# python deps +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# project files +COPY . . + +# static / media +RUN mkdir -p /app/staticfiles /app/media + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/Dockerrun.aws.json b/Dockerrun.aws.json new file mode 100644 index 0000000..3416a5b --- /dev/null +++ b/Dockerrun.aws.json @@ -0,0 +1,12 @@ +{ + "AWSEBDockerrunVersion": "1", + "Image": { + "Name": "bimvocado/kitup-web:latest", + "Update": "true" + }, + "Ports": [ + { + "ContainerPort": 8000 + } + ] +} \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index b81d8bc..aeaf095 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,15 +1,3 @@ -""" -Django settings for config project. - -Generated by 'django-admin startproject' using Django 5.2.10. - -For more information on this file, see -https://docs.djangoproject.com/en/5.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.2/ref/settings/ -""" - from pathlib import Path import os from dotenv import load_dotenv @@ -166,12 +154,12 @@ DATABASES = { "default": { - "ENGINE": os.getenv("DB_ENGINE", "django.db.backends.postgresql"), - "NAME": os.getenv("DB_NAME", "startlinedev"), - "USER": os.getenv("DB_USER", "isujong"), - "PASSWORD": os.getenv("DB_PASSWORD", ""), - "HOST": os.getenv("DB_HOST", "localhost"), - "PORT": os.getenv("DB_PORT", "5432"), + "ENGINE": os.getenv("DB_ENGINE"), + "NAME": os.getenv("DB_NAME"), + "USER": os.getenv("DB_USER"), + "PASSWORD": os.getenv("DB_PASSWORD"), + "HOST": os.getenv("DB_HOST"), + "PORT": os.getenv("DB_PORT"), } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..211f4a5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +version: "3.8" + +services: + db: + image: postgres:15-alpine + container_name: kitup_db + environment: + POSTGRES_DB: kitup + POSTGRES_USER: kitup + POSTGRES_PASSWORD: kitup123 + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U kitup"] + interval: 10s + timeout: 5s + retries: 5 + + web: + image: bimvocado/kitup-web:latest + container_name: kitup_web + command: > + sh -c " + python manage.py migrate && + python manage.py collectstatic --noinput && + python manage.py runserver 0.0.0.0:8000 + " + volumes: + - .:/app + - static_volume:/app/staticfiles + - media_volume:/app/media + ports: + - "8000:8000" + environment: + DEBUG: "True" + DB_ENGINE: django.db.backends.postgresql + DB_NAME: kitup + DB_USER: kitup + DB_PASSWORD: kitup123 + DB_HOST: db + DB_PORT: 5432 + REDIS_HOST: redis + REDIS_PORT: 6379 + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + + redis: + image: redis:7-alpine + container_name: kitup_redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + +volumes: + postgres_data: + static_volume: + media_volume: + redis_data: From 6872ee4632129d30b7f5bfbb9d17e817346b4bcb Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 31 Jan 2026 14:30:48 +0900 Subject: [PATCH 054/380] =?UTF-8?q?=EC=95=BC=EB=A7=90=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FETCH_H | 0 FETCH_HEAD | 0 FETCH_HLineDev | 0 docker-compose.yml | 15 ++++++++------- 4 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 FETCH_H create mode 100644 FETCH_HEAD create mode 100644 FETCH_HLineDev diff --git a/FETCH_H b/FETCH_H new file mode 100644 index 0000000..e69de29 diff --git a/FETCH_HEAD b/FETCH_HEAD new file mode 100644 index 0000000..e69de29 diff --git a/FETCH_HLineDev b/FETCH_HLineDev new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 211f4a5..a7cd1ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,14 @@ version: "3.8" services: - db: - image: postgres:15-alpine - container_name: kitup_db - environment: - POSTGRES_DB: kitup - POSTGRES_USER: kitup - POSTGRES_PASSWORD: kitup123 + +db: + image: postgres:15-alpine + container_name: kitup_db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data ports: From 214570992654f358f5fc1b907ba60b1cc1503658 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 17:47:02 +0900 Subject: [PATCH 055/380] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index d4a040f..43b42cc 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -95,13 +95,30 @@ def onboarding_profile(request): @login_required def mypage(request): - """마이페이지""" + """ + 마이페이지 조회 뷰 + + - 로그인한 사용자의 정보, 역할 레벨, 팀 프로젝트 참여 내역 등을 조회 + - 'account/mypage.html' 템플릿을 렌더링 + - 프로젝트 내역은 team_members -> team -> project 경로로 조회한다. + """ + user = request.user + + # 역할별 스킬 레벨 (user_role_levels + roles) role_levels = user.role_levels.select_related("role").all() - + + # 팀 프로젝트 참여 내역 (team_members + role + team + project) + memberships = ( + user.team_members + .select_related("team__project", "role") + .order_by("-joined_at") + ) + context = { - "user": user, + "user_obj": user, "role_levels": role_levels, + "memberships": memberships, } return render(request, "account/mypage.html", context) From d67a8626a8d6e2ff1271000704b6982587d5d056 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 18:15:52 +0900 Subject: [PATCH 056/380] =?UTF-8?q?docs:=20issue,=20pr=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature.md | 18 ++++++++++++++++++ .github/pull_request_template.md | 9 +++++++++ 2 files changed, 27 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..bc7733b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,18 @@ +--- +name: Feature +about: 새로운 기능 추가 / 개선 +title: "[FEAT] " +labels: ["feature"] +assignees: [] +--- + +## 📌 작업 범위 + +- [ ] + +## ✅ TODO + +- [ ] + +## 📝 참고 사항 + \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d64db49 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## 🔥 작업 내용 + + +## 🔗 연관 이슈 + +- resolves # + +## ⚠️ 참고 사항 + From dd80bb041fe8bcc2a7e127684e0069921957ac9a Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 20:14:38 +0900 Subject: [PATCH 057/380] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- env | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 env diff --git a/env b/env deleted file mode 100644 index e69de29..0000000 From 90beecd0150190b6832ce3bc7823e5feac75dbd3 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 20:25:15 +0900 Subject: [PATCH 058/380] =?UTF-8?q?feat:=20=EB=A0=88=EB=B2=A8=20=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 43b42cc..a608976 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -42,9 +42,16 @@ def check_email(request): @login_required def level_test(request): - """레벨 진단 테스트""" - # TODO: 레벨 테스트 로직 구현 - return render(request, "accounts/level_test.html") + """ + 레벨 진단 테스트 페이지 렌더링 + + - 특정 역할(role_code)에 대한 테스트를 진행 + - role_code는 GET 파라미터로 전달받음 -> 프론트에서 설정 필요 + - 'accounts/level_test.html' 템플릿을 렌더링 + """ + role_code = request.GET.get("role") + context = {"role_code": role_code} + return render(request, "accounts/level_test.html", context) @login_required From 3eb0d8750cce2b1726c0de142f58691718a455c2 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 20:33:37 +0900 Subject: [PATCH 059/380] =?UTF-8?q?feat:=20=EB=A0=88=EB=B2=A8=20=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=20=ED=9B=84=20=EC=A0=9C=EC=B6=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index a608976..e17c691 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,12 +1,13 @@ +from datetime import timezone from django.contrib.auth.decorators import login_required -from django.shortcuts import render, redirect +from django.shortcuts import get_object_or_404, render, redirect from django.contrib.auth import logout from django.contrib import messages -from django.http import JsonResponse +from django.http import HttpResponseBadRequest, JsonResponse from django.views.decorators.http import require_GET from .forms import OnboardingForm, ProfileUpdateForm -from .models import User +from .models import Role, User, UserRoleLevel @require_GET @@ -53,6 +54,32 @@ def level_test(request): context = {"role_code": role_code} return render(request, "accounts/level_test.html", context) +@login_required +def level_submit(request): + """ + 레벨 테스트 결과 제출 처리 + + - POST 요청으로 역할 코드(role_code)와 레벨(level)을 전달받음 + - UserRoleLevel 모델에 결과 저장 또는 업데이트 + - 제출 후 테스트 결과 페이지로 리다이렉트 + """ + if request.method != "POST": + return HttpResponseBadRequest("잘못된 요청입니다.") + + role_code = request.POST.get("role_code") + role = get_object_or_404(Role, code=role_code) + level = request.POST.get("level") + + UserRoleLevel.objects.update_or_create( + user=request.user, + role=role, + defaults={ + "level": int(level), + "last_diagnosed_at": timezone.now(), + }, + ) + + return redirect("test:test_result") + f"?role={role_code}" @login_required def test_result(request): From 2c6dab3d87b5678e0b347adacb1615ffe2062a21 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 20:37:44 +0900 Subject: [PATCH 060/380] =?UTF-8?q?feat:=20=EB=A0=88=EB=B2=A8=20=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=20=EA=B2=B0=EA=B3=BC=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index e17c691..5ea3cd5 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -83,9 +83,28 @@ def level_submit(request): @login_required def test_result(request): - """레벨 테스트 결과""" - # TODO: 테스트 결과 로직 구현 - return render(request, "accounts/test_result.html") + """ + 레벨 테스트 결과 + + - 특정 역할(role_code)에 대한 사용자의 레벨 정보를 조회 + - 'test/test_result.html' 템플릿을 렌더링 + - 템플릿에 사용자 정보, 역할명, 레벨 전달 + """ + role_code = request.GET.get("role") + + role = get_object_or_404(Role, code=role_code) + url_level = ( + UserRoleLevel.objects.filter(user=request.user, role=role) + .select_related("role") + .first() + ) + + context = { + "user_obj": request.user, + "role": role, # role.name 출력 가능 + "level": url_level.level if url_level else None, + } + return render(request, "test/test_result.html", context) @login_required From 5b4b90e6b7913f16df0bf6a678ec70e20dd921c7 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 20:52:01 +0900 Subject: [PATCH 061/380] =?UTF-8?q?chore:=20test/submit=20url=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index d142bc3..dbdb285 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -9,6 +9,7 @@ # 레벨 진단 (Test) path("level-test/", views.level_test, name="level_test"), # level_test.html + path("level-test/submit/", views.level_submit, name="level_submit"), path("level-test/result/", views.test_result, name="test_result"), # test_result.html # 마이페이지 From ccbcf460b0e0329e43cd92d08756af430f6caea7 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 20:55:38 +0900 Subject: [PATCH 062/380] =?UTF-8?q?fix:=20import=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 5ea3cd5..2b84c18 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,4 +1,5 @@ -from datetime import timezone +from django.urls import reverse +from django.utils import timezone from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, render, redirect from django.contrib.auth import logout @@ -66,7 +67,7 @@ def level_submit(request): if request.method != "POST": return HttpResponseBadRequest("잘못된 요청입니다.") - role_code = request.POST.get("role_code") + role_code = request.POST.get("role") role = get_object_or_404(Role, code=role_code) level = request.POST.get("level") @@ -79,7 +80,7 @@ def level_submit(request): }, ) - return redirect("test:test_result") + f"?role={role_code}" + return redirect(f"{reverse('accounts:test_result')}?role={role_code}") @login_required def test_result(request): @@ -104,7 +105,7 @@ def test_result(request): "role": role, # role.name 출력 가능 "level": url_level.level if url_level else None, } - return render(request, "test/test_result.html", context) + return render(request, "accounts/test_result.html", context) @login_required From 1c740aa7c0d1ce4c0355e9bcd8eaeed187ef1a1d Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 21:15:25 +0900 Subject: [PATCH 063/380] =?UTF-8?q?fix:=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=20=EC=A4=91=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 2b84c18..5cda072 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -49,11 +49,11 @@ def level_test(request): - 특정 역할(role_code)에 대한 테스트를 진행 - role_code는 GET 파라미터로 전달받음 -> 프론트에서 설정 필요 - - 'accounts/level_test.html' 템플릿을 렌더링 + - 'account/level_test.html' 템플릿을 렌더링 """ role_code = request.GET.get("role") context = {"role_code": role_code} - return render(request, "accounts/level_test.html", context) + return render(request, "account/level_test.html", context) @login_required def level_submit(request): @@ -105,7 +105,7 @@ def test_result(request): "role": role, # role.name 출력 가능 "level": url_level.level if url_level else None, } - return render(request, "accounts/test_result.html", context) + return render(request, "account/test_result.html", context) @login_required @@ -125,7 +125,7 @@ def profile_edit(request): form = ProfileUpdateForm(instance=request.user) context = {"form": form} - return render(request, "accounts/profile_edit.html", context) + return render(request, "account/profile_edit.html", context) @login_required From e9f3136d532501baa84b858d6f2b372eb2af8deb Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 31 Jan 2026 21:15:56 +0900 Subject: [PATCH 064/380] =?UTF-8?q?test:=20test=20code=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=ED=9B=84=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/tests.py | 3 -- apps/accounts/tests/__init__.py | 0 apps/accounts/tests/test_level.py | 55 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) delete mode 100644 apps/accounts/tests.py create mode 100644 apps/accounts/tests/__init__.py create mode 100644 apps/accounts/tests/test_level.py diff --git a/apps/accounts/tests.py b/apps/accounts/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/apps/accounts/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/accounts/tests/__init__.py b/apps/accounts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/accounts/tests/test_level.py b/apps/accounts/tests/test_level.py new file mode 100644 index 0000000..7110e2b --- /dev/null +++ b/apps/accounts/tests/test_level.py @@ -0,0 +1,55 @@ +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth import get_user_model +from django.utils import timezone + +from ..models import Role, UserRoleLevel + +User = get_user_model() + +class LevelFlowTests(TestCase): + def setUp(self): + self.password = "testpass1234" + self.user = User.objects.create_user(username="testuser", password=self.password) + + # ✅ 온보딩/프로필 완료 가드가 있으면 이게 필요 + self.user.nickname = "tester" + self.user.save(update_fields=["nickname"]) + + ok = self.client.login(username="testuser", password=self.password) + self.assertTrue(ok) + + self.backend = Role.objects.create(code="BACKEND", name="백엔드") + Role.objects.create(code="FRONTEND", name="프론트엔드") + Role.objects.create(code="PM", name="PM(기획)") + + def test_get_level_test_ok(self): + url = reverse("accounts:level_test") + "?role=BACKEND" + res = self.client.get(url) + # 디버깅 필요하면 아래 1줄 잠깐 켜봐 + # print(res.status_code, res.headers.get("Location")) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context.get("role_code"), "BACKEND") + + def test_submit_creates_user_role_level(self): + url = reverse("accounts:level_submit") + + # ✅ view는 role로 받는다 + res = self.client.post(url, data={"role": "BACKEND", "level": "3"}) + self.assertEqual(res.status_code, 302) + + obj = UserRoleLevel.objects.get(user=self.user, role=self.backend) + self.assertEqual(obj.level, 3) + + def test_result_shows_level(self): + UserRoleLevel.objects.create( + user=self.user, + role=self.backend, + level=2, + last_diagnosed_at=timezone.now(), + ) + + url = reverse("accounts:test_result") + "?role=BACKEND" + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context.get("level"), 2) From c55db65a7464b90850b93fee5efda1d5114494f3 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 22:30:05 +0900 Subject: [PATCH 065/380] =?UTF-8?q?docs:=20base.html=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/base.html b/templates/base.html index 2e5be52..72768b7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,10 +15,10 @@

KITUP

{% if user.is_authenticated %} -

팀매칭

-

내 프로젝트

-

프로젝트 기록

-

마이페이지

+

팀매칭

+

내 프로젝트

+

프로젝트 기록

+

마이페이지

로그아웃

{% else %}

로그인

@@ -34,4 +34,4 @@

KITUP

- \ No newline at end of file + From 42e0d4353064f4eae5c9ae436ca3a319d969507d Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 31 Jan 2026 22:40:07 +0900 Subject: [PATCH 066/380] =?UTF-8?q?chore:=20templates=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20html=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/projects/{team.html => kitup_detail.html} | 0 templates/projects/{team_apply.html => kitup_list.html} | 0 templates/projects/project_detail.html | 0 templates/projects/project_list.html | 0 templates/teams/team.html | 0 templates/teams/team_apply.html | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename templates/projects/{team.html => kitup_detail.html} (100%) rename templates/projects/{team_apply.html => kitup_list.html} (100%) create mode 100644 templates/projects/project_detail.html create mode 100644 templates/projects/project_list.html create mode 100644 templates/teams/team.html create mode 100644 templates/teams/team_apply.html diff --git a/templates/projects/team.html b/templates/projects/kitup_detail.html similarity index 100% rename from templates/projects/team.html rename to templates/projects/kitup_detail.html diff --git a/templates/projects/team_apply.html b/templates/projects/kitup_list.html similarity index 100% rename from templates/projects/team_apply.html rename to templates/projects/kitup_list.html diff --git a/templates/projects/project_detail.html b/templates/projects/project_detail.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/projects/project_list.html b/templates/projects/project_list.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/teams/team.html b/templates/teams/team.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html new file mode 100644 index 0000000..e69de29 From 040b5fcd3f1a0f369d22b537857242db0e1f562b Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 16:01:17 +0900 Subject: [PATCH 067/380] =?UTF-8?q?chore:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=EC=97=90=20=EB=94=B0=EB=A5=B8=20url?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/urls.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/projects/urls.py b/apps/projects/urls.py index 6e11a63..49baa3a 100644 --- a/apps/projects/urls.py +++ b/apps/projects/urls.py @@ -4,7 +4,16 @@ app_name = "projects" urlpatterns = [ - # 프로젝트 대시보드 + # 프로젝트 대시보드 (현재 프로젝트) path("dashboard/", views.dashboard, name="dashboard"), # dashboard.html path("dashboard//", views.dashboard_detail, name="dashboard_detail"), # dashboard.html (특정 프로젝트) + path("dashboard//edit/", views.dashboard_update, name="dashboard_update"), # dashboard_update.html + + # 지난 프로젝트 + path("", views.project_list, name="project_list"), # project_list.html + path("/", views.project_detail, name="project_detail"), # project_detail.html + + # KITUP 프로젝트 (모든 프로젝트) + path("all/", views.kitup_list, name="kitup_list"), # kitup_list.html + path("all//", views.kitup_detail, name="kitup_detail"), # kitup_detail.html ] From c4686ea0ae3f1d7cd1a1d8deae49ecc23d779641 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 16:13:02 +0900 Subject: [PATCH 068/380] =?UTF-8?q?chore:=20=EB=8D=94=EB=AF=B8=20=EB=B7=B0?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 62 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index 9c35e90..1d7cf19 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -1,22 +1,78 @@ from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required +from django.contrib import messages # from .models import Project @login_required def dashboard(request): - """프로젝트 대시보드 (전체)""" + """현재 프로젝트 대시보드""" # TODO: 대시보드 로직 구현 return render(request, "projects/dashboard.html") @login_required def dashboard_detail(request, project_id): - """특정 프로젝트 대시보드""" - # TODO: 특정 프로젝트 대시보드 로직 구현 + """나의 현재 프로젝트 대시보드""" + # TODO: 단 하나뿐 나의 프로젝트 대시보드 # project = get_object_or_404(Project, id=project_id) context = { "project_id": project_id, } return render(request, "projects/dashboard.html", context) + + +@login_required +def dashboard_update(request, project_id): + """나의 현재 프로젝트 대시보드 수정""" + # TODO: 프로젝트 정보 수정 로직 + # project = get_object_or_404(Project, id=project_id, created_by=request.user) + if request.method == "POST": + # 정보 업데이트 + messages.success(request, "프로젝트가 업데이트되었습니다.") + return render(request, "projects/dashboard.html") + + context = { + "project_id": project_id, + } + return render(request, "projects/dashboard_update.html", context) + + +@login_required +def project_list(request): + """지난 프로젝트 리스트""" + # TODO: 지난 프로젝트 리스트 로직 구현 + # projects = request.user.created_teams.all() + context = {} + return render(request, "projects/project_list.html", context) + + +@login_required +def project_detail(request, project_id): + """지난 프로젝트 상세""" + # TODO: 지난 프로젝트 상세 로직 구현 + # project = get_object_or_404(Project, id=project_id) + context = { + "project_id": project_id, + } + return render(request, "projects/project_detail.html", context) + + +@login_required +def kitup_list(request): + """모든 KITUP 프로젝트 리스트""" + # TODO: 모든 프로젝트 리스트 로직 구현 + context = {} + return render(request, "projects/kitup_list.html", context) + + +@login_required +def kitup_detail(request, project_id): + """모든 KITUP 프로젝트 상세""" + # TODO: 모든 프로젝트 상세 로직 구현 + # project = get_object_or_404(Project, id=project_id) + context = { + "project_id": project_id, + } + return render(request, "projects/kitup_detail.html", context) From 82c97f0c0d67d4a19314b3054da8c3ab54e95241 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 16:24:22 +0900 Subject: [PATCH 069/380] =?UTF-8?q?feat:=20user=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=97=90=20=EA=B9=83=ED=97=88=EB=B8=8C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EB=94=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 3aa7232..3cc58ea 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -31,6 +31,14 @@ class User(AbstractUser): help_text="자기소개", ) + github_id = models.CharField( + max_length=39, + unique=True, + null=True, + blank=True, + help_text="GitHub 아이디", + ) + team_ban_count = models.PositiveSmallIntegerField( default=0, help_text="남은 팀플 참여 금지 횟수", From 6ccd8b8cefe7a1d9b0cf16b49eef22ae9c582ba1 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 16:25:53 +0900 Subject: [PATCH 070/380] chore: migration --- .../accounts/migrations/0004_user_github_id.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/accounts/migrations/0004_user_github_id.py diff --git a/apps/accounts/migrations/0004_user_github_id.py b/apps/accounts/migrations/0004_user_github_id.py new file mode 100644 index 0000000..37e238c --- /dev/null +++ b/apps/accounts/migrations/0004_user_github_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-02-01 07:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_user_team_ban_count_report'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='github_id', + field=models.CharField(blank=True, help_text='GitHub 아이디', max_length=39, null=True, unique=True), + ), + ] From 58ccdc3694ee577fd5c6e42e6cbee065a00ba57c Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 16:54:38 +0900 Subject: [PATCH 071/380] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B0=BE=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/settings.py b/config/settings.py index b9868ec..e01c47d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -112,6 +112,9 @@ # EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") # DEFAULT_FROM_EMAIL = "KITUP " +# 비밀번호 재설정 +ACCOUNT_PASSWORD_RESET_ON_CHANGE = False # 비밀번호 변경 시 재로그인 불필요 + LOGIN_URL = "/accounts/login/" LOGIN_REDIRECT_URL = "/" # 로그인 성공 후 LOGOUT_REDIRECT_URL = "/" # 로그아웃 후 From d13dbb69c935b82d0b4c75d61bbf0aac266202a0 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 19:18:41 +0900 Subject: [PATCH 072/380] =?UTF-8?q?feat:=20team=5Fapply=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/teams/views.py b/apps/teams/views.py index 7ab49fc..a57255b 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -6,6 +6,8 @@ from rest_framework.decorators import action from drf_spectacular.utils import extend_schema, extend_schema_view +from apps.accounts.models import UserRoleLevel + from .models import Team, TeamMember from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer @@ -16,9 +18,33 @@ @login_required def team_apply(request): - """팀 매칭 신청 페이지""" - # TODO: 팀 매칭 신청 로직 구현 - return render(request, "teams/team_apply.html") + """ + 팀 매칭 신청 페이지 + + - 유저의 역할별 레벨 정보를 함께 전달 + - 'teams/team_apply.html' 템플릿을 렌더링 + - 딕셔너리 형태로 역할 코드와 UserRoleLevel 객체 전달 + """ + user = request.user + + # 유저의 역할별 레벨 + role_levels = ( + UserRoleLevel.objects + .filter(user=user) + .select_related("role") + ) + + role_level_map = { + rl.role.code: rl + for rl in role_levels + } + + context = { + "user_obj": user, + "role_levels": role_level_map, + } + + return render(request, "teams/team_apply.html", context) @login_required From 3dd7403f545fdc70771e97a58da5089e2a4c0463 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sun, 1 Feb 2026 20:54:03 +0900 Subject: [PATCH 073/380] =?UTF-8?q?chore:=20settings.py=EC=9D=98=20Allowed?= =?UTF-8?q?=20host=20=EC=88=98=EC=A0=95=20->=20=EC=84=9C=EB=B2=84=EA=B4=80?= =?UTF-8?q?=EB=A0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index e01c47d..db8f1cb 100644 --- a/config/settings.py +++ b/config/settings.py @@ -18,7 +18,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['kitup.duckdns.org', '3.37.88.175', 'localhost', '127.0.0.1',] # Application definition From 89b1641981ec62d23a1f92e8c8978ebdf38957ec Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sun, 1 Feb 2026 20:58:18 +0900 Subject: [PATCH 074/380] =?UTF-8?q?refactor:=20docker-compose=20=EB=93=A4?= =?UTF-8?q?=EC=97=AC=EC=93=B0=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a7cd1ff..231740f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,13 @@ version: "3.8" services: - -db: - image: postgres:15-alpine - container_name: kitup_db - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + db: # services보다 두 칸 들여쓰기 + image: postgres:15-alpine + container_name: kitup_db + environment: + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data ports: @@ -19,7 +18,7 @@ db: timeout: 5s retries: 5 - web: + web: # services보다 두 칸 들여쓰기 image: bimvocado/kitup-web:latest container_name: kitup_web command: > @@ -50,7 +49,7 @@ db: redis: condition: service_started - redis: + redis: # services보다 두 칸 들여쓰기 image: redis:7-alpine container_name: kitup_redis ports: @@ -62,4 +61,4 @@ volumes: postgres_data: static_volume: media_volume: - redis_data: + redis_data: \ No newline at end of file From 46ccfff5f96555f704d8c113b602dc29c1d656ff Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sun, 1 Feb 2026 21:16:03 +0900 Subject: [PATCH 075/380] =?UTF-8?q?fix:=20crsf=20=EC=9D=B8=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/settings.py b/config/settings.py index db8f1cb..959d183 100644 --- a/config/settings.py +++ b/config/settings.py @@ -20,6 +20,11 @@ ALLOWED_HOSTS = ['kitup.duckdns.org', '3.37.88.175', 'localhost', '127.0.0.1',] +CSRF_TRUSTED_ORIGINS = [ + 'http://kitup.duckdns.org', + 'http://kitup.duckdns.org:8000', + 'https://kitup.duckdns.org', +] # Application definition From 7f1e1f48fed900fa8a9820660a084b8e9cbb7e6a Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 21:41:14 +0900 Subject: [PATCH 076/380] =?UTF-8?q?feat:=20=EC=97=B4=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EB=B0=8F=20=EC=A0=9C=EC=B6=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/apps/teams/views.py b/apps/teams/views.py index a57255b..fd09782 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -1,12 +1,15 @@ -from django.shortcuts import render, redirect +from django.http import HttpResponseBadRequest +from django.shortcuts import get_object_or_404, render, redirect from django.contrib.auth.decorators import login_required from rest_framework import viewsets +from django.utils import timezone from rest_framework.response import Response from rest_framework.decorators import action from drf_spectacular.utils import extend_schema, extend_schema_view -from apps.accounts.models import UserRoleLevel +from apps.accounts.models import Role, UserRoleLevel +from apps.projects.models import ProjectApplication from .models import Team, TeamMember from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer @@ -49,10 +52,40 @@ def team_apply(request): @login_required def passion_test(request): - """열정 테스트 페이지""" - # TODO: 열정 테스트 로직 구현 + """ + 열정 테스트 페이지 + + - 'teams/passion_test.html' 템플릿을 렌더링 + """ return render(request, "teams/passion_test.html") +@login_required +def passion_submit(request): + """ + 열정 테스트 결과 제출 처리 + + - POST 요청으로 열정 레벨(passion_level)을 전달받음 + - ProjectApplication 모델에 열정 레벨 저장 또는 업데이트 + - 제출 후 팀 매칭 결과 페이지로 리다이렉트 + """ + if request.method != "POST": + return HttpResponseBadRequest("잘못된 요청입니다.") + + passion_level = request.POST.get("passion_level") + + role_code = request.POST.get("role") + role = get_object_or_404(Role, code=role_code) + + ProjectApplication.objects.update_or_create( + user=request.user, + role=role, + defaults={ + "passion_level": int(passion_level), + }, + ) + + return redirect("teams:team_status") + @login_required def team_status(request): From 10270d1cf0b1c6a984adff3ab91e27b1112ec87b Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 23:18:55 +0900 Subject: [PATCH 077/380] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8D=B8=20season=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20projectApplication=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EB=B0=8F=20admin=EC=9D=B4=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EA=B0=84=20=EC=84=A4=EC=A0=95=20=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0005_user_passion_level.py | 19 ++++++ apps/accounts/models.py | 7 ++ apps/projects/admin.py | 27 +++++++- apps/projects/migrations/0002_season.py | 33 +++++++++ apps/projects/models.py | 68 +++++++++++++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 apps/accounts/migrations/0005_user_passion_level.py create mode 100644 apps/projects/migrations/0002_season.py diff --git a/apps/accounts/migrations/0005_user_passion_level.py b/apps/accounts/migrations/0005_user_passion_level.py new file mode 100644 index 0000000..197f699 --- /dev/null +++ b/apps/accounts/migrations/0005_user_passion_level.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.10 on 2026-02-01 14:08 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_user_github_id'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='passion_level', + field=models.SmallIntegerField(blank=True, help_text='열정 레벨 (1~4)', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4)]), + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 3cc58ea..9ccbbcc 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -39,6 +39,13 @@ class User(AbstractUser): help_text="GitHub 아이디", ) + passion_level = models.SmallIntegerField( + null=True, + blank=True, + validators=[MinValueValidator(1), MaxValueValidator(4)], + help_text="열정 레벨 (1~4)", + ) + team_ban_count = models.PositiveSmallIntegerField( default=0, help_text="남은 팀플 참여 금지 횟수", diff --git a/apps/projects/admin.py b/apps/projects/admin.py index f28d27e..e9427ca 100644 --- a/apps/projects/admin.py +++ b/apps/projects/admin.py @@ -1,6 +1,31 @@ from django.contrib import admin -from .models import Project, ProjectApplication +from .models import Season, Project, ProjectApplication + + +@admin.register(Season) +class SeasonAdmin(admin.ModelAdmin): + list_display = ["name", "status", "is_active", "matching_start", "matching_end", "project_start", "project_end"] + list_filter = ["status", "is_active", "created_at"] + search_fields = ["name"] + ordering = ["-created_at"] + actions = ["activate_season", "deactivate_season"] + + def activate_season(self, request, queryset): + """시즌 활성화 (이전 활성 시즌은 자동 비활성화)""" + # 모든 시즌 비활성화 + Season.objects.all().update(is_active=False) + # 선택된 시즌만 활성화 + queryset.update(is_active=True) + self.message_user(request, "시즌이 활성화되었습니다.") + + def deactivate_season(self, request, queryset): + """시즌 비활성화""" + queryset.update(is_active=False) + self.message_user(request, "시즌이 비활성화되었습니다.") + + activate_season.short_description = "✅ 선택된 시즌 활성화" + deactivate_season.short_description = "❌ 선택된 시즌 비활성화" @admin.register(Project) diff --git a/apps/projects/migrations/0002_season.py b/apps/projects/migrations/0002_season.py new file mode 100644 index 0000000..9528195 --- /dev/null +++ b/apps/projects/migrations/0002_season.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.10 on 2026-02-01 14:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Season', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='시즌명 (예: 2026년 1월 시즌)', max_length=100)), + ('status', models.CharField(choices=[('UPCOMING', '예정'), ('MATCHING', '팀매칭 중'), ('IN_PROJECT', '프로젝트 진행 중'), ('ENDED', '종료')], default='UPCOMING', max_length=20)), + ('matching_start', models.DateTimeField(help_text='팀매칭 시작')), + ('matching_end', models.DateTimeField(help_text='팀매칭 종료')), + ('project_start', models.DateTimeField(help_text='프로젝트 시작')), + ('project_end', models.DateTimeField(help_text='프로젝트 종료')), + ('is_active', models.BooleanField(default=False, help_text='현재 진행 중인 시즌')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'seasons', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['is_active'], name='seasons_is_acti_2e3a86_idx'), models.Index(fields=['status'], name='seasons_status_dc032e_idx')], + }, + ), + ] diff --git a/apps/projects/models.py b/apps/projects/models.py index 48188c0..ec7e7d5 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -1,6 +1,74 @@ from django.conf import settings from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator +from django.utils import timezone + + +class Season(models.Model): + """ + 시즌 관리 + - 관리자가 팀매칭 기간과 프로젝트 기간을 설정 + - 여러 시즌 동시 운영 가능 + """ + + class Status(models.TextChoices): + UPCOMING = "UPCOMING", "예정" + MATCHING = "MATCHING", "팀매칭 중" + IN_PROJECT = "IN_PROJECT", "프로젝트 진행 중" + ENDED = "ENDED", "종료" + + name = models.CharField( + max_length=100, + help_text="시즌명 (예: 2026년 1월 시즌)", + ) + + status = models.CharField( + max_length=20, + choices=Status.choices, + default=Status.UPCOMING, + ) + + # 팀매칭 기간 + matching_start = models.DateTimeField(help_text="팀매칭 시작") + matching_end = models.DateTimeField(help_text="팀매칭 종료") + + # 프로젝트 기간 + project_start = models.DateTimeField(help_text="프로젝트 시작") + project_end = models.DateTimeField(help_text="프로젝트 종료") + + is_active = models.BooleanField( + default=False, + help_text="현재 진행 중인 시즌", + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "seasons" + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["is_active"]), + models.Index(fields=["status"]), + ] + + def __str__(self) -> str: + return f"{self.name} ({self.status})" + + def is_matching_period(self) -> bool: + """팀매칭 기간인지 확인""" + now = timezone.now() + return self.matching_start <= now <= self.matching_end + + def is_project_period(self) -> bool: + """프로젝트 기간인지 확인""" + now = timezone.now() + return self.project_start <= now <= self.project_end + + @classmethod + def get_active_season(cls): + """현재 활성화된 시즌 반환""" + return cls.objects.filter(is_active=True).first() class Project(models.Model): From 5a20638b719919a26c512404c8d535308551a81b Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 1 Feb 2026 23:51:24 +0900 Subject: [PATCH 078/380] =?UTF-8?q?feat:=20team=5Fapply=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=98=EB=A6=AC,=20=EC=97=B4=EC=A0=95=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=20=EC=9C=A0=EC=A0=80=EC=97=90=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 48 ++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/apps/teams/views.py b/apps/teams/views.py index fd09782..06f219e 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -9,7 +9,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from apps.accounts.models import Role, UserRoleLevel -from apps.projects.models import ProjectApplication +from apps.projects.models import Season from .models import Team, TeamMember from .serializers import TeamSerializer, TeamCreateSerializer, TeamMemberSerializer @@ -24,11 +24,15 @@ def team_apply(request): """ 팀 매칭 신청 페이지 + - 활성화된 시즌 확인 + - 팀매칭 기간인지 확인 - 유저의 역할별 레벨 정보를 함께 전달 - 'teams/team_apply.html' 템플릿을 렌더링 - 딕셔너리 형태로 역할 코드와 UserRoleLevel 객체 전달 + - is_matching_period 플래그로 분기 처리 """ user = request.user + season = Season.get_active_season() # 유저의 역할별 레벨 role_levels = ( @@ -41,10 +45,15 @@ def team_apply(request): rl.role.code: rl for rl in role_levels } + + # 팀 매칭 기간 여부 + is_matching_period = season and season.is_matching_period() if season else False context = { "user_obj": user, "role_levels": role_level_map, + "season": season, + "is_matching_period": is_matching_period, } return render(request, "teams/team_apply.html", context) @@ -55,8 +64,13 @@ def passion_test(request): """ 열정 테스트 페이지 - - 'teams/passion_test.html' 템플릿을 렌더링 + - 열정 레벨이 이미 있으면 team_status로 리다이렉트 + - 없으면 'teams/passion_test.html' 템플릿을 렌더링 """ + if request.user.passion_level: + # 이미 열정 테스트 완료 + return redirect("teams:team_status") + return render(request, "teams/passion_test.html") @login_required @@ -65,7 +79,7 @@ def passion_submit(request): 열정 테스트 결과 제출 처리 - POST 요청으로 열정 레벨(passion_level)을 전달받음 - - ProjectApplication 모델에 열정 레벨 저장 또는 업데이트 + - User 모델에 열정 레벨 저장 - 제출 후 팀 매칭 결과 페이지로 리다이렉트 """ if request.method != "POST": @@ -73,25 +87,27 @@ def passion_submit(request): passion_level = request.POST.get("passion_level") - role_code = request.POST.get("role") - role = get_object_or_404(Role, code=role_code) - - ProjectApplication.objects.update_or_create( - user=request.user, - role=role, - defaults={ - "passion_level": int(passion_level), - }, - ) + request.user.passion_level = int(passion_level) + request.user.save(update_fields=["passion_level"]) return redirect("teams:team_status") @login_required def team_status(request): - """팀 매칭 결과/대기 페이지""" - # TODO: 팀 매칭 상태 로직 구현 - return render(request, "teams/team.html") + """ + 팀 매칭 결과/대기 페이지 + + - 팀 매칭 결과 표시 + - 시즌 정보 전달 + """ + season = Season.get_active_season() + + # TODO: 팀 조회 로직 + context = { + "season": season, + } + return render(request, "teams/team.html", context) # ================================ From 3e9f2004e2250fa54e7afa101524af3a3db95af7 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 2 Feb 2026 00:13:58 +0900 Subject: [PATCH 079/380] =?UTF-8?q?feat:=20team=5Fstatus=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/teams/views.py b/apps/teams/views.py index 06f219e..1bacb2a 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -98,14 +98,45 @@ def team_status(request): """ 팀 매칭 결과/대기 페이지 - - 팀 매칭 결과 표시 - - 시즌 정보 전달 + - 팀 매칭 기간: 매칭 대기 화면 + - 프로젝트 기간: 팀원 정보 화면 + - 'teams/team.html' 템플릿을 렌더링 + - is_matching_period 플래그로 분기 처리 """ season = Season.get_active_season() + team = None + team_members_data = [] + + if season: + # 현재 사용자의 팀 조회 + team = Team.objects.filter( + project__season=season, + members__user=request.user + ).prefetch_related( + 'members__user', + 'members__role' + ).distinct().first() + + # 프로젝트 기간에만 팀원 정보 수집 + if team and season.is_project_period(): + for member in team.members.all(): + # 해당 역할의 레벨 조회 + role_level = UserRoleLevel.objects.filter( + user=member.user, + role=member.role + ).first() + + team_members_data.append({ + 'user': member.user, + 'role': member.role, + 'level': role_level.level if role_level else None, + }) - # TODO: 팀 조회 로직 context = { "season": season, + "team": team, + "team_members": team_members_data, + "is_matching_period": season.is_matching_period() if season else False, } return render(request, "teams/team.html", context) From 715c0ca95b59129ed28a578e63312161d544d35c Mon Sep 17 00:00:00 2001 From: knana6 Date: Mon, 2 Feb 2026 12:36:49 +0900 Subject: [PATCH 080/380] fix: email verification css unify --- static/css/signup.css | 2 +- templates/account/verification_sent.html | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/static/css/signup.css b/static/css/signup.css index 178c583..4b4629f 100644 --- a/static/css/signup.css +++ b/static/css/signup.css @@ -23,7 +23,7 @@ body { background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - padding: 40px 30px; + padding: 48px 40px; } .title { diff --git a/templates/account/verification_sent.html b/templates/account/verification_sent.html index a820f96..65ee073 100644 --- a/templates/account/verification_sent.html +++ b/templates/account/verification_sent.html @@ -6,7 +6,7 @@ {% block content %}
-

📧 이메일 인증

+

이메일 인증

입력하신 이메일 주소로 인증 메일을 발송했습니다.

이메일을 확인하고 인증 링크를 클릭해주세요.

@@ -20,17 +20,27 @@

📧 이메일 인증

{% endblock %} From 4ddf9960b3c38c39c7171b72287ec38e1ae73545 Mon Sep 17 00:00:00 2001 From: knana6 Date: Mon, 2 Feb 2026 14:03:40 +0900 Subject: [PATCH 084/380] fix: signup padding2 --- static/css/signup.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/signup.css b/static/css/signup.css index 955bb73..9849643 100644 --- a/static/css/signup.css +++ b/static/css/signup.css @@ -22,7 +22,7 @@ body { .signup-card { background: white; border-radius: 16px; - padding: 48px 40px; + padding: 48px 40px !important; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); } From 35f3e0157018eaf3f2253382a50c052b7a832e28 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 2 Feb 2026 14:15:18 +0900 Subject: [PATCH 085/380] =?UTF-8?q?fix:=20signup.css=EA=B0=80=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EC=95=88=20=EB=90=98=EB=8D=98=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/signup.css | 54 +++++--- templates/account/signup.html | 239 ++++++++++++++-------------------- 2 files changed, 130 insertions(+), 163 deletions(-) diff --git a/static/css/signup.css b/static/css/signup.css index 9849643..01f7e49 100644 --- a/static/css/signup.css +++ b/static/css/signup.css @@ -22,7 +22,7 @@ body { .signup-card { background: white; border-radius: 16px; - padding: 48px 40px !important; + padding: 48px 40px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); } @@ -65,36 +65,23 @@ body { border-radius: 6px; font-size: 14px; outline: none; - transition: background-color 0.2s; } .input:focus { background-color: #e8e8e8; } -.input::placeholder { - color: #999; -} - .check-btn { padding: 12px 20px; background-color: #4a7cff; color: white; border: none; border-radius: 6px; - font-size: 14px; - font-weight: 500; cursor: pointer; - white-space: nowrap; - transition: background-color 0.2s; -} - -.check-btn:hover { - background-color: #3a6cef; } .submit-btn { - width: 100%; + margin-top: 10px; padding: 14px; background-color: #4a7cff; color: white; @@ -102,11 +89,38 @@ body { border-radius: 6px; font-size: 16px; font-weight: 600; - cursor: pointer; - margin-top: 10px; - transition: background-color 0.2s; } -.submit-btn:hover { - background-color: #3a6cef; +.message { + font-size: 12px; +} + +.message.success { + color: #22c55e; +} + +.message.error { + color: #ef4444; +} + +.error-messages .error { + color: #ef4444; + font-size: 14px; +} + +.social-login { + margin-top: 24px; + text-align: center; +} + +.social-icons { + display: flex; + justify-content: center; + gap: 16px; +} + +.social-icon img { + width: 40px; + height: 40px; + border-radius: 50%; } diff --git a/templates/account/signup.html b/templates/account/signup.html index 1f9d397..d6e0388 100644 --- a/templates/account/signup.html +++ b/templates/account/signup.html @@ -4,162 +4,115 @@ {% block title %}회원가입 - KITUP{% endblock %} {% block header %} - + {% endblock %} {% block content %} -
- -
- -

+ project_img +

KITUP 프로젝트

From 4566d59c52c69eedb2cc15c085f232f0079778f9 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 2 Feb 2026 21:48:10 +0900 Subject: [PATCH 092/380] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/config/settings.py b/config/settings.py index 959d183..01d76e4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -106,16 +106,14 @@ ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 # 인증 링크 유효기간 (일) ACCOUNT_EMAIL_SUBJECT_PREFIX = "[KITUP] " # 이메일 제목 접두사 -# 이메일 발송 설정 (개발용 - 콘솔 출력) -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" -# 배포 시 실제 SMTP 설정으로 변경 -# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -# EMAIL_HOST = "smtp.gmail.com" -# EMAIL_PORT = 587 -# EMAIL_USE_TLS = True -# EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") -# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") -# DEFAULT_FROM_EMAIL = "KITUP " +# 이메일 발송 설정 +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = os.getenv("EMAIL_HOST", "smtp.gmail.com") +EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587)) +EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "True") == "True" +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "KITUP") # 비밀번호 재설정 ACCOUNT_PASSWORD_RESET_ON_CHANGE = False # 비밀번호 변경 시 재로그인 불필요 @@ -170,7 +168,7 @@ "key": "", } ], - "SCOPE": ["user:email"], # 이메일 가져오려면 이거 필수급 + "SCOPE": ["user:email"], }, } From cf9b3a7a787a2ad5a92de2416bb77af3c0678bd2 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 2 Feb 2026 23:00:59 +0900 Subject: [PATCH 093/380] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 2 +- .../account/email/email_confirmation_message.txt | 11 +++++++++++ .../account/email/email_confirmation_subject.txt | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 templates/account/email/email_confirmation_message.txt create mode 100644 templates/account/email/email_confirmation_subject.txt diff --git a/config/settings.py b/config/settings.py index 01d76e4..4dd951d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -113,7 +113,7 @@ EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "True") == "True" EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") -DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "KITUP") +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", '"KITUP" ') # 비밀번호 재설정 ACCOUNT_PASSWORD_RESET_ON_CHANGE = False # 비밀번호 변경 시 재로그인 불필요 diff --git a/templates/account/email/email_confirmation_message.txt b/templates/account/email/email_confirmation_message.txt new file mode 100644 index 0000000..7cd3409 --- /dev/null +++ b/templates/account/email/email_confirmation_message.txt @@ -0,0 +1,11 @@ +안녕하세요, KITUP입니다. + +아래 링크를 클릭하시면 이메일 인증이 완료됩니다. + +{{ activate_url }} + +본인이 요청한 것이 아니라면 이 메일을 무시해주세요. + +감사합니다. + +KITUP 팀 드림 diff --git a/templates/account/email/email_confirmation_subject.txt b/templates/account/email/email_confirmation_subject.txt new file mode 100644 index 0000000..940c865 --- /dev/null +++ b/templates/account/email/email_confirmation_subject.txt @@ -0,0 +1 @@ +이메일 인증을 완료해주세요 \ No newline at end of file From dfd95d1c53dadd25b67f6adcbdb394f224957831 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 2 Feb 2026 23:48:03 +0900 Subject: [PATCH 094/380] =?UTF-8?q?feat:=20TechStack=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/admin.py | 15 +++++- .../0006_techstack_user_tech_stacks.py | 32 +++++++++++++ apps/accounts/models.py | 46 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 apps/accounts/migrations/0006_techstack_user_tech_stacks.py diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index 5ca41f9..8994dce 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from .models import User, Role, UserRoleLevel +from .models import User, Role, UserRoleLevel, TechStack class UserRoleLevelInline(admin.TabularInline): @@ -20,10 +20,21 @@ class UserAdmin(BaseUserAdmin): inlines = [UserRoleLevelInline] fieldsets = BaseUserAdmin.fieldsets + ( - ("프로필 정보", {"fields": ("nickname", "profile_image", "bio")}), + ("프로필 정보", {"fields": ("nickname", "profile_image", "bio", "tech_stacks")}), ) +@admin.register(TechStack) +class TechStackAdmin(admin.ModelAdmin): + list_display = ["id", "name", "category", "created_at"] + list_filter = ["category"] + search_fields = ["name"] + ordering = ["category", "name"] + fieldsets = [ + ("기본 정보", {"fields": ["name", "category"]}), + ] + + @admin.register(Role) class RoleAdmin(admin.ModelAdmin): list_display = ["id", "code", "name", "created_at"] diff --git a/apps/accounts/migrations/0006_techstack_user_tech_stacks.py b/apps/accounts/migrations/0006_techstack_user_tech_stacks.py new file mode 100644 index 0000000..01a530b --- /dev/null +++ b/apps/accounts/migrations/0006_techstack_user_tech_stacks.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.10 on 2026-02-02 14:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0005_user_passion_level'), + ] + + operations = [ + migrations.CreateModel( + name='TechStack', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='기술 이름 (Python, React 등)', max_length=50, unique=True)), + ('category', models.CharField(choices=[('LANGUAGE', '프로그래밍 언어'), ('FRONTEND', '프론트엔드'), ('BACKEND', '백엔드'), ('DATABASE', '데이터베이스'), ('TOOL', '개발 도구')], help_text='기술 카테고리', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'db_table': 'tech_stacks', + 'ordering': ['category', 'name'], + 'indexes': [models.Index(fields=['category'], name='tech_stacks_categor_f30f5c_idx')], + }, + ), + migrations.AddField( + model_name='user', + name='tech_stacks', + field=models.ManyToManyField(blank=True, help_text='사용자가 보유한 기술 스택', related_name='users', to='accounts.techstack'), + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 9ccbbcc..02abf05 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -3,6 +3,45 @@ from django.core.validators import MinValueValidator, MaxValueValidator +class TechStack(models.Model): + """ + 기술 스택 (마스터 데이터) + - 프로그래밍 언어, 프레임워크, 도구 등 + - 관리자만 추가/수정 가능 + """ + + class Category(models.TextChoices): + LANGUAGE = "LANGUAGE", "프로그래밍 언어" + FRONTEND = "FRONTEND", "프론트엔드" + BACKEND = "BACKEND", "백엔드" + DATABASE = "DATABASE", "데이터베이스" + TOOL = "TOOL", "개발 도구" + + name = models.CharField( + max_length=50, + unique=True, + help_text="기술 이름 (Python, React 등)", + ) + + category = models.CharField( + max_length=20, + choices=Category.choices, + help_text="기술 카테고리", + ) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "tech_stacks" + ordering = ["category", "name"] + indexes = [ + models.Index(fields=["category"]), + ] + + def __str__(self) -> str: + return f"{self.name} ({self.get_category_display()})" + + class User(AbstractUser): """ Custom User for StartLine.dev @@ -46,6 +85,13 @@ class User(AbstractUser): help_text="열정 레벨 (1~4)", ) + tech_stacks = models.ManyToManyField( + TechStack, + related_name="users", + blank=True, + help_text="사용자가 보유한 기술 스택", + ) + team_ban_count = models.PositiveSmallIntegerField( default=0, help_text="남은 팀플 참여 금지 횟수", From c389813f143ee94248dc662cf50ecdf546076d57 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 2 Feb 2026 23:56:03 +0900 Subject: [PATCH 095/380] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9,=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EA=B8=B0=EC=88=A0=20?= =?UTF-8?q?=EC=8A=A4=ED=83=9D=20=EA=B3=A0=EB=A5=BC=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EA=B2=8C=EB=81=94=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/forms.py | 87 +++++++++++++++++++++-- templates/account/onboarding_profile.html | 16 ++++- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 21825d1..6b6a655 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -1,15 +1,39 @@ from django import forms -from .models import User +from .models import User, TechStack class OnboardingForm(forms.ModelForm): + tech_stacks = forms.ModelMultipleChoiceField( + queryset=TechStack.objects.all().order_by("category", "name"), + widget=forms.CheckboxSelectMultiple, + required=False, + label="기술 스택 (선택)", + help_text="보유한 기술을 선택하세요", + ) + class Meta: model = User - fields = ["nickname", "profile_image", "bio"] + fields = ["nickname", "github_id", "profile_image", "tech_stacks"] widgets = { - "bio": forms.Textarea(attrs={"rows": 3}), + "nickname": forms.TextInput(attrs={ + "class": "form-control", + "placeholder": "닉네임을 입력하세요", + }), + "github_id": forms.TextInput(attrs={ + "class": "form-control", + "placeholder": "GitHub 아이디 (선택)", + }), + "profile_image": forms.FileInput(attrs={ + "class": "form-control", + "accept": "image/*", + }), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields["tech_stacks"].initial = self.instance.tech_stacks.all() + def clean_nickname(self): nick = (self.cleaned_data.get("nickname") or "").strip() if not nick: @@ -18,15 +42,56 @@ def clean_nickname(self): raise forms.ValidationError("이미 사용 중인 닉네임입니다.") return nick + def clean_github_id(self): + github_id = (self.cleaned_data.get("github_id") or "").strip() + if github_id and User.objects.filter(github_id=github_id).exclude(pk=self.instance.pk).exists(): + raise forms.ValidationError("이미 등록된 GitHub 아이디입니다.") + return github_id or None + + def save(self, commit=True): + user = super().save(commit) + if commit: + user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) + return user + class ProfileUpdateForm(forms.ModelForm): + tech_stacks = forms.ModelMultipleChoiceField( + queryset=TechStack.objects.all().order_by("category", "name"), + widget=forms.CheckboxSelectMultiple, + required=False, + label="기술 스택 (선택)", + help_text="보유한 기술을 선택하세요", + ) + class Meta: model = User - fields = ["nickname", "profile_image", "bio"] + fields = ["nickname", "github_id", "profile_image", "bio", "tech_stacks"] widgets = { - "bio": forms.Textarea(attrs={"rows": 3}), + "nickname": forms.TextInput(attrs={ + "class": "form-control", + "placeholder": "닉네임을 입력하세요", + }), + "github_id": forms.TextInput(attrs={ + "class": "form-control", + "placeholder": "GitHub 아이디 (선택)", + }), + "profile_image": forms.FileInput(attrs={ + "class": "form-control", + "accept": "image/*", + }), + "bio": forms.Textarea(attrs={ + "class": "form-control", + "rows": 3, + "placeholder": "자기소개를 입력하세요", + }), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields["tech_stacks"].initial = self.instance.tech_stacks.all() + def clean_nickname(self): nick = (self.cleaned_data.get("nickname") or "").strip() if not nick: @@ -34,3 +99,15 @@ def clean_nickname(self): if User.objects.filter(nickname=nick).exclude(pk=self.instance.pk).exists(): raise forms.ValidationError("이미 사용 중인 닉네임입니다.") return nick + + def clean_github_id(self): + github_id = (self.cleaned_data.get("github_id") or "").strip() + if github_id and User.objects.filter(github_id=github_id).exclude(pk=self.instance.pk).exists(): + raise forms.ValidationError("이미 등록된 GitHub 아이디입니다.") + return github_id or None + + def save(self, commit=True): + user = super().save(commit) + if commit: + user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) + return user diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html index 5c2a253..43a8083 100644 --- a/templates/account/onboarding_profile.html +++ b/templates/account/onboarding_profile.html @@ -8,12 +8,26 @@

{% if user.nickname %}프로필 수정{% else %}프로필 설정{% endif %}< {{ form.nickname.label_tag }} {{ form.nickname }}

+
+ {{ form.github_id.errors }} + {{ form.github_id.label_tag }} {{ form.github_id }} +
+
{{ form.profile_image.errors }} {{ form.profile_image.label_tag }} {{ form.profile_image }}
- From 10d8f42a95e86ea06df92fd0a04f6f3394b09c08 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 3 Feb 2026 00:33:06 +0900 Subject: [PATCH 096/380] =?UTF-8?q?fix=20:=20initial.html&css=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/main.css | 301 +++++++++++++++++++++++++++-------------- templates/initial.html | 63 ++++++++- 2 files changed, 255 insertions(+), 109 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index dfc20ec..6d4a12b 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,216 +1,311 @@ body { - font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; - margin: 0; padding: 0; + font-family: + "Pretendard Variable", + Pretendard, + -apple-system, + BlinkMacSystemFont, + system-ui, + Roboto, + "Helvetica Neue", + "Segoe UI", + "Apple SD Gothic Neo", + "Noto Sans KR", + "Malgun Gothic", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + sans-serif; + margin: 0; + padding: 0; } /* 메인 */ .main-container { - width: 85%; - margin: 0 auto; + width: 85%; + margin: 0 auto; } .main-container img { - width: 100%; - margin-top: 30px; + width: 100%; + margin-top: 30px; } /* 회고 */ .main-note { - width: 85%; - margin: 100px auto 0; + width: 85%; + margin: 100px auto 0; } .main-note .main-note-title { - width: 100%; - display: flex; - align-items: center; + width: 100%; + display: flex; + align-items: center; } .main-note .main-note-title img { - width: 7%; + width: 7%; } .main-note .main-note-title h3 { - font-size: 25px; + font-size: 25px; } .main-note .main-note-content { - margin-top: 25px; - width: 100%; min-height: 100px; - padding: 35px 45px; - background: #F9F9F9; - border-radius: 20px; + margin-top: 25px; + width: 100%; + min-height: 100px; + padding: 35px 45px; + background: #f9f9f9; + border-radius: 20px; } .main-note .main-note-content .login_content { - display: flex; - align-items: center; - gap: 20px; + display: flex; + align-items: center; + gap: 20px; } .main-note .main-note-content .login_content .m_title { - width: 20%; - font-weight: 600; + width: 20%; + font-weight: 600; } .main-note .main-note-content .login_content .m_content { - width: 70%; + width: 70%; } .main-note .main-note-content .login_content .m_date { - width: 10%; - font-size: 16px; - color: #999999; + width: 10%; + font-size: 16px; + color: #999999; } .main-note .main-note-content .unlogin_content { - font-size: 20px; font-weight: 600; - text-align: center; align-items: center; + font-size: 20px; + font-weight: 600; + text-align: center; + align-items: center; } /* 팀매칭 */ .main-team { - margin-top: 90px; - width: 100%; height: 500px; - padding: 90px 0; - background: #F6F8FF; - text-align: center; + margin-top: 90px; + width: 100%; + height: 500px; + padding: 90px 0; + background: #f6f8ff; + text-align: center; } .main-team h3 { - margin-bottom: 20px; - font-size: 35px; + margin-bottom: 20px; + font-size: 35px; } .main-team .m_team_stack { - width: 90%; - margin: 40px auto 0; - display: flex; - justify-content: space-between; + width: 90%; + margin: 40px auto 0; + display: flex; + justify-content: space-between; } /* WEB 기획 */ .main-team .m_team_stack .m_design { - padding: 25px 0; - width: 30%; height: 5%; - text-align: center; - background: #fff; - border: 3px solid #00B9B050; - border-radius: 40px; + padding: 25px 0; + width: 30%; + height: 5%; + text-align: center; + background: #fff; + border: 3px solid #00b9b050; + border-radius: 40px; } .main-team .m_team_stack .m_design h3 { - font-weight: 550; font-size: 25px; + font-weight: 600; + font-size: 25px; } .main-team .m_team_stack .m_design img { - width: 50px; height: 50px; + width: 50px; + height: 50px; } -.main-team .m_team_stack .m_design .m_nolevel { - margin-top: 5px; - color: #FF0202; - font-size: 16px; +.main-team .m_team_stack .m_nolevel { + margin-top: 5px; + color: #ff0202; + font-size: 16px; } .main-team .m_team_stack .m_design .m_design_btn { - margin-top: 15px; - padding: 10px 0; - width: 250px; height: 45px; - background: #00B9B0; color: #fff; - border: none; border-radius: 30px; - font-size: 18px; font-weight: 600; + margin-top: 15px; + padding: 10px 0; + width: 250px; + height: 45px; + background: #00b9b0; + color: #fff; + border: none; + border-radius: 30px; + font-size: 18px; + font-weight: 550; } /* WEB 프론트엔드 */ .main-team .m_team_stack .m_frontend { - padding: 25px 0; - width: 30%; height: 5%; - text-align: center; - background: #fff; - border: 3px solid #FFCE5350; - border-radius: 40px; + padding: 25px 0; + width: 30%; + height: 5%; + text-align: center; + background: #fff; + border: 3px solid #ffce5350; + border-radius: 40px; } .main-team .m_team_stack .m_frontend h3 { - font-weight: 550; font-size: 25px; + font-weight: 600; + font-size: 25px; } .main-team .m_team_stack .m_frontend img { - width: 50px; height: 50px; + width: 50px; + height: 50px; } .main-team .m_team_stack .m_frontend .m_level { - margin-top: 5px; - color: #000; - font-size: 16px; + margin-top: 5px; + color: #000; + font-size: 16px; } .main-team .m_team_stack .m_frontend .m_front_btn { - margin-top: 15px; - padding: 5px 0; - width: 250px; height: 45px; - background: #FFCE53; color: #fff; - border: none; border-radius: 30px; - font-size: 18px; font-weight: 600; + margin-top: 15px; + padding: 5px 0; + width: 250px; + height: 45px; + background: #ffce53; + color: #fff; + border: none; + border-radius: 30px; + font-size: 18px; + font-weight: 550; } /* WEB 백엔드 */ .main-team .m_team_stack .m_backend { - padding: 25px 0; - width: 30%; height: 5%; - text-align: center; - background: #fff; - border: 3px solid #FF3E8850; - border-radius: 40px; + padding: 25px 0; + width: 30%; + height: 5%; + text-align: center; + background: #fff; + border: 3px solid #ff3e8850; + border-radius: 40px; } .main-team .m_team_stack .m_backend h3 { - font-weight: 550; font-size: 25px; + font-weight: 600; + font-size: 25px; } .main-team .m_team_stack .m_backend img { - width: 50px; height: 50px; + width: 50px; + height: 50px; } .main-team .m_team_stack .m_backend .m_level { - margin-top: 5px; - color: #000; - font-size: 16px; + margin-top: 5px; + color: #000; + font-size: 16px; } .main-team .m_team_stack .m_backend .m_back_btn { - margin-top: 15px; - padding: 10px 0; - width: 250px; height: 45px; - background: #FF3E88; color: #fff; - border: none; border-radius: 30px; - font-size: 18px; font-weight: 600; + margin-top: 15px; + padding: 10px 0; + width: 250px; + height: 45px; + background: #ff3e88; + color: #fff; + border: none; + border-radius: 30px; + font-size: 18px; + font-weight: 550; } /* KITUP 프로젝트 */ .main-project { - width: 85%; - margin: 0 auto; + width: 85%; + margin: 0 auto; } .main-project .m_project_title { - margin-top: 100px; - display: flex; - align-items: center; + margin-top: 100px; + display: flex; + align-items: center; } .main-project .m_project_title img { - width: 7%; + width: 7%; } .main-project .m_project_title h3 { - font-size: 25px; + font-size: 25px; + padding: 12px 4px 0; +} + +.main-project .m_project_content { + margin-top: 25px; + width: 100%; + min-height: 100px; +} + +.main-project .m_project_content .m_noproject { + font-size: 20px; + font-weight: 600; + text-align: center; + align-items: center; + padding: 35px 45px; + background: #f9f9f9; + border-radius: 20px; + text-align: center; +} + +.main-project .m_project_content .m_project { + display: flex; + gap: 30px; + width: 100%; + overflow-x: auto; + padding-bottom: 10px; + -webkit-overflow-scrolling: touch; +} + +.main-project .m_project_content .m_project > div { + flex: 0 0 300px; + height: 300px; + padding: 20px; + background: #f9f9f9; + border-radius: 20px; +} + +.main-project .m_project_content .m_project div h1 { + height: 65%; + background: #666; + color: #fff; + font-size: 30px; + border-radius: 15px; + padding: 45px 15px; +} + +.main-project .m_project_content .m_project div h3 { + font-size: 22px; font-weight: 600; + padding: 10px 0; +} + +.main-project .m_project_content .m_project div p { + font-size: 16px; } /* 푸터 */ footer { - background: #1D294B; - margin-top: 245px; - width: 100%; height: 150px; -} \ No newline at end of file + background: #1d294b; + margin-top: 200px; + width: 100%; + height: 150px; +} diff --git a/templates/initial.html b/templates/initial.html index f55d9a8..89b609c 100644 --- a/templates/initial.html +++ b/templates/initial.html @@ -37,6 +37,7 @@

팀 매칭 모집이 시작됐어요

WEB 기획

+ {% if user.is_authenticated %} nolevel

아직 레벨 진단이 완료되지 않았어요!

@@ -56,9 +57,17 @@

WEB 기획

+ {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %}

WEB 프론트엔드

+ {% if user.is_authenticated %} + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %}

WEB 백엔드

+ {% if user.is_authenticated %} - Level4 -

Lv4

- + Level4 +

Lv4

+ + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %}
@@ -113,10 +137,37 @@

WEB 백엔드

KITUP 프로젝트

- -
- + + + +
+
+

서비스이미지

+

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+

서비스이미지

+

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+

서비스이미지

+

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+

서비스이미지

+

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+

서비스이미지

+

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
{% endblock %} From 149e057b6a6fd476cb2e1022ed3f2a3f7c9c4221 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 3 Feb 2026 01:10:37 +0900 Subject: [PATCH 097/380] =?UTF-8?q?initial.html&css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/main.css | 10 +++------- static/images/example_img.png | Bin 0 -> 217269 bytes templates/initial.html | 10 +++++----- 3 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 static/images/example_img.png diff --git a/static/css/main.css b/static/css/main.css index 6d4a12b..e118af1 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -280,17 +280,13 @@ body { flex: 0 0 300px; height: 300px; padding: 20px; - background: #f9f9f9; + background: #EAF0FF; border-radius: 20px; } -.main-project .m_project_content .m_project div h1 { - height: 65%; - background: #666; - color: #fff; - font-size: 30px; +.main-project .m_project_content .m_project div img { border-radius: 15px; - padding: 45px 15px; + height: 65%; } .main-project .m_project_content .m_project div h3 { diff --git a/static/images/example_img.png b/static/images/example_img.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3fd99aa124d35fba398b0c74440c0f78063c44 GIT binary patch literal 217269 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

_y`!wISJ%G%|!N${xUzncU(vj`TCX?)0 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�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 프로젝트

-

서비스이미지

+ 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

-

서비스이미지

+ 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

-

서비스이미지

+ 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

-

서비스이미지

+ 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

-

서비스이미지

+ 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

From 8047530ba8cc4e406763cc7f40d2fca740f3d5b9 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 3 Feb 2026 02:26:18 +0900 Subject: [PATCH 098/380] =?UTF-8?q?fix=20:=20initial.html&css=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/main.css | 11 ++++- templates/account/onboarding_profile.html | 4 ++ templates/initial.html | 52 ++++++++++++++++++----- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index e118af1..53bfb16 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -87,6 +87,15 @@ body { align-items: center; } +.m_note_btn { + display: block; + width: 20%; + margin: 30px auto; + background: #1D294B; color: #fff; + font-weight: 500; font-size: 16px; + border-radius: 20px; padding: 7px 30px; +} + /* 팀매칭 */ .main-team { margin-top: 90px; @@ -113,7 +122,7 @@ body { .main-team .m_team_stack .m_design { padding: 25px 0; width: 30%; - height: 5%; + height: 10%; text-align: center; background: #fff; border: 3px solid #00b9b050; diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html index 43a8083..24ae8d8 100644 --- a/templates/account/onboarding_profile.html +++ b/templates/account/onboarding_profile.html @@ -1,3 +1,7 @@ +{% extends 'base.html' %} {% load static %} {% block header %} + +{% endblock %} {% block content %} +

{% if user.nickname %}프로필 수정{% else %}프로필 설정{% endif %}

diff --git a/templates/initial.html b/templates/initial.html index 2e83090..87dfdf3 100644 --- a/templates/initial.html +++ b/templates/initial.html @@ -20,6 +20,11 @@

오늘의 작업을 기록해보세요.

로그인 후 이용해보세요.

{% endif %}
+ {% if user.is_authenticated %} + + {% else %} {% endif %}
@@ -59,8 +64,12 @@

WEB 기획

--> {% else %} nolevel -

로그인 후 레벨을 확인해보세요.

- {% endif %} @@ -92,7 +101,11 @@

WEB 프론트엔드

{% else %} nolevel

로그인 후 레벨을 확인해보세요.

- {% endif %} @@ -124,7 +137,11 @@

WEB 백엔드

{% else %} nolevel

로그인 후 레벨을 확인해보세요.

- {% endif %} @@ -143,27 +160,42 @@

KITUP 프로젝트

- 예시이미지 + 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

- 예시이미지 + 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

- 예시이미지 + 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

- 예시이미지 + 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

-
- 예시이미지 +
+ 예시이미지

서비스명

서비스명은 이러이러한 기능을 하는 플랫폼이다

From 3b4122025a970c46149449d296b3bb730929f6a9 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 3 Feb 2026 02:27:10 +0900 Subject: [PATCH 099/380] feat: onboarding_profile.css --- static/css/onboarding_profile.css | 353 ++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 static/css/onboarding_profile.css diff --git a/static/css/onboarding_profile.css b/static/css/onboarding_profile.css new file mode 100644 index 0000000..d54d7a0 --- /dev/null +++ b/static/css/onboarding_profile.css @@ -0,0 +1,353 @@ +/* Onboarding Profile CSS - Works with existing HTML structure */ + +/* CSS Variables */ +:root { + --primary-blue: #4169E1; + --primary-hover: #365AC3; + --light-blue: #C5D7F5; + --text-primary: #212529; + --text-secondary: #6C757D; + --border-color: #E5E8EB; + --background-gray: #F8F9FA; + --white: #FFFFFF; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +/* Page Title Styling */ +h1 { + text-align: center; + font-size: 24px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 40px; + letter-spacing: -0.5px; +} + +/* Form Container - Creates the white card */ +form { + max-width: 600px; + margin: 0 auto; + background-color: var(--white); + border-radius: 20px; + padding: 48px 40px; + box-shadow: var(--shadow-md); + border: 1px solid var(--border-color); +} + +/* All form field containers */ +form > div { + margin-bottom: 32px; +} + +form > div:last-of-type { + margin-bottom: 40px; +} + +/* First div - Profile Image Section */ +form > div:first-child { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 40px; + position: relative; +} + +/* Hide the default label and error for profile image */ +form > div:first-child label { + display: none; +} + +/* Profile Image Styling */ +form > div:first-child input[type="file"] { + position: absolute; + width: 120px; + height: 120px; + opacity: 0; + cursor: pointer; + z-index: 10; + border-radius: 50%; +} + +/* Create the circular profile preview using ::before pseudo-element */ +form > div:first-child::before { + content: ''; + display: block; + width: 120px; + height: 120px; + border-radius: 50%; + background: linear-gradient(135deg, var(--light-blue) 0%, #B3CDFC 100%); + box-shadow: var(--shadow-md); + margin-bottom: 24px; + position: relative; +} + +/* User icon using ::after pseudo-element */ +form > div:first-child::after { + content: ''; + position: absolute; + top: 35px; + left: 50%; + transform: translateX(-50%); + width: 50px; + height: 50px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='rgba(255,255,255,0.7)' stroke-width='1.5'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + pointer-events: none; + z-index: 5; +} + +/* Camera icon button overlay */ +form > div:first-child input[type="file"]::before { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 36px; + height: 36px; + border-radius: 50%; + background-color: var(--white); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236C757D' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z M15 13a3 3 0 11-6 0 3 3 0 016 0z'/%3E%3C/svg%3E"); + background-size: 18px; + background-position: center; + background-repeat: no-repeat; + border: 3px solid var(--white); + box-shadow: var(--shadow-md); + cursor: pointer; + transition: transform 0.2s; + z-index: 15; +} + +form > div:first-child input[type="file"]:hover::before { + transform: scale(1.1); +} + +/* Second div - Nickname field with button */ +form > div:nth-child(2) { + position: relative; +} + +/* Nickname input styling */ +form > div:nth-child(2) input[type="text"] { + width: 100%; + padding: 12px 80px 12px 16px; + border: 1px solid var(--border-color); + border-radius: 10px; + font-size: 15px; + background-color: var(--background-gray); + transition: all 0.2s; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; +} + +form > div:nth-child(2) input[type="text"]:focus { + outline: none; + border-color: var(--primary-blue); + background-color: var(--white); + box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.1); +} + +/* Create "확인" button using ::after on the second div */ +form > div:nth-child(2)::after { + content: '확인'; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + margin-top: 14px; /* Adjust for label height */ + background-color: var(--primary-blue); + color: var(--white); + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + pointer-events: auto; +} + +form > div:nth-child(2)::after:hover { + background-color: var(--primary-hover); +} + +/* Labels */ +label { + display: block; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +/* General Input Fields (GitHub ID) */ +input[type="text"] { + width: 100%; + padding: 12px 16px; + border: 1px solid var(--border-color); + border-radius: 10px; + font-size: 15px; + background-color: var(--background-gray); + transition: all 0.2s; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; +} + +input[type="text"]:focus { + outline: none; + border-color: var(--primary-blue); + background-color: var(--white); + box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.1); +} + +/* Tech Stacks Container */ +form > div:last-of-type > div { + max-height: 300px !important; + overflow-y: auto !important; + border: 1px solid var(--border-color) !important; + padding: 10px !important; + border-radius: 10px !important; + background-color: var(--background-gray) !important; + margin-top: 8px; +} + +form > div:last-of-type > div::-webkit-scrollbar { + width: 8px; +} + +form > div:last-of-type > div::-webkit-scrollbar-track { + background: transparent; +} + +form > div:last-of-type > div::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +form > div:last-of-type > div::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* Tech stacks select/checkboxes inside the container */ +form > div:last-of-type select, +form > div:last-of-type input[type="checkbox"] { + margin: 4px 0; +} + +/* Help text for tech stacks */ +form > div:last-of-type small { + display: block; + margin-top: 8px; + font-size: 13px; + color: var(--text-secondary); +} + +/* Error Messages */ +.errorlist { + list-style: none; + padding: 0; + margin: 0 0 8px 0; +} + +.errorlist li { + color: #DC3545; + font-size: 13px; + padding: 6px 12px; + background-color: #FFE5E8; + border-radius: 6px; + margin-bottom: 4px; +} + +/* Submit Button */ +button[type="submit"] { + width: 100%; + padding: 14px 24px; + background-color: var(--primary-blue); + color: var(--white); + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + box-shadow: var(--shadow-sm); + font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; + margin-top: 0; +} + +button[type="submit"]:hover { + background-color: var(--primary-hover); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +button[type="submit"]:active { + transform: translateY(0); +} + +/* Animation */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +form { + animation: fadeIn 0.4s ease-out; +} + +/* Responsive Design */ +@media (max-width: 768px) { + form { + padding: 32px 24px; + border-radius: 16px; + } + + h1 { + font-size: 20px; + margin-bottom: 32px; + } + + form > div:first-child::before { + width: 100px; + height: 100px; + } + + form > div:first-child input[type="file"] { + width: 100px; + height: 100px; + } + + form > div:first-child::after { + width: 40px; + height: 40px; + top: 30px; + } + + form > div:nth-child(2) input[type="text"] { + padding-right: 16px; + } + + form > div:nth-child(2)::after { + position: relative; + display: block; + width: 100%; + margin-top: 8px; + text-align: center; + right: auto; + top: auto; + transform: none; + } +} + +@media (max-width: 480px) { + form { + padding: 24px 20px; + } +} \ No newline at end of file From 749de0fa3b04bdb415786536fbbc05559d9bbeb6 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 3 Feb 2026 03:10:29 +0900 Subject: [PATCH 100/380] =?UTF-8?q?fix:=20onboarding=5Fprofile.css=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/onboarding_profile.css | 133 ++++++++++++++++------ templates/account/onboarding_profile.html | 1 + 2 files changed, 101 insertions(+), 33 deletions(-) diff --git a/static/css/onboarding_profile.css b/static/css/onboarding_profile.css index d54d7a0..c5e4324 100644 --- a/static/css/onboarding_profile.css +++ b/static/css/onboarding_profile.css @@ -1,4 +1,4 @@ -/* Onboarding Profile CSS - Works with existing HTML structure */ +/* Onboarding Profile CSS - With Styled Tech Stacks */ /* CSS Variables */ :root { @@ -54,23 +54,17 @@ form > div:first-child { position: relative; } -/* Hide the default label and error for profile image */ +/* Hide the default label for profile image */ form > div:first-child label { display: none; } -/* Profile Image Styling */ +/* Hide the actual file input */ form > div:first-child input[type="file"] { - position: absolute; - width: 120px; - height: 120px; - opacity: 0; - cursor: pointer; - z-index: 10; - border-radius: 50%; + display: none; } -/* Create the circular profile preview using ::before pseudo-element */ +/* Profile Image Preview Container */ form > div:first-child::before { content: ''; display: block; @@ -79,11 +73,18 @@ form > div:first-child::before { border-radius: 50%; background: linear-gradient(135deg, var(--light-blue) 0%, #B3CDFC 100%); box-shadow: var(--shadow-md); - margin-bottom: 24px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; position: relative; + z-index: 1; } -/* User icon using ::after pseudo-element */ +form > div:first-child:hover::before { + transform: scale(1.02); + box-shadow: var(--shadow-lg); +} + +/* User icon - default state */ form > div:first-child::after { content: ''; position: absolute; @@ -96,15 +97,20 @@ form > div:first-child::after { background-size: contain; background-repeat: no-repeat; pointer-events: none; - z-index: 5; + z-index: 2; } -/* Camera icon button overlay */ -form > div:first-child input[type="file"]::before { +/* Camera icon button - positioned at bottom right */ +form > div:first-child { + padding-bottom: 20px; +} + +form > div:first-child label[for]:not([style*="display: none"])::after, +form > div:first-child > *:last-child::after { content: ''; position: absolute; - bottom: 0; - right: 0; + bottom: 20px; + right: calc(50% - 60px + 6px); width: 36px; height: 36px; border-radius: 50%; @@ -117,11 +123,24 @@ form > div:first-child input[type="file"]::before { box-shadow: var(--shadow-md); cursor: pointer; transition: transform 0.2s; - z-index: 15; + z-index: 3; + pointer-events: auto; +} + +/* Make the entire profile image area clickable */ +form > div:first-child { + cursor: pointer; } -form > div:first-child input[type="file"]:hover::before { - transform: scale(1.1); +/* Add a class for when image is loaded */ +form > div:first-child.has-image::after { + display: none; +} + +form > div:first-child.has-image::before { + background-size: cover; + background-position: center; + background-repeat: no-repeat; } /* Second div - Nickname field with button */ @@ -156,7 +175,7 @@ form > div:nth-child(2)::after { right: 4px; top: 50%; transform: translateY(-50%); - margin-top: 14px; /* Adjust for label height */ + margin-top: 14px; background-color: var(--primary-blue); color: var(--white); padding: 10px 20px; @@ -201,15 +220,20 @@ input[type="text"]:focus { box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.1); } -/* Tech Stacks Container */ +/* Tech Stacks Container - NEW STYLING */ form > div:last-of-type > div { max-height: 300px !important; overflow-y: auto !important; border: 1px solid var(--border-color) !important; - padding: 10px !important; + padding: 16px !important; border-radius: 10px !important; - background-color: var(--background-gray) !important; + background-color: var(--white) !important; margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-start; + align-content: flex-start; } form > div:last-of-type > div::-webkit-scrollbar { @@ -229,10 +253,53 @@ form > div:last-of-type > div::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); } -/* Tech stacks select/checkboxes inside the container */ -form > div:last-of-type select, +/* Hide default select/checkbox display */ +form > div:last-of-type select { + display: none; +} + +/* Style checkboxes as chips/tags */ form > div:last-of-type input[type="checkbox"] { - margin: 4px 0; + display: none; +} + +/* Checkbox labels styled as chips */ +form > div:last-of-type label { + display: inline-flex; + align-items: center; + padding: 8px 16px; + background-color: var(--background-gray); + border: 2px solid var(--border-color); + border-radius: 20px; + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; + margin: 0; + user-select: none; +} + +form > div:last-of-type label:hover { + background-color: var(--light-blue); + border-color: var(--primary-blue); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +/* Checked state */ +form > div:last-of-type input[type="checkbox"]:checked + label { + background-color: var(--primary-blue); + border-color: var(--primary-blue); + color: var(--white); + font-weight: 600; +} + +/* Add checkmark icon to checked items */ +form > div:last-of-type input[type="checkbox"]:checked + label::before { + content: '✓'; + margin-right: 6px; + font-weight: bold; } /* Help text for tech stacks */ @@ -319,11 +386,6 @@ form { height: 100px; } - form > div:first-child input[type="file"] { - width: 100px; - height: 100px; - } - form > div:first-child::after { width: 40px; height: 40px; @@ -344,6 +406,11 @@ form { top: auto; transform: none; } + + form > div:last-of-type label { + font-size: 13px; + padding: 6px 12px; + } } @media (max-width: 480px) { diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html index 24ae8d8..9c31a59 100644 --- a/templates/account/onboarding_profile.html +++ b/templates/account/onboarding_profile.html @@ -35,3 +35,4 @@

{% if user.nickname %}프로필 수정{% else %}프로필 설정{% endif %}< {% if user.nickname %}수정{% else %}저장{% endif %} +{% endblock %} \ No newline at end of file From 13eb46807a9cdd24141f9a41a043b6b23bbd7ec3 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 15:57:11 +0900 Subject: [PATCH 101/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EB=AA=85=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20form=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/forms.py | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 apps/projects/forms.py diff --git a/apps/projects/forms.py b/apps/projects/forms.py new file mode 100644 index 0000000..864de7d --- /dev/null +++ b/apps/projects/forms.py @@ -0,0 +1,107 @@ +from django import forms +from .models import Project + + +class ProjectDashboardEditForm(forms.ModelForm): + """ + 프로젝트 대시보드 수정 폼 + 팀원이 수정 가능한 필드만 포함 + """ + + class Meta: + model = Project + fields = [ + "title", # 서비스명 + "description", # 서비스 소개 + "project_image", # 프로젝트 프로필 사진 + "team_rules", # 팀 규칙 + "related_links", # 관련 링크 + "is_favorite", # 즐겨찾기 + ] + widgets = { + "title": forms.TextInput(attrs={ + "class": "form-control", + "placeholder": "서비스명을 입력하세요", + "maxlength": "120", + }), + "description": forms.Textarea(attrs={ + "class": "form-control", + "placeholder": "서비스에 대해 설명해주세요", + "rows": 4, + }), + "project_image": forms.FileInput(attrs={ + "class": "form-control", + "accept": "image/*", + }), + "team_rules": forms.Textarea(attrs={ + "class": "form-control", + "placeholder": "팀 규칙을 마크다운 형식으로 작성해주세요\n\n예:\n# 회의 규칙\n- 주 1회 수요일 19시\n- 지각 3회 = 경고\n\n# 코드 리뷰\n- PR 생성 후 2시간 내 리뷰\n- 최소 2명 승인 필수", + "rows": 6, + }), + "is_favorite": forms.CheckboxInput(attrs={ + "class": "form-check-input", + }), + } + + def clean_title(self): + """서비스명 유효성 검사""" + title = self.cleaned_data.get("title", "").strip() + if not title: + raise forms.ValidationError("서비스명은 필수입니다.") + return title + + +class ProjectRelatedLinksForm(forms.Form): + """ + 관련 링크 별도 폼 (AJAX 업데이트용) + """ + + notion_url = forms.URLField( + required=False, + label="Notion", + widget=forms.URLInput(attrs={ + "class": "form-control", + "placeholder": "Notion 링크를 입력하세요", + }), + ) + + figma_url = forms.URLField( + required=False, + label="Figma", + widget=forms.URLInput(attrs={ + "class": "form-control", + "placeholder": "Figma 링크를 입력하세요", + }), + ) + + github_url = forms.URLField( + required=False, + label="GitHub", + widget=forms.URLInput(attrs={ + "class": "form-control", + "placeholder": "GitHub 링크를 입력하세요", + }), + ) + + def clean(self): + """링크가 1개 이상 입력되는지 확인""" + cleaned_data = super().clean() + has_link = any([ + cleaned_data.get("notion_url"), + cleaned_data.get("figma_url"), + cleaned_data.get("github_url"), + ]) + if not has_link: + raise forms.ValidationError("최소 1개 이상의 링크를 입력해주세요.") + return cleaned_data + + def to_dict(self): + """폼 데이터를 딕셔너리로 변환 (JSONField용)""" + if not self.is_valid(): + return {} + + return { + "notion": self.cleaned_data.get("notion_url") or None, + "figma": self.cleaned_data.get("figma_url") or None, + "github": self.cleaned_data.get("github_url") or None, + } From 2fef57d1e1ef396ef7c4a46bd583fc14514c3300 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 15:57:36 +0900 Subject: [PATCH 102/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EB=AA=85=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A8=EB=8D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/models.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/projects/models.py b/apps/projects/models.py index ec7e7d5..c4923bd 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -143,6 +143,31 @@ class Status(models.TextChoices): help_text="활동 지역 (오프라인 시)", ) + # 대시보드에서 팀원이 수정 가능한 필드 + project_image = models.ImageField( + upload_to="projects/", + null=True, + blank=True, + help_text="프로젝트 프로필 사진", + ) + + team_rules = models.TextField( + null=True, + blank=True, + help_text="팀 규칙 (마크다운)", + ) + + related_links = models.JSONField( + default=dict, + blank=True, + help_text="관련 링크 (Notion, Figma, GitHub 등)", + ) + + is_favorite = models.BooleanField( + default=False, + help_text="즐겨찾기 여부", + ) + current_stage = models.ForeignKey( "guides.GuideStage", on_delete=models.SET_NULL, From 801926289d7ef513c11d3ef9be893939b791a6e7 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 16:49:23 +0900 Subject: [PATCH 103/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...favorite_project_project_image_and_more.py | 33 +++++ apps/projects/urls.py | 4 +- apps/projects/views.py | 137 ++++++++++++++++-- 3 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 apps/projects/migrations/0003_project_is_favorite_project_project_image_and_more.py diff --git a/apps/projects/migrations/0003_project_is_favorite_project_project_image_and_more.py b/apps/projects/migrations/0003_project_is_favorite_project_project_image_and_more.py new file mode 100644 index 0000000..f41a727 --- /dev/null +++ b/apps/projects/migrations/0003_project_is_favorite_project_project_image_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.10 on 2026-02-03 07:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0002_season'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='is_favorite', + field=models.BooleanField(default=False, help_text='즐겨찾기 여부'), + ), + migrations.AddField( + model_name='project', + name='project_image', + field=models.ImageField(blank=True, help_text='프로젝트 프로필 사진', null=True, upload_to='projects/'), + ), + migrations.AddField( + model_name='project', + name='related_links', + field=models.JSONField(blank=True, default=dict, help_text='관련 링크 (Notion, Figma, GitHub 등)'), + ), + migrations.AddField( + model_name='project', + name='team_rules', + field=models.TextField(blank=True, help_text='팀 규칙 (마크다운)', null=True), + ), + ] diff --git a/apps/projects/urls.py b/apps/projects/urls.py index d70035c..378341c 100644 --- a/apps/projects/urls.py +++ b/apps/projects/urls.py @@ -6,8 +6,8 @@ urlpatterns = [ # 프로젝트 대시보드 (현재 프로젝트) path("dashboard/", views.dashboard, name="dashboard"), # dashboard.html - path("dashboard//", views.dashboard_detail, name="dashboard_detail"), # dashboard.html (특정 프로젝트) - path("dashboard//edit/", views.dashboard_update, name="dashboard_update"), # dashboard_update.html + path("dashboard//", views.dashboard_detail, name="dashboard_detail"), # 대시보드 조회 + path("dashboard//edit/", views.dashboard_edit, name="dashboard_edit"), # 대시보드 수정 # 지난 프로젝트 path("", views.project_list, name="project_list"), # project_list.html diff --git a/apps/projects/views.py b/apps/projects/views.py index 257f00d..d0b925a 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -3,10 +3,12 @@ from django.contrib import messages from django.http import JsonResponse from django.core.exceptions import ValidationError -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_POST, require_http_methods -from apps.projects.models import Season +from apps.projects.models import Season, Project +from apps.projects.forms import ProjectDashboardEditForm, ProjectRelatedLinksForm from apps.projects.services import TeamMatchingService +from apps.teams.models import Team, TeamMember @login_required @@ -17,30 +19,135 @@ def dashboard(request): @login_required +@require_http_methods(["GET"]) def dashboard_detail(request, project_id): - """나의 현재 프로젝트 대시보드""" - # TODO: 단 하나뿐 나의 프로젝트 대시보드 - # project = get_object_or_404(Project, id=project_id) + """ + 프로젝트 대시보드 조회 (읽기 전용) + + 읽기 전용 정보: + - 진행기간 (starts_at, ends_at) + - 팀 정보 (team composition) + - 진척도 (guide_stage progress) + """ + project = get_object_or_404(Project, id=project_id) + + # 팀원 확인 + is_team_member = TeamMember.objects.filter( + team__project=project, + user=request.user, + is_active=True + ).exists() + + if not is_team_member: + messages.error(request, "팀원만 접근할 수 있습니다.") + return redirect("projects:dashboard") + + # 팀 정보 + team = project.team + members = team.members.filter(is_active=True).select_related("user", "role") + member_count_by_role = team.get_member_count_by_role() + + # 시즌 정보 + season = None + active_season = Season.get_active_season() + if active_season: + if active_season.project_start <= project.created_at <= active_season.project_end: + season = active_season + + # 가이드 진척도 계산 + guide_progress = None + if project.current_stage: + from apps.guides.models import GuideTask, GuideTaskProgress + + total_tasks = GuideTask.objects.filter( + card__stage=project.current_stage + ).count() + + completed_tasks = GuideTaskProgress.objects.filter( + task__card__stage=project.current_stage, + project=project, + user=request.user, + is_completed=True + ).count() + + progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) + + guide_progress = { + 'stage': project.current_stage, + 'total_tasks': total_tasks, + 'completed_tasks': completed_tasks, + 'progress_percent': progress_percent, + } + context = { - "project_id": project_id, + "project": project, + "team": team, + "members": members, + "member_count_by_role": member_count_by_role, + "season": season, + "guide_progress": guide_progress, + "is_team_member": is_team_member, } + return render(request, "projects/dashboard.html", context) @login_required -def dashboard_update(request, project_id): - """나의 현재 프로젝트 대시보드 수정""" - # TODO: 프로젝트 정보 수정 로직 - # project = get_object_or_404(Project, id=project_id, created_by=request.user) +@require_http_methods(["GET", "POST"]) +def dashboard_edit(request, project_id): + """ + 프로젝트 대시보드 수정 (팀원만) + + 수정 가능 필드: + - 서비스명 (title) + - 서비스 소개 (description) + - 프로필 사진 (project_image) + - 팀 규칙 (team_rules) + - 관련 링크 (related_links) + - 즐겨찾기 (is_favorite) + """ + project = get_object_or_404(Project, id=project_id) + + # 팀원 권한 확인 + is_team_member = TeamMember.objects.filter( + team__project=project, + user=request.user, + is_active=True + ).exists() + + if not is_team_member: + messages.error(request, "팀원만 수정할 수 있습니다.") + return redirect("projects:dashboard_detail", project_id=project_id) + if request.method == "POST": - # 정보 업데이트 - messages.success(request, "프로젝트가 업데이트되었습니다.") - return render(request, "projects/dashboard.html") + form = ProjectDashboardEditForm(request.POST, request.FILES, instance=project) + links_form = ProjectRelatedLinksForm(request.POST) + + if form.is_valid() and links_form.is_valid(): + project = form.save(commit=False) + project.related_links = links_form.to_dict() + project.save() + + messages.success(request, "✅ 프로젝트 정보가 수정되었습니다.") + return redirect("projects:dashboard_detail", project_id=project_id) + else: + messages.error(request, "❌ 입력 오류가 있습니다. 다시 확인해주세요.") + else: + form = ProjectDashboardEditForm(instance=project) + related_links = project.related_links or {} + links_form = ProjectRelatedLinksForm(initial={ + "notion_url": related_links.get("notion"), + "figma_url": related_links.get("figma"), + "github_url": related_links.get("github"), + }) context = { - "project_id": project_id, + "project": project, + "form": form, + "links_form": links_form, } - return render(request, "projects/dashboard_update.html", context) + + return render(request, "projects/dashboard_edit.html", context) @login_required From 098f014432d3d73d54873ad5db531eb63974ba0c Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 17:55:01 +0900 Subject: [PATCH 104/380] =?UTF-8?q?feat:=20=EC=A7=84=ED=96=89=20=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index d0b925a..c80563d 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -13,9 +13,25 @@ @login_required def dashboard(request): - """현재 프로젝트 대시보드""" - # TODO: 대시보드 로직 구현 - return render(request, "projects/dashboard.html") + """ + 현재 프로젝트 대시보드 진입점 + + - 사용자가 속한 현재 진행 중인 프로젝트가 있으면 해당 프로젝트 대시보드로 리다이렉트 + - 없으면 "현재 진행중인 프로젝트가 없어요" 페이지 렌더링 + """ + + # 사용자의 현재 진행 중인 프로젝트 찾기 + team_member = TeamMember.objects.filter( + user=request.user, + is_active=True + ).select_related('team__project').first() + + # 프로젝트 있으면 상세 페이지로 리다이렉트 + if team_member and team_member.team.project: + return redirect('projects:dashboard_detail', project_id=team_member.team.project.id) + + # 프로젝트 없으면 "현재 진행중인 프로젝트가 없어요" 페이지 표시 + return render(request, "projects/dashboard.html", {"has_project": False}) @login_required From 8f8e82313e1051e81a6917f23a110b0e08325a1f Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 18:16:09 +0900 Subject: [PATCH 105/380] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=EC=9D=98=20?= =?UTF-8?q?=EA=B3=BC=EA=B1=B0=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 51 +++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index c80563d..d0223bc 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -167,22 +167,57 @@ def dashboard_edit(request, project_id): @login_required +@require_http_methods(["GET"]) def project_list(request): - """지난 프로젝트 리스트""" - # TODO: 지난 프로젝트 리스트 로직 구현 - # projects = request.user.created_teams.all() - context = {} + """과거 프로젝트 리스트 (완료된 프로젝트)""" + # 사용자가 속했던 모든 팀의 프로젝트 + projects = Project.objects.filter( + team__members__user=request.user, + team__members__is_active=False # 비활성 (완료된 팀) + ).distinct().select_related('team').order_by('-created_at') + + context = { + "projects": projects, + } return render(request, "projects/project_list.html", context) @login_required +@require_http_methods(["GET"]) def project_detail(request, project_id): - """지난 프로젝트 상세""" - # TODO: 지난 프로젝트 상세 로직 구현 - # project = get_object_or_404(Project, id=project_id) + """과거 프로젝트 상세 (조회만)""" + project = get_object_or_404(Project, id=project_id) + + # 사용자가 해당 프로젝트에 속했었는지 확인 + is_member = TeamMember.objects.filter( + team__project=project, + user=request.user + ).exists() + + if not is_member: + messages.error(request, "접근 권한이 없습니다.") + return redirect("projects:project_list") + + # 팀 정보 + team = project.team + members = team.members.all().select_related("user", "role") + member_count_by_role = team.get_member_count_by_role() + + # 시즌 정보 + season = None + active_season = Season.get_active_season() + if active_season: + if active_season.project_start <= project.created_at <= active_season.project_end: + season = active_season + context = { - "project_id": project_id, + "project": project, + "team": team, + "members": members, + "member_count_by_role": member_count_by_role, + "season": season, } + return render(request, "projects/project_detail.html", context) From e6c3e620e67cb7e23cfaaef6d2663a2c5536a29b Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 18:18:43 +0900 Subject: [PATCH 106/380] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/urls.py | 2 +- apps/projects/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/projects/urls.py b/apps/projects/urls.py index 378341c..47b301b 100644 --- a/apps/projects/urls.py +++ b/apps/projects/urls.py @@ -7,7 +7,7 @@ # 프로젝트 대시보드 (현재 프로젝트) path("dashboard/", views.dashboard, name="dashboard"), # dashboard.html path("dashboard//", views.dashboard_detail, name="dashboard_detail"), # 대시보드 조회 - path("dashboard//edit/", views.dashboard_edit, name="dashboard_edit"), # 대시보드 수정 + path("dashboard//edit/", views.dashboard_update, name="dashboard_edit"), # 대시보드 수정 # 지난 프로젝트 path("", views.project_list, name="project_list"), # project_list.html diff --git a/apps/projects/views.py b/apps/projects/views.py index d0223bc..f1c637d 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -110,7 +110,7 @@ def dashboard_detail(request, project_id): @login_required @require_http_methods(["GET", "POST"]) -def dashboard_edit(request, project_id): +def dashboard_update(request, project_id): """ 프로젝트 대시보드 수정 (팀원만) @@ -163,7 +163,7 @@ def dashboard_edit(request, project_id): "links_form": links_form, } - return render(request, "projects/dashboard_edit.html", context) + return render(request, "projects/dashboard_update.html", context) @login_required From 0108c520c7b014f23d6f7a86b52ddc99085fd77c Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 18:47:41 +0900 Subject: [PATCH 107/380] =?UTF-8?q?refactor:=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B2=B9=EC=B9=98=EB=8A=94=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 135 +++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 73 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index f1c637d..5f89abd 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -11,6 +11,64 @@ from apps.teams.models import Team, TeamMember +# ================================ +# Helper 함수 +# ================================ + +def _get_project_context(project, user): + """ + 프로젝트 상세 정보 context 생성 (dashboard_detail, project_detail에서 공유) + """ + team = project.team + members = team.members.filter(is_active=True).select_related("user", "role") + member_count_by_role = team.get_member_count_by_role() + + # 시즌 정보 + season = None + active_season = Season.get_active_season() + if active_season: + if active_season.project_start <= project.created_at <= active_season.project_end: + season = active_season + + # 가이드 진척도 계산 + guide_progress = None + if hasattr(project, 'current_stage') and project.current_stage: + try: + from apps.guides.models import GuideTask, GuideTaskProgress + + total_tasks = GuideTask.objects.filter( + card__stage=project.current_stage + ).count() + + completed_tasks = GuideTaskProgress.objects.filter( + task__card__stage=project.current_stage, + project=project, + user=user, + is_completed=True + ).count() + + progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) + + guide_progress = { + 'stage': project.current_stage, + 'total_tasks': total_tasks, + 'completed_tasks': completed_tasks, + 'progress_percent': progress_percent, + } + except: + # GuideTask 모델이 없거나 데이터가 없으면 None으로 처리 + guide_progress = None + + return { + "project": project, + "team": team, + "members": members, + "member_count_by_role": member_count_by_role, + "season": season, + "guide_progress": guide_progress, + } + + @login_required def dashboard(request): """ @@ -37,14 +95,7 @@ def dashboard(request): @login_required @require_http_methods(["GET"]) def dashboard_detail(request, project_id): - """ - 프로젝트 대시보드 조회 (읽기 전용) - - 읽기 전용 정보: - - 진행기간 (starts_at, ends_at) - - 팀 정보 (team composition) - - 진척도 (guide_stage progress) - """ + """프로젝트 대시보드 조회 (진행 중인 프로젝트)""" project = get_object_or_404(Project, id=project_id) # 팀원 확인 @@ -58,52 +109,8 @@ def dashboard_detail(request, project_id): messages.error(request, "팀원만 접근할 수 있습니다.") return redirect("projects:dashboard") - # 팀 정보 - team = project.team - members = team.members.filter(is_active=True).select_related("user", "role") - member_count_by_role = team.get_member_count_by_role() - - # 시즌 정보 - season = None - active_season = Season.get_active_season() - if active_season: - if active_season.project_start <= project.created_at <= active_season.project_end: - season = active_season - - # 가이드 진척도 계산 - guide_progress = None - if project.current_stage: - from apps.guides.models import GuideTask, GuideTaskProgress - - total_tasks = GuideTask.objects.filter( - card__stage=project.current_stage - ).count() - - completed_tasks = GuideTaskProgress.objects.filter( - task__card__stage=project.current_stage, - project=project, - user=request.user, - is_completed=True - ).count() - - progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) - - guide_progress = { - 'stage': project.current_stage, - 'total_tasks': total_tasks, - 'completed_tasks': completed_tasks, - 'progress_percent': progress_percent, - } - - context = { - "project": project, - "team": team, - "members": members, - "member_count_by_role": member_count_by_role, - "season": season, - "guide_progress": guide_progress, - "is_team_member": is_team_member, - } + context = _get_project_context(project, request.user) + context["is_team_member"] = is_team_member return render(request, "projects/dashboard.html", context) @@ -198,25 +205,7 @@ def project_detail(request, project_id): messages.error(request, "접근 권한이 없습니다.") return redirect("projects:project_list") - # 팀 정보 - team = project.team - members = team.members.all().select_related("user", "role") - member_count_by_role = team.get_member_count_by_role() - - # 시즌 정보 - season = None - active_season = Season.get_active_season() - if active_season: - if active_season.project_start <= project.created_at <= active_season.project_end: - season = active_season - - context = { - "project": project, - "team": team, - "members": members, - "member_count_by_role": member_count_by_role, - "season": season, - } + context = _get_project_context(project, request.user) return render(request, "projects/project_detail.html", context) From 0802b2f473b73281ff56b1c9d4275239464829e9 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 18:52:28 +0900 Subject: [PATCH 108/380] =?UTF-8?q?test:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/tests.py | 172 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/apps/projects/tests.py b/apps/projects/tests.py index 11e2b00..d523511 100644 --- a/apps/projects/tests.py +++ b/apps/projects/tests.py @@ -2,6 +2,7 @@ 팀 매칭 알고리즘 테스트 """ from django.test import TestCase +from django.urls import reverse from django.utils import timezone from datetime import timedelta from django.core.exceptions import ValidationError @@ -152,3 +153,174 @@ def test_team_composition(self): self.assertEqual(fe_count, 2) self.assertEqual(be_count, 2) + +class ProjectDashboardViewTest(TestCase): + """프로젝트 대시보드 뷰 테스트""" + + def setUp(self): + """테스트 데이터 준비""" + # 시즌 생성 + self.season = Season.objects.create( + name="2026년 1월 시즌", + status=Season.Status.IN_PROJECT, + matching_start=timezone.now() - timedelta(days=10), + matching_end=timezone.now() - timedelta(days=5), + project_start=timezone.now() - timedelta(days=3), + project_end=timezone.now() + timedelta(days=30), + is_active=True, + ) + + # 역할 생성 + self.pm_role = Role.objects.create(code="PM", name="프로덕트 매니저") + self.fe_role = Role.objects.create(code="FE", name="프론트엔드") + self.be_role = Role.objects.create(code="BE", name="백엔드") + + # 사용자 생성 후 프로필 완성 + self.pm_user = User.objects.create_user( + username="pm_user", + email="pm@test.com", + password="testpass123", + ) + self.pm_user.nickname = "PM 유저" + self.pm_user.save() + + self.fe_user = User.objects.create_user( + username="fe_user", + email="fe@test.com", + password="testpass123", + ) + self.fe_user.nickname = "FE 유저" + self.fe_user.save() + + # 프로젝트 생성 + self.project = Project.objects.create( + title="테스트 프로젝트", + description="테스트 설명", + status=Project.Status.IN_PROGRESS, + ) + + # 팀 생성 + self.team = Team.objects.create( + project=self.project, + name="테스트 팀", + ) + + # 팀 멤버 추가 + self.pm_member = TeamMember.objects.create( + team=self.team, + user=self.pm_user, + role=self.pm_role, + is_active=True, + ) + self.fe_member = TeamMember.objects.create( + team=self.team, + user=self.fe_user, + role=self.fe_role, + is_active=True, + ) + + def test_dashboard_no_project(self): + """진행 중인 프로젝트 없을 때""" + user = User.objects.create_user( + username="no_project", + email="no@test.com", + password="testpass123", + ) + user.nickname = "No Project User" + user.save() + + self.client.login(username="no_project", password="testpass123") + + response = self.client.get(reverse("projects:dashboard")) + self.assertEqual(response.status_code, 200) + self.assertFalse(response.context["has_project"]) + + def test_dashboard_with_project(self): + """진행 중인 프로젝트 있을 때""" + self.client.login(username="pm_user", password="testpass123") + + response = self.client.get(reverse("projects:dashboard")) + # 프로젝트 있으면 redirect + self.assertEqual(response.status_code, 302) + + def test_dashboard_detail_access(self): + """대시보드 상세 조회""" + self.client.login(username="pm_user", password="testpass123") + + url = reverse("projects:dashboard_detail", kwargs={"project_id": self.project.id}) + print(f"\n🔍 Testing URL: {url}") + print(f" Project ID: {self.project.id}") + print(f" User: pm_user") + print(f" TeamMember exists: {TeamMember.objects.filter(user=self.pm_user, is_active=True).exists()}") + + response = self.client.get(url) + + print(f" Response status: {response.status_code}") + if response.status_code == 302: + print(f" Redirected to: {response.url}") + + self.assertEqual(response.status_code, 200) + + def test_dashboard_detail_not_member(self): + """대시보드 상세 - 팀원 아닐 때""" + user = User.objects.create_user( + username="non_member", + email="non@test.com", + password="testpass123", + ) + self.client.login(username="non_member", password="testpass123") + + response = self.client.get( + reverse("projects:dashboard_detail", kwargs={"project_id": self.project.id}) + ) + self.assertEqual(response.status_code, 302) # redirect + + def test_project_list_access(self): + """과거 프로젝트 리스트""" + self.client.login(username="pm_user", password="testpass123") + + response = self.client.get(reverse("projects:project_list")) + self.assertEqual(response.status_code, 200) + + def test_project_detail_access(self): + """과거 프로젝트 상세 조회""" + self.client.login(username="pm_user", password="testpass123") + + response = self.client.get( + reverse("projects:project_detail", kwargs={"project_id": self.project.id}) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["project"], self.project) + + def test_dashboard_update_get(self): + """대시보드 수정 폼 조회""" + self.client.login(username="pm_user", password="testpass123") + + response = self.client.get( + reverse("projects:dashboard_edit", kwargs={"project_id": self.project.id}) + ) + self.assertEqual(response.status_code, 200) + self.assertIn("form", response.context) + self.assertIn("links_form", response.context) + + def test_dashboard_update_post(self): + """대시보드 정보 수정""" + self.client.login(username="pm_user", password="testpass123") + + data = { + "title": "수정된 프로젝트명", + "description": "수정된 설명", + "is_favorite": True, + } + response = self.client.post( + reverse("projects:dashboard_edit", kwargs={"project_id": self.project.id}), + data, + ) + + # 수정 후 redirect + self.assertEqual(response.status_code, 302) + + # 데이터 확인 + self.project.refresh_from_db() + self.assertEqual(self.project.title, "수정된 프로젝트명") + self.assertTrue(self.project.is_favorite) From a2880bc0a8b3b788ea0537e26af5f7493e3ab972 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 19:07:38 +0900 Subject: [PATCH 109/380] =?UTF-8?q?feat:=20kitup=20=EA=B3=BC=EA=B1=B0=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index 5f89abd..c11e767 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -211,21 +211,33 @@ def project_detail(request, project_id): @login_required +@require_http_methods(["GET"]) def kitup_list(request): - """모든 KITUP 프로젝트 리스트""" - # TODO: 모든 프로젝트 리스트 로직 구현 - context = {} + """모든 KITUP 프로젝트 리스트 (진행 중인 프로젝트)""" + # 활성화된 프로젝트만 조회 (상태: IN_PROGRESS 또는 MATCHED) + projects = Project.objects.filter( + status__in=[Project.Status.IN_PROGRESS, Project.Status.MATCHED] + ).select_related('team').order_by('-created_at') + + context = { + "projects": projects, + } return render(request, "projects/kitup_list.html", context) @login_required +@require_http_methods(["GET"]) def kitup_detail(request, project_id): - """모든 KITUP 프로젝트 상세""" - # TODO: 모든 프로젝트 상세 로직 구현 - # project = get_object_or_404(Project, id=project_id) - context = { - "project_id": project_id, - } + """모든 KITUP 프로젝트 상세 (조회만)""" + project = get_object_or_404(Project, id=project_id) + + # 활성화된 프로젝트만 조회 가능 + if project.status not in [Project.Status.IN_PROGRESS, Project.Status.MATCHED]: + messages.error(request, "조회할 수 없는 프로젝트입니다.") + return redirect("projects:kitup_list") + + context = _get_project_context(project, request.user) + return render(request, "projects/kitup_detail.html", context) From 97730dda1ae90992f16b5eb6e622acebe867501a Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 3 Feb 2026 19:23:09 +0900 Subject: [PATCH 110/380] =?UTF-8?q?refactor:=20ARCHIVED=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=EB=A7=8C=20?= =?UTF-8?q?=EB=9C=A8=EA=B2=8C=EB=81=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index c11e767..3f9c801 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -213,10 +213,10 @@ def project_detail(request, project_id): @login_required @require_http_methods(["GET"]) def kitup_list(request): - """모든 KITUP 프로젝트 리스트 (진행 중인 프로젝트)""" - # 활성화된 프로젝트만 조회 (상태: IN_PROGRESS 또는 MATCHED) + """모든 KITUP 프로젝트 리스트 (완료된 보관 프로젝트)""" + # 보관된 프로젝트만 조회 (ARCHIVED 상태) projects = Project.objects.filter( - status__in=[Project.Status.IN_PROGRESS, Project.Status.MATCHED] + status=Project.Status.ARCHIVED ).select_related('team').order_by('-created_at') context = { @@ -228,13 +228,8 @@ def kitup_list(request): @login_required @require_http_methods(["GET"]) def kitup_detail(request, project_id): - """모든 KITUP 프로젝트 상세 (조회만)""" - project = get_object_or_404(Project, id=project_id) - - # 활성화된 프로젝트만 조회 가능 - if project.status not in [Project.Status.IN_PROGRESS, Project.Status.MATCHED]: - messages.error(request, "조회할 수 없는 프로젝트입니다.") - return redirect("projects:kitup_list") + """모든 KITUP 프로젝트 상세 (보관된 프로젝트 조회만)""" + project = get_object_or_404(Project, id=project_id, status=Project.Status.ARCHIVED) context = _get_project_context(project, request.user) From 9f66623bb3c32283fa1acd97340e6a11efb4f73b Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 3 Feb 2026 22:46:11 +0900 Subject: [PATCH 111/380] =?UTF-8?q?feat:=20Retrospectives=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/api_urls.py | 8 +++++-- apps/reflections/forms.py | 12 ++++++++++ apps/reflections/models.py | 6 +++++ apps/reflections/serializers.py | 41 ++++++++++++++++++++++++++++++++ apps/reflections/views.py | 39 ++++++++++++++++++++++++++++++ project.zip | Bin 0 -> 177393 bytes 6 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 apps/reflections/forms.py create mode 100644 apps/reflections/serializers.py create mode 100644 project.zip diff --git a/apps/reflections/api_urls.py b/apps/reflections/api_urls.py index 690b131..4527a27 100644 --- a/apps/reflections/api_urls.py +++ b/apps/reflections/api_urls.py @@ -1,4 +1,8 @@ from django.urls import path +from rest_framework.routers import DefaultRouter +from .views import RetrospectiveViewSet -urlpatterns = [ -] +router = DefaultRouter() +router.register("retrospectives",RetrospectiveViewSet, basename="retrospective") + +urlpatterns = router.urls \ No newline at end of file diff --git a/apps/reflections/forms.py b/apps/reflections/forms.py new file mode 100644 index 0000000..3b4ca93 --- /dev/null +++ b/apps/reflections/forms.py @@ -0,0 +1,12 @@ +# reflections/forms.py +from django import forms +from .models import Retrospective + +class RetrospectiveForm(forms.ModelForm): + class Meta: + model = Retrospective + fields = ["project", "title", "content_md", "bookmarked"] + widgets = { + "title": forms.TextInput(attrs={"placeholder": "제목(선택)"}), + "content_md": forms.Textarea(attrs={"rows": 16, "placeholder": "마크다운으로 작성하세요"}), + } diff --git a/apps/reflections/models.py b/apps/reflections/models.py index c2702f9..5888d4e 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -32,6 +32,12 @@ class Retrospective(models.Model): help_text="회고 내용 (마크다운)", ) + bookmarked = models.BooleanField( + default= False, + # TODO 회고인데 찜 -> 어감 이상함, 즐겨찾기나 북마크로 수정 + help_text="찜 여부" + ) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/apps/reflections/serializers.py b/apps/reflections/serializers.py new file mode 100644 index 0000000..7754ae7 --- /dev/null +++ b/apps/reflections/serializers.py @@ -0,0 +1,41 @@ +# reflections/serializers.py +from rest_framework import serializers +from .models import Retrospective + + +class RetrospectiveReadSerializer(serializers.ModelSerializer): + username = serializers.CharField(source="user.nickname", read_only=True) + project_id = serializers.IntegerField(source="project.id", read_only=True) + + class Meta: + model = Retrospective + fields = [ + "id", + "project_id", + "user", + "username", + "title", + "content_md", + "bookmarked", + "created_at", + "updated_at", + ] + read_only_fields = [ + "id", + "project_id", + "user", + "username", + "created_at", + "updated_at" + ] + def validate_content_md(self, value): + if not value.strip(): + raise serializers.ValidationError("내용은 비어 있을 수 없습니다.") + return value + + +class RetrospectiveWriteSerializer(serializers.ModelSerializer): + # 입력은 project FK를 그대로 받는게 Swagger에서 제일 단순합니다. (정수) + class Meta: + model = Retrospective + fields = ["project", "title", "content_md", "bookmarked"] diff --git a/apps/reflections/views.py b/apps/reflections/views.py index f9cfd44..828693b 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -2,6 +2,13 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated +from rest_framework.exceptions import PermissionDenied + +from drf_spectacular.utils import extend_schema_view, extend_schema +from .models import Retrospective +from .serializers import RetrospectiveReadSerializer # from .models import Reflection @@ -57,3 +64,35 @@ def note_delete(request, note_id): messages.success(request, "회고가 삭제되었습니다.") return redirect("reflections:note_list") return redirect("reflections:note_detail", note_id=note_id) + +@extend_schema_view( + list=extend_schema(summary="회고 목록 조회", tags=["Retrospectives"]), + retrieve=extend_schema(summary="회고 상세 조회", tags=["Retrospectives"]), + create=extend_schema(summary="회고 생성", tags=["Retrospectives"]), + update=extend_schema(summary="회고 전체 수정", tags=["Retrospectives"]), + partial_update=extend_schema(summary="회고 부분 수정", tags=["Retrospectives"]), + destroy=extend_schema(summary="회고 삭제", tags=["Retrospectives"]), +) +class RetrospectiveViewSet(viewsets.ModelViewSet): + serializer_class = RetrospectiveReadSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + # 내 회고만 + return ( + Retrospective.objects + .filter(user=self.request.user) + .select_related("project", "user") + .order_by("-created_at") + ) + + def perform_create(self, serializer): + # user는 서버에서 강제 + serializer.save(user=self.request.user) + + def get_object(self): + # pk 직접 접근 차단 + obj = super().get_object() + if obj.user_id != self.request.user.id: + raise PermissionDenied("본인 회고만 접근 가능합니다.") + return obj \ No newline at end of file diff --git a/project.zip b/project.zip new file mode 100644 index 0000000000000000000000000000000000000000..901c838a428d74e5058795ad9d96cbd1b9d888cd GIT binary patch literal 177393 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 z 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 Date: Wed, 4 Feb 2026 15:05:34 +0900 Subject: [PATCH 112/380] =?UTF-8?q?chore:=20=EC=B4=88=EA=B8=B0=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=A9=94=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 3 +-- config/views.py | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/config/urls.py b/config/urls.py index 6a49cf4..3816b79 100644 --- a/config/urls.py +++ b/config/urls.py @@ -8,8 +8,7 @@ urlpatterns = [ - path("", initial_view, name="initial"), # 초기 화면 (initial.html) - path("main/", main_view, name="main"), # 메인 화면 (main.html) + path("", main_view, name="main"), # 메인 화면 (main.html) path("admin/", admin.site.urls), # allauth (로그인/소셜로그인) diff --git a/config/views.py b/config/views.py index b3be8ed..92c7ece 100644 --- a/config/views.py +++ b/config/views.py @@ -1,15 +1,5 @@ from django.shortcuts import render - -# 초기 화면 (initial.html) -def initial_view(request): - """ - 초기 화면 (initial.html) - - 랜딩 페이지 - """ - return render(request, "initial.html") - - # 메인 화면 (main.html) def main_view(request): """ From 88ab31dccb478a21e97c05463b44fa892618ed3f Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 4 Feb 2026 16:06:15 +0900 Subject: [PATCH 113/380] =?UTF-8?q?feat:=20main=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/views.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/config/views.py b/config/views.py index 92c7ece..fb01c79 100644 --- a/config/views.py +++ b/config/views.py @@ -1,10 +1,60 @@ from django.shortcuts import render +from datetime import date + +from apps.accounts.models import UserRoleLevel +from apps.projects.models import Project, Season +from apps.reflections.models import Retrospective # 메인 화면 (main.html) def main_view(request): """ 메인 화면 (main.html) - - 비로그인: 컨텐츠들 제목 섹션 - 로그인 후 이용해보세요. - - 로그인: 각 섹션 클릭 시 해당하는 html로 이동 + - 비로그인: request.user가 AnonymousUser이므로 템플릿에서 자동 분기 + - 로그인: + 1. 오늘의 작업 기록 (회고) + 2. 팀 매칭 모집 (현재 시즌 프로젝트들) + 3. KITUP 프로젝트 (보관된 프로젝트들) """ - return render(request, "main.html") + + user = request.user + context = {} + + # 로그인 상태만 추가 데이터 조회 + if user.is_authenticated: + """회고 부분""" + + """팀 매칭 부분""" + season = Season.get_active_season() + is_matching_period = season and season.is_matching_period() if season else False + + # 유저의 역할별 레벨 + role_levels = ( + UserRoleLevel.objects + .filter(user=user) + .select_related("role") + ) + + role_level_map = { + rl.role.code: rl.level + for rl in role_levels + } + + # 현재 시즌의 모집 중인 프로젝트들 + matching_projects = None + if season and is_matching_period: + matching_projects = Project.objects.filter( + status__in=[Project.Status.OPEN, Project.Status.MATCHED] + ).select_related('team').order_by('-created_at')[:6] + + context["is_matching_period"] = is_matching_period + context["role_levels"] = role_level_map + context["matching_projects"] = matching_projects + + """KITUP 프로젝트 부분""" + archived_projects = Project.objects.filter( + status=Project.Status.ARCHIVED + ).select_related('team').order_by('-created_at') + + context["archived_projects"] = archived_projects + + return render(request, "main.html", context) From 22599dd14368dcce354050d1f781d528ce3d8107 Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 4 Feb 2026 16:34:21 +0900 Subject: [PATCH 114/380] =?UTF-8?q?chore:=20initial=5Fview=20import=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/urls.py b/config/urls.py index 3816b79..ee4fafe 100644 --- a/config/urls.py +++ b/config/urls.py @@ -3,7 +3,7 @@ from django.conf import settings from django.conf.urls.static import static from django.views.generic import RedirectView -from .views import initial_view, main_view +from .views import main_view from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView urlpatterns = [ From 5760630698b2a6d7349ae90202e20400b64f17fc Mon Sep 17 00:00:00 2001 From: issuejong Date: Wed, 4 Feb 2026 16:43:46 +0900 Subject: [PATCH 115/380] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/admin.py | 102 ++++++++++++++++++++++++++++++++++++-- apps/projects/admin.py | 109 ++++++++++++++++++++++++++++++++++++++++- apps/teams/admin.py | 40 +++++++++++++++ 3 files changed, 246 insertions(+), 5 deletions(-) diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index 8994dce..e5f179b 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -1,7 +1,9 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.utils.translation import ngettext +from django.contrib import messages -from .models import User, Role, UserRoleLevel, TechStack +from .models import User, Role, UserRoleLevel, TechStack, Report class UserRoleLevelInline(admin.TabularInline): @@ -13,26 +15,76 @@ class UserRoleLevelInline(admin.TabularInline): @admin.register(User) class UserAdmin(BaseUserAdmin): - list_display = ["id", "username", "nickname", "email", "is_staff", "created_at"] - list_filter = ["is_staff", "is_active", "created_at"] + list_display = ["id", "username", "nickname", "email", "passion_level", "team_ban_count", "is_staff", "created_at"] + list_filter = ["is_staff", "is_active", "created_at", "passion_level"] search_fields = ["username", "nickname", "email"] ordering = ["-created_at"] inlines = [UserRoleLevelInline] + actions = ["clear_passion_level", "clear_team_ban", "ban_user"] fieldsets = BaseUserAdmin.fieldsets + ( ("프로필 정보", {"fields": ("nickname", "profile_image", "bio", "tech_stacks")}), + ("관리 정보", {"fields": ("passion_level", "team_ban_count")}), ) + + def clear_passion_level(self, request, queryset): + """열정 레벨 초기화""" + count = queryset.update(passion_level=None) + self.message_user( + request, + ngettext( + f"{count}명의 열정 레벨이 초기화되었습니다.", + f"{count}명의 열정 레벨이 초기화되었습니다.", + count, + ), + ) + clear_passion_level.short_description = "🔄 선택된 사용자의 열정 레벨 초기화" + + def clear_team_ban(self, request, queryset): + """팀 밴 횟수 초기화""" + count = queryset.update(team_ban_count=0) + self.message_user( + request, + ngettext( + f"{count}명의 팀플 금지가 해제되었습니다.", + f"{count}명의 팀플 금지가 해제되었습니다.", + count, + ), + ) + clear_team_ban.short_description = "🔓 선택된 사용자의 팀플 금지 해제" + + def ban_user(self, request, queryset): + """사용자에게 팀 밴 1회 추가""" + count = 0 + for user in queryset: + user.team_ban_count += 1 + user.save() + count += 1 + self.message_user( + request, + ngettext( + f"{count}명에게 팀플 금지 1회가 추가되었습니다.", + f"{count}명에게 팀플 금지 1회가 추가되었습니다.", + count, + ), + ) + ban_user.short_description = "⛔ 선택된 사용자에게 팀 밴 1회 추가" @admin.register(TechStack) class TechStackAdmin(admin.ModelAdmin): - list_display = ["id", "name", "category", "created_at"] + list_display = ["id", "name", "category", "user_count", "created_at"] list_filter = ["category"] search_fields = ["name"] ordering = ["category", "name"] fieldsets = [ ("기본 정보", {"fields": ["name", "category"]}), ] + + def user_count(self, obj): + """이 기술을 보유한 사용자 수""" + return obj.users.count() + user_count.short_description = "사용자 수" @admin.register(Role) @@ -42,6 +94,48 @@ class RoleAdmin(admin.ModelAdmin): ordering = ["code"] +@admin.register(Report) +class ReportAdmin(admin.ModelAdmin): + list_display = ["id", "reporter", "reported_user", "reason_preview", "status", "created_at"] + list_filter = ["status", "created_at"] + search_fields = ["reporter__nickname", "reported_user__nickname", "reason"] + ordering = ["-created_at"] + actions = ["approve_and_ban"] + readonly_fields = ["reporter", "reported_user", "reason", "created_at"] + + def reason_preview(self, obj): + """신고 사유 미리보기 (50자)""" + return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason + reason_preview.short_description = "신고 사유" + + def approve_and_ban(self, request, queryset): + """신고 승인 및 피신고자 밴""" + pending_reports = queryset.filter(status=Report.Status.PENDING) + count = 0 + + for report in pending_reports: + # 피신고자에게 팀 밴 2회 추가 + report.reported_user.team_ban_count += 2 + report.reported_user.save() + + # 신고 상태 업데이트 + report.status = Report.Status.APPROVED + report.admin_note = f"관리자 일괄 처리: {request.user.username}" + report.save() + count += 1 + + self.message_user( + request, + ngettext( + f"{count}명이 밴 처리되었습니다.", + f"{count}명이 밴 처리되었습니다.", + count, + ), + messages.SUCCESS, + ) + approve_and_ban.short_description = "✅ 신고 승인 및 피신고자 밴" + + @admin.register(UserRoleLevel) class UserRoleLevelAdmin(admin.ModelAdmin): list_display = ["id", "user", "role", "level", "last_diagnosed_at", "updated_at"] diff --git a/apps/projects/admin.py b/apps/projects/admin.py index e9427ca..d4bbc96 100644 --- a/apps/projects/admin.py +++ b/apps/projects/admin.py @@ -1,6 +1,10 @@ from django.contrib import admin +from django.contrib import messages +from django.core.exceptions import ValidationError +from django.utils.translation import ngettext from .models import Season, Project, ProjectApplication +from .services import TeamMatchingService @admin.register(Season) @@ -9,7 +13,7 @@ class SeasonAdmin(admin.ModelAdmin): list_filter = ["status", "is_active", "created_at"] search_fields = ["name"] ordering = ["-created_at"] - actions = ["activate_season", "deactivate_season"] + actions = ["activate_season", "deactivate_season", "run_team_matching"] def activate_season(self, request, queryset): """시즌 활성화 (이전 활성 시즌은 자동 비활성화)""" @@ -24,6 +28,32 @@ def deactivate_season(self, request, queryset): queryset.update(is_active=False) self.message_user(request, "시즌이 비활성화되었습니다.") + def run_team_matching(self, request, queryset): + """팀 매칭 알고리즘 실행""" + for season in queryset: + try: + result = TeamMatchingService.run_matching(season.id) + self.message_user( + request, + f"✅ [{season.name}] 팀 매칭 완료: " + f"{result['teams_created']}팀 생성, " + f"{result['total_matched']}명 매칭", + messages.SUCCESS, + ) + except ValidationError as e: + self.message_user( + request, + f"❌ [{season.name}] 팀 매칭 실패: {str(e)}", + messages.ERROR, + ) + except Exception as e: + self.message_user( + request, + f"❌ [{season.name}] 팀 매칭 오류: {str(e)}", + messages.ERROR, + ) + run_team_matching.short_description = "🤝 팀 매칭 알고리즘 실행" + activate_season.short_description = "✅ 선택된 시즌 활성화" deactivate_season.short_description = "❌ 선택된 시즌 비활성화" @@ -34,6 +64,83 @@ class ProjectAdmin(admin.ModelAdmin): list_filter = ["status", "created_at"] search_fields = ["title", "description"] ordering = ["-created_at"] + actions = [ + "change_status_to_open", + "change_status_to_matched", + "change_status_to_in_progress", + "change_status_to_completed", + "change_status_to_archived", + ] + + def change_status_to_open(self, request, queryset): + """상태 변경: 모집중""" + count = queryset.update(status=Project.Status.OPEN) + self.message_user( + request, + ngettext( + f"{count}개 프로젝트가 '모집중' 상태로 변경되었습니다.", + f"{count}개 프로젝트가 '모집중' 상태로 변경되었습니다.", + count, + ), + messages.SUCCESS, + ) + change_status_to_open.short_description = "📢 상태 변경: 모집중" + + def change_status_to_matched(self, request, queryset): + """상태 변경: 매칭완료""" + count = queryset.update(status=Project.Status.MATCHED) + self.message_user( + request, + ngettext( + f"{count}개 프로젝트가 '매칭완료' 상태로 변경되었습니다.", + f"{count}개 프로젝트가 '매칭완료' 상태로 변경되었습니다.", + count, + ), + messages.SUCCESS, + ) + change_status_to_matched.short_description = "✅ 상태 변경: 매칭완료" + + def change_status_to_in_progress(self, request, queryset): + """상태 변경: 진행중""" + count = queryset.update(status=Project.Status.IN_PROGRESS) + self.message_user( + request, + ngettext( + f"{count}개 프로젝트가 '진행중' 상태로 변경되었습니다.", + f"{count}개 프로젝트가 '진행중' 상태로 변경되었습니다.", + count, + ), + messages.SUCCESS, + ) + change_status_to_in_progress.short_description = "⚙️ 상태 변경: 진행중" + + def change_status_to_completed(self, request, queryset): + """상태 변경: 완료""" + count = queryset.update(status=Project.Status.COMPLETED) + self.message_user( + request, + ngettext( + f"{count}개 프로젝트가 '완료' 상태로 변경되었습니다.", + f"{count}개 프로젝트가 '완료' 상태로 변경되었습니다.", + count, + ), + messages.SUCCESS, + ) + change_status_to_completed.short_description = "🎉 상태 변경: 완료" + + def change_status_to_archived(self, request, queryset): + """상태 변경: 보관됨""" + count = queryset.update(status=Project.Status.ARCHIVED) + self.message_user( + request, + ngettext( + f"{count}개 프로젝트가 '보관됨' 상태로 변경되었습니다.", + f"{count}개 프로젝트가 '보관됨' 상태로 변경되었습니다.", + count, + ), + messages.SUCCESS, + ) + change_status_to_archived.short_description = "📦 상태 변경: 보관됨" @admin.register(ProjectApplication) diff --git a/apps/teams/admin.py b/apps/teams/admin.py index a8b34e7..c850e4e 100644 --- a/apps/teams/admin.py +++ b/apps/teams/admin.py @@ -1,4 +1,7 @@ from django.contrib import admin +from django.contrib import messages +from django.utils import timezone +from django.utils.translation import ngettext from .models import Team, TeamMember @@ -24,3 +27,40 @@ class TeamMemberAdmin(admin.ModelAdmin): list_filter = ["role", "is_active"] search_fields = ["user__nickname", "team__name"] ordering = ["-joined_at"] + actions = ["deactivate_members", "reactivate_members"] + + def deactivate_members(self, request, queryset): + """팀 멤버 비활성화 (탈퇴 처리)""" + now = timezone.now() + count = 0 + + for member in queryset.filter(is_active=True): + member.is_active = False + member.left_at = now + member.save() + count += 1 + + self.message_user( + request, + ngettext( + f"{count}명이 팀에서 제거되었습니다.", + f"{count}명이 팀에서 제거되었습니다.", + count, + ), + messages.SUCCESS, + ) + deactivate_members.short_description = "🚪 선택된 멤버 비활성화 (탈퇴)" + + def reactivate_members(self, request, queryset): + """팀 멤버 재활성화""" + count = queryset.filter(is_active=False).update(is_active=True, left_at=None) + self.message_user( + request, + ngettext( + f"{count}명이 팀에 재입장했습니다.", + f"{count}명이 팀에 재입장했습니다.", + count, + ), + messages.SUCCESS, + ) + reactivate_members.short_description = "🔄 선택된 멤버 재활성화" From 58821627d484c70a60042142b408e66763ca568b Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 17:56:10 +0900 Subject: [PATCH 116/380] feat: team_apply.html&css --- static/css/dashboard.css | 0 static/css/team_apply.css | 181 ++++++++++++++++++++++++++++++ templates/projects/dashboard.html | 8 ++ templates/teams/team_apply.html | 135 ++++++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 static/css/dashboard.css create mode 100644 static/css/team_apply.css diff --git a/static/css/dashboard.css b/static/css/dashboard.css new file mode 100644 index 0000000..e69de29 diff --git a/static/css/team_apply.css b/static/css/team_apply.css new file mode 100644 index 0000000..e9f3cd7 --- /dev/null +++ b/static/css/team_apply.css @@ -0,0 +1,181 @@ +body { + background: #f6f8ff; +} + +.team_matching { + margin-top: 100px; + text-align: center; +} + +.team_matching h3 { + font-size: 30px; + margin-bottom: 20px; +} + +.team_matching p { + font-size: 15px; +} + +.t_stack { + width: 90%; + display: flex; + margin: 40px auto 0; + justify-content: center; + gap: 40px; +} + +/* WEB 기획 */ +.t_stack .t_design { + padding: 20px 0; + width: 25%; + height: 3%; + text-align: center; + background: #fff; + border: 3px solid #00b9b050; + border-radius: 40px; +} + +.t_stack .t_design h3 { + font-weight: 600; + font-size: 20px; +} + +.t_stack .t_design img { + width: 40px; + height: 40px; +} + +.t_stack .t_nolevel { + margin-top: 5px; + color: #ff0202; + font-size: 14px; +} + +.t_stack .t_design .t_design_btn { + margin-top: 15px; + padding: 5px 0; + width: 150px; + height: 30px; + background: #00b9b0; + color: #fff; + border: none; + border-radius: 30px; + font-size: 14px; + font-weight: 550; +} + +/* WEB 프론트엔드 */ +.t_stack .t_frontend { + padding: 20px 0; + width: 25%; + height: 3%; + text-align: center; + background: #fff; + border: 3px solid #ffce5350; + border-radius: 40px; +} + +.t_stack .t_frontend h3 { + font-weight: 600; + font-size: 20px; +} + +.t_stack .t_frontend img { + width: 40px; + height: 40px; +} + +.t_stack .t_frontend .t_level { + margin-top: 5px; + color: #000; + font-size: 14px; +} + +.t_stack .t_frontend .t_front_btn { + margin-top: 15px; + padding: 5px 0; + width: 150px; + height: 30px; + background: #ffce53; + color: #fff; + border: none; + border-radius: 30px; + font-size: 14px; + font-weight: 550; +} + +/* WEB 백엔드 */ +.t_stack .t_backend { + padding: 20px 0; + width: 25%; + height: 3%; + text-align: center; + background: #fff; + border: 3px solid #ff3e8850; + border-radius: 40px; +} + +.t_stack .t_backend h3 { + font-weight: 600; + font-size: 20px; +} + +.t_stack .t_backend img { + width: 40px; + height: 40px; +} + +.t_stack .t_backend .t_level { + margin-top: 5px; + color: #000; + font-size: 14px; +} + +.t_stack .t_backend .t_back_btn { + margin-top: 15px; + padding: 5px 0; + width: 150px; + height: 30px; + background: #ff3e88; + color: #fff; + border: none; + border-radius: 30px; + font-size: 14px; + font-weight: 550; +} + +.match_wait { + margin-top: 120px; + text-align: center; + align-items: center; +} + +.match_wait h3 { + font-size: 27px; + font-weight: 600; + margin-bottom: 20px; +} + +.match_wait form { + display: flex; + justify-content: space-between; + align-items: center; + margin: 30px auto 0; + width: 60%; height: 40px; + background: #fff; + border-radius: 25px; + padding: 10px 10px 10px 20px; +} + +.match_wait form input { + border: none; + font-size: 15px; + color: #888888; +} + +.match_wait form button { + font-size: 15px; + background: #4272EF; color: #fff; + border: none; border-radius: 20px; + padding: 7px 15px; +} \ No newline at end of file diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index e69de29..f34e873 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} +{% load static %} +{% block header %} + +{% endblock %} +{% block content %} +프로젝트 +{% endblock %} \ No newline at end of file diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index e69de29..89e6ccd 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -0,0 +1,135 @@ +{% extends 'base.html' %} {% load static %} {% block header %} + +{% endblock %} {% block content %} + +

+

팀 매칭 모집 기간이 시작됐어요!

+

+ 6주의 기간동안 희망하는 스택으로 프로젝트를 진행하고, 실력을 쌓아보아요. +

+
+
+

WEB 기획

+ + + {% if user.is_authenticated %} + nolevel +

아직 레벨 진단이 완료되지 않았어요!

+ + + + + + + + + + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+

WEB 프론트엔드

+ {% if user.is_authenticated %} + + + + + Level1 +

Lv1

+ + + + + + + + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+

WEB 백엔드

+ {% if user.is_authenticated %} + + + + + + + + + + + Level4 +

Lv4

+ + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+ + + + + {% endblock %} +
+

From 28e87953b47608151756a7ca0be1c15a8acee566 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 17:58:30 +0900 Subject: [PATCH 117/380] =?UTF-8?q?fix=20:=20main.html=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20initial.html=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/initial.html | 205 -------------------------------------- templates/main.html | 217 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 204 insertions(+), 218 deletions(-) delete mode 100644 templates/initial.html diff --git a/templates/initial.html b/templates/initial.html deleted file mode 100644 index 87dfdf3..0000000 --- a/templates/initial.html +++ /dev/null @@ -1,205 +0,0 @@ -{% extends 'base.html' %} {% load static %} {% block header %} - -{% endblock %} {% block content %} -
- 메인이미지 -
-
-
- 회고이미지 -

오늘의 작업을 기록해보세요.

-
-
- {% if user.is_authenticated %} - - {% else %} -

로그인 후 이용해보세요.

- {% endif %} -
- {% if user.is_authenticated %} - - {% else %} {% endif %} -
-
- - -

팀 매칭 모집이 시작됐어요

-

- 6주의 기간동안 희망하는 스택으로 프로젝트를 진행하고, 실력을 쌓아보아요. -

- - - - -
-
-

WEB 기획

- - - {% if user.is_authenticated %} - nolevel -

아직 레벨 진단이 완료되지 않았어요!

- - - - - - - - - - {% else %} - nolevel -

로그인 후 레벨을 확인해보세요.

- - {% endif %} -
-
-

WEB 프론트엔드

- {% if user.is_authenticated %} - - - - - Level1 -

Lv1

- - - - - - - - {% else %} - nolevel -

로그인 후 레벨을 확인해보세요.

- - {% endif %} -
-
-

WEB 백엔드

- {% if user.is_authenticated %} - - - - - - - - - - - Level4 -

Lv4

- - {% else %} - nolevel -

로그인 후 레벨을 확인해보세요.

- - {% endif %} -
-
-
-
-
- project_img -

KITUP 프로젝트

-
-
- - - - -
-
- 예시이미지 -

서비스명

-

서비스명은 이러이러한 기능을 하는 플랫폼이다

-
-
- 예시이미지 -

서비스명

-

서비스명은 이러이러한 기능을 하는 플랫폼이다

-
-
- 예시이미지 -

서비스명

-

서비스명은 이러이러한 기능을 하는 플랫폼이다

-
-
- 예시이미지 -

서비스명

-

서비스명은 이러이러한 기능을 하는 플랫폼이다

-
-
- 예시이미지 -

서비스명

-

서비스명은 이러이러한 기능을 하는 플랫폼이다

-
-
-
-
-{% endblock %} diff --git a/templates/main.html b/templates/main.html index 2438a5c..87dfdf3 100644 --- a/templates/main.html +++ b/templates/main.html @@ -1,14 +1,205 @@ -{% extends 'base.html' %} -{% load static %} - - -{% block header %} - +{% extends 'base.html' %} {% load static %} {% block header %} + +{% endblock %} {% block content %} +
+ 메인이미지 +
+
+
+ 회고이미지 +

오늘의 작업을 기록해보세요.

+
+
+ {% if user.is_authenticated %} + + {% else %} +

로그인 후 이용해보세요.

+ {% endif %} +
+ {% if user.is_authenticated %} + + {% else %} {% endif %} +
+
+ + +

팀 매칭 모집이 시작됐어요

+

+ 6주의 기간동안 희망하는 스택으로 프로젝트를 진행하고, 실력을 쌓아보아요. +

+ + + + +
+
+

WEB 기획

+ + + {% if user.is_authenticated %} + nolevel +

아직 레벨 진단이 완료되지 않았어요!

+ + + + + + + + + + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+

WEB 프론트엔드

+ {% if user.is_authenticated %} + + + + + Level1 +

Lv1

+ + + + + + + + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+

WEB 백엔드

+ {% if user.is_authenticated %} + + + + + + + + + + + Level4 +

Lv4

+ + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+
+
+
+ project_img +

KITUP 프로젝트

+
+
+ + + + +
+
+ 예시이미지 +

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+ 예시이미지 +

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+ 예시이미지 +

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+ 예시이미지 +

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+ 예시이미지 +

서비스명

+

서비스명은 이러이러한 기능을 하는 플랫폼이다

+
+
+
+
{% endblock %} - -{% block content %} -
- 메인화면 -
-{% endblock %} - From 0eeb2843f88479e2f92105edb3f42c5fb5873557 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 18:42:04 +0900 Subject: [PATCH 118/380] =?UTF-8?q?fix=20:=20base.html=20&=20css=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/base.css | 76 +++++++++++++++++++++++++++++++++++++++++++-- static/js/set.txt | 1 - templates/base.html | 12 +++++-- 3 files changed, 83 insertions(+), 6 deletions(-) delete mode 100644 static/js/set.txt diff --git a/static/css/base.css b/static/css/base.css index 6811ccd..dd76e37 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -37,7 +37,7 @@ header .header_menu { display: flex; } -header .header_menu a { +header .header_menu > a { display: block; color: #4272EF; width: 110px; height: 40px; @@ -45,13 +45,13 @@ header .header_menu a { border-radius: 20px; } -header .header_menu a:hover { +header .header_menu > a:hover { background: #4272EF; color: #fff; transition: 0.3s ease; } -header .header_menu a p { +header .header_menu > a p { padding: 13px auto; font-size: 14px; font-weight: bold; line-height: 40px; @@ -60,4 +60,74 @@ header .header_menu a p { main { padding-top: 60px; min-height: calc(100vh - 60px); +} + +.dropdown { + position: relative; + display: inline-block; + margin-left: 20px; +} + +.dropdown > a { + display: block; + color: #4272EF; + width: 110px; height: 40px; + text-align: center; + border-radius: 20px; +} + +.dropdown > a:hover { + background: #4272EF; + color: #fff; + transition: 0.5s ease; +} + +.dropdown > a p { + padding: 13px auto; + font-size: 14px; font-weight: bold; + line-height: 40px; +} + +.dropdown-content { + display: none; + position: absolute; + top: 42px; + left: -10px; + background: #fff; + min-width: 130px; + overflow: hidden; + z-index: 1001; + padding-top: 3px; + border-bottom-left-radius: 15px; + border-bottom-right-radius: 15px; +} + +.dropdown-content::before { + content: ''; + position: absolute; + top: -3px; + left: 0; + right: 0; + height: 3px; + background: transparent; +} + +.dropdown-content a { + display: block; + color: #4272EF; + padding: 12px 0; + text-decoration: none; + font-size: 14px; + font-weight: 600; + text-align: center; +} + +.dropdown-content a:hover { + background: #4272EF; + color: #fff; + transition: 0.5s ease; +} + +.dropdown:hover .dropdown-content { + display: block; } \ No newline at end of file diff --git a/static/js/set.txt b/static/js/set.txt deleted file mode 100644 index 1b5c358..0000000 --- a/static/js/set.txt +++ /dev/null @@ -1 +0,0 @@ -초기 \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 926485c..92680e8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -16,7 +16,13 @@

KITUP

{% if user.is_authenticated %}

팀매칭

-

내 프로젝트

+

KITUP 프로젝트

회고

마이페이지

@@ -35,5 +41,7 @@

KITUP

+ + - + \ No newline at end of file From 5089f5183bfc91f87e28b6a09ed3f24eb7df43d6 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 18:45:30 +0900 Subject: [PATCH 119/380] =?UTF-8?q?fix=20:=20base.html=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index 92680e8..fe363c9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,7 +23,7 @@

KITUP

지난 프로젝트
-

KITUP 프로젝트

+

KITUP 프로젝트

회고

마이페이지

로그아웃

From aeb84dd775befb69bd9c99c7a504522ef9a62b02 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 18:59:40 +0900 Subject: [PATCH 120/380] =?UTF-8?q?fix=20:=20base.html=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index fe363c9..57f7c30 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,7 +23,7 @@

KITUP

지난 프로젝트
-

KITUP 프로젝트

+

KITUP 프로젝트

회고

마이페이지

로그아웃

From 0a48847bc866832890db84914b60dd8aa5d0c96c Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 19:26:07 +0900 Subject: [PATCH 121/380] =?UTF-8?q?fix=20:=20team=5Fapply=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20css=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team_apply.css | 43 ++++++++++++++++++++++++--------- templates/teams/team_apply.html | 34 +++++++++++++------------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/static/css/team_apply.css b/static/css/team_apply.css index e9f3cd7..c88ee83 100644 --- a/static/css/team_apply.css +++ b/static/css/team_apply.css @@ -2,6 +2,10 @@ body { background: #f6f8ff; } +button:hover { + cursor: pointer; +} + .team_matching { margin-top: 100px; text-align: center; @@ -27,7 +31,7 @@ body { /* WEB 기획 */ .t_stack .t_design { padding: 20px 0; - width: 25%; + width: 27%; height: 3%; text-align: center; background: #fff; @@ -54,20 +58,25 @@ body { .t_stack .t_design .t_design_btn { margin-top: 15px; padding: 5px 0; - width: 150px; - height: 30px; + width: 70%; + height: 33px; background: #00b9b0; color: #fff; border: none; border-radius: 30px; - font-size: 14px; + font-size: 15px; font-weight: 550; } +.t_stack .t_design .t_design_btn:hover { + background: #019890; + transition: 0.5s ease-in-out; +} + /* WEB 프론트엔드 */ .t_stack .t_frontend { padding: 20px 0; - width: 25%; + width: 27%; height: 3%; text-align: center; background: #fff; @@ -94,20 +103,25 @@ body { .t_stack .t_frontend .t_front_btn { margin-top: 15px; padding: 5px 0; - width: 150px; - height: 30px; + width: 70%; + height: 33px; background: #ffce53; color: #fff; border: none; border-radius: 30px; - font-size: 14px; + font-size: 15px; font-weight: 550; } +.t_stack .t_frontend .t_front_btn:hover { + background: #E2BF67; + transition: 0.5s ease-in-out; +} + /* WEB 백엔드 */ .t_stack .t_backend { padding: 20px 0; - width: 25%; + width: 27%; height: 3%; text-align: center; background: #fff; @@ -134,16 +148,21 @@ body { .t_stack .t_backend .t_back_btn { margin-top: 15px; padding: 5px 0; - width: 150px; - height: 30px; + width: 70%; + height: 33px; background: #ff3e88; color: #fff; border: none; border-radius: 30px; - font-size: 14px; + font-size: 15px; font-weight: 550; } +.t_stack .t_backend .t_back_btn:hover { + background: #C03067; + transition: 0.5s ease-in-out; +} + .match_wait { margin-top: 120px; text-align: center; diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index 89e6ccd..49a774f 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -15,23 +15,23 @@

WEB 기획

{% if user.is_authenticated %} nolevel

아직 레벨 진단이 완료되지 않았어요!

- + + --> + --> + --> + --> {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -51,23 +51,23 @@

WEB 프론트엔드

+ --> Level1

Lv1

- + + --> + --> + --> {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -87,23 +87,23 @@

WEB 백엔드

+ --> + --> + --> + --> Level4

Lv4

- + {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -118,8 +118,8 @@

WEB 백엔드

- - + - -

팀 매칭 모집이 시작됐어요

-

- 6주의 기간동안 희망하는 스택으로 프로젝트를 진행하고, 실력을 쌓아보아요. -

+ + -
-
-

WEB 기획

- - - {% if user.is_authenticated %} - nolevel -

아직 레벨 진단이 완료되지 않았어요!

- - - - - - - - - - {% else %} - nolevel -

로그인 후 레벨을 확인해보세요.

- - {% endif %} -
-
-

WEB 프론트엔드

- {% if user.is_authenticated %} - - - - - Level1 -

Lv1

- - - - - - - - {% else %} - nolevel -

로그인 후 레벨을 확인해보세요.

- - {% endif %} -
-
-

WEB 백엔드

- {% if user.is_authenticated %} - - - - - - - - - - - Level4 -

Lv4

- - {% else %} - nolevel -

로그인 후 레벨을 확인해보세요.

- - {% endif %} -
+ + + +
+

팀 매칭 결과가 발표됐어요.

+

+ 6주의 기간동안 비슷한 실력과 열정을 가진 팀원들과 멋진 서비스를 + 완성해보아요. +

+

결과 보러가기

diff --git a/templates/teams/team.html b/templates/teams/team.html index e69de29..3c84127 100644 --- a/templates/teams/team.html +++ b/templates/teams/team.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} {% load static %} {% block header %} + +{% endblock %} +{% block content %} +팀결과 +{% endblock %} \ No newline at end of file From bdaa3695005b866c7d582e97c9abffb0f16fdceb Mon Sep 17 00:00:00 2001 From: bimvocado Date: Wed, 4 Feb 2026 20:23:17 +0900 Subject: [PATCH 124/380] =?UTF-8?q?fix:=20yml=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 231740f..ce46934 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,11 @@ version: "3.8" services: - db: # services보다 두 칸 들여쓰기 + db: image: postgres:15-alpine container_name: kitup_db + env_file: + - .env environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} @@ -13,14 +15,17 @@ services: ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U kitup"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 - web: # services보다 두 칸 들여쓰기 - image: bimvocado/kitup-web:latest + web: + image: ghcr.io/pirogramming/startlinedev/web:latest + build: . container_name: kitup_web + env_file: + - .env command: > sh -c " python manage.py migrate && @@ -28,17 +33,16 @@ services: python manage.py runserver 0.0.0.0:8000 " volumes: - - .:/app + - .:/app - static_volume:/app/staticfiles - media_volume:/app/media ports: - "8000:8000" environment: - DEBUG: "True" DB_ENGINE: django.db.backends.postgresql - DB_NAME: kitup - DB_USER: kitup - DB_PASSWORD: kitup123 + DB_NAME: ${POSTGRES_DB} + DB_USER: ${POSTGRES_USER} + DB_PASSWORD: ${POSTGRES_PASSWORD} DB_HOST: db DB_PORT: 5432 REDIS_HOST: redis @@ -49,7 +53,7 @@ services: redis: condition: service_started - redis: # services보다 두 칸 들여쓰기 + redis: image: redis:7-alpine container_name: kitup_redis ports: From 27803201721fadfa9b0cfa9e41c57dc52cd96c2d Mon Sep 17 00:00:00 2001 From: bimvocado Date: Wed, 4 Feb 2026 20:31:59 +0900 Subject: [PATCH 125/380] =?UTF-8?q?feat:=20deploy.yml=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1352a06 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,54 @@ +name: Deploy to EC2 with GHCR + +on: + push: + branches: + - develop + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ghcr.io/${{ github.repository }}/web:latest + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Deploy to EC2 via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd /home/ubuntu/kitup + # 서버에서도 GHCR 이미지를 받을 수 있게 로그인 + echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + # 최신 이미지 가져오고 컨테이너 재실행 + docker-compose pull web + docker-compose up -d --build web + + # 후처리 작업 (DB 동기화 및 정적파일 수집) + docker-compose exec -T web python manage.py migrate + docker-compose exec -T web python manage.py collectstatic --noinput \ No newline at end of file From a6ee796f72d009735ae11a5ae0505ffb09fa8929 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Wed, 4 Feb 2026 20:34:39 +0900 Subject: [PATCH 126/380] =?UTF-8?q?fix:=20deploy=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1352a06..512dde8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,7 +28,7 @@ jobs: with: context: . push: true - tags: ghcr.io/${{ github.repository }}/web:latest + tags: ghcr.io/pirogramming/startlinedev/web:latest deploy: needs: build-and-push From bc3104191a2abe5a301e2cc8b9ece5abc4927e39 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 20:56:54 +0900 Subject: [PATCH 127/380] feat : team.html & css --- static/css/main.css | 2 +- static/css/team.css | 104 ++++++++++++++++++++++++++++++++++ static/images/default_img.png | Bin 0 -> 13634 bytes templates/base.html | 2 - templates/teams/team.html | 66 ++++++++++++++++++++- 5 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 static/css/team.css create mode 100644 static/images/default_img.png diff --git a/static/css/main.css b/static/css/main.css index 9636992..4af3fc9 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -252,7 +252,7 @@ body { .m_team_result:hover { background: #4272EF; - transition: 0.5 ease-in-out; + transition: 0.3s ease-in-out; } .m_team_result p { diff --git a/static/css/team.css b/static/css/team.css new file mode 100644 index 0000000..4c5eef9 --- /dev/null +++ b/static/css/team.css @@ -0,0 +1,104 @@ +body { + background: #f6f8ff; +} + +/* 매칭 성공 */ +.team_success { + margin-top: 50px; + text-align: center; + align-items: center; +} + +.team_success h3 { + font-size: 25px; font-weight: 550; +} + +.team_member { + margin-top: 30px; + display: flex; + justify-content: center; gap: 20px; +} + +.t_member { + position: relative; + width: 17%; + height: 250px; + padding: 20px; + background: #fff; + border-radius: 20px; + box-shadow: 0 2px 2px 0 #999; +} + +.t_member .profile_img { + width: 120px; height: 120px; +} + +.t_member .level_img { + width: 40px; height: 40px; + position: absolute; + top: 100px; right: 30px; +} + +.t_member .u_name { + margin-top: 20px; + font-size: 18px; font-weight: 500; +} + +.t_member .u_stack { + margin: 10px auto 0; + width: 80%; + padding: 5px 10px; + color: #FFCE53; + border-radius: 20px; + box-shadow: 1px 2px 2px 1px #FFCE53; + font-size: 16px; +} + +.go_project { + display: block; + width: 20%; height: 40px; + margin: 50px auto 0; + padding: 10px 10px; + background: #1F4CC0; + text-decoration: none; color: #fff; + border-radius: 20px; +} + +.go_project p { + font-size: 15px; font-weight: 500; +} + +.go_project:hover { + background: #4272EF; + transition: 0.3s ease; +} + +/* 매칭 실패 */ +.team_fail { + margin-top: 15%; + text-align: center; + align-items: center; +} + +.team_fail h3 { + font-size: 30px; font-weight: 550; +} + +.team_fail a { + display: block; + width: 20%; height: 40px; + margin: 50px auto 0; + padding: 10px 10px; + background: #1F4CC0; + text-decoration: none; color: #fff; + border-radius: 20px; +} + +.team_fail a p { + font-size: 18px; font-weight: 500; +} + +.team_fail a:hover { + background: #4272EF; + transition: 0.3s ease; +} \ No newline at end of file diff --git a/static/images/default_img.png b/static/images/default_img.png new file mode 100644 index 0000000000000000000000000000000000000000..620fe6081985ac479938366cf1e996b247e51cfb GIT binary patch literal 13634 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 bu4oQOt0=T+XTE@cc>=W6^&eKLq9Xo3+5eil literal 0 HcmV?d00001 diff --git a/templates/base.html b/templates/base.html index 57f7c30..de45e44 100644 --- a/templates/base.html +++ b/templates/base.html @@ -41,7 +41,5 @@

KITUP

- - \ No newline at end of file diff --git a/templates/teams/team.html b/templates/teams/team.html index 3c84127..f31a444 100644 --- a/templates/teams/team.html +++ b/templates/teams/team.html @@ -1,6 +1,66 @@ {% extends 'base.html' %} {% load static %} {% block header %} +{% endblock %} {% block content %} + +
+

+ 팀 매칭이 완료 되었어요.
+ 내 프로젝트에서 업데이트 된 팀 정보를 확인해보세요. +

+
+
+ 프로필사진 + level +

user1 (Lv3)

+

프론트엔드

+
+
+ 프로필사진 + level +

user1 (Lv3)

+

프론트엔드

+
+
+ 프로필사진 + level +

user1 (Lv3)

+

프론트엔드

+
+
+ 프로필사진 + level +

user1 (Lv3)

+

프론트엔드

+
+
+ 프로필사진 + level +

user1 (Lv3)

+

프론트엔드

+
+
+

내 프로젝트로 이동

+
+ + {% endblock %} -{% block content %} -팀결과 -{% endblock %} \ No newline at end of file From b2aa63d2dec8e9de827aac21adcd403ff48e0496 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Wed, 4 Feb 2026 21:00:58 +0900 Subject: [PATCH 128/380] =?UTF-8?q?fix:=20main=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/main.html b/templates/main.html index a1d4de2..33dedcf 100644 --- a/templates/main.html +++ b/templates/main.html @@ -55,10 +55,10 @@

KITUP 프로젝트

- +

진행된 KITUP 프로젝트가 없습니다.

-
+
{% endblock %} From 2a9a45693a12c4c311f4e2aee2e68901d3a95efe Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Wed, 4 Feb 2026 23:14:25 +0900 Subject: [PATCH 129/380] =?UTF-8?q?test:=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8C=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/views.py | 13 +++++++++---- config/settings.py | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 828693b..04a0ca4 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -3,8 +3,8 @@ from django.contrib import messages from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated -from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import PermissionDenied, NotAuthenticated from drf_spectacular.utils import extend_schema_view, extend_schema from .models import Retrospective @@ -75,13 +75,16 @@ def note_delete(request, note_id): ) class RetrospectiveViewSet(viewsets.ModelViewSet): serializer_class = RetrospectiveReadSerializer - permission_classes = [IsAuthenticated] + permission_classes = [AllowAny] def get_queryset(self): + u = self.request.user + if not u.is_authenticated: + return Retrospective.objects.none() # 내 회고만 return ( Retrospective.objects - .filter(user=self.request.user) + .filter(user=self.request.user.id) .select_related("project", "user") .order_by("-created_at") ) @@ -92,6 +95,8 @@ def perform_create(self, serializer): def get_object(self): # pk 직접 접근 차단 + if not self.request.user.is_authenticated: + raise NotAuthenticated() obj = super().get_object() if obj.user_id != self.request.user.id: raise PermissionDenied("본인 회고만 접근 가능합니다.") diff --git a/config/settings.py b/config/settings.py index 4dd951d..21950ea 100644 --- a/config/settings.py +++ b/config/settings.py @@ -258,5 +258,6 @@ "SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"], "SERVERS": [ {"url": "http://localhost:8000", "description": "Development"}, + {"url": "http://127.0.0.1:8000", "description": "local"}, ], } From 04d5357027b5d70e13e05158c6a47c8a808fab62 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Thu, 5 Feb 2026 00:05:03 +0900 Subject: [PATCH 130/380] =?UTF-8?q?fix:=20Retrospectives=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0002_retrospective_bookmarked.py | 17 +++++++++++++ .../0003_alter_retrospective_project.py | 25 +++++++++++++++++++ apps/reflections/models.py | 2 ++ apps/reflections/serializers.py | 3 +++ apps/reflections/views.py | 17 ++++++++++--- config/settings.py | 5 +++- 6 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 apps/reflections/migrations/0002_retrospective_bookmarked.py create mode 100644 apps/reflections/migrations/0003_alter_retrospective_project.py diff --git a/apps/reflections/migrations/0002_retrospective_bookmarked.py b/apps/reflections/migrations/0002_retrospective_bookmarked.py new file mode 100644 index 0000000..0034240 --- /dev/null +++ b/apps/reflections/migrations/0002_retrospective_bookmarked.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.10 on 2026-02-04 14:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reflections", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="retrospective", + name="bookmarked", + field=models.BooleanField(default=False, help_text="찜 여부"), + ), + ] diff --git a/apps/reflections/migrations/0003_alter_retrospective_project.py b/apps/reflections/migrations/0003_alter_retrospective_project.py new file mode 100644 index 0000000..a0e7091 --- /dev/null +++ b/apps/reflections/migrations/0003_alter_retrospective_project.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.10 on 2026-02-04 14:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0003_project_is_favorite_project_project_image_and_more"), + ("reflections", "0002_retrospective_bookmarked"), + ] + + operations = [ + migrations.AlterField( + model_name="retrospective", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="retrospectives", + to="projects.project", + ), + ), + ] diff --git a/apps/reflections/models.py b/apps/reflections/models.py index 5888d4e..4dfb888 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -13,6 +13,8 @@ class Retrospective(models.Model): "projects.Project", on_delete=models.CASCADE, related_name="retrospectives", + # TODO 테스트용 nullable + null=True, blank=True, ) user = models.ForeignKey( diff --git a/apps/reflections/serializers.py b/apps/reflections/serializers.py index 7754ae7..1bd4271 100644 --- a/apps/reflections/serializers.py +++ b/apps/reflections/serializers.py @@ -39,3 +39,6 @@ class RetrospectiveWriteSerializer(serializers.ModelSerializer): class Meta: model = Retrospective fields = ["project", "title", "content_md", "bookmarked"] + extra_kwargs = { + "project": {"required": False, "allow_null": True}, + } \ No newline at end of file diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 04a0ca4..e56b014 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -8,7 +8,7 @@ from drf_spectacular.utils import extend_schema_view, extend_schema from .models import Retrospective -from .serializers import RetrospectiveReadSerializer +from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer # from .models import Reflection @@ -75,7 +75,7 @@ def note_delete(request, note_id): ) class RetrospectiveViewSet(viewsets.ModelViewSet): serializer_class = RetrospectiveReadSerializer - permission_classes = [AllowAny] + permission_classes = [IsAuthenticated] def get_queryset(self): u = self.request.user @@ -84,13 +84,14 @@ def get_queryset(self): # 내 회고만 return ( Retrospective.objects - .filter(user=self.request.user.id) + .filter(user_id=self.request.user.id) .select_related("project", "user") .order_by("-created_at") ) def perform_create(self, serializer): # user는 서버에서 강제 + print("AUTH:", self.request.user, self.request.user.is_authenticated) serializer.save(user=self.request.user) def get_object(self): @@ -100,4 +101,12 @@ def get_object(self): obj = super().get_object() if obj.user_id != self.request.user.id: raise PermissionDenied("본인 회고만 접근 가능합니다.") - return obj \ No newline at end of file + return obj + + def get_serializer_class(self): + if self.action in ("list", "retrieve"): + return RetrospectiveReadSerializer + return RetrospectiveWriteSerializer + + + diff --git a/config/settings.py b/config/settings.py index 21950ea..060b228 100644 --- a/config/settings.py +++ b/config/settings.py @@ -244,7 +244,10 @@ "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, # 개발 중에는 인증 없이 API 테스트 가능하도록 설정 - "DEFAULT_AUTHENTICATION_CLASSES": [], + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", + ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.AllowAny", ], From 82bbb0f269e4072cf80765d811019ce60951ccc8 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Thu, 5 Feb 2026 00:15:23 +0900 Subject: [PATCH 131/380] =?UTF-8?q?docs:=20gitignore=EC=97=90=20.zip=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++++- project.zip | Bin 177393 -> 0 bytes 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 project.zip diff --git a/.gitignore b/.gitignore index d876c8c..7fd024b 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,7 @@ htmlcov/ .cache/ *.pem -kitup-key.pem \ No newline at end of file +kitup-key.pem + +# AI용 프로젝트 압축 파일 +*.zip \ No newline at end of file diff --git a/project.zip b/project.zip deleted file mode 100644 index 901c838a428d74e5058795ad9d96cbd1b9d888cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177393 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 z 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 Date: Thu, 5 Feb 2026 00:44:05 +0900 Subject: [PATCH 132/380] =?UTF-8?q?Feat:=20Retrospective=20=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=20CRUD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/forms.py | 2 +- apps/reflections/views.py | 48 +++++++++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/apps/reflections/forms.py b/apps/reflections/forms.py index 3b4ca93..54fcd2f 100644 --- a/apps/reflections/forms.py +++ b/apps/reflections/forms.py @@ -7,6 +7,6 @@ class Meta: model = Retrospective fields = ["project", "title", "content_md", "bookmarked"] widgets = { - "title": forms.TextInput(attrs={"placeholder": "제목(선택)"}), + "title": forms.TextInput(attrs={"placeholder": "제목"}), "content_md": forms.Textarea(attrs={"rows": 16, "placeholder": "마크다운으로 작성하세요"}), } diff --git a/apps/reflections/views.py b/apps/reflections/views.py index e56b014..730ddea 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -9,13 +9,19 @@ from drf_spectacular.utils import extend_schema_view, extend_schema from .models import Retrospective from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer -# from .models import Reflection +from .forms import RetrospectiveForm @login_required def note_list(request): """회고 목록""" # TODO: 회고 목록 로직 구현 + notes = ( + Retrospective.objects + .filter(user = request.user) # 해당 유저의 회고만 + .select_related("project") # + .order_by("-created_at") # 시간 최신순 + ) return render(request, "reflections/note_list.html") @@ -24,18 +30,27 @@ def note_create(request): """회고 작성""" # TODO: 회고 작성 로직 구현 if request.method == "POST": - # 폼 처리 로직 - pass - return render(request, "reflections/note_create.html") + form = RetrospectiveForm(request.POST) + if form.is_valid(): + note = form.save(commit=False) + note.user = request.user + note.save() + messages.success(request, "회고가 작성되었습니다.") + return redirect("reflections:note_detail", note_id=note.id) + else: + form = RetrospectiveForm() + + context = {"form": form} + return render(request, "reflections/note_create.html", context) @login_required def note_detail(request, note_id): """회고 상세""" # TODO: 회고 상세 로직 구현 - # note = get_object_or_404(Reflection, id=note_id) + note = get_object_or_404(Retrospective, id=note_id, user=request.user) context = { - "note_id": note_id, + "note": note, } return render(request, "reflections/note_detail.html", context) @@ -44,12 +59,20 @@ def note_detail(request, note_id): def note_update(request, note_id): """회고 수정""" # TODO: 회고 수정 로직 구현 - # note = get_object_or_404(Reflection, id=note_id) + note = get_object_or_404(Retrospective, id=note_id, user=request.user) + if request.method == "POST": - # 폼 처리 로직 - pass + form = RetrospectiveForm(request.POST, instance=note) + if form.is_valid(): + form.save + messages.success(request, "회고가 수정되었습니다.") + return redirect("reflections:note_detail", note_id = note.id) + else: + form = RetrospectiveForm(instance=note) + context = { - "note_id": note_id, + "note": note, + "form": form, } return render(request, "reflections/note_update.html", context) @@ -58,13 +81,14 @@ def note_update(request, note_id): def note_delete(request, note_id): """회고 삭제""" # TODO: 회고 삭제 로직 구현 - # note = get_object_or_404(Reflection, id=note_id) + note = get_object_or_404(Retrospective, id=note_id) if request.method == "POST": - # note.delete() + note.delete() messages.success(request, "회고가 삭제되었습니다.") return redirect("reflections:note_list") return redirect("reflections:note_detail", note_id=note_id) + @extend_schema_view( list=extend_schema(summary="회고 목록 조회", tags=["Retrospectives"]), retrieve=extend_schema(summary="회고 상세 조회", tags=["Retrospectives"]), From 3cb628798b42a457fa87909fb140e38dcc9bbacb Mon Sep 17 00:00:00 2001 From: plumbestie Date: Thu, 5 Feb 2026 15:25:29 +0900 Subject: [PATCH 133/380] =?UTF-8?q?fix=20:=20team.css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/team.css b/static/css/team.css index 4c5eef9..a5cbb2e 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -1,5 +1,5 @@ body { - background: #f6f8ff; + background: #EAF0FF; } /* 매칭 성공 */ From 10c3fd71ac3de7ce4a3b00ae874a3fffc8da8007 Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 5 Feb 2026 16:04:04 +0900 Subject: [PATCH 134/380] =?UTF-8?q?docs:=20=EC=9D=B4=EC=8A=88=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 16 ++++++++++++++++ .github/ISSUE_TEMPLATE/feature.md | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b4cc3d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +## 📌 버그 설명 +무엇이 잘못 동작하는지 간단히 작성 + +## 📍 발생 위치 +API: `` + +## 🔁 재현 방법 +1. +2. +3. + +## ❗ 실제 결과 +에러 메시지 또는 로그 + +## ✅ 기대 결과 +정상 동작 설명 diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index bc7733b..cca7920 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -1,7 +1,7 @@ --- name: Feature about: 새로운 기능 추가 / 개선 -title: "[FEAT] " +title: "[Feat] " labels: ["feature"] assignees: [] --- From 93ace6ef7ad8e8910bf0084ed6e07117cabbac58 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Thu, 5 Feb 2026 17:38:24 +0900 Subject: [PATCH 135/380] =?UTF-8?q?docs:=20project=EC=9D=98=20nullable=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/reflections/models.py b/apps/reflections/models.py index 4dfb888..41385a2 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -14,6 +14,7 @@ class Retrospective(models.Model): on_delete=models.CASCADE, related_name="retrospectives", # TODO 테스트용 nullable + # 플젝 외의 개인 회고의 목적 있으면 nullable 유지 null=True, blank=True, ) From aa07c15ab6f045dbb1b66f985ead937f1bcd991f Mon Sep 17 00:00:00 2001 From: plumbestie Date: Thu, 5 Feb 2026 17:55:00 +0900 Subject: [PATCH 136/380] =?UTF-8?q?feat=20:=20dashboard.html=20&=20css=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 36 +++++++++++++++++++++++++++++++ templates/projects/dashboard.html | 8 ++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index e69de29..00ab492 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -0,0 +1,36 @@ +.p_header { + width: 90%; + height: 50px; + margin: 0 auto; + display: flex; +} + +.p_header a { + display: block; + width: 50%; + text-align: center; + padding: 15px; + text-decoration: none; + background: #eaf0ff; +} + +.p_header .p_dashboard { + background: #eaf0ff; + color: #1d294b; +} + +.p_header .p_mission { + background: #fff; + color: #cad9ff; +} + +.p_header .p_mission:hover { + background: #eaf0ff; + color: #1d294b; + transition: 0.3s ease-in-out; +} + +.p_header a p { + font-size: 18px; + font-weight: 700; +} diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index f34e873..d640a73 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -4,5 +4,11 @@ {% endblock %} {% block content %} -프로젝트 +

+

DASHBOARD

+

MISSION

+
+
+ +
{% endblock %} \ No newline at end of file From ac15f3c54e4cfe324f59c83cc779ade43ae74e36 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Thu, 5 Feb 2026 18:19:37 +0900 Subject: [PATCH 137/380] =?UTF-8?q?feat:=20=ED=9A=8C=EA=B3=A0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EA=B2=80=EC=83=89/=EC=A0=95=EB=A0=AC/=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/views.py | 76 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 730ddea..3c3df6a 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -1,6 +1,7 @@ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib import messages +from django.db.models import Q from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated, AllowAny @@ -11,18 +12,87 @@ from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer from .forms import RetrospectiveForm +from apps.projects.models import Project +from apps.teams.models import TeamMember @login_required def note_list(request): - """회고 목록""" + """ + 회고 목록 조회 + 쿼리스트링: + q: 검색 + roles: 스택 필터링 ("BACKEND", "PM" 식의 복수 선택 가능, none은 개인 회고 조회) + bookmarked: 북마크 필터링 + sort: 정렬 키워드 (new, old, title) + """ # TODO: 회고 목록 로직 구현 - notes = ( + qs = ( Retrospective.objects .filter(user = request.user) # 해당 유저의 회고만 .select_related("project") # .order_by("-created_at") # 시간 최신순 ) - return render(request, "reflections/note_list.html") + + # 검색(제목/본문/프로젝트명) + q = request.GET.get("q", "").strip() + if q: + qs = qs.filter( + Q(title__icontains=q) | + Q(content_md__icontains=q) | + Q(project__name__icontains=q) + ) + + # 스택 필터 + role_codes = request.GET.getlist("roles") + if role_codes : + get_personnal_retro = "none" in role_codes + + role_project_ids = ( + TeamMember.objects + .filter(user=request.user, role__code__in=role_codes ) + .values_list("team__project_id", flat=True) + .distinct() + ) + + if get_personnal_retro and role_project_ids: + qs = qs.filter( + Q(project__isnull=True) | + Q(project_id__in = role_project_ids) + ) + elif get_personnal_retro: + qs = qs.filter(project__isnull=True) + elif role_project_ids: + qs = qs.filter(project_id__in = role_project_ids) + + # 북마크 필터 + bookmarked = request.GET.get("bookmarked") + if bookmarked in ("1", "true", "True"): + qs = qs.filter(bookmarked=True) + + # 정렬 + sort = request.GET.get("sort", "new") + if sort == "old": + qs = qs.order_by("created_at") + elif sort == "title": + qs = qs.order_by("title") + else: + qs = qs.order_by("-created_at") + + my_projects = ( + Project.objects + .filter(member__user=request.user) + .distinct() + .order_by("name") + ) + + context = { + "notes" : qs, + "my_projects": my_projects, # 내 프로젝트 조회 -> 필터에 보여주기 + "q" : q, + "bookmarked": bookmarked, + "sort": sort, + } + return render(request, "reflections/note_list.html", context) @login_required From 174abc41cad2c42a96bb66cf36d3f594b69fc7c6 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 00:49:04 +0900 Subject: [PATCH 138/380] =?UTF-8?q?feat:=20=ED=9A=8C=EA=B3=A0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A3=BC=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../retrospective_default.json | 90 +++++++++++++++++++ apps/reflections/models.py | 18 +++- apps/reflections/serializers.py | 50 ++++++++++- .../services/retrospective_guide.py | 47 ++++++++++ apps/reflections/views.py | 80 ++++++++++++----- 5 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 apps/reflections/guide_templates/retrospective_default.json create mode 100644 apps/reflections/services/retrospective_guide.py diff --git a/apps/reflections/guide_templates/retrospective_default.json b/apps/reflections/guide_templates/retrospective_default.json new file mode 100644 index 0000000..baaeecc --- /dev/null +++ b/apps/reflections/guide_templates/retrospective_default.json @@ -0,0 +1,90 @@ +{ + "key": "default", + "title": "오늘의 회고 작성 가이드", + "intro": [ + "아래 질문에 따라 오늘의 작업을 정리해 주세요.", + "단순한 감정 기록이 아니라, 업무 내용과 판단 과정이 남는 회고를 목표로 합니다." + ], + "questions": [ + { + "id": "q1_work_done", + "order": 1, + "title": "오늘 내가 맡아서 수행한 업무는 무엇이었나요?", + "hint": "오늘 직접 담당하여 진행한 작업을 구체적으로 적어주세요.", + "examples": [ + "user 모델 설계 및 마이그레이션", + "main.html 레이아웃 구현", + "레벨 진단 설문 로직 구현" + ] + }, + { + "id": "q2_why_design", + "order": 2, + "title": "이 업무를 왜 이렇게 설계하거나 구현했나요?", + "hint": "해당 작업에서 어떤 선택을 했고, 그 이유는 무엇이었는지 적어주세요.", + "examples": [ + "추후 역할별 레벨 확장을 고려해 테이블을 분리했습니다.", + "프론트와 데이터 연결을 단순화하기 위해 URL 구조를 정리했습니다." + ] + }, + { + "id": "q3_blockers", + "order": 3, + "title": "진행 중에 어려웠던 점이나 막혔던 부분은 무엇이었나요?", + "hint": "작업하면서 마주한 문제, 혼란스러웠던 점을 적어주세요.", + "examples": [ + "allauth와 커스텀 유저 모델 충돌 문제", + "프론트에서 필요한 데이터 구조와 백엔드 설계 간의 차이" + ] + }, + { + "id": "q4_resolution", + "order": 4, + "title": "그 문제를 어떻게 해결했나요? (또는 왜 아직 해결하지 못했나요?)", + "hint": "문제 해결 과정이나, 해결하지 못했다면 그 이유를 적어주세요.", + "examples": [ + "공식 문서를 참고해 settings 구조를 수정했습니다.", + "시간 부족으로 임시 처리 후 이슈로 남겼습니다." + ] + }, + { + "id": "q5_sync", + "order": 5, + "title": "오늘 팀원과 공유하거나 조정한 내용은 무엇이었나요?", + "hint": "협업 과정에서 의사소통하거나 합의한 내용을 적어주세요.", + "examples": [ + "프론트와 API 응답 구조에 대해 논의했습니다.", + "PM과 기능 범위 조정을 진행했습니다." + ] + }, + { + "id": "q6_learnings", + "order": 6, + "title": "오늘 작업을 통해 새롭게 알게 된 점이나 배운 점은 무엇인가요?", + "hint": "기술, 협업 방식, 문제 해결 측면에서의 배움을 적어주세요.", + "examples": [ + "Docker 멀티 컨테이너 구조에 대한 이해가 깊어졌습니다.", + "API 설계 시 일관성이 중요하다는 것을 느꼈습니다." + ] + }, + { + "id": "q7_next", + "order": 7, + "title": "다음 작업에서 개선하거나 달리 해보고 싶은 점은 무엇인가요?", + "hint": "다음 작업을 위해 개선하고 싶은 부분이나 계획을 적어주세요.", + "examples": [ + "API 명세를 더 일찍 공유하고 싶습니다.", + "마이그레이션 파일 관리를 더 체계적으로 하고 싶습니다." + ] + }, + { + "id": "q8_one_liner", + "order": 8, + "title": "오늘 작업의 결과를 한 문장으로 요약한다면?", + "hint": "오늘의 작업을 핵심 한 문장으로 정리해 주세요.", + "examples": [ + "팀 매칭 신청 플로우의 백엔드 구조를 완성했습니다." + ] + } + ] +} diff --git a/apps/reflections/models.py b/apps/reflections/models.py index 41385a2..60a596a 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -24,6 +24,13 @@ class Retrospective(models.Model): related_name="retrospectives", ) + # 어떤 질문 템플릿으로 작성했는지 (default/compact) + template_key = models.CharField( + max_length=32, + default="default", + help_text="회고 질문 템플릿 키 (e.g., default, compact)", + ) + title = models.CharField( max_length=120, null=True, @@ -31,13 +38,21 @@ class Retrospective(models.Model): help_text="회고 제목", ) + # 질문별 답변 원본(JSON): { "q1_work_done": "...md...", ... } + answers_json = models.JSONField( + default=dict, + blank=True, + help_text="질문별 답변 원본(JSON). 값은 마크다운 텍스트 문자열을 권장", + ) + content_md = models.TextField( help_text="회고 내용 (마크다운)", + blank=True, + default="", ) bookmarked = models.BooleanField( default= False, - # TODO 회고인데 찜 -> 어감 이상함, 즐겨찾기나 북마크로 수정 help_text="찜 여부" ) @@ -49,6 +64,7 @@ class Meta: indexes = [ models.Index(fields=["project", "user"]), models.Index(fields=["created_at"]), + models.Index(fields=["template_key", "created_at"]), ] def __str__(self) -> str: diff --git a/apps/reflections/serializers.py b/apps/reflections/serializers.py index 1bd4271..b7a853f 100644 --- a/apps/reflections/serializers.py +++ b/apps/reflections/serializers.py @@ -1,6 +1,7 @@ # reflections/serializers.py from rest_framework import serializers from .models import Retrospective +from .services.retrospective_guide import load_guide, build_markdown class RetrospectiveReadSerializer(serializers.ModelSerializer): @@ -15,6 +16,8 @@ class Meta: "user", "username", "title", + "template_key", + "answers_json", "content_md", "bookmarked", "created_at", @@ -35,10 +38,49 @@ def validate_content_md(self, value): class RetrospectiveWriteSerializer(serializers.ModelSerializer): - # 입력은 project FK를 그대로 받는게 Swagger에서 제일 단순합니다. (정수) class Meta: model = Retrospective - fields = ["project", "title", "content_md", "bookmarked"] + fields = ( + "id", + "project", + "title", + "template_key", + "answers_json", + "bookmarked", + ) extra_kwargs = { - "project": {"required": False, "allow_null": True}, - } \ No newline at end of file + "template_key": {"required": False}, + "answers_json": {"required": False}, + } + + def validate_answers_json(self, v): + if v is None: + return {} + if not isinstance(v, dict): + raise serializers.ValidationError("answers_json은 객체(JSON dict)여야 합니다.") + return v + + def _rebuild_content_md(self, instance_or_data: dict, template_key: str, answers_json: dict, title: str | None): + guide = load_guide(template_key) + return build_markdown(guide, answers_json, title=title) + + def create(self, validated_data): + template_key = validated_data.get("template_key") or "default" + answers_json = validated_data.get("answers_json") or {} + title = validated_data.get("title") + + validated_data["content_md"] = self._rebuild_content_md( + validated_data, template_key, answers_json, title + ) + return super().create(validated_data) + + def update(self, instance, validated_data): + # 기존 값과 병합해서 md 재생성 + template_key = validated_data.get("template_key", instance.template_key or "default") + answers_json = validated_data.get("answers_json", instance.answers_json or {}) + title = validated_data.get("title", instance.title) + + validated_data["content_md"] = self._rebuild_content_md( + validated_data, template_key, answers_json, title + ) + return super().update(instance, validated_data) \ No newline at end of file diff --git a/apps/reflections/services/retrospective_guide.py b/apps/reflections/services/retrospective_guide.py new file mode 100644 index 0000000..f468419 --- /dev/null +++ b/apps/reflections/services/retrospective_guide.py @@ -0,0 +1,47 @@ +import json +from pathlib import Path +from django.conf import settings + +GUIDE_DIR = Path(settings.BASE_DIR) / "reflections" / "guides" +ALLOWED_TPLS = {"default", "compact"} # 지금은 default만 쓰면 {"default"}로 + +def load_guide(template_key: str) -> dict: + """ + ### load_guide : {질문}.json 파일을 읽고 dict 형식으로 리턴 + :param tpl:str : retrospective_{tpl}.json 형식으로 읽을 질문 템플릿 지정 + :return -> dict: .json 파일을 변환한 dict + """ + if template_key not in ALLOWED_TPLS: + template_key = "default" + path = GUIDE_DIR / f"retrospective_{template_key}.json" + return json.loads(path.read_text(encoding="utf-8")) + +def build_markdown(guide: dict, answers: dict, title: str | None = None) -> str: + lines = [] + title_text = title.strip() if title else guide.get("title", "회고") + lines.append(f"# 📝 {title_text}") + lines.append("") + + intro = guide.get("intro") or [] + if intro: + lines.append("> " + "\n> ".join(intro)) + lines.append("") + lines.append("---") + lines.append("") + + questions = sorted(guide.get("questions", []), key=lambda x: x.get("order", 0)) + for q in questions: + order = q.get("order") + qtitle = q.get("title", "") + if order: + lines.append(f"## {order}️⃣ {qtitle}") + else: + lines.append(f"## {qtitle}") + lines.append("") + ans = (answers.get(q.get("id")) or "").strip() + lines.append(ans if ans else "_(작성 내용 없음)_") + lines.append("") + lines.append("---") + lines.append("") + + return "\n".join(lines).rstrip() + "\n" diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 3c3df6a..82d3452 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -2,30 +2,36 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db.models import Q +from django.conf import settings +# from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.exceptions import PermissionDenied, NotAuthenticated - +import json from drf_spectacular.utils import extend_schema_view, extend_schema + from .models import Retrospective from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer from .forms import RetrospectiveForm +from .services.retrospective_guide import load_guide, build_markdown from apps.projects.models import Project from apps.teams.models import TeamMember + @login_required def note_list(request): """ 회고 목록 조회 + 쿼리스트링: - q: 검색 - roles: 스택 필터링 ("BACKEND", "PM" 식의 복수 선택 가능, none은 개인 회고 조회) - bookmarked: 북마크 필터링 - sort: 정렬 키워드 (new, old, title) + :q: 검색 + :roles: 스택 필터링 ("BACKEND", "PM" 식의 복수 선택 가능, none은 개인 회고 조회) + :bookmarked: 북마크 필터링 + :sort: 정렬 키워드 (new, old, title) """ - # TODO: 회고 목록 로직 구현 qs = ( Retrospective.objects .filter(user = request.user) # 해당 유저의 회고만 @@ -97,20 +103,41 @@ def note_list(request): @login_required def note_create(request): - """회고 작성""" - # TODO: 회고 작성 로직 구현 - if request.method == "POST": - form = RetrospectiveForm(request.POST) - if form.is_valid(): - note = form.save(commit=False) - note.user = request.user - note.save() - messages.success(request, "회고가 작성되었습니다.") - return redirect("reflections:note_detail", note_id=note.id) - else: - form = RetrospectiveForm() + """ + 회고 작성 + + 쿼리스트링: + :tpl: 선택할 질문 템플릿 (현재는 default 하나만) + + """ + tpl = request.GET.get("tpl") or "default" + guide = load_guide(tpl) - context = {"form": form} + if request.method == "POST": + title = (request.POST.get("title") or "빈 제목").strip() + if not title: + context = {"guide": guide, "tpl": tpl, "error": "제목은 필수입니다."} + return render(request, "reflections/note_create.html", context) + + answers = dict() # qid: "답변 내용" 형식 + for q in guide["questions"]: + qid = q["id"] + answers[qid] = (request.POST.get(f"a__{qid}") or "빈 답변 내용").strip() + + content_md = build_markdown(guide, answers) + + Retrospective.objects.create( + author= request.user, + template_key=tpl, + title=title, + answers_json=answers, + content_md = content_md, + ) + return redirect("reflections:note_list") + context = { + "guide": guide, + "tpl": tpl, + } return render(request, "reflections/note_create.html", context) @@ -170,6 +197,8 @@ def note_delete(request, note_id): class RetrospectiveViewSet(viewsets.ModelViewSet): serializer_class = RetrospectiveReadSerializer permission_classes = [IsAuthenticated] + # filter_backends = [DjangoFilterBackend] + filterset_fields = ["project", "bookmarked", "template_key"] def get_queryset(self): u = self.request.user @@ -185,7 +214,7 @@ def get_queryset(self): def perform_create(self, serializer): # user는 서버에서 강제 - print("AUTH:", self.request.user, self.request.user.is_authenticated) + # print("AUTH:", self.request.user, self.request.user.is_authenticated) serializer.save(user=self.request.user) def get_object(self): @@ -197,10 +226,19 @@ def get_object(self): raise PermissionDenied("본인 회고만 접근 가능합니다.") return obj + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + def get_serializer_class(self): if self.action in ("list", "retrieve"): return RetrospectiveReadSerializer return RetrospectiveWriteSerializer - + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + obj = serializer.instance + read = RetrospectiveReadSerializer(obj, context=self.get_serializer_context()) + return Response(read.data, status=status.HTTP_201_CREATED) From 678943fbcf03ded1d61aea5fdc9e0f2afe60a53e Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 02:05:35 +0900 Subject: [PATCH 139/380] =?UTF-8?q?chore:=20migrations=20=EC=A7=84?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...004_retrospective_answers_json_and_more.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 apps/reflections/migrations/0004_retrospective_answers_json_and_more.py diff --git a/apps/reflections/migrations/0004_retrospective_answers_json_and_more.py b/apps/reflections/migrations/0004_retrospective_answers_json_and_more.py new file mode 100644 index 0000000..e2d7ccb --- /dev/null +++ b/apps/reflections/migrations/0004_retrospective_answers_json_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2.10 on 2026-02-05 15:54 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0003_project_is_favorite_project_project_image_and_more"), + ("reflections", "0003_alter_retrospective_project"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="retrospective", + name="answers_json", + field=models.JSONField( + blank=True, + default=dict, + help_text="질문별 답변 원본(JSON). 값은 마크다운 텍스트 문자열을 권장", + ), + ), + migrations.AddField( + model_name="retrospective", + name="template_key", + field=models.CharField( + default="default", + help_text="회고 질문 템플릿 키 (e.g., default, compact)", + max_length=32, + ), + ), + migrations.AlterField( + model_name="retrospective", + name="content_md", + field=models.TextField(blank=True, default="", help_text="회고 내용 (마크다운)"), + ), + migrations.AddIndex( + model_name="retrospective", + index=models.Index( + fields=["template_key", "created_at"], + name="retrospecti_templat_94e10e_idx", + ), + ), + ] From 20473fbdc9c0aa568f98023685e5e0a8f6e585ff Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 02:06:26 +0900 Subject: [PATCH 140/380] =?UTF-8?q?fix:=20API=20=EC=83=81=EC=9D=98=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EB=94=94=EB=B2=84=EA=B9=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/retrospective_guide.py | 7 +- apps/reflections/views.py | 148 +++++++++++++----- 2 files changed, 117 insertions(+), 38 deletions(-) diff --git a/apps/reflections/services/retrospective_guide.py b/apps/reflections/services/retrospective_guide.py index f468419..4f55a23 100644 --- a/apps/reflections/services/retrospective_guide.py +++ b/apps/reflections/services/retrospective_guide.py @@ -1,8 +1,9 @@ import json from pathlib import Path -from django.conf import settings +from django.apps import apps -GUIDE_DIR = Path(settings.BASE_DIR) / "reflections" / "guides" +APP_PATH = Path(apps.get_app_config("reflections").path) +GUIDE_DIR = APP_PATH / "guide_templates" ALLOWED_TPLS = {"default", "compact"} # 지금은 default만 쓰면 {"default"}로 def load_guide(template_key: str) -> dict: @@ -34,7 +35,7 @@ def build_markdown(guide: dict, answers: dict, title: str | None = None) -> str: order = q.get("order") qtitle = q.get("title", "") if order: - lines.append(f"## {order}️⃣ {qtitle}") + lines.append(f"## {order} {qtitle}") else: lines.append(f"## {qtitle}") lines.append("") diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 82d3452..76a0313 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -2,15 +2,17 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db.models import Q -from django.conf import settings -# from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import viewsets +from rest_framework import viewsets, status from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.exceptions import PermissionDenied, NotAuthenticated -import json -from drf_spectacular.utils import extend_schema_view, extend_schema +from drf_spectacular.utils import ( + extend_schema_view, + extend_schema, + OpenApiParameter, + OpenApiTypes, +) from .models import Retrospective from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer @@ -45,7 +47,7 @@ def note_list(request): qs = qs.filter( Q(title__icontains=q) | Q(content_md__icontains=q) | - Q(project__name__icontains=q) + Q(project__title__icontains=q) ) # 스택 필터 @@ -86,9 +88,11 @@ def note_list(request): my_projects = ( Project.objects + # Project -> team 에서 멤버에 포함되는지 여부 로직 + # TODO 로직 확인해보기 .filter(member__user=request.user) .distinct() - .order_by("name") + .order_by("title") ) context = { @@ -127,7 +131,7 @@ def note_create(request): content_md = build_markdown(guide, answers) Retrospective.objects.create( - author= request.user, + user= request.user, template_key=tpl, title=title, answers_json=answers, @@ -161,7 +165,7 @@ def note_update(request, note_id): if request.method == "POST": form = RetrospectiveForm(request.POST, instance=note) if form.is_valid(): - form.save + form.save() messages.success(request, "회고가 수정되었습니다.") return redirect("reflections:note_detail", note_id = note.id) else: @@ -178,7 +182,7 @@ def note_update(request, note_id): def note_delete(request, note_id): """회고 삭제""" # TODO: 회고 삭제 로직 구현 - note = get_object_or_404(Retrospective, id=note_id) + note = get_object_or_404(Retrospective, id=note_id, user=request.user) if request.method == "POST": note.delete() messages.success(request, "회고가 삭제되었습니다.") @@ -187,7 +191,41 @@ def note_delete(request, note_id): @extend_schema_view( - list=extend_schema(summary="회고 목록 조회", tags=["Retrospectives"]), + list=extend_schema( + summary="회고 목록 조회", + tags=["Retrospectives"], + parameters=[ + OpenApiParameter( + name="q", + type=OpenApiTypes.STR, + required=False, + location=OpenApiParameter.QUERY, + description="검색 (title/content_md/project.name 부분일치)", + ), + OpenApiParameter( + name="roles", + type=OpenApiTypes.STR, + required=False, + location=OpenApiParameter.QUERY, + description='스택 필터(복수 가능). 예: roles=BACKEND&roles=PM 또는 roles=none(개인회고)', + many=True, + ), + OpenApiParameter( + name="bookmarked", + type=OpenApiTypes.STR, + required=False, + location=OpenApiParameter.QUERY, + description='북마크 필터. true/1/True면 bookmarked=True', + ), + OpenApiParameter( + name="sort", + type=OpenApiTypes.STR, + required=False, + location=OpenApiParameter.QUERY, + description="정렬 (new, old, title). 기본 new", + ), + ], + ), retrieve=extend_schema(summary="회고 상세 조회", tags=["Retrospectives"]), create=extend_schema(summary="회고 생성", tags=["Retrospectives"]), update=extend_schema(summary="회고 전체 수정", tags=["Retrospectives"]), @@ -195,50 +233,90 @@ def note_delete(request, note_id): destroy=extend_schema(summary="회고 삭제", tags=["Retrospectives"]), ) class RetrospectiveViewSet(viewsets.ModelViewSet): - serializer_class = RetrospectiveReadSerializer permission_classes = [IsAuthenticated] - # filter_backends = [DjangoFilterBackend] - filterset_fields = ["project", "bookmarked", "template_key"] + + def get_serializer_class(self): + if self.action in ("list", "retrieve"): + return RetrospectiveReadSerializer + return RetrospectiveWriteSerializer def get_queryset(self): u = self.request.user if not u.is_authenticated: return Retrospective.objects.none() - # 내 회고만 - return ( + + # base qs + qs = ( Retrospective.objects - .filter(user_id=self.request.user.id) + .filter(user=u) # 해당 유저의 회고만 .select_related("project", "user") - .order_by("-created_at") + .order_by("-created_at") # 기본 최신순 ) + # 검색(제목/본문/프로젝트명) + q = (self.request.query_params.get("q") or "").strip() + if q: + qs = qs.filter( + Q(title__icontains=q) | + Q(content_md__icontains=q) | + Q(project__title__icontains=q) + ) + + # 스택 필터(roles=BACKEND&roles=PM&roles=none ...) + role_codes = self.request.query_params.getlist("roles") + if role_codes: + get_personal_retro = "none" in role_codes + + # 원본 로직 그대로: TeamMember에서 role__code로 필터, project_id 목록 추출 + role_project_ids = ( + TeamMember.objects + .filter(user=u, role__code__in=role_codes) + .values_list("team__project_id", flat=True) + .distinct() + ) + + if get_personal_retro and role_project_ids: + qs = qs.filter(Q(project__isnull=True) | Q(project_id__in=role_project_ids)) + elif get_personal_retro: + qs = qs.filter(project__isnull=True) + elif role_project_ids: + qs = qs.filter(project_id__in=role_project_ids) + else: + # roles는 있는데 매칭되는 project가 하나도 없고 none도 없으면 결과 없음 + qs = qs.none() + + # 북마크 필터 + bookmarked = self.request.query_params.get("bookmarked") + if bookmarked in ("1", "true", "True"): + qs = qs.filter(bookmarked=True) + + # 정렬 + sort = self.request.query_params.get("sort", "new") + if sort == "old": + qs = qs.order_by("created_at") + elif sort == "title": + qs = qs.order_by("title") + else: + qs = qs.order_by("-created_at") + + return qs + def perform_create(self, serializer): - # user는 서버에서 강제 - # print("AUTH:", self.request.user, self.request.user.is_authenticated) serializer.save(user=self.request.user) def get_object(self): - # pk 직접 접근 차단 if not self.request.user.is_authenticated: raise NotAuthenticated() obj = super().get_object() if obj.user_id != self.request.user.id: raise PermissionDenied("본인 회고만 접근 가능합니다.") return obj - - def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) - - def get_serializer_class(self): - if self.action in ("list", "retrieve"): - return RetrospectiveReadSerializer - return RetrospectiveWriteSerializer - + def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - obj = serializer.instance + # 생성 후 ReadSerializer로 응답(원하면 제거 가능) + write = RetrospectiveWriteSerializer(data=request.data, context=self.get_serializer_context()) + write.is_valid(raise_exception=True) + obj = write.save(user=request.user) read = RetrospectiveReadSerializer(obj, context=self.get_serializer_context()) return Response(read.data, status=status.HTTP_201_CREATED) - + From d0facdb8d48dd2f633509715178d3e094c0d09a4 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 02:18:54 +0900 Subject: [PATCH 141/380] =?UTF-8?q?fix=20:=20dashboard.html=20&=20css=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 351 ++++++++++++++++++++++++++++++ static/images/bookmark.png | Bin 0 -> 15278 bytes static/images/calender.png | Bin 0 -> 17545 bytes static/images/link.png | Bin 0 -> 19255 bytes static/images/progress.png | Bin 0 -> 23953 bytes static/images/report.png | Bin 0 -> 1321 bytes static/images/rule.png | Bin 0 -> 15573 bytes static/images/service_default.png | Bin 0 -> 2642 bytes static/images/team_flag.png | Bin 0 -> 12534 bytes templates/projects/dashboard.html | 138 +++++++++++- 10 files changed, 486 insertions(+), 3 deletions(-) create mode 100644 static/images/bookmark.png create mode 100644 static/images/calender.png create mode 100644 static/images/link.png create mode 100644 static/images/progress.png create mode 100644 static/images/report.png create mode 100644 static/images/rule.png create mode 100644 static/images/service_default.png create mode 100644 static/images/team_flag.png diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 00ab492..471be15 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -1,3 +1,4 @@ +/* 프로젝트 헤더 */ .p_header { width: 90%; height: 50px; @@ -34,3 +35,353 @@ font-size: 18px; font-weight: 700; } + +.dashboard { + width: 85%; + margin: 0 auto; + text-align: center; +} + +.dashboard > h3 { + font-size: 40px; font-weight: 650; + margin: 150px auto 30px; +} + +.dashboard > a { + display: block; + text-align: center; + width: 200px; height: 40px; + margin: 0 auto; + text-decoration: none; + background: #4272EF; + border-radius: 20px; +} + +.dashboard > a:hover { + background: #1F4CC0; + transition: 0.3s ease; +} + +.dashboard > a > p { + color: #fff; + font-size: 20px; font-weight: 500; + line-height: 40px; +} + +/* 프로젝트 개요 */ +.d_service { + margin: 60px auto 0; + text-align: center; +} + +.d_service > img { + width: 500px; +} + +.d_service > h3 { + margin-top: 20px; + font-size: 35px; font-weight: 600; +} + +.d_service > p { + margin-top: 15px; + font-size: 24px; +} + +/* 진행기간 & 즐겨찾기 */ +.d_period { + display: flex; + margin-top: 70px; + align-items: center; +} + +.d_period > img { + width: 40px; height: 40px; + margin-right: 7px; +} + +.d_period > p { + font-size: 18px; +} + +.d_period > p > .period_title { + font-weight: 600; font-size: 22px; + margin-right: 5px; +} + +.d_bookmark { + text-align: start; +} + +.b_title { + display: flex; + margin-top: 15px; + align-items: center; +} + +.b_title > img { + width: 40px; height: 40px; + margin-right: 7px; +} + +.b_title > p { + font-weight: 600; font-size: 22px; + margin-top: 2px; +} + +.b_content { + margin-top: 5px; + margin-left: 47px; +} + +.b_content > p { + font-size: 18px; +} + +hr { + margin-top: 50px; + border: 2px solid #F1F1F1; +} + +/* 컨텐츠 */ +.d_team { + text-align: start; + margin-top: 50px; +} + +.t_title { + display: flex; + align-items: center; + position: relative; +} + +.t_title > img { + width: 40px; height: 40px; + margin-right: 7px; +} + +.t_title > h3 { + font-weight: 600; font-size: 22px; + margin-top: 2px; +} + +.t_title > .t_report { + position: absolute; + top: 0; right: 0; + display: flex; + align-items: center; +} + +.t_report > img { + width: 20px; height: 20px; + margin-right: 7px; +} + +.t_report a { + text-decoration: none; + font-size: 14px; + color: #FF0000; +} + +.t_content { + margin-top: 5px; +} + +.d_team > .t_content > p { + margin-left: 47px; + font-size: 18px; +} + +.d_team > .t_content > p > .t_role { + font-weight: 600; font-size: 19px; + margin-right: 5px; +} + +.t_member { + margin-top: 20px; + display: flex; + justify-content: center; + gap: 15px +} + +.t_member > .member { + position: relative; + background: #fff; + box-shadow: 1px 2px 2px 1px #ccc; + padding: 15px 25px; + width: 200px; + text-align: center; + border-radius: 20px; +} + +.member > .profile { + width: 100px; height: 100px; +} + +.member > .level { + width: 35px; height: 35px; + position: absolute; + top: 90px; right: 35px; +} + +.member > h3 { + margin-top: 15px; + margin-bottom: 15px; + font-size: 20px; font-weight: 550; +} + +.member > .info { + text-align: start; + font-size: 16px; +} + +.member > .role_design { + font-size: 16px; + margin: 15px auto; + padding: 5px 0; + width: 70%; height: 30px; + border: 1px solid #00B9B0; + border-radius: 20px; + color: #00B9B0; + box-shadow: 1px 2px 2px 1px #00B9B0; +} + +.member > .role_frontend { + font-size: 16px; + margin: 15px auto; + padding: 5px 0; + width: 70%; height: 30px; + border: 1px solid #FFCE53; + border-radius: 20px; + color: #FFCE53; + box-shadow: 1px 2px 2px 1px #FFCE53; +} + +.member > .role_backend { + font-size: 16px; + margin: 15px auto; + padding: 5px 0; + width: 70%; height: 30px; + border: 1px solid #FF3E88; + border-radius: 20px; + color: #FF3E88; + box-shadow: 1px 2px 2px 1px #FF3E88; +} + +.d_rule { + margin-top: 50px; + text-align: start; +} + +.r_title { + display: flex; + align-items: center; + position: relative; +} + +.r_title > img { + width: 40px; height: 40px; + margin-right: 7px; +} + +.r_title > h3 { + font-weight: 600; font-size: 22px; + margin-top: 2px; +} + +.r_content { + margin-left: 47px; + margin-top: 5px; +} + +.r_content > p { + font-size: 18px; + line-height: 22px; +} + +.d_link { + margin-top: 30px; + text-align: start; +} + +.l_title { + display: flex; + align-items: center; + position: relative; +} + +.l_title > img { + width: 40px; height: 40px; + margin-right: 7px; +} + +.l_title > h3 { + font-weight: 600; font-size: 22px; + margin-top: 2px; +} + +.l_content { + margin-left: 47px; + margin-top: 5px; +} + +.l_content > p { + font-size: 18px; + line-height: 22px; +} + +.d_progress { + margin-top: 30px; + text-align: start; +} + +.p_title { + display: flex; + align-items: center; + position: relative; +} + +.p_title > img { + width: 50px; height: 50px; +} + +.p_title > h3 { + font-weight: 600; font-size: 22px; + margin-top: 2px; +} + +.p_content { + position: relative; + margin: 5px 0 100px 47px ; +} + +.p_bar { + width: 100%; height: 20px; + background: #ccc; + border-radius: 20px; +} + +.p_real { + position: absolute; + top: 0; + width: 60%; height: 20px; + background: #5A88FF; + border-radius: 20px; +} + +button { + width: 200px; height: 40px; + background: #4272EF; + border: none; + border-radius: 20px; + color: #fff; + font-size: 18px; font-weight: 500; + text-align: center; + margin-bottom: 200px; +} + +button:hover { + cursor: pointer; + background: #1F4CC0; + transition: 0.3s ease; +} diff --git a/static/images/bookmark.png b/static/images/bookmark.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d90cc2d8c769443f3e2d392d7c0818bbb629af GIT binary patch literal 15278 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 LN)pv#MuGnis--Vw literal 0 HcmV?d00001 diff --git a/static/images/calender.png b/static/images/calender.png new file mode 100644 index 0000000000000000000000000000000000000000..e7a99740f07298e5e4f0f38bca335b25f62c3767 GIT binary patch literal 17545 zcmV)nK%KvdP)005u}1^@s6i_d2*00009a7bBm001mY z001mY0i`{bsQ>@~0drDELIAGL9O(c600d`2O+f$vv5yP@^*K~#7F?R|NW zCdXChFYCMd?w+2WL!*&KGnO>Eti#4=Y|D!lV`O83(=03)vkRYQ+N4d|q)pnSP1>YQ+N4d|q)pnSP1>YQ+N4d|q-&4F zI?pd>dfDW?hUH5_vOo% zA%Dx(PyJr5dA}w|H<(wy`qd-veCInq_R)`iv~%p(F?!;OC%QlTvp@TH(!BvwK^b0t zq`~)=x4fnGlRx>BA2@jMVEg#-5f4ukZcfb3S_uhN&C<}MpP#xD7(T*KEMyIEz z=l1Q}*LvOSUY9qUP3+vcQ+)1opL;|;&#kPi%sl<{(_Op_>EVYTuF1Tf-Lq%UX?eEQR$dFakN@0^p*(~m#?_%f~|x}I8#l5DvI-}_IVJlQ&M;D7>OmghNS z!Dq5N&L4gBv2j@;f2zXa{@J&-^D{Grz9@bKTj zbivn+PIz63>n&ThOs=l3zM)=k(B#yV=3P~SqJ{+H>dHz^z1~}Hx#gB2yu1b_O6O~K zR#(49O8C4f!KWNu-lrxfvsSB_udKG;pmalAM|3>_kVZyE_DTR|A`@bMem?TNbnz0p zy>2EeXlQC^XeVA~%D5b8?QFN(L-LnhlzHVef8`45^?J6nyrfjBTX7xb{koEXi;IhU z)wS2{;`!&EgRBf>Sq37)ks~kaYe%}xUA{bnml@5?&28_>yme*X4;^}5=T!&++2C;a zg%?n()%CNq2JFRkMAy|IMCyDyRJWrF|LLcmf*g3@rkiepl7=rFegSn^39GBiSX^D* zikBHlplxjr52Go6pa1;lB?u2;VqyZ)3VjD zsg{1^=uv#~lb^)CSM0;Wl||VN&7ogmrxYPA}q`30ZwK_ zGlLT+PN?n~larHpK>~62&RtuMA3urfg{~_A&d<$F43CeiQLpNfaPZ)hTKY*~O6k}_ zLsrfesSn+J^UdSWJo5}*PBbz#HQs5jqEWBwJW6wmmVl};Dj=?|wlOmN94=g(o5XcQ z|C~DDRgY48Sw_P{Ll~DJRIUEhsgr6J0NZ!$!1nFi(2~Gx_j)*V;#3DOGg|F*+H#DK z;Z{=?oWLJ@-J`MU_cL>A+Vz01^0Sv>1G;C7_!s(!cMx_11}-hld|n zU0go!(C!=W%6h$<<`fPf}4H7q?)X$$y!v4*6UYc2-Yb zg>#oT8J}y(go#3ch2e2JPk;|jl^(1AB!4~Gwf5At+e4?DW2N1}(vloZN!4Ouc@_1n z*3I(H%>TX8qkn1)i;2kz%+AhYMK;2%TesqY2OdzPU|i^c-hRi9ANav{KlHZw`F34% zE-C&_RL^mt=R-mIYVW6)w<~B{-hd zuyxz^{57w6jZlhsTZY-SYZq?6?RF^B#Lnp{yk_ebh^57;-ty{H-f2r)Sx?orW+EpB zL zd}~&(**vR;1m8MVS>cQXUPl&K zPqyNY1X)L#R(evR3Hwv6yH;;tOy2LDnaRH5;fFCSM}ulaeACT0YyYX~Y23U24oq#C z7R%B!k;?+lMP`2zOc2)1{Cez{!~E3zoeF1u&~E;j*uC@TZsfwl#$$idy(gPqm%>lE zb-9~FiNv1#NifP^yGQ!IqlC51d#zEEz^P^T@7;qhKk%T=zXI*f9XoYFD?M;(d|XUT zOvw@Rs*E$2apgw3_RM`(hF4{_B72H67Z-5y;v$}Z;e2-V?8@!8@7@2?zx;jQbmr3U zzT@wH|6RLpu6--S_T)2O0MBiOfR59(41sBVpm zA0k$UWMv?QA2C1wKuNi~u#}mhX`P+XArY$*XxFNJr0CctAd+Oc;pWRw({`m8Y zi(LsjWj@Ww7QXqJ%IMjj>ylI%sR&d@q5!CxVpB>2Iqjs4uA?KPx^ff{6)6RLLKeP| z5)V03VG?k7@Fc)9^&{eXV;~A~iE3x` zni;gweM07Q#|<}Vvx_1HI7Kz-HzpgQoRYL^2Z{~M7lfr85rdRVRdZ=p8u<_PCiXn| z8ykz$YvSRpBc;-;k65iWK0KwCHr4#YI+ta`RFuAcP7a zLL{whXOFUOHX;{y6g~ziPqWZn=d8PS_hfh_*Bdw>_HqLTm>xh4Bvg4R;0c?5J@shw zuN$m#mpXK5-dTPePkM}R!53PI`ANLj=T9H*F04%6d-{+5^KZg*$CZO{BLLX@-WzVX z^Nyjhcg=O%8hoyM%hO7?&$$Ji`ZK+gC@`oMN?Eu~H84^7kdy>!UR2HxYyWPy17T{< zBq3sH6w-_JL4FN*OLRsGJW)ZSQQUn~dEAZ}Dgc#9Lst%YK(8593$xsrjAr8isPn&I z@~;|hF3UiUQ}p17N*hm{K9%t3q@vo4E&#@n$0=9#Jis9VU_ zPT%;ahn5yq!PP_FoCGwvAS?NJj~0HR^oSK!y_Q*Xg4BIlL%Q{!KYw07FD@>jBS(Ky z#We5a?EM5Vpjco<0}glZJ=ybG5|Igz2z=g6M1W0yJkgw`95~Zt4uV~4n4 z)9%lf3?&g60mk%0T_L&Fh16_;qOq|tZKilpPQaf%cMc~d5W7puvW|?D)ZMw4JS8L7 z^qzAk+xD_9j2CCf_Ben8WCphy$k+k9IDYJyo)>@d$Pwi2 z6=^A`M`_3DQjr%v(9$m?65kBjYbOsK!?(WqjXxS*s)nkTfZ4vW$**0_du9193JXo5 zY*IplMrB$pmB?*!+D8pmTr9tfFRPOc|JwvAS1F&QzY%U_=d6R1a|Q z$%A<2^Uv!1D;+s!>A3jWU^im5$R8o;7+er`h;b8C3~DddKd)_tZN1 z)SB(XRX|eta3fx;bUO*=z^7&m6ac%cD_Syc*|J50>g?IGcd zpT4^!cV9GBDgj?HGWyneGY}9+S|R6HAb?QV3dr=-s<6gq(`_2ynO_G+igl#~Y|E}^ zT28&G?&#u$i+J|gXK_h(LqpO~tpTWPV?^H08Y%!DVBSrX=zs`eL`eoxwIUK`QP*aE zgc#r=0lgmS+6Qg>l>FDNDq*JU)byM*IrVZqE23tMnvI53M>={C^6azE=#?>3YO6>F z_yG@`{ucublx8XE6-+~IMQUFdhhyLYCdo`|WOZ|kR0iW={ znk;XA;CeCmHsY&foFUCzw(MGQUVb&EP2Dsl^Y<=v20HYeE#E}g^V%xw>& zLaG#ix7LU6Z1-(#uEo0w5O%?c@J456@vp;30X+y)Qcu3!mcQk88!b6&e{S}iUj3m0 ztV*|wQVL8AjhH1jQab2?hy|Sgv9wd(IElo~Ni9m;Jr4aSGSw}E-7tyG-U{Cni=+W6 zW3n>3*X7U660K+nNRTt#xG;ACM~@uU@2*N9Dk<2$bP>BpTUts+5}rxx)UB+Zbi>nE z#;dWLVR>O5x4&ZhU8s;M1K_S*yGDkGn$sluD}b(Yt-ftR7#Ds+{Zs`gJudVHx`I`! zpOiY#p{JkGdqC8?xm3JWx4cVE(rcZmYb(G`Wl^avfrtLd$zL_?*lafJsvP*nsLU)Uj~&Hx&pfTe<)U>&R2j;PJ;F(J8r#s z92HSz0305goDezJzno8;X{T@-@Y&AScRfN&zn1(d01l}&f-v1&t`+heU1?*Sl-=%` z=MG_fbPQWYM{(c&JFsPZ9IJ9fcy4|k^U_}p9m!d96HYuwR+5Eh3ynx(vJr72Nm65U zs&l!)Mu0nqy)b4M5%j-WEz{GD4VmvDImj3uY8ivG%*z#tz)MOWJa-0T5_q@WxEnJQ z6PTHt#BDOFH*A$=nMM;eIY1-38mK7w%D~mPbPz_uE?h48tl?sNcnTF!Wh=D|9=u(!e8KIT@2SO6I9mf~0I&FlGle`s>P;*OHPzEnK~L`ZR8mCYamg zJA3ZGA7c`D`^Ltx@R|RI?A$EQPmH50X9RZVW-%rb9z2nVm&`K~eWfCEW^UZuze1+5 z@#KlcPflx8;8Ra_;8m|FolgQ#cE6+2=sq&8@W8&knAkRh zOsOBweil22#WgDL=17N$0 zafm$0Uz$@V3vBVrTGSKlf}JXsu?&N>)H~lcU|JA#ZtmRd$c~{GM_>K=hs4Z72heD? zu=K(USoqVwz?kf^+ERKfshOBY%g5p&m3FUW(<4_$Y2yh42@gsRNOHF&wJJDJLmE!? z^88p2vn3@96(SQI9coG`H;yY;mT>s;0$OUt^!d|Ry?6<`-uPDReZ@Y#!m|769mqfW z=UA1E^@U@{amzh>u&kC?6XTij<=7Pwqb{QvZz;ZrnW2T5-8KW0Os{;_+bwKYoi)t}c}|*Hq~&$-+1ay~_fvM;@XfoiW%q8WuuHiq zCEmQ$i>BrL@CW6T)K}G-dP%xZAW1vAF9W5kq_S5?_@2qXrUoY>-@^bCPXbtRK0r3&KaCsTER0H=aF5$ATiR!$j+VecZ1doWy8e8)Fl4#3&$|gSuxTt z63ey`iok!=ctjt`7!`qifqD?V+7K$DDmNuHZLjLPr?hSbuUQdXoFr1MSuZ7EBV%Qf zXezx>>PN$?D|`Or)QR^V7#h9p#*ch5e{ps$Ywg&ERXHl&D#81dG*6t8UG#0Y-;Fa8 zu!0K!D~V!B7^Hv)R903Cu^!0t)EBT0s7M>52yG%D*uGLTYsQ6H{j6+)nI4qX@U?ff z@R3KK#Yc~x!CUvv;L>}45A8ee16@3i=5vo?cxEU5_Whrt-+TMRVpd7N2>40kDu_hR zAJ0RO;zUkyka}o_7Am6309ccguwKuNx*rtvkhEveT@ab&Mr0ON%J|e$AT*^^Xvqo~ zk+U4znnT(5v{xSdxx>%C=R3FVeA|~_xgw>3^ph|A!^$E)I(r7cc*ngsx4Mc{t3&df zstHkdgut%@T%qnF9ndVGb=Sl@R`k=JfJ;vGXBXh+8JQK|GVf~EBOh+z*T3@1@xvc` z4DIDbyj2dg8fOn{fF54x^#0R_J~#I>U-xC(mz1TDIwsB(Tf`(@TG89vQ`@$$|Z%($3#(nF~fp)p0nX<4?^>s7WUurdG|9hD*p2$WPh zbLA3+cS%`!-Dp9n%oQymw*27U+qcMB6KdxqhV)W=IYZFv!gXgMjZ_RUK!`+5>w}_5 zM+eF?RniqF_&5Kw8waIe`Lj~T4NBZg)=gd8GQ#h9=zhKXuPvVW84d{H!b za@~a~XVy6i`z+vBinuAf%UumN{jk{U`VHY)GT8$4Qpz`E!R|x4#sP(U-(`VyO%}CU zLhv<%BE+8x6mu&=U5c{iVaq@S$s`!?+-7<8d}XyQ4bmNbzohO%VIis$2JRpcEGTV~ zrh>Ah@rb}98v!ftbMoK!Qzm{H)lg-I=uGR+tp5Vo`?LIW zFsDH4ME`v3!E znnua9k4&HdGGczNL8>&(B9A`G)YVh9OY)r_LGB=+6gV7Q)E`{DFp+{3Nrh7^r4lft zl^^*rcOEwGx+4?}bRY7-yJ-;tfTtS(PsMmCzAwbX*(-BfgNR8tiuhs?=rEC{S}9cu zKz}5QrL-Fi`dXo^3Lq>iyt3|)7ral$$H$iAM|w~w;JDkO0ti(Y!IDg+VxZ9N3PLP# z*z5k9fTYl9C=M6mic5w{BLO1)mfgqQuPE}DR0y$qEm%-3Ren4hSrC>DE=yf(19Rg^ z=bP2)=*^x-cYa=<5EVv=VWeoR7Gd=3SK8PSF*b(;Ca1ZrDk`s6k$3swr22?s{uM#_>S zX?PdX;GAEYQwKTOBjHS1=K>qvV{wnl{7m7{r!o-E~G~%(RSX5w75M64y@& z{$Dax4?xdh$cbRRMrJd_%U&dI${x0S`V1z&;cIa7+rA#1HPr_tOx?YS~+S>7zy;D%+jw10LE_18U!UH|&qkgu*fv|Ecs*1s!q`A~1pl5}vGs?30(ocE*a&?FkhE&nMLHv@GR8~e zeD{e@Veub73^BDOeuYZ-25cFm2n6enQ7KglK(`+wZaEHQLHekBvgR5g*ybN=XA{!4 zbz<8D{>6X)Wwb9`FbgsJ<=n;3UN1>O_ydkg#(UK3( z$x46t=YI}0wbfHfvea~x@z%iGoFg-#i|GdsVCh@G5#5z#Wa?ZSUmyUn24JM0k;bh? z4Tn$7;?Cdr6V&7sT-NC((es2-@x^>)oXrOwMEl4WWI4`447HM;$^IAclGoIc@M;9N z=ZZplFP*BDfLwQRVod}jdPxUfZ*CqnkUDZg?AorGTfBTFwXc>^+1#g=j9a&&xqTZ} zW@jZJ8=g&G5d{#+3hz!%>J`VUvH`@(r+HJEoz~+^=WA$4ssJ-+A3FkSM#bUFO5f}G zN6Szz$MVpKKJAL63c~lD43cKfc%#vA)Fz|~Am_2|*!=(Zzv7Q7F{hea4u}1BH=L?uO zk$ymCL@{u1`fdsVh?Ib|BHDoS z0g2u5p*zJsWfBnx_PauSR%aPR_%O2DbE1?Yfyx_7)cvfN5Dn~4Vc6~~GgSdV>8oxg zykFcDHK?M0BGfp-`K^yC;lP_ei-mPMU9Oj(?pRdNQW}t5|0e* zs5UB*d`RlvGPu6MRRExYtHUgk2z+n=L@E}q@Ii>y@$)|>IX#$Cer!XHE6p?*mX9jW zXRBBO#)}4sxuw7(l*~Gqhjq(d00VV@5of$G69D)$DR9SxR=*gd%UZlf6-?!?I6HO3 z&j8|HtjC{7_;9{ar7)(Py5M<1944>!gvv@+!vuaE^bqMd&M)eD7zm>?+9rwkHnrSIYvO!5IJSO!F)k#Gq~7I^Nx zYgPhAWt;%ddC(gEv7Wnj>%VFV$l_1kOnjkIat?+;Y>?}O@pVZ&op|2b0|0F0$)M;L z1B6K$fNuo61`-L)*AhDKt7shq(T04cf#s1s7rSTfPKB4(&ZtrS=2ymfSNz6!;^@bvR#dP<>Mj0Dv`_D7M(Elk)3HVU{LE zX(I?gA2?A5T%)ZPdzvcmf}PY36&gV3z7yPDyitiUHhp*OHQ^bOJx8^*qb~QFjC`c z06a0kxj}%NNe}}jNNF&lc&dx@z%k~*>HKoPs(er?ZzKXnu?({zlygJ{-UCQkE95Pg z%!KR8wJ8DJVg#o9A<7p6Ub*Da7gquHpzi)8*4DdBWO^N8o+oJ~>2uyZ4}f1P#>20Z zU=gIrXeme?ihy7AGl!L5tqE4y+PNO=ibU7B+{rtjA zyP%Am8{Cp4{V!UT4473gjYbpaj=ZRjJH_aZ9r_edCaU0*=nGJ@l0mJcR*OkjPMt=5 zWW-mjSsF%R4W8m7bJAGo@AFFY z=Cj}WAV;9=xD4UqR4akWT!)8-aOqFqr%eay3*tI;hhq)^Uf%NBmIZzL@HqbV>%SQf zeD`&Yo=QGs!Y5m{Q*w8AkVmA4a~gfZEhlTmn>9GUmVP;_ljW6CVBU zci^7y`CjC96FTh1^vo^db<6ZAjR|@sPDAFu_S9$5nLUGSa;lhSPT>fXk|<1}*am7H z)fqm^sE#&dPe@oSY?+Ua%i@|}EebX)I9z2BSKK?_p!0Rl0SpDqxh6wiz zzc5u5l$r+~+PMSGEmN|vdvK_8CmMJVf}f&bJ{UcH!!B%{yMnVn{9{s*)_i6AtCM2h z%}I0NSMM*W0^Yt2b^A?|Ahg^{aLbE5hsBkH`T)()^j5sG)5evb|1VmShrK5@e!oW~ zGv#ZyEZ7#R!{|?*%e1q}EoNz$A;Ezc z{9U)}D&HM%(e6m8H$H~3i81{~=FkFmc8rM6Q1M~By9ubfaKU+SX_~O6+(V@(2lXLD zwS;|W-(FXzaEJ?1Sm;BP954*yxX&%PH~m&JIwg}7$#BGH$Zw4Ma`S`=wsU8 z0cRJh3hu^u;y`tLtIwNdF4u-!c^h7A8RSN1HnqZz9w{@^R791}g!CJPaH*c&`)Bt) z1x+uv5=tG@P2}9%M`(!I4v9^n+4quB9U&w}Od1iD@Wc4@Ur+Uw;OuU>PsjQB6+tW_ zeQS@m<>n5){=ipm^sWu(j^S|c5yH_NE?veK=QGMT0qLm-zJfH_Mw#@wG{@QV(>&fQ1KpEqaY|QKUhEmR2nzGCfedlrb^8)@rstDQo+EL z_@^KQ%F8f>MInwfq1c|60LQ(TRs!Og078!?1wT!2?)UB zV1P**O5lkiKvPssEZschJ>-MHMYfS0+*=;)t-$|@k|II-e43!7U^0M@F;G~mJ zZj(6--)4YG2qz_wKPl7{j;TQ5%{1y+4lCMi|NYy{#@TbDPw-K2uo3!ikEk=2TTS>= z55gc9egy0)$K88#kTD7>Lw4q?&zO|_9%MZt&DfW{tc^~2t5L5qkqkAp{m^9Hk2e8| zj=qGOvkm4TlfKHoN<9mzq&=p?E~M==Yc|!V=51pNd$4u7IT^E! z8qj2Hlf2t7ADLxBqp6ZAHbe*Z;>uK`!+490y3gUQCIN&n&oyZ}>C9ij)cvnCIj>nm zFf05RfQB+#NCsFru6*tpWK$FRD@3q1Nq>^510ihTicrvM3w!6%WsKiVk;G3%`k?Q7IeX^@};Q~hPyhDGuM;pM^zbu$o zye*J52v^>^3}pDuk;CZ8nI(O`4R{)0<}Y|DDcrQ3vl9Z|xUPB3QK=N%keLv357*W; z1zWU5CbP>$$M=%ib8zb7dA#{!@5j!2??G36dTaf};cL}iYE3y4^6`Jbqu=}vw03OQ zp8_-9P8Qf#o#Z7`De8lPXHOi*-S7E*-1yK#GW}J=+E_rmw;dLkk<@IVeeg*fe#iGf zOik(&??IugO!?Rb!0dtQi=mf~9>w6_e&6N05HH(;*|hiM(N*ckOiTN<^52BBozrW~?O9(WB#UvUdoFI+-hPEAEE-ikXBbOD7dh%a= zESl6AHQ9PaWM3{}Qm@2O(%XU#frT&_u@8nl7be$-ZY%w7-SpRN8^SCeC3ZH1OE(&M z6FZeP=)1mf*gNXvnQM1jCL!sB1L&L!zefU8^9vOklwu5)d|5!j6nYZ~?BY`J4a9p2 z=QBXMYw_p?1gvc0k+MxA57q)N62*E=Z}78ngIR?N72z9yVRu|&S0pC^uR%MYSuhnk zTlEWuer|5yVM48V$>~)preFgv354X;axf0#6ZOH(M`Yd+{KI`;6uFmUJ&KFn7>r<* z3=D!Uw|;VMHU$G3dzUTx4FE;#28#DAYr2M4M4Q6kT`D3;rzlTA7=eTAwn!1hy1p>r zd=P8innWmIAd!B7KldEe9D#vF%z8-mMZp>r1HZe5k`Z1{To+s{{=R9uu9izd1S#yL zo~ye^QZj!%6>^R42}xxPB`#5!^EVh^BB|tSUm)G1IuI+~HsPRNOfymuy!dl6m~b`8 z_6rv;PzX{cnl2+&*Y@WY>#mJJR>J+lg(8U4rX+e_VN}kmd%iXoBbaZu6hTI;0WxVv zvE=i(6L$ApB=U-fSguFZuQ8-5fW0yyWcgrrp@CB_3&%$OtYrM1?FNZUnJA_~4`k|( zJ}=RLNCJk{H1PXGs9ZuY0AEcltWO(vJew;}>rpKFxX3&RSgYi71;$7!u1*ND1Ht*k zeF1C~(s%yRhM=oR*Y9a@34^6gv45)nQD7DkSO?DZ8iOWs(Fz7W>&7=J5o;8dm-JdC zRXPaa3lba%Ls3#x(573Wf_%~Xm+N{WQ+85e15hkEL?4M4`07h^bvh_~{DQgos8En} zysHCi7*ULakYJ6Tzijs zkYDsW7&|srqR<#9#Fc`OlCW`9UrfmpoBbgMI*O%`4GQm$;*P(8>YhpiMiQTGaB?6E z6-9{@22kj(>Dt|xu85E0+;adS(+9b9EJ#AGe? zVuf86Q(U0~rcObZB9}e`VXTsbYhEgZX@Ca6^H2h+-L8&@dYEH;3wILyqWG zq%V8|02KL=4}=H|9-KsL*0WnmwJsL4SfzsWL(&L9tTA{gRMa<3h^rZm6VllVVvWFq zkVzwA6<(yo*0n~u`X`JHHD?l!1Bz_7}&hiz^KU^1=X*!avq$fcsX8Tx{nCP zKYcoYI-S6M>y4$A&QtpaKxR?qYNvx%y=kA5crQMDV>Sz)=N z^yk2Utu2FK-;r}gZr)P)Yz~j$%s)MbPyF2naR0;qGAu~Fisqu?sma2B@lW21g%^)v zY}XCG+crIu5x4wnVh;VHSx5W1=kSp~|8u|M?KE zJo_Amwrw+h{3HUHvJvD;!p!&NvH`R%UdBg%>$ma7cYPn)I{%`uhA(!o{IebgvH>qX z_86899z-@V;k`||2pi&qE8MiFsUy2Novg3CPRfG)bcIuKS2>>SPhWN0ZQuTloz@?p zUs*;tCc_q+O(kQ6=s&G$*$CmGB~VS~_Q>gD*m}!8Q>Ma9&I`MGH-6v7Nem+Len`c6 z;tYm&?eumvx60QF4bNZT**v#uH5`87Fedix0qjt zm)`UfeZ?@>$5QOwB`bfqZI0Zq!pp7g%d9=d0ZF}>*`_Tr*PdnkJGUDh$d$g8ew6S> z$;TB}Ww&+9-hIeAxpvDOs*{Fgcrxp`Glw2wHV5^(J7?tO7min6fr3MS9~S@DqhVnk0Jilo&MvIj8 z>}chxdni;#74M2GGZA?7#p23I3WRQe?HSV_VDo(-VIFo5Vny0dxSVI#NPo+BQ!le-4><6bVEwQIUeBB z;ORy@1%7$q7Dx4Ioa{<09v3&yS0PT8VM$6G-iqp|Vmn|cbXUj`2qJL753qcA>&FeS zc>*$kS6&K2ia_Xz!m~H~Pz;jjz7&Kp=veg@0Z9P>6s1kRf{}#t5wJWthKWV)MyEdC zNgR~MUAPwG^yZfc{O%o|t{eqR2{?x8sQTqt6obV&M?d4nBxD3AbC;Lkr{6m~H7Wyy zi%(DjWDXJ({CteAfn^Dz9`O7nO`-ry=EWtQdzS;a1Za_Tk0Qp}nWvCYkgOqI zE;UV>F>qsvh^Sk3j$=}~eje3P^+)t-&V{r0YATFyZ5U^;OPAX#=UTmvm2@2pt`C;7l-QS7svf11&*!jz` zP08ieN;2EktMupI^Vc}_zW1ZKZAPD4V<$a)0}bXM*z=-o%oi@A`I-l?``>*Rx+|;f z2wvgrLDm-PiZBt4@;Q@Jd5a(U2+sfY-=Ma2t3BPonuA0zxDc_r;fInt`%tb9Ix=U& zEXc68U8`rqI3k2k0r-shg7zh53*W&gNJ%*niioZ(!m4c{Y~`#PBX5;s5Ew zy$}4(ui^OZuSTcSMyt`(r|@UCqJ7I2G50Mw)%5(Zz*qi{Ujj*9^cmB>?-EgBqTt3J zC1`Jb-2u#f`cbr(m(grC;ie%q2t5$syMOi6kZvz5&g0=<{8>FC)9`c2u5T3nJ%Key zroZjLgIIj>87#>Lt>(q!+F>9>NZ;i_+X^X6$tfmlQkpEHI@+L_5U^GrDkxY9L$O5% zbldkjoi4=8v|f3!dYLbOWX9b=NCvJ;5Dv-Kza+bJYBY^R1_xn~LJo|*ZWo=&3Dl*i zTb5m5zXbv7E=G|4UD*XUwoPN@g`*&KR2tb%%{4sR&H3-Cz?{RfI6AC>*puCGdHNzQ zw?34sLy@*l11C|johcWn%P}_HdeK8CGyc^kRwKWqOY)zmSoRqlP^5LUUKOS)DuO;bS zNv*?!F0VW{N(wOxK)*ppUL1sw=Z`QhZ=OVTRPC-fTge`TA};$Fgn_t)Ufb+or@Gci zZ!ipo81hi3vCR0!$fOb|&f6gAr}oH*Y&(jjZQK}n11Pfrf|XZSO32Y` z_GAQmFKzeu-9;TBui~(~9cqXMqN8 z0q7gfjI~(7f=VTvF9i-#5+0DQdmfgTW2lZc?07bpA%3tD2cdS0^K5+wJ%YoR)Ii~L zD&dhRk%;hJrJ=z9Fc8m7l5ojGKGDFF3kS~Adhuf|eNaSFz6e72D@78JedgBkpez#E zwR<=b2NMGw#n6i-5x^x+DD6RqyNRONhbNaxq<~w`0uSt+f!H7_yG1nZmVi7Yoa%+f z3tj~@+znP51z}#djUY3_#sE(@N6^B?41bjYN7~cBhXJUT? zG<(GG=@)`nNS;;;*+d>41bOQq< z#rj^tr{@dCY}C6}b-=h{<7aMWDTF5^?*W4VE6{o-3xsdFvG_yRMge@}8#{OtiXQtA z0nDiE_+kQus1$^8cWnI2J{W%$7-V6yNc{_di-G6)a$(%J=nz2YK%5CNC@1UEhOLH) zMx4&yW+Xkt$lb(Uvk2~kNFVeMN0A8Y55kn+wEtayD_@Sn zoosXxxJ(jyUdhMe^`vB$4#0W<;GQCiJ7AKP2L;%2P#6r*I`OZ~hi?R&0p8uFT|O~@ zv#)VF@TupUbYf>m zC3S6cp)Lb92t5Pg0Z*Y!eB}qp%|TDJo}{CJ%fiG@DF&@%L<~ZQSKbgDgH|VK$=}&{ za{eVzbrMjBJOvuB!~zJ(xj26YxCkm&OK~S^3N#OUtidH-Dv980J*Pa5Cyo$aw9vn-JY@7Zp);T`+2$@U_rnnvJjq2;;60+h$g;4c6eO zf5TLJmeofS^&p1bQSboYP8*CWTaG1!SWzoA{o+QyyhxFiCL#<9 zBEyL*beMu^G#WT@?5JLQHnnpH@@`m7%-6j`TQv;uuszpW3+-ovvMv7Z^sAph%K-3mMc05_>tW96zbgXyIic?3_2)DrHv= zYT@ggY8m$(_&z-=Jz3FT472o!+u&O?p zs(1Xe)xd?Jv4B5fRCW_e`i%__~tJ0Uej1CQB{x3g(?v(`$PfzzifXnuN=!hz2%rtVdt>s#o3ms-^tU?D_M0`MfKjbmx9Jy&&x25Z=!h zErz7TSh{cl7Z)%3wQ*5A{wG*m=IR!Tsv{&OrfxuEe2krI9L{rbOWGqIEq_g!zS@s5 zH+LHTi%G(zsp0E6SCy2p(;9>gr?u-_FL<-zTvu&5|x+Ce?R?FZ=hA=()3X=xGSF8(b z(hzXp6AXZ2%jJbxVVyB4gXA0<-}ULM$$wX+->74J-`*H>B5Z_;Kq;cYnFAI9Bf2z=ND$ z@Q()~#kGJ z$-DJ(&TBB06jop-&&(CvFzoYUuenQKs~LHCCbC_>@uQrqDG6*4fp}nnFu?G<5RySk z;Dxe`dW#EZfAiR}i>QdIoQk6}tIMCMsey=da91h;8GD9*PE9oNJ4g$Nhb8u076-Xh z%0U>wK#U*9KSj?V){qc`7(QMGY(m0o%mbgBP2)xsM+^#|W***iqiVjrz~qmj5q)tq z?5-hVckj|^wp!x3Q)eGVg;eQW@Y#0ze~*lfX`XWg(}Pj`4e@I{C58lS0ayhP`tVf# z9#Zg95eSGumUF;FmB)BN0vzx#3_M8RqqxyVjY{MY} zR~h&Ssi|nIv?@Gv=FA_VLaGvg;_%X?zZu&y*`{vSH?=|~M*w04hazx*GB8n5^%a%H9WsS9U506!#p=~O8IA3t?!=?r!MN)DPb*Pt{-4@spWR8I5^N>)=- z0i@i|-xoj#1{B*prBao6`X!Pu0bwCs;^D|eT6h=CFMLgkH4IA1A_W+^1J{GJi^6#D z7D33nQU}7EwHogSO5!#5wPq`uJ#*?W|6+D_0oN!1wI%B5E35C`v2B|id3AmMa$m@Z zD;n-TwA28W;0Hn^Jqn;D#MMl3xcG=S6E3Ilv6Gf7S{;MGm;!4-JRS!KGs z=Zx<>?M8W8Yk&|uzgGi7P!dl7vzLs#TBqb_>0m?8a5SNIEibf=U zN>EOZj?|7HKmNP#JoMxrz+fh{_LoSNiBd+2!QPMWzwgrQxpNa)t){B;mXPm!*~R#_f&#fOH{5%(vjcvj?;D_#+UaU{{Rt zA=31$p9tIv{_L>B&iDn^Fq}bJP9A-C*3c#@!+BM!as~5-4=XakoiH1EomcV)~Qsj$Ic=A+3Ur z7Q#neeA3H$@Pi>5l$PkY(yXu3CPeN0NgJIH?Ld<81OmZG#m#I0|*0KG=H6f032{OoG1da z=$7&?B9Ntk3h#1v&L@j2n!1a-|ENng?s+q-i5Z#m#o03-`hyo*U-Lilc!$+x<+PCi zRHWAi$Y1Zyk&#zD*xK@Y+Z)*{JJM;@_A9JRieMD+vx_hQAO=x}6r^c<3@|7G3zCXt z@0s{nqYyl;vNVLAS9Lz_NWo|^h8yh&=3fTH!uv@B5;31XjSNX66HLnacSyO9OPMes z=Zw!RTz=v`r;dK_Z!KMZ96?2^yz6ze(ExPhThLvt)~z?yM}J^`_4;nteRPQ; zw#ua0x`Pn@EMnloPR55X8zrk^fRKhThD;=t9|O>UCzfg=iX-Bkr6X8T)O;g?_0fm*ruwYS@X-eRnV6T zfR2b1LCqPD9cvPoHJ!#K%zSICr)1`I7{B!a5 za-(2OD5pW{W0N*%lQwCSHffVKX_Gc-lQwCSHffVKX_Gc-lQwCSHffVKX_Gc-lQwCS kHffVKX_Gc-lh&mF4|^&qKQ{Qe5dZ)H07*qoM6N<$f;uba=>Px# literal 0 HcmV?d00001 diff --git a/static/images/link.png b/static/images/link.png new file mode 100644 index 0000000000000000000000000000000000000000..608ed1dbc74b104651c20b91d5c4e26f5d27762b GIT binary patch literal 19255 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 MnOGaw7@;2g7rCd^HUIzs literal 0 HcmV?d00001 diff --git a/static/images/progress.png b/static/images/progress.png new file mode 100644 index 0000000000000000000000000000000000000000..fb45d7556572930de3a98c07c6f40d3aecd50e29 GIT binary patch literal 23953 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 ~+Ub)u_rntfS+wVg`_53B^QZd5@8^z>A>g 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 literal 0 HcmV?d00001 diff --git a/static/images/rule.png b/static/images/rule.png new file mode 100644 index 0000000000000000000000000000000000000000..6259038341edc2ee2577cd4acd61323f48883069 GIT binary patch literal 15573 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%0DNfbC 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 -

DASHBOARD

-

MISSION

+

DASHBOARD

+

MISSION

- + +
+ 서비스이미지 +

{서비스명}

+

{서비스는 어떤 문제를 해결하기 위한 어떤 서비스입니다.}

+
+
+ 달력이미지 +

진행기간 2026/01/25 ~ 2026/02/19

+
+
+
+ 북마크이미지 +

즐겨찾기

+
+
+

즐겨찾기 컨텐츠 1

+

즐겨찾기 컨텐츠 2

+

즐겨찾기 컨텐츠 3

+
+
+
+
+
+ 팀 아이콘 +

Team

+ +
+
+

PM + {% for member in members %} + {% if member.role.name == "기획자" %} + {{ member.user.username }} + {% endif %} + {% endfor %}user1

+

FE + {% for member in members %} + {% if member.role.name == "프론트엔드" %} + {{ member.user.username }} + {% endif %} + {% endfor %} + user2 user3

+

BE + {% for member in members %} + {% if member.role.name == "백엔드" %} + {{ member.user.username }} + {% endif %} + {% endfor %} + user4, user4

+
+ +
+ 프로필사진 + 레벨사진 +

user1

+

✉️ user1@naver.com

+

🖥️ @user

+

기획자

+
+
+ 프로필사진 + 레벨사진 +

user1

+

✉️ user1@naver.com

+

🖥️ @user

+

프론트엔드

+
+
+ 프로필사진 + 레벨사진 +

user1

+

✉️ user1@naver.com

+

🖥️ @user

+

백엔드

+
+
+ 프로필사진 + 레벨사진 +

user1

+

✉️ user1@naver.com

+

🖥️ @user

+

기획자

+
+
+ 프로필사진 + 레벨사진 +

user1

+

✉️ user1@naver.com

+

🖥️ @user

+

기획자

+
+
+
+
+
+
+ rule +

팀 규칙

+
+
+

1. 규칙 1번입니다.

+

2. 규칙 2번입니다.

+

3. 규칙 3번입니다.

+

4. 규칙 4번입니다.

+

5. 규칙 5번입니다.

+
+
+ +
+
+ progress +

진척도

+
+
+
+
+
+
+ +
{% endblock %} \ No newline at end of file From 147655614616627775fa72a50f6b980cf0efb67a Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 13:09:43 +0900 Subject: [PATCH 142/380] =?UTF-8?q?feat:=20=EB=B0=94=EB=80=90=20=EA=B8=B0?= =?UTF-8?q?=ED=9A=8D=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EB=AA=A8=EB=8D=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B7=B8=EC=99=80=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=EB=93=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/admin.py | 39 ++--- ...rogress_remove_guidecard_stage_and_more.py | 106 ++++++++++++ apps/guides/models.py | 161 +++++++++--------- .../0004_remove_project_current_stage.py | 17 ++ apps/projects/models.py | 9 - 5 files changed, 223 insertions(+), 109 deletions(-) create mode 100644 apps/guides/migrations/0003_projectprogress_remove_guidecard_stage_and_more.py create mode 100644 apps/projects/migrations/0004_remove_project_current_stage.py diff --git a/apps/guides/admin.py b/apps/guides/admin.py index 8d66705..7e962f7 100644 --- a/apps/guides/admin.py +++ b/apps/guides/admin.py @@ -1,12 +1,6 @@ from django.contrib import admin -from .models import GuideStage, GuideCard, GuideTask, GuideTaskProgress - - -class GuideCardInline(admin.TabularInline): - model = GuideCard - extra = 0 - fields = ["role", "title", "order_no", "is_active"] +from .models import GuideCard, GuideTask, GuideTaskProgress, ProjectProgress class GuideTaskInline(admin.TabularInline): @@ -15,35 +9,34 @@ class GuideTaskInline(admin.TabularInline): fields = ["title", "order_no", "is_required"] -@admin.register(GuideStage) -class GuideStageAdmin(admin.ModelAdmin): - list_display = ["id", "code", "title", "order_no", "is_active", "created_at"] - list_filter = ["is_active"] - search_fields = ["code", "title"] - ordering = ["order_no"] - inlines = [GuideCardInline] - - @admin.register(GuideCard) class GuideCardAdmin(admin.ModelAdmin): - list_display = ["id", "stage", "role", "title", "order_no", "is_active"] - list_filter = ["stage", "role", "is_active"] + list_display = ["id", "role", "order_no", "title", "is_active", "created_at"] + list_filter = ["role", "is_active"] search_fields = ["title"] - ordering = ["stage__order_no", "role", "order_no"] + ordering = ["role", "order_no"] inlines = [GuideTaskInline] @admin.register(GuideTask) class GuideTaskAdmin(admin.ModelAdmin): list_display = ["id", "card", "title", "order_no", "is_required"] - list_filter = ["is_required", "card__stage"] + list_filter = ["is_required", "card__role"] search_fields = ["title"] - ordering = ["card__stage__order_no", "card__order_no", "order_no"] + ordering = ["card__role", "card__order_no", "order_no"] @admin.register(GuideTaskProgress) class GuideTaskProgressAdmin(admin.ModelAdmin): - list_display = ["id", "task", "project", "user", "is_completed", "completed_at"] + list_display = ["id", "task", "project", "is_completed", "completed_at"] list_filter = ["is_completed", "project"] - search_fields = ["task__title", "user__nickname"] + search_fields = ["task__title", "project__title"] + ordering = ["-updated_at"] + + +@admin.register(ProjectProgress) +class ProjectProgressAdmin(admin.ModelAdmin): + list_display = ["id", "project", "role", "completed_tasks", "total_tasks", "progress_percent"] + list_filter = ["role", "project"] + search_fields = ["project__title"] ordering = ["-updated_at"] diff --git a/apps/guides/migrations/0003_projectprogress_remove_guidecard_stage_and_more.py b/apps/guides/migrations/0003_projectprogress_remove_guidecard_stage_and_more.py new file mode 100644 index 0000000..9b925ef --- /dev/null +++ b/apps/guides/migrations/0003_projectprogress_remove_guidecard_stage_and_more.py @@ -0,0 +1,106 @@ +# Generated by Django 5.2.10 on 2026-02-06 04:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_techstack_user_tech_stacks'), + ('guides', '0002_initial'), + ('projects', '0004_remove_project_current_stage'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('completed_tasks', models.IntegerField(default=0, help_text='완료한 태스크 수')), + ('total_tasks', models.IntegerField(default=0, help_text='전체 태스크 수')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'db_table': 'project_progress', + }, + ), + migrations.RemoveField( + model_name='guidecard', + name='stage', + ), + migrations.AlterModelOptions( + name='guidecard', + options={'ordering': ['role', 'order_no']}, + ), + migrations.RemoveConstraint( + model_name='guidetaskprogress', + name='uq_task_project_user', + ), + migrations.RemoveIndex( + model_name='guidecard', + name='guide_cards_stage_i_132d3c_idx', + ), + migrations.RemoveIndex( + model_name='guidetaskprogress', + name='guide_task__project_5c19f5_idx', + ), + migrations.RemoveField( + model_name='guidetaskprogress', + name='user', + ), + migrations.AlterField( + model_name='guidecard', + name='content_md', + field=models.TextField(help_text='미션 설명 (마크다운)'), + ), + migrations.AlterField( + model_name='guidecard', + name='order_no', + field=models.IntegerField(default=0, help_text='역할별 미션 순서'), + ), + migrations.AlterField( + model_name='guidecard', + name='title', + field=models.CharField(help_text='미션 제목', max_length=120), + ), + migrations.AlterField( + model_name='guidetaskprogress', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guide_task_progress', to='projects.project'), + ), + migrations.AddIndex( + model_name='guidecard', + index=models.Index(fields=['role', 'order_no'], name='guide_cards_role_id_05238e_idx'), + ), + migrations.AddIndex( + model_name='guidetaskprogress', + index=models.Index(fields=['project'], name='guide_task__project_814d38_idx'), + ), + migrations.AddIndex( + model_name='guidetaskprogress', + index=models.Index(fields=['task'], name='guide_task__task_id_5123d1_idx'), + ), + migrations.AddConstraint( + model_name='guidetaskprogress', + constraint=models.UniqueConstraint(fields=('task', 'project'), name='uq_task_project'), + ), + migrations.AddField( + model_name='projectprogress', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_progress', to='projects.project'), + ), + migrations.AddField( + model_name='projectprogress', + name='role', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_progress', to='accounts.role'), + ), + migrations.DeleteModel( + name='GuideStage', + ), + migrations.AddConstraint( + model_name='projectprogress', + constraint=models.UniqueConstraint(fields=('project', 'role'), name='uq_project_role'), + ), + ] diff --git a/apps/guides/models.py b/apps/guides/models.py index a5f22b1..b444134 100644 --- a/apps/guides/models.py +++ b/apps/guides/models.py @@ -1,68 +1,15 @@ from django.conf import settings from django.db import models - - -class GuideStage(models.Model): - """ - 가이드 단계 - - 팀플 진행 순서를 단계별로 정의 - - code는 시드/운영용 안정적 식별자 - """ - - code = models.CharField( - max_length=40, - unique=True, - help_text="단계 코드 (예: S01_KICKOFF, S02_ERD)", - ) - - title = models.CharField( - max_length=120, - help_text="단계 제목", - ) - - description = models.TextField( - null=True, - blank=True, - help_text="단계 설명", - ) - - order_no = models.IntegerField( - default=0, - help_text="정렬 순서", - ) - - is_active = models.BooleanField( - default=True, - help_text="활성화 여부", - ) - - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - db_table = "guide_stages" - ordering = ["order_no"] - indexes = [ - models.Index(fields=["order_no"]), - models.Index(fields=["is_active"]), - ] - - def __str__(self) -> str: - return f"[{self.order_no}] {self.title}" +from django.db.models import Count, Q class GuideCard(models.Model): """ - 가이드 카드 - - 각 단계(stage)에서 역할별로 제공되는 상세 가이드 - - 마크다운 형식 콘텐츠 + 역할별 미션 카드 + - 순차적으로 진행되는 미션 + - 역할(PM/FE/BE)별로 개별 미션 제공 """ - stage = models.ForeignKey( - GuideStage, - on_delete=models.CASCADE, - related_name="cards", - ) - role = models.ForeignKey( "accounts.Role", on_delete=models.CASCADE, @@ -72,16 +19,16 @@ class GuideCard(models.Model): title = models.CharField( max_length=120, - help_text="카드 제목", + help_text="미션 제목", ) content_md = models.TextField( - help_text="상세 가이드 내용 (마크다운)", + help_text="미션 설명 (마크다운)", ) order_no = models.IntegerField( default=0, - help_text="정렬 순서", + help_text="역할별 미션 순서", ) is_active = models.BooleanField( @@ -93,21 +40,35 @@ class GuideCard(models.Model): class Meta: db_table = "guide_cards" - ordering = ["stage", "role", "order_no"] + ordering = ["role", "order_no"] indexes = [ - models.Index(fields=["stage", "role", "order_no"]), + models.Index(fields=["role", "order_no"]), models.Index(fields=["is_active"]), ] def __str__(self) -> str: - return f"{self.stage.code} - {self.role.code}: {self.title}" + return f"{self.role.code} 미션 {self.order_no}: {self.title}" + + def get_progress(self, project) -> dict: + """프로젝트별 이 미션의 완료율""" + tasks = self.tasks.all() + completed = tasks.filter( + progress__project=project, + progress__is_completed=True + ).distinct().count() + total = tasks.count() + + return { + "completed": completed, + "total": total, + "percent": int((completed / total * 100) if total > 0 else 0), + } class GuideTask(models.Model): """ 가이드 태스크 (체크리스트 항목) - - 각 카드에 포함된 세부 할 일 - - 퀘스트 형식으로 진행 + - 각 미션(카드)에 포함된 세부 할 일 """ card = models.ForeignKey( @@ -153,7 +114,8 @@ def __str__(self) -> str: class GuideTaskProgress(models.Model): """ 가이드 태스크 진행 상황 - - 프로젝트 × 사용자 × 태스크 별 완료 여부 + - 프로젝트 × 태스크 별 완료 여부 + - 역할별 진척도를 추적 """ task = models.ForeignKey( @@ -165,13 +127,7 @@ class GuideTaskProgress(models.Model): project = models.ForeignKey( "projects.Project", on_delete=models.CASCADE, - related_name="task_progress", - ) - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="task_progress", + related_name="guide_task_progress", ) is_completed = models.BooleanField( @@ -190,14 +146,65 @@ class Meta: db_table = "guide_task_progress" constraints = [ models.UniqueConstraint( - fields=["task", "project", "user"], - name="uq_task_project_user", + fields=["task", "project"], + name="uq_task_project", ), ] indexes = [ - models.Index(fields=["project", "user"]), + models.Index(fields=["project"]), + models.Index(fields=["task"]), ] def __str__(self) -> str: status = "✓" if self.is_completed else "○" - return f"{status} {self.task.title} ({self.user})" + return f"{status} {self.task.title} ({self.project})" + + +class ProjectProgress(models.Model): + """ + 프로젝트 역할별 진척도 + - 각 역할(PM/FE/BE)의 미션 진행 상황 요약 + """ + + project = models.ForeignKey( + "projects.Project", + on_delete=models.CASCADE, + related_name="role_progress", + ) + + role = models.ForeignKey( + "accounts.Role", + on_delete=models.CASCADE, + related_name="project_progress", + ) + + completed_tasks = models.IntegerField( + default=0, + help_text="완료한 태스크 수", + ) + + total_tasks = models.IntegerField( + default=0, + help_text="전체 태스크 수", + ) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "project_progress" + constraints = [ + models.UniqueConstraint( + fields=["project", "role"], + name="uq_project_role", + ), + ] + + def __str__(self) -> str: + percent = int((self.completed_tasks / self.total_tasks * 100) if self.total_tasks > 0 else 0) + return f"{self.project} - {self.role.code}: {percent}%" + + @property + def progress_percent(self) -> int: + """진척도 퍼센트""" + return int((self.completed_tasks / self.total_tasks * 100) if self.total_tasks > 0 else 0) diff --git a/apps/projects/migrations/0004_remove_project_current_stage.py b/apps/projects/migrations/0004_remove_project_current_stage.py new file mode 100644 index 0000000..06cf870 --- /dev/null +++ b/apps/projects/migrations/0004_remove_project_current_stage.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.10 on 2026-02-06 04:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0003_project_is_favorite_project_project_image_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='current_stage', + ), + ] diff --git a/apps/projects/models.py b/apps/projects/models.py index c4923bd..b7e3455 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -168,15 +168,6 @@ class Status(models.TextChoices): help_text="즐겨찾기 여부", ) - current_stage = models.ForeignKey( - "guides.GuideStage", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="projects", - help_text="현재 진행 중인 가이드 단계", - ) - created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From c9e166ab5dc57d59ecbccc7d7618c8812e3db3fb Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 13:43:42 +0900 Subject: [PATCH 143/380] feat : dashboard_update.html --- templates/projects/dashboard.html | 2 +- templates/projects/dashboard_update.html | 145 +++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 templates/projects/dashboard_update.html diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index 0b9d666..eb202a9 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -140,7 +140,7 @@

진척도

- + {% endblock %} \ No newline at end of file diff --git a/templates/projects/dashboard_update.html b/templates/projects/dashboard_update.html new file mode 100644 index 0000000..d7dde56 --- /dev/null +++ b/templates/projects/dashboard_update.html @@ -0,0 +1,145 @@ +{% extends 'base.html' %} +{% load static %} +{% block header %} + +{% endblock %} +{% block content %} +
+

DASHBOARD

+

MISSION

+
+
+
+ {% csrf_token %} + + +
+ {% if project.project_image %} + 서비스이미지 + {% else %} + 서비스이미지 + {% endif %} +

{{ project.title }}

+

{{ project.description }}

+
+ + +
+ 달력이미지 +

진행기간 {{ season.project_start|date:"Y/m/d" }} ~ {{ season.project_end|date:"Y/m/d" }}

+
+ + +
+
+ 북마크이미지 +

즐겨찾기

+
+
+ {{ form.is_favorite }} +
+
+ +
+ + +
+
+ 팀 아이콘 +

Team

+ +
+
+

PM + {% for member in members %} + {% if member.role.name == "기획자" %} + {{ member.user.username }} + {% endif %} + {% endfor %} +

+

FE + {% for member in members %} + {% if member.role.name == "프론트엔드" %} + {{ member.user.username }} + {% endif %} + {% endfor %} +

+

BE + {% for member in members %} + {% if member.role.name == "백엔드" %} + {{ member.user.username }} + {% endif %} + {% endfor %} +

+
+ {% for member in members %} +
+ {% if member.user.profile_image %} + 프로필사진 + {% else %} + 프로필사진 + {% endif %} + 레벨사진 +

{{ member.user.username }}

+

✉️ {{ member.user.email }}

+

🖥️ @{{ member.user.username }}

+ {% if member.role.name == "기획자" %} +

기획자

+ {% elif member.role.name == "프론트엔드" %} +

프론트엔드

+ {% elif member.role.name == "백엔드" %} +

백엔드

+ {% endif %} +
+ {% endfor %} +
+
+
+ + +
+
+ rule +

팀 규칙

+
+
+ {{ form.team_rules }} +
+
+ + + + + +
+
+ progress +

진척도

+
+
+
+ {% if guide_progress %} +
+ {% else %} +
+ {% endif %} +
+
+ + +
+
+{% endblock %} \ No newline at end of file From 0b344c4ce7e88b82c8b6a0b8214e3faaa3a64f2a Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 14:24:28 +0900 Subject: [PATCH 144/380] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EC=9E=84=EC=8B=9C=20HTML=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/templatetags/__init__.py | 0 .../templatetags/reflections_extras.py | 9 ++ apps/reflections/views.py | 60 +++++++--- templates/reflections/_note_form.html | 110 +++++++++++++++++ templates/reflections/note_create.html | 11 ++ templates/reflections/note_detail.html | 113 ++++++++++++++++++ templates/reflections/note_update.html | 11 ++ 7 files changed, 298 insertions(+), 16 deletions(-) create mode 100644 apps/reflections/templatetags/__init__.py create mode 100644 apps/reflections/templatetags/reflections_extras.py create mode 100644 templates/reflections/_note_form.html diff --git a/apps/reflections/templatetags/__init__.py b/apps/reflections/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/reflections/templatetags/reflections_extras.py b/apps/reflections/templatetags/reflections_extras.py new file mode 100644 index 0000000..4a24185 --- /dev/null +++ b/apps/reflections/templatetags/reflections_extras.py @@ -0,0 +1,9 @@ +from django import template + +register = template.Library() + +@register.filter(name="get_item") +def get_item(d, key): + if not d: + return "" + return d.get(key, "") diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 76a0313..0ab56fd 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -114,13 +114,13 @@ def note_create(request): :tpl: 선택할 질문 템플릿 (현재는 default 하나만) """ - tpl = request.GET.get("tpl") or "default" - guide = load_guide(tpl) + tpl_key = request.GET.get("tpl") or "default" + guide = load_guide(tpl_key) if request.method == "POST": title = (request.POST.get("title") or "빈 제목").strip() if not title: - context = {"guide": guide, "tpl": tpl, "error": "제목은 필수입니다."} + context = {"guide": guide, "tpl": tpl_key, "error": "제목은 필수입니다."} return render(request, "reflections/note_create.html", context) answers = dict() # qid: "답변 내용" 형식 @@ -132,7 +132,7 @@ def note_create(request): Retrospective.objects.create( user= request.user, - template_key=tpl, + template_key=tpl_key, title=title, answers_json=answers, content_md = content_md, @@ -140,7 +140,8 @@ def note_create(request): return redirect("reflections:note_list") context = { "guide": guide, - "tpl": tpl, + "tpl": tpl_key, + "answers": {}, } return render(request, "reflections/note_create.html", context) @@ -152,32 +153,59 @@ def note_detail(request, note_id): note = get_object_or_404(Retrospective, id=note_id, user=request.user) context = { "note": note, + "guide": load_guide(note.template_key), + "answers": note.answers_json or {}, } return render(request, "reflections/note_detail.html", context) @login_required def note_update(request, note_id): - """회고 수정""" - # TODO: 회고 수정 로직 구현 + """회고 수정 - note_create와 동일하게 guide 기반으로 렌더/저장""" note = get_object_or_404(Retrospective, id=note_id, user=request.user) + tpl = note.template_key or "default" + guide = load_guide(tpl) + + # 기존 답변(answers_json)로 textarea 기본값 채우기 + existing_answers = note.answers_json or {} + if request.method == "POST": - form = RetrospectiveForm(request.POST, instance=note) - if form.is_valid(): - form.save() - messages.success(request, "회고가 수정되었습니다.") - return redirect("reflections:note_detail", note_id = note.id) - else: - form = RetrospectiveForm(instance=note) - + title = (request.POST.get("title") or "빈 제목").strip() + if not title: + context = { + "note": note, + "guide": guide, + "tpl": tpl, + "answers": existing_answers, + "error": "제목은 필수입니다.", + } + return render(request, "reflections/note_update.html", context) + + answers = {} + for q in guide["questions"]: + qid = q["id"] + answers[qid] = (request.POST.get(f"a__{qid}") or "").strip() + + content_md = build_markdown(guide, answers) + + note.title = title + note.answers_json = answers + note.content_md = content_md + note.save(update_fields=["title", "answers_json", "content_md", "updated_at"]) + + return redirect("reflections:note_detail", note_id=note.id) + context = { "note": note, - "form": form, + "guide": guide, + "tpl": tpl, + "answers": existing_answers, } return render(request, "reflections/note_update.html", context) + @login_required def note_delete(request, note_id): """회고 삭제""" diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html new file mode 100644 index 0000000..c5866eb --- /dev/null +++ b/templates/reflections/_note_form.html @@ -0,0 +1,110 @@ + + +{% load reflections_extras %} + +{% comment %} +필수 context: +- guide: {"title": "...", "questions":[{"id":"q1", "label":"..."}, ...]} +- tpl: template key +- answers: dict (qid -> text) (create는 빈 dict 가능) +- note: (update에서만 있을 수 있음) +- error: (선택) +{% endcomment %} + +
+ {% if error %} +
+ {{ error }} +
+ {% endif %} + + +
+ +
+ + +
+ + template: {{ tpl }} + + {% if note %} + + {{ note.created_at|date:"Y.m.d (D)" }} + + {% endif %} +
+ + +
+ {% for q in guide.questions %} +
+ + +
+
+ {{ q.order|default:forloop.counter }}. {{ q.title }} +
+ {% if q.hint %} +
+ {{ q.hint }} +
+ {% endif %} + {% if q.examples %} + + 예시) + +
    + + {% for ex in q.examples %} +
  • {{ ex }}
  • + {% endfor %} +
+ {% endif %} +
+ +
+ +
+
+ {% endfor %} +
+ + +
+ 내보내기용 마크다운 보기 +
+ +

+ * content_md는 저장/내보내기용이며, 작성 UI의 메인은 answers_json 입니다. +

+
+
+ + +
+ + 목록 + + +
+
diff --git a/templates/reflections/note_create.html b/templates/reflections/note_create.html index e69de29..f2b10d5 100644 --- a/templates/reflections/note_create.html +++ b/templates/reflections/note_create.html @@ -0,0 +1,11 @@ + + +{% extends "base.html" %} +{% load reflections_extras %} + +{% block content %} +
+ {% csrf_token %} + {% include "reflections/_note_form.html" with guide=guide tpl=tpl answers=answers %} +
+{% endblock %} diff --git a/templates/reflections/note_detail.html b/templates/reflections/note_detail.html index e69de29..31e13f5 100644 --- a/templates/reflections/note_detail.html +++ b/templates/reflections/note_detail.html @@ -0,0 +1,113 @@ + + + +{% extends "base.html" %} +{% load reflections_extras %} + +{% block title %}회고 상세{% endblock %} + +{% block content %} +
+ + +
+
+
+ {{ note.title }} +
+
+ 템플릿: {{ note.template_key }} · + 생성: {{ note.created_at|date:"Y-m-d H:i" }} · + 수정: {{ note.updated_at|date:"Y-m-d H:i" }} +
+
+ + +
+ + +
+
+ + bookmarked: {{ note.bookmarked|yesno:"true,false" }} + + + {% if note.project %} + + project: {{ note.project.title }} + + {% else %} + + 개인 회고 + + {% endif %} +
+
+ + +
+ {% for q in guide.questions %} +
+ + +
+
+ {{ q.order|default:forloop.counter }}. {{ q.title }} +
+ + {% if q.hint %} +
+ {{ q.hint }} +
+ {% endif %} + + {% if q.examples %} +
    + {% for ex in q.examples %} +
  • {{ ex }}
  • + {% endfor %} +
+ {% endif %} +
+ +
+ +
+
+ {% endfor %} +
+ + +
+ + content_md 보기(내보내기용) + +
+ +
+
+ +
+{% endblock %} diff --git a/templates/reflections/note_update.html b/templates/reflections/note_update.html index e69de29..3a711b3 100644 --- a/templates/reflections/note_update.html +++ b/templates/reflections/note_update.html @@ -0,0 +1,11 @@ + + +{% extends "base.html" %} +{% load reflections_extras %} + +{% block content %} +
+ {% csrf_token %} + {% include "reflections/_note_form.html" with note=note guide=guide tpl=tpl answers=answers %} +
+{% endblock %} From 007468806c8ba40e2275babe75004d374e5f5429 Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 14:25:11 +0900 Subject: [PATCH 145/380] =?UTF-8?q?feat:=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/urls.py | 3 +- apps/guides/views.py | 84 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/apps/guides/urls.py b/apps/guides/urls.py index bf91e5e..010f3bb 100644 --- a/apps/guides/urls.py +++ b/apps/guides/urls.py @@ -5,6 +5,5 @@ urlpatterns = [ # 미션/체크리스트 페이지 - path("mission/", views.mission, name="mission"), # mission.html - path("mission//", views.mission_detail, name="mission_detail"), # mission.html (특정 프로젝트) + path("mission/", views.mission, name="mission"), ] diff --git a/apps/guides/views.py b/apps/guides/views.py index 6bbc86d..d41d982 100644 --- a/apps/guides/views.py +++ b/apps/guides/views.py @@ -1,21 +1,83 @@ from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required +from django.db.models import Count, Q -# from .models import Guide +from apps.projects.models import Project +from apps.teams.models import TeamMember +from .models import GuideCard, GuideTask, GuideTaskProgress, ProjectProgress +from .services import GuideService @login_required def mission(request): - """미션/체크리스트 페이지""" - # TODO: 미션 로직 구현 - return render(request, "guides/mission.html") - - -@login_required -def mission_detail(request, project_id): - """특정 프로젝트 미션 페이지""" - # TODO: 특정 프로젝트 미션 로직 구현 + """사용자의 미션 페이지 (현재 프로젝트)""" + # 사용자가 참여한 프로젝트 조회 + project = ( + Project.objects + .filter(team__members__user=request.user, team__members__is_active=True) + .first() + ) + + if not project: + return render(request, "guides/mission.html", {"project": None}) + + # 사용자의 역할 조회 + team_member = get_object_or_404( + TeamMember, + team=project.team, + user=request.user, + is_active=True + ) + role = team_member.role + + # 해당 역할의 모든 미션 조회 + guide_cards = GuideCard.objects.filter( + role=role, + is_active=True + ).prefetch_related('tasks') + + # 각 미션의 진행도 계산 + mission_data = [] + for card in guide_cards: + tasks = card.tasks.all() + completed_tasks = GuideTaskProgress.objects.filter( + task__in=tasks, + project=project, + is_completed=True + ).count() + + # 태스크 데이터 + task_progress_data = [] + for task in tasks: + progress = GuideTaskProgress.objects.filter( + task=task, + project=project + ).first() + + task_progress_data.append({ + 'task': task, + 'is_completed': progress.is_completed if progress else False, + 'completed_at': progress.completed_at if progress else None, + }) + + mission_data.append({ + 'card': card, + 'content_html': GuideService.render_markdown(card.content_md), + 'total_tasks': tasks.count(), + 'completed_tasks': completed_tasks, + 'progress_percent': int((completed_tasks / tasks.count() * 100) if tasks.count() > 0 else 0), + 'task_progress_data': task_progress_data, + }) + + # 모든 역할의 진척도 (PM/FE/BE 전부 표시) + all_role_progress = ProjectProgress.objects.filter( + project=project + ).select_related('role') + context = { - "project_id": project_id, + 'project': project, + 'role': role, + 'mission_data': mission_data, + 'all_role_progress': all_role_progress, } return render(request, "guides/mission.html", context) From 4cd2d077c40c596755fe24ddf188311274798263 Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 14:25:50 +0900 Subject: [PATCH 146/380] =?UTF-8?q?feat:=20service=EB=A1=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20ajax=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20api=5Fview=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/api_urls.py | 16 +++- apps/guides/api_views.py | 174 +++++++++++++++++++++++++++++++++++++++ apps/guides/services.py | 96 +++++++++++++++++++++ 3 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 apps/guides/api_views.py create mode 100644 apps/guides/services.py diff --git a/apps/guides/api_urls.py b/apps/guides/api_urls.py index e2d5eea..cf2d6e4 100644 --- a/apps/guides/api_urls.py +++ b/apps/guides/api_urls.py @@ -1,5 +1,19 @@ from django.urls import path +from . import api_views + +app_name = "guides_api" urlpatterns = [ - # 가이드 API URL은 추후 추가 + # 태스크 완료/미완료 처리 + path( + "projects//tasks//toggle/", + api_views.toggle_task_completion, + name="toggle_task", + ), + # 프로젝트 역할별 진척도 조회 + path( + "projects//progress/", + api_views.get_project_progress, + name="project_progress", + ), ] diff --git a/apps/guides/api_views.py b/apps/guides/api_views.py new file mode 100644 index 0000000..0010119 --- /dev/null +++ b/apps/guides/api_views.py @@ -0,0 +1,174 @@ +from django.shortcuts import get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse +from django.views.decorators.http import require_http_methods +from django.utils import timezone +from django.db.models import Count +import json + +from apps.projects.models import Project +from apps.teams.models import TeamMember +from .models import GuideTask, GuideTaskProgress, ProjectProgress, GuideCard + + +@login_required +@require_http_methods(["PATCH"]) +def toggle_task_completion(request, project_id, task_id): + """ + 태스크 완료/미완료 토글 + + Request: + PATCH /api/guides/projects/{project_id}/tasks/{task_id}/toggle/ + { + "is_completed": true + } + """ + try: + # JSON body 파싱 + body = json.loads(request.body) + is_completed = body.get('is_completed', False) + + # 권한 확인: 사용자가 이 프로젝트의 팀원인가? + project = get_object_or_404( + Project, + id=project_id, + team__members__user=request.user, + team__members__is_active=True + ) + + task = get_object_or_404(GuideTask, id=task_id) + + # 태스크가 사용자의 역할에 속하는가? + team_member = TeamMember.objects.get( + team=project.team, + user=request.user, + is_active=True + ) + + if task.card.role != team_member.role: + return JsonResponse( + {"error": "이 미션은 당신의 역할이 아닙니다"}, + status=403 + ) + + # GuideTaskProgress 생성 또는 업데이트 + progress, created = GuideTaskProgress.objects.get_or_create( + task=task, + project=project + ) + + # 완료 상태 변경 + if is_completed and not progress.is_completed: + progress.is_completed = True + progress.completed_at = timezone.now() + elif not is_completed and progress.is_completed: + progress.is_completed = False + progress.completed_at = None + + progress.save() + + # ProjectProgress 업데이트 + _update_project_progress(project, team_member.role) + + return JsonResponse({ + "success": True, + "is_completed": progress.is_completed, + "completed_at": progress.completed_at.isoformat() if progress.completed_at else None, + }) + + except json.JSONDecodeError: + return JsonResponse( + {"error": "Invalid JSON"}, + status=400 + ) + except Exception as e: + return JsonResponse( + {"error": str(e)}, + status=400 + ) + + +@login_required +@require_http_methods(["GET"]) +def get_project_progress(request, project_id): + """ + 프로젝트의 역할별 진척도 조회 + + Response: + { + "project": {...}, + "progress": [ + { + "role": "PM", + "completed_tasks": 5, + "total_tasks": 10, + "progress_percent": 50 + }, + ... + ] + } + """ + try: + # 권한 확인 + project = get_object_or_404( + Project, + id=project_id, + team__members__user=request.user, + team__members__is_active=True + ) + + # 모든 역할의 진척도 + progress_data = [] + for role_progress in ProjectProgress.objects.filter(project=project): + progress_data.append({ + "role": role_progress.role.code, + "completed_tasks": role_progress.completed_tasks, + "total_tasks": role_progress.total_tasks, + "progress_percent": role_progress.progress_percent, + }) + + return JsonResponse({ + "success": True, + "project_id": project.id, + "project_title": project.title, + "progress": progress_data, + }) + + except Exception as e: + return JsonResponse( + {"error": str(e)}, + status=400 + ) + + +def _update_project_progress(project, role): + """ + ProjectProgress 업데이트 로직 + 특정 역할의 진척도를 계산하고 저장 + """ + # 해당 역할의 모든 미션과 태스크 + guide_cards = GuideCard.objects.filter(role=role, is_active=True) + + total_tasks = 0 + completed_tasks = 0 + + for card in guide_cards: + tasks = card.tasks.all() + total_tasks += tasks.count() + + completed = GuideTaskProgress.objects.filter( + task__in=tasks, + project=project, + is_completed=True + ).count() + completed_tasks += completed + + # ProjectProgress 생성 또는 업데이트 + progress, created = ProjectProgress.objects.get_or_create( + project=project, + role=role + ) + + progress.total_tasks = total_tasks + progress.completed_tasks = completed_tasks + progress.save() diff --git a/apps/guides/services.py b/apps/guides/services.py new file mode 100644 index 0000000..4c17e92 --- /dev/null +++ b/apps/guides/services.py @@ -0,0 +1,96 @@ +""" +가이드 관련 비즈니스 로직 +""" +import markdown +import bleach +from django.db.models import Count + +from .models import GuideCard, GuideTask, GuideTaskProgress, ProjectProgress + + +class GuideService: + """가이드 서비스""" + + @staticmethod + def render_markdown(content): + """마크다운을 HTML로 변환 (XSS 방지)""" + if not content: + return "" + + html = markdown.markdown( + content, + extensions=['tables', 'fenced_code', 'nl2br', 'toc'] + ) + + # XSS 방지: 안전한 태그만 허용 + allowed_tags = [ + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'p', 'br', 'strong', 'em', 'u', 'del', + 'ul', 'ol', 'li', + 'blockquote', + 'code', 'pre', + 'table', 'thead', 'tbody', 'tr', 'th', 'td', + 'a', 'img', + ] + + allowed_attributes = { + 'a': ['href', 'title', 'target'], + 'img': ['src', 'alt', 'title'], + 'code': ['class'], + } + + html = bleach.clean(html, tags=allowed_tags, attributes=allowed_attributes) + return html + + @staticmethod + def get_role_progress(project, role): + """ + 역할별 진척도 조회 + + Returns: + { + 'role': Role, + 'completed_tasks': int, + 'total_tasks': int, + 'progress_percent': int, + } + """ + guide_cards = GuideCard.objects.filter(role=role, is_active=True) + + total_tasks = 0 + completed_tasks = 0 + + for card in guide_cards: + tasks = card.tasks.all() + total_tasks += tasks.count() + + completed = GuideTaskProgress.objects.filter( + task__in=tasks, + project=project, + is_completed=True + ).count() + completed_tasks += completed + + progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) + + return { + 'role': role, + 'completed_tasks': completed_tasks, + 'total_tasks': total_tasks, + 'progress_percent': progress_percent, + } + + @staticmethod + def get_all_role_progress(project): + """프로젝트의 모든 역할 진척도""" + progress_list = [] + + for proj_progress in ProjectProgress.objects.filter(project=project): + progress_list.append({ + 'role': proj_progress.role, + 'completed_tasks': proj_progress.completed_tasks, + 'total_tasks': proj_progress.total_tasks, + 'progress_percent': proj_progress.progress_percent, + }) + + return progress_list From d5e075b443fe5428da58f7831fb873b3ddd52295 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 14:27:49 +0900 Subject: [PATCH 147/380] feat : mission.html&css --- static/css/mission.css | 283 ++++++++++++++++++++++++++++++ static/images/rocket.png | Bin 0 -> 62598 bytes static/js/mission.js | 38 ++++ templates/guides/mission.html | 113 ++++++++++++ templates/projects/dashboard.html | 2 +- 5 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 static/css/mission.css create mode 100644 static/images/rocket.png create mode 100644 static/js/mission.js diff --git a/static/css/mission.css b/static/css/mission.css new file mode 100644 index 0000000..4673137 --- /dev/null +++ b/static/css/mission.css @@ -0,0 +1,283 @@ +/* 프로젝트 헤더 */ +.p_header { + width: 90%; + height: 50px; + margin: 0 auto; + display: flex; +} + +.p_header a { + display: block; + width: 50%; + text-align: center; + padding: 15px; + text-decoration: none; + background: #eaf0ff; +} + +.p_header .p_dashboard { + background: #fff; + color: #cad9ff; +} + +.p_header .p_mission { + background: #eaf0ff; + color: #1d294b; +} + +.p_header .p_dashboard:hover { + background: #eaf0ff; + color: #1d294b; + transition: 0.3s ease-in-out; +} + +.p_header a p { + font-size: 18px; + font-weight: 700; +} + +/* 진척도 */ +.mission_progress { + background: #eaf0ff; + width: 90%; height: 350px; + padding: 50px 20px; + margin: 0 auto; +} + +.mp_title { + display: flex; + align-items: center; +} + +.mp_title > img { + width: 100px; height: 100px; +} + +.mp_title > div > h3 { + font-size: 25px; font-weight: 600; + margin-bottom: 15px; +} + +.mp_title > div > p { + font-size: 14px; + color: #FF0000; +} + +.pm_progress { + margin: 20px 0 15px 100px; + display: flex; + align-items: center; +} + +.pm_progress > h3 { + width: 30px; + font-size: 20px; font-weight: 550; + margin-right: 15px; +} + +.pm_progress > .pm_bar { + position: relative; + width: 85%; height: 15px; + background: #DDDDDD; + border-radius: 20px; +} +.pm_progress > .pm_bar > .pm_real { + position: absolute; + top: 0; left: 0; + width: 60%; height: 100%; + background: #37D3BF; + border-radius: 20px; +} + +.front_progress { + margin: 20px 0 15px 100px; + display: flex; + align-items: center; +} + +.front_progress > h3 { + width: 30px; + font-size: 20px; font-weight: 550; + margin-right: 15px; +} + +.front_progress > .front_bar { + position: relative; + width: 85%; height: 15px; + background: #DDDDDD; + border-radius: 20px; +} + +.front_progress > .front_bar > .front_real { + position: absolute; + top: 0; left: 0; + width: 50%; height: 100%; + background: #FFDF6E; + border-radius: 20px; +} + +.back_progress { + margin: 20px 0 15px 100px; + display: flex; + align-items: center; +} + +.back_progress > h3 { + width: 30px; + font-size: 20px; font-weight: 550; + margin-right: 15px; +} + +.back_progress > .back_bar { + position: relative; + width: 85%; height: 15px; + background: #DDDDDD; + border-radius: 20px; +} + +.back_progress > .back_bar > .back_real { + position: absolute; + top: 0; left: 0; + width: 70%; height: 100%; + background: #FF69A4; + border-radius: 20px; +} + +/* 미션 아이템 */ +.mission_item { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.timeline_dot { + display: flex; + flex-direction: column; + align-items: center; + width: 60px; + flex-shrink: 0; +} + +.circle { + width: 20px; + height: 20px; + background: #4272EF; + border-radius: 50%; + border: 4px solid #fff; + box-shadow: 0 0 0 2px #4272EF; + z-index: 5; + position: relative; +} + +.line { + width: 3px; + height: 100%; + background: #4272EF; + flex: 1; + margin-top: -2px; +} + +.mission_item:last-child .line { + display: none; +} + +/* 카드 래퍼 */ +.card_wrapper { + flex: 1; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* 미션 카드 */ +.mission_card { + background: #F8F9FF; + border: 2px solid #E0E7FF; + border-radius: 20px; + padding: 20px 25px; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 8px rgba(66, 114, 239, 0.08); +} + +.mission_card:hover { + border-color: #4272EF; + box-shadow: 0 4px 12px rgba(66, 114, 239, 0.15); +} + +.card_header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.card_header h3 { + font-size: 18px; + font-weight: 600; + color: #4272EF; + margin: 0; +} + +.check_icon { + width: 28px; + height: 28px; + transition: transform 0.3s ease; +} + +.mission_card:hover .check_icon { + transform: scale(1.1); +} + +/* 카드 내용 (기본 숨김) */ +.card_content { + max-height: 0; + overflow: hidden; + opacity: 0; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + margin-top: 0; +} + +.card_description { + font-size: 14px; + color: #6B7280; + margin: 15px 0 10px 0; +} + +.mission_input { + width: 100%; + padding: 12px 16px; + border: 2px solid #E0E7FF; + border-radius: 12px; + font-size: 15px; + color: #9CA3AF; + background: #fff; + transition: all 0.3s ease; + box-sizing: border-box; +} + +.mission_input:focus { + outline: none; + border-color: #4272EF; + color: #1d294b; +} + +/* 활성화된 카드 */ +.mission_card.active { + background: #fff; + border-color: #4272EF; + box-shadow: 0 8px 24px rgba(66, 114, 239, 0.2); + transform: scale(1.02); +} + +.mission_card.active .card_content { + max-height: 300px; + opacity: 1; + margin-top: 15px; +} + +/* 완료된 카드 */ +.mission_card.completed .check_icon { + /* check.png로 변경될 예정 */ +} + +.mission_card.completed { + opacity: 0.8; +} \ No newline at end of file diff --git a/static/images/rocket.png b/static/images/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..d8862529eb48e3f00940351fe6c0b0a960a0a680 GIT binary patch literal 62598 zcmeFY^;4T|)HT{7#Vxo?f;$waP&~nkyK9RSD{jRTJXrA-cXxL!(n4`}cQ1D6^M3Q5 z^Dms4?}sFldoq)IUwdC`ueJ7d!&H>zu+YiSU%h&T1p!N|y?TWJ`u9S8^Kymbg3Iyc z1I-bv>+>%4bKMK2d$yQs-YzA7K1*n9bcWG$g2@#w?JY!NghA&Ck~a54DglAJ*ytUdMUP_F-u6=(-^(^?6bFD^eWFE2Jadgt4Ik%57W zii8mGp6364{NE+`zg6)6pN-&=AN1eNu6#(}T`|Q*SR)O^{kM@lglW;B4-Eq6*LbgK zMK>hS(O!P$<3RhIfi)t`7twv>;5RgQ4PT3h<>!c+zR6e)m+QqKmpi%Lo_V@TbzgZ@ zx}c(C4Yo#t#=by8_cI#_4}fk++U)!7`m_7oZ1mNmc)M5QNkNcgqi1^FY~w7uu5x>F|~H7EltvqC1_9~aN-{5)tJ%q$#24r zGiR4YB&2R;Y&y_Ow5LlO5Y5=2j+^S5{!nxWbi^mbi#oUGsyw3oPn|tx%*+I=CR@i` z`{d@ocNwG~%~7)v0%?#U=)jT)3=oJAso+ze<9N=^)$!lE7)T-rIlwU-_#YT%qWf?{ zK{UfS=N01auHzWfWqz9OGZPi#?6TGrh6cUAgJO|@>;&OXxK~Jdg#<`jfsR!$l3Y7Dm|A z#N|8n-_j&LXmfy#sT201GbtVR1sjeJ(|NdgZ4 zp(3lY|3=+Hx{y{4kJ%kVCO0=zyW(2}4fQ!9>CS3gLLolxR;otQXK7akrJ$4$l9GWI zW6sRK7%CWCYXm5GfR}<$?14E;OyC&#y56nxc7L9*C6LeNP5J zc-PD|F$`rHdQ~n9ghJ=Hmv!a+Kv?DL0k

%NX<@tkHti2``D$-_xWQ@3Qr+jym|c zoCXejZY*r6csMxd>2}58Rj0O+lnc_0AB~%b^0uwCW+mmTZl}Z(b?w&_phm+9J zX~N^KBarf{>F8tc!}SPFO(p8i(F1ezy$Jc@=*gB0H=cnbNTh2J%-RF6)ip4~n{JQw zZMRiTljqgA-{z~r$qsg6Tkdc&hZ@Bqi_xd&sKAV0!&rI=Y$gLey>m zp%RMH4^&iZS&}N{SW9sfb^7GObEVWmWKmQrVskqZ>Rax!k+lD+4)Y%tn9^?q`^8qY zu4k`hJv|vOtFk>lNR3Ya%8^U+h}3KX(F8i8zLqYeF|*u6AH}j`(k;})vw}kDfWZ&|`aJ8WXE0$)_9H{)}yeD7QZ?;}%yp}Q#!1V}+LX;DiaklW`HgZ)agHTo3@O}Zh8Dxkooo1#g{igpWE z?K7Nt)6-U1pRC}D&9Ije-6f9D7zVBMK5esBH-bYM0j||Bb83mscT{yTyaPgbzNT(5 zvyF^2TX<~Yk(!lWVy;XaTxBQXW1Y-7g^Q18wOd&(-~+vC3yAc$bnpQCkM^%$o=_+9 z&?-H+kE7QMSxs!4{kN$Z-~R&v^nd8qJ%lb|nqkn;7k4Vqk>Y=y#A)O`*FTYFogYE` z;Vsde?^LP=-a40wlSG24G$Clj&-PP@q7?ejFla4+R-NMYhm^+UJnAq8&?b;JJ&s0Fk_K1#jSipm zFoI2`@2;h`A-@hs#|2!=d2Uk;D*}bl6O@2UR45sF9+aEj!UH9{&tU1QMqN6QNDrn+#$(XW~hP3X9mJ3!4$(dp}X6 ze58{X5QU^m(%rC9L#mw1h0tHRGBeuwOMeeOVYIU=A~Wgm8qKq9xV-A%h3r?P>e$G* z93>q_LK5(T&s4Ck0!B|x>r>7`n4a(~#PFJW5-n@BrN@Eh&N9I~2pi8|XSFUyr1h{= zxli-rSeITD;P=0^hmN*4AL6rJYCxe`&Fnk%D9b z4oGcVGcOo?PP^cxxeOhn_+1Qz!EjuOqn{|!X?*K8%_cv2iZyRI-403G-G1z3?7crBzaAe)x#5_yfH2_k zKuOj-KcZY~#9i+)8-T8Jka)+7EAq7eA_xq2usKYAnB%r(w^#h z@pi-YF&pnEy}SE*tm(<7EaUeFJ@R4)hpyDpl(K2%bv}7lOBh~w)f?21U&BE4v02*|{+!j`|7?OQwjf~>oIqnJ0 zEs*+VUF*;ri>F`Cof|y27+bm8J#I4X)dBNs7^s$(=KF1710GA;*ET67%Z7&EfDU$x zRY%(fQ6sOSNSxQNML6)0CV3TReO*_YZbjR%5Y`A@81hSzi=&NV*6Q+k3GF!OQ-*Xr zeykg_>dJ%g;bwy^L{tdO;lV%^(!9ZQ1x^8WN zD0D8~vNkv^b@4*J$vHT(Lz>J4I2mOwY+WmCHiY_f3#!A^-IZ?Idpx2+KseFQt7ds2 zX9o)1O|d)l&ft3RuX8LEvQe1!qv7P5sN3 zpH<2-LlreFNO16Zp}gfgpMF0CvPyh~@ZdR4s`O|yN!Fk2OBhjQT5?nUGOzzBp_%oO zvdfx{b*58Wo^6lIWsIh}9Z-_{Q4&9SB2wMN=X}k5yfcTo>G7|bVNjHBpoo31LD*ig zs{0sjLT}QS!z{J{^G}00-y%l8zaV)x^PB(5y#s^M?P07xs>E2Zb>BFqPG4Jtn8*{< zhei99NzJrH@y9!w5kS;e%Z%1V77!Obwv@1)V={%G6gvp)XDXuQY{qI@4{-@CPI4P>9@Zo09h+=}h0}GE~I&b1tq``4AHWbL3#0A*IPq#agH$;SJ>ZJ9*ZRBfHDQS48t2Z=j{67Al3S7_ zM{|u%Ps63XzP|F+w?dRgWuY><#i)o%e8~bd`f&(KAEX%kSLu2GM|$PBxqrtuI0~Hzk-9e>uA$?{%1e%3i=P`E2WL??d{#o0Og|X92yql z)O(#XTU6{Q<%XZ^{lsp4w2@oxQBF$(j0|Y{{{+&oDVV7!5pZ)aW1pKY4)k4Hc3P<(w zu4>lleP0qKs1(~w>H$}Jze9yGNGCG#tg|zp1U~F_&HrWgKwGY1&Nz*sSP;oqQ_Mk@ zrn;`wSnyEM+!LDOQoRXFsgC|B_c}cwDP=zXLv<`k=XETlFbOSR~bb6gZ5V}RD}t@j!$};)kuRzlSr)1Pk;5d zcyA(7O7~hNlEm8_*9F|zj9+BcBPyuH!r`5V0!zFSzl3I#R;~8xU=>`H?~@`otxoH{ zX9AgzVI$gsBQzM_pQtYn+Cw795@!74n5!`k zLU^1vcU2$EXGGiEtgy-#Td8)~rhbd_McJ?r#Ajz_%A_gRm@c_i%~nTVsn3c6J{QwY zOJ$=<7H1m@;UhM&aa8GqWs%SU-%%H7Kt}3zcQDN>bNk;Udd?Jcs(X9W!_|xuY2I^e zDkfU!w1@UI%^;7|ZSjq202Xv}{@_i1>QS=gYB=h8G7$Sql@|i?%M@Zl&c#zkQ^9`0 zwB2Ab5Rq*0F1eDAS?R=S&E}hh2B^X|T*KM)H(HNK1#~<>RNlcO0H`#8; zEDxr3dmG(@J<5oA zJNF?zaKk*?hl_R)5;_q8A);-`J9@~a_4=OtF>ige8v0<{jC-w+h$ z4_`{-->6nPsM_l^tqcnL%>ngK>OJ#4lZlJ(On6^9mym{?LbJ*sOr_fpMXNI5SE3hg zUX;eAy-*p&dmY$R@G!ekZ3Z$*YxL#8mnr6vhofP+%15!7jcK_3Eaaf2Y78pz{c5h) zoH+9x%$e4~P`7Z+XC#=a0M%TQili*jX1X{}V#C-$L4__+lA-_en}ZD)5;?aVWPUW)CGl9Fhvrfe`>Jc*2K z;^)Y4b5YQ|xS6@Wddf%gUdc7mBU0h(>w}Xfv^zSyfJ(amg zZPkkAgQ(j5TDBkg=z*V6VG7P$**3kCQ*CSe4SrWo9idMR>5*l@gjoMLltk1I@wuh8 zeZn@|4kxI9JEM&nX%|otqf|p=^W-9*O3`OFw^ zZ#705zkT=aZlN8yY?g{fuEZU)f%S+p*ngn=Y(tP^5KY{cvA?{Hs4ehE?ai?i-5?r1 zdL)QJi*QJ>UN>XN;q|p9{>+<0RFu{HJKo@4ILa3?jSSBoW@Zjh6mZ8Sz@P0(2-EKu znZST{Z+CE3(ab#qkw|5{DelbNX7VTB9Us7N~0gbc)dScUC1C@-{AZj@-}=rEza{!!)(Ci&T#EVKgCNv&gHz^3SwtbxI8Gozw7z45}c zLA7625Wzd!cu@OqN(ka%y_{1Xb08y_>`|Qs0%^)>`C<2ka1dj{?!X?JD{4v7?g6Am zf50~`Fj6r$-!5mQMmS6w4!{r12Zz&xVA|Dd?U1CrHrfccGToc<56cZ<2otERR4>#h z*jms?jEn2Ec{r6kChv%MzR2}MwcVEs9`Vih)J6Przwq$ySp9u**GnZyf2$F?i=E?A z2|6Q@2g{$AmXvpo@s4%sT4#h{a5lbYW<2-3^1`y;gTFE$#`dVEjF9l~vw94EUc z9Y@*wsHEzyk5jTmci>^^CJ^SVs=~74EWdgu`|ykBK*EWwx7#PFeC&2B;ZH1Xfi%f8 zY%eq5dwqENCvCB=^D@aMbD_ju*J+DjU_pY8*^6dX+Ghr4PN+Hb9bT@rJ3Pz@9Ory} z+a3PTv0WpL{N8^T65$Ycg%$`*H6bab}F`0Y9`#K(u^{@cWF=AX4|2C?cxr9e~7m3AHfSi{0=nGR;i zKo6t~Rmo+SQ0$%c)YItBdKa_C+Tce%Sw>WEHy8L>^>dL%vtu(2^G>D2-d@g zs6mOtQnSJZR_!Q5MPqHpw@NDHWYNn=RNrbo%rw@!fAS67_6SooQ5*^#NRiL}$!DZz zn|>!(yVFYn98Dngy!;(7ajW0qJT8*;{Em?t&9v{IA99M>7p850*Rqk`AsZ4l^gKs_ zT@l|G6KWkWO`>DHqZo^vBO?(^gZ`W79w70SzgB3PU*BVO+1qKd2Pd0W%zEQ5Rxw6F zOAEO*82_-`kcj{zIVDrVkcqD7n-)t0J~voFZNl!Y)59$iw%A!C9cO{D6KQ?NH6N4x z;@3HpzN#C__ZmzHAz*y$W4TuMa^8bt@BT!~Us>U^x*n6QW@du{9N&|D@brrV*dIN@ zLurUd>hGNKzb5r_WKD%u-VLL=XW3C2@=w42=?TzaX!_5r1 zje4pwrjC;D(2eitk201XM?@U=4k*gp>vNmd5Ez5lBiXq9YUj%pBu1 z@okF=$Rj&$y`fs4OJrNFFwxb3Xn(#{)ux9Ec|23H!?a}#G_1yV!`d9AV~*< zf#}(@YS}p0ib?(snd9#sAN1{M8^T;Ge1v{YM$CC1%lK@PbeD7RPI4ODF^G>xYA>q- zMO;{5M{#E&4t8X=V(PsIUJ!N&*As61;IL;sPJY4$k-$#}8>_$XRG*;VwI@{n{@xi# zW5e<@`FQH<;JLYqfErBr&p_|yMX`x|?1S@h>%YJzFelb>*kujq8~gW?%`VLB|+D z0km|fvB4~zY^Zg@bkryeT4D+J;WAA3`)AB{r2@E=8J%SfD*+02e;F^I|H`1j)#Ca$&m5kFevY3mwL>+oBygm5r1z*8t=rzmoq8_z};BrKV_zq z(3$Pzn_lxdr|G+b#2KUMSRm8q5g^_MVPhEURQmh80JV=g0LF()O{qtRmZxK!fPX#x ze-epQK}LDxDPy^6I}rE9epsCxBk3U53Gc`ajkf|^KOGzd2?6mGLlxCE7vK!kHk8}L zf?2w}s>7dPUG+TNA!VcdNtv@@Q(G;@of z;Qd4g5Hs8cVpvG;QsW{hCIl;{d24yrepaSmb!^EgVL7ABCR&eDhe9Q-QT_`4#5iuq zBQT6J&dNjL2fAh^#%&wmbJrr83g#wh++_l!z*gC*HQ)WPno@P^d;Ufer4ipl{c5neF8D2l_ag1@!;ka5%|MFJAHGO@J-BJGQxo;0d^+mp;AiPjMWQTmH?kd!@zWxbk zgAna#9)QeeDeD)somOEE=X%ETP}$TffjVJH?U~+ODmH9$oR14MdxsOdxC{ zWx+tdxeZw-P=FU;o<5LkI{+eYGnJ~Mu$|B$4Ir44W%FeYI+!sXRWOb`AEQk6UzPYn zwGdQTqHkOvDt^2~_j!Cq==0^gBf<<>Wa$P2kYX`Kn^zp1>N=e}|7RFb@)blH01>^g z-Dd?EEpV?+SRgu`y#pcwpLg=Tw?;Z%C@Cx8A1_rWabUUj|G~;;pjzYXZ*5ya_u5aP zf_W+#fE9tTh3J%tr=AJ#*MP|CNW{el`cdp%8-M>(_@T1uIRCIqEtA!~u#qp*QvehK z2SEVw*|YvuI^Fg8G-TNXH66MGaf0P<+xIJZ)a$uyV48|=x$$yyweT&|>0f#$s~Mew zaCIb5YA#fHA$qF9gS2rrv*pMwcil6^xI2CPo9D#@mwNS3xlBfVMC4BN+uhi=O7D{^ z628dx-z&s<#UFZ7;VVcB2hKakhU3XK*EfisPNyCFiVV)m>ty|N`_i*->jKWGsWDlf zy}6@m+VP~o2~L`2S@T|H!U&TSC8H(qndy?BT&A?V)i#>a@pwn9%pd81#tu7es?@_( zY>$5u1axdrz9{HiO81=CS}@BA8*NgEAhPv(hVaz)tvj`aBHByv_cjr2dOGLFTTR+g zQP}z=_U0Nn{IF82;92Q?Q$ZL(e|dRjdYFyDb>2<|6f^0NH$6uSNpPR7vi^O+v&YHC`qzKS z!}%|HxHDRz)5fa%k0W-AaljHRUb*Xw?@g7wGP0tUK&}P9!4!I0Q`OJJcmx>zQf5A& z`rMrDrZXNT6L2U!dAkJf6_04C4ILGd?5j#kilJ|2XU$0-qaShC0ws~jd-bjLT`3gJ zYWv6|Bz94y=J7mhAW9{mf=U96AR#smVU?GWCD=#%kserB;YJk-74lV=$Pe zXVES^OTb@%Z!`vIvVVze#gM7V{rQuA4@9?^tY33)`cw2;tP9A;)t4WwkZG;eoO!4ICyxvQ z?#5n09l1wH;v3ZqTzjK&00WCk#yP8ZuSKUfo?{;}^M0#mfxswA1Z3z940gL&s$A$k zt?5h5hmU2wY`>!_J2RXVkV$f|#HA|fK<;v~)r2m8ZX$2e4*NoOlxZ*Y3TZSXT& z9+hijDCK4+Gm73->Y(Zqz7m_HMvrK>izBtC1bCTsHF`YVH6z$}dfQm7siCJnEtRU? z7Nl}EbOk8%273GbKU8=bdONJ-_BCA(ovU`C5bH}HiZq^R6$K(CHyGPiZAiEp!AX3t z#qD7Y*WY#GYF|VhcO^()a(NKkdVp=h^8i)tQ0($y_FCPS#0}EbDy% z-(=+PTa(&KFop>v5CNj=?pCBAHH?`0WLW$>!emZuo+lM*a04tLFz>D>7GBE0vN4nI2`QjtlNv;+=(BT}`0`&+$8(5zbt zCGv#xQ|jmR5E<3(lr-%;iakE~4sa^&$@QRezi)=tpykbl`*PGZJO_0u*LTjdN&l!K zL*5Kbbe`K=M9Fc=24rF@2ydLnaeia^Q<}{azd_Om*f*2zPf!06(SNL@0Co9~_)`#a z=9BV5jqzXn?N8oew6r#vWa19J7LiQj;KD?_4N~|%G`&io(lSluL$`?ga=G&|Z=ElIt784Kml8~Vqd9_UEj z=>JBuYXqGxC@A-wSj}Pr@t}A7Pm7#0I()B_q-Ss44v4GbSLT{N<&vp!N#}lqLP501 z8|*`_Q|}Ws|Li#Ww-Z(5X~xA14l#p9cm3##$Km+|*F z_bAD_i>(b|nGu|lN1d-VPJG|T#>F*Lis=Q0)IOdXFLvzk$G&)F-T#Iwg2<;idB4Rg3WjPDWl!(`iM$Bp7!KQoXfLNo z5ajqRgk(Ks60CDq>u&bHXpu)Uj8;r@#-#{)E;q8kDN2W=HZVb+MZ9<}?wA=O7?#r)Uo2&1Ww zNmuIFq01ee_!RokHLzj*tZU=@HLtwUCp6Ux*H!=h0ZcpTvZ?tqeV=NUM6++mz0P%q3oH zf4MTX)#&ZI(_M6VNN%p5thetA#UfW>A(TdDaC!o~=qG;|`^Mns7fE52Ur_aNKG<6Z z?c;4JFZV+>@sB2u5pY;g(SViJHNmNI=WSIUv#r~eo>$HEPU}i96{679M^$^2$1)i(jKsaNaz45^AHa>9X8oF=xvF8BJ2W^e(X{PD_+v0 z{1qL=(^EsnJ75n%FW;x?{Bc_-KLoWQOrjKW(#MV0 z!N-@7w)i|$OG7^?$p=T5msl(a_^n#%bzP$(bYjPLG#9a*9a_GRhJDU zLqdItApI>=6yyBVU4Ao<`^THv`^~nQR;_zeoq0pd9&J17hxi+D53mu>%khTr*DuIp z8X_63k8^|VW5ofH^+hb9%sP_xtc>6GT!#PpGFf=NBq?^wR%-+Kon$itc#~q>lODug z$+KGi(Wmu?P zoj8Idf)!YN1#28$-4k!0Hi>-Jw%F+7^ujA||7(MZiD^SOVN-OI%b-)n{7BDmqzQ-{ zDuQICjxaqBtq!90kBQ?1jr(F34@76Xtsv^90B-(#$ad}R@rysD(&tnjXxlxkA9_?; zO-&h`;p9s@tc9~vdks*!e|RBk6945Er4f^V@x>f@vLldwyDf z=#P}^9zQrB+SfRylg)LFzUxe4S9zRf8VOEDH$CE!7t+jf z_2>t|+PcG#mt!-y@mA{h3PeQQ^nc=DIdYmpa&c|&DDui}u@XHN4*&7ZR)iTYWV>`av{xN%S=)^@M66t?6CFS{4A?4Kl|Bs?R0PG z9839fC8^=T^eRwb2}F2ED8ZwFZQymMT+u2x4VvKB@*mlE0$J!L8`mgWl!f#pe=O2;&)Zsqn;h zJ6VpJR2EUHdz4qB>-n<|a3^q9RF8;{wn;R0_IA`A;?_f zyM)hysMJJ6SHL@+whHVLCWHWU74}_En{=nCq)*|<0-rMJ*pnjDzs5SM7yr(9>!g(l z8#R}bZKMZ^8W&e(@cB`NT_}Db(+w~Xh`J2jD>5c}p=dW(jSO$}kAn_+Isw!tK6&;^ zlNsYsi$(gzh?i&Y4UE4dwl%&6I6B3AJ8o3}FwXi@Fj~=~mG;|_Su5@JGAD1No}^7C zF#_7k#0+)&R{8x3G94(G5b3|f_SpXHsleY{kFpmT(UCNR1yvwy`T(0p1>QE%Wu<-3 z-xap|lIF0fl>Ds-m{JAM{aZ{6Y|XLA0Yg%)Y%4T2s%1qhz*^Z7V*=UJY&rB=m_wf6 zFCtM5qr%5>t0@MLjXny~Um!n~c?SF5$;->sT3J=??}9SvhXqk$D7(J(Lk>0fB;IZiU54yYKA%tU0)Mr<^!9_qqGx_pCqe z*@kx_S>INZBMo)nHZ7k3vv$2iX8P>?pyxa_k^J+{Q`j>0Fdc0v%|9`sMN-ak<6De{ zS&TAQXun-@HVlZ1ER>ku3?sQOe1Oi}; zPJa8&?0gp(vkF6@mUNnwE%h3TP}x0^dia%Nf)Vlc7TYJUXBn{!eoHrSu7-W17rJFg zb$Gzsy0MJ1jPU=4ifgpU>v%OA&>W` z*3`Emi{efM-weKRhyF#Wl)+7ylonzH+|)D3bK7Snf%27R6P-sWga)Fi`f_fT!psL^ za+nh0$}z@docvy|wFiMWr$Q;8+3rKN9A^M_)PJAw%1`J|BDz;4i5`CsFfgjC424!o z{)fyGxA5UvZ|iuE5N1iskst9ZTRG!$2jjvcf8g};848Lb zT~%4T8%OFdT==iiRehBVem4=Wiv__8M$z#=T9Dhc(Pvbs?O7fE9%=Xb`1VgVZ2xvL z@|4dpqn$}`_+eE=Ak9VOD=Hx&ZZxMZM^iWBQl9K2rV%9`rW#$7jmr!(Ky$S+omdVn zYM)A0|g>a9zVGkH{nB05PSF;iQ?4E&S3e4Xf86=r082J5HY z3I;~=mt9`R7Rd=>o#K*`)(BH7+7!0c z(6imeT4W>NFp$H#i7NFgVi>cA-gKrR zEc)h|J^GJr<83zv_ggL|dBFlxA0LB6$4$j9*kw|(N!{Duzc20iw3W#tE-nO{-KnqH z*iFnPy)Xq(<~FC1OVxxy>H(Dj%dz4%7rsQXW?!SJJe!6jW5r}1rs9t1M49i zx4PRUKh;mW)Wj2Ree&1SjIS%cbd7waNS=^3!77#~tx5sP2xv!fOc5BQi(&{j+khGc zo+DRe%oFhRt4$$sI1(Fko1VJXSneIiK?Z58n5w4bfIOoOdA-$|Cq z;!tK&ApjsG+OZQ58AwYT0w&d+y2s?MikWgN9Evy^T$5#IM(^4$t|PB%nTYt{vybm_ z(&3M8PA;%IGU?7Vd9YWIQJ%d+Oth_uCdWcA&(3YDyA0pVow6}=@_G}7P2KOi&bzH0 zV=7e?375&LhR)=kr+%ZaJ-1v)NB-2oG`A?kd;cuP%Uy-~zu}{I)|a;*@V^NW;6DU; z85K#A>m8X;Y%lnZRy=ja!2@BT4f2pK<#b}JCi)Q|a(PLpw;^9DG}o!!AfCP|UtV>O zZPJ30Z_rb&Q{*Ra#)7WMNi`eZ_D;FQl8lL`Ij7xDB(t%E5<@Z^OT;+lND>{reu3#+ zBYtqnSnxaKtB!%T6mHbi7Dub&&rc=;jAx7|-khJk4pq~=$C_pv{V3fsn(z%1IW)83 zlmcpmkda@{w>w_$BE0xYTB!fbCDvEsbG+BjR&m$-O`~g`avJ7Q--P$g)}z)_byH2>`4hCy4n;{6i00b#9lb^f{JS!}q=t$|T9eOn9p@w30If@Qi z#EDeg+Rfa04lgHASPE)}VDu>}*?DnKlJL><%MapCr>lVf z4V_V_{BU2dEt+s;3GCUMawPcM4^6uoEy&;+stz_l)yrM*R?sa96*>WLG6UImE-F8j zsy%|+)Rb%z;eS$fC+&>Lt77szPoNa<5RGYtB*LPMYDPr5zZyu33O9f9If3qBI*Z|T zY|UMSBdI2*Xel+JBgaJE){qkm|ABK|9=q(iUhGbrH6`Vx=+(fMX}ask>rHY53vm1& zfF94*&xj=gr30GZ45NA7k0eXx)Oe8v$du%ybA{vJfTrmiv2snP@k@eZ4%ywKlRjl9 zJLnB_U9dOoW9aRWc%G!Qw(H(_yT|26!WSEZ_1^=v!SNIoY?!SQm?4}|OTCP21=uvF zZNMx6n2|-CZd6KL;b^IFq9!Xt0AJ^P7vZ$yu;Dg1^<#I}R)yR6V5;6TIF6a=lpxNa zsXxTi(n@QkU>zS&XT|XJPP;rnEoc$s-<~za0-`bFE)<>^7iRWpwpxL+qz~ z{Llp=V<~S?oC+OnOwyYodbU7s8WyS3g#yIl1<^*+U)Xiw+T8yTZAMTaOrH0!|3 z#(Ds)&ttyB>bUqdXEOd)GAm~rL7Kql-t`Fp5fL$-cRK)0LB8?OS?!H!m2raS=d_gW zjCF?cqmuzrXaXt!Z?7b;?+?c;>vm!;j^hjMV!V?GFCGB;KM!CH)UVLeJZ*=;;$Pj1 z&x+R>8Yna#%sR&>+LmtBN@m7mdt+Y`#zpW0vXjpRPu^fkyKCPM7iWZ$@X(~ zFGumBB(;jUF^uM&oxHdaHBaoi>p%A+^zx~OF#J?2=Vn8F}$^cl!Uk8)9B z1i-HgtrFRdliX>oFQ00Rk7SzaHEI}=8xsDQj3JC6%Ck zu;MlFJBNVs!~b|%>AfDVPKTUC9xm(`BDOlqi$$Wq0AWKJ zviZS&xGKM5k!;dOz~pjTc$Wrt;ipnU`~!AKRN#6;gvF{7)S8q;Dj#=_Y_`m3TE*m_sjC1sX!^>qsJ^dl6%Y`R?q=w&p+jKk zZjf&2mImn=x;v!1B_*X>x@#!ulCEd`{onW7e46Xp=bXLv+IKC=#YK-32u#e8p##vY zkGBE14RcP($xywQmX$G;P|^RMDo2j{sCZQVxg;a1k25(*o--dA z@>{u&DqKxbL2c&BmhRR4CGrzuZfn!VCcFV~>?+FBb%vg%_lH1%F)oI+Iq)`5y=)l_d zW$TecwaT=mhJ06Tcs#%i8B&wNH!RCcfDv0!yayHqXm zf@N`DJrNR5lq*v?Uw0W(iltOO%CjtOidr?WX@eXMw07C90Jl8R^!15~ph`N6(7>CL z=21`7QQ*LdWp{^SVSYv!Vp0#ZkeaX5Pfm8IJisxOMvda6`f05)6xCU4yN$0(Juool z!!q~qu{Z9`%<`C8v+@R~*SIQPp5sK|lGdM3?d&o@6RcZ)q?R^E^=rnq(9f58dSADK z$^f1l>m4myLzs5DU2px?)(+QYXDT|vPuap5wiY?{bhE6~Y@uv7C;d}E+l#H7an0LQ z7#c|V-|P2P5i+sT@n5SP`+Gi^w7OXdk=x8<^b0OYr9W)0xcuaCXdb?s5qIv(r){GV z*O#V)y$z5x)obD?1X)@PRcC&cmn@@8a6`Jz(=Yq3_cVQmWf3R-!`2vP>U7o^^jH3w`BqtR5L2)6(S23kgt*Vd^ma zA_9bbyPAY3qg>S)`FY`a)F?V+3g0c|j~Q!w=B z-C`b`b>7d&7kj!&J`5jP3?*^>aQh;Zhw?$oM9)tmH$N59tjygn&NMDQE9QqZp@oI^&x>N$9)-^f1s4&>-H7f3Ulb3*sqB@OCpD$0to* zXT^X<@?vpW_4GYSeZ~SBc;+%#wAj`)vmX!m)6)lorD>gx6nP6+Wt7F@GG;t-Ln9~o z@%IgV5GFRExuu9?J?oQxxDA6b#-46CMRFd+Ql9o*VsYb|X36L{^~0C%%)W%$UujZP zt954h;s^y3?*&)t_u4ndA_vT19}}kcCim7Sqb@WtV9&ht2btCAN?37mh=qOrpciYOwoE)jI18UP~B6Pm7-=) zg$KpOg6OHOE2pJl^p1uCM8(jXw_5t_V8vMl1F~}3g)g1682Kp`pTCCY53&`6`+ zza6pPjY{7a<&YJ=z!Wp=kzs5hy3Gn4OfxNNiUbz%7sXWzYjg{SsC<@``+KTuQ)M^5 zWW=c<-;k{ue1nh7n=3;shr_6Ys&AM4tH^XYBAHEx&rHe)#P?Q%|D-oac_Z~l8oC?nawiSZ1}Q44F$@FtV(T)J8e z@FT977VqtZ02L&kxMVjdP}bDUsX5H4Va{?*iJi|W_U6NR4C}x)AG#((KN_-}j6PAn z%S67XJ8S|1g6_r5IQj}4)u_Mv{3%h3x*@xWdeEs>nbMDx7orF01UQ2q`ppi~TXyA* z`cpG=`@OUc1Fc?vqtspPvi1GiN8o7Pr$ntL)3!*_7}v=fmxdFxHl{=Yp+v0rdBHAY zZ7V}-Y>Fs9E^#P+>IHJt5?xEs{5bS+0IW?%^`{|Yqytb^$EoNcvhU8A*p1q}s-7NN zXrtv=FmGQoUYzY%zHW6SBA$na^0PY!co*Uilv16lNKvYi_rF^&)$(cYQip9Z^oK|} ziY#+aKMT*3j8Z2j4%tQI2I*g8-PQo8JxTi|MCMGUHAF`4sb0hM2@nW$B!VV*X9Mey zg(59%9V?1bzoJ;HGhe^|JWd;Q!J+@qrfqfxefM%&^ZNer=C~*9`eFi`3brtp<6`9nA@pSYRjdA~#_UTTa)@f$CMd(e+Q>0uPoFpStq)@6lT&`KM7>q7LUqHH(@a+@d@IUXlQ-aCci{0S* zv$@G6+zaV_Nb>0|?%QnLY$CgwHXpU57`Vx)t(hJZfa1+q@DbJZTbRrfgQgPiQh zDx`xmAvW&1y(uo#9PuI4n1iy4P}_h2(LRdEqJEmE?LpC)2G&lb$(LbmlfM(;9L`sy zf3R&Ja(ck4xy4UEx-(mO*&mZUK*>JG0xBb58tMl!>rha3r;CXWuAKA)W4URj(^Eo* zuRx>gT`hCIRXQ7!Jw%02jJb#jbYz={aQrf-27O-P zEL~0`y{qPgiFL^mOouT1&$+|r^?jhF(d4fLPd~Snabr#G`uj)u^$9my+WFvdQsjnu zG+urh9v={J?%ZzKN4H$|n7QF{p-aKsp8NNo@=XNC2QS~Nm&bE$$0ymp*MB#oxV}cz zM-U((U@}15TQ%nWVeuVF89f@UMRRg06^)_wQ=rD>PX>0Q!+WsuqiR9+{{HM>(j(H0ADpb{_3L>39pXF z4CzlmmTcg<_T~qyn|sxg0`t&BxOf-1o^1w9W>NN$r`TMn&2^8{vayGDetwE1t3CL^ zf7v7aYuXtp^{G6IhSABPUUe`=jr`>-gY@o*!TI?r@k^x7I?d$96R<7<;^~JABiCRz z#CKae?}$zNtrSb_&5_Gb8TPfm9hIBtmQnsTL=xU+85IzU637o1KJ^cg-=*Xs8Ic)!)?)#8W9)gx3tj85oe> zC2XE==^n3mz{8Ms@kSoNh?u*2{*?2ViznBkBaI%_;Ca`ppICQu98`aCuyjkBnU0UE zV_U#a#5GpVEtFHhVkd-XrpIi$Q1^HgZqX(+Vv+>i`vy8}HAAcp!)Yj5buH86jotgH z_B`WQJ<_!&nlkAl&h7)E?FWj*2?O|D30xr#g!6|90;NwY>}Kn~#3(A?{HHD>JFoEZ z@jwbtsgtPMPk`Vi`+>9P^)oXmq4iF`pA;X0d^H$@xZU#f=xI7SItYl4wGaEZdaqg@i+yVM zFA%5fCLlAI5whHjNc~chrUW0LfVI_5cJ-g74e?7v&icJjC`n36pHHw(8C8dH$Df1v z5NV7=7flAvU6)i^8XHJ*TZ7rC7Bg9P_w{o}%lQ(%$v9lBnYF`PgKabDGF(y9r@dIu z9;=Kf9SeBF(dtfV-L%fYwk=Xp&OMRkQ$o_C9HQyLt5zB^Rp&^`@Qg`|tH4)ZJBlq*q` zRmz(#q3zlD?-G<8t$3`70G}9QBV>=7ncT#T9xiu6YJojJOBbx4hBv0ns5e{VUH2>H z@C^=Y0+O(c!Qt|<;Ii99i8&7+Rrhd6ZKBGm^rmuvicF`aRjg(%J zeMWS*+qX1ZF=GwHXFl1;mcw$CTJLL$)1LusWL|fe+(ZpQr5YT9Kj}5%>Mvv6Jemdl_g9P5+93UUBHzH(`3x+IDV=sxTr(!{V@eAst)Q^S*nXOX}PhF%c`+$T|!?0Yfl-mii2PcsgI<;(%Jkd zizV-OPXf*@Rl^PjhK9gu;tvlG8n$P5hnkCZ+uW*a3{cnHj<-fn(_LoX-f|f6GgBKs z4Weo+W{)_`jjc0=HUy!*@9L2HL6Y4KGRa?du+rQUILF3WOuh#oEp%*BwLbtSd;9T? zLV^*o6qLqM`ishf=0CIWSARvpQq#8hKI>_y5tpf=P#Dx88CL=)k0K)-#;)a9cBv$$ zAD-2yJ#Mm1yMI6aQ`&}GVvCUoRnV}=qBEAiEX-K=&e@+fF@NQ31`={Ro{*S2Q zPtSjcuNex+JtX@D}UwSh#;^-}_m=uD03>#vm7c z*J3qwD|=51j&vt5T(iAWyyp6o+f30UhjTij&EG3%$ zY#k!{Tp+5|Zt>-d4o1A;f{oA3l9kE$Vf5W>NheHWaF`2ENN={bEZ^TkWO{qb(sKUG zK)iobN8e5m7oK@k=wcYWz_>Fna495?qt#bOc-nI0?|W?Ii1kYfLG+qyauB8FCu*#S z+91r7p<2Nv4t^7)?G||vus+6DsY$-XLXAAa|8t`6;z#to6e(N-3OyQnTd5cClyRvr z3-kHs62IPZ;3)xcI9X@RZ)OBM0dPKqNiI24`zCZ}VzNK%J_$tprWhp|PB)G&0R^bO zCwn{>rqLSWc(j2&r)qmUJk9vU>g!g_(ZakY?EjqySXqY0kFZ3({X>n(_Fx#>%Xl!G zL_FI~gg_h5tuFPBtCeJ|+tzFwS3#Eo*mZ7UtPON9>up@v>^OVWQy}3Fs;lFWlqwYG z6cJ$^p~KJbazqWdUO4MjXGbl0f{DLxHO*%mS)I;pNe4uxphwe}Kctb8WG!hZl z4Imp!*X6PYZG%=aNI)jC$zG=cU(iKf>Ksk&+~B+Z7UM&%y?q~75JRaNz*-HVYSt=9 zSSaElUm&BV#hKw1cuSF!BKXqpV#*{CIT&Bcm>>y>Hgjk!t)uhRuERNzKaH%b^|xt% zjbeTjZY8S}r$Uz16P>hg9eAHtl&+R=)K|9=-Q4p$K@tmD9D3cId^70znE5K?dYnkg zoBBxy^FJ_#+S9D=HkK~l!Px%>9Q_%+ev^HlT(#lJ(!@w0dJcAFvUY6LC9XZ`Vzf5BA(>@{=L>#?E1yWRiCr$APoH| z@jMjKzZv^bG|?NX_D6%)n6CTG08oda6i1TmZc~wVrZyLo10CVpS_Kv{2R%baoMerP z^QvaiZ^T_M^#13Ftb_7K-pCHE_egtqdwu(?dHRHSX0KT-)lI4vX&+1y3r{Xl*!rHi zX1<7C@=47{{p2wsrLqENC@)@?d`Bw_BN$rpOh=B|uG^<{22t}Ig44$r*cSTQvNK8! zE*&y8W>D~dr7*2zZPw8M$aLeym8h$I>V{tbnyC+-z74E>_pf2SdkHpR8AzNmJ@X-w zDPRUFB!augEl2BzJYjD0gOqkt17N= zVu(>rlJny_vx{DT@BVC}hNDlIR;p&gFD8T@rH*4+GC~yRx3=rKBi_(-{f7eZ8h`xL zp*Hd09&C&KPtgBPe|61I%R}X%zbUI@97e@?L}z0U5OG9{Qpa%|ghl<~|NLPIwaJE+ zutHe}CFDjT#B@!;EXvS@^5yLHIe;E~JH(pt@*;wYj=ueLH91(6Y8cqud41Bdo8pL6 z??S_vOyz&nyzy;#8~C*6*r`WSiQZo-ohoJ=H1S3mS=TUsD<)qEmop(i5l5<=+HIz} zGkVE7hWBNsfy13-G2pzEerpU_(~2dfJY3^e$(Z(Uq8E+T2_GClaI%~3_2#+duCjrW zAPI_axc>fz1n$h0zh2=^!vny;X}VWe7XreHAt8*1r#Y%uwaBE_I&0h|lKg~aYp&RF zV^q;cVas(&zhHhc$g`s)YE^Pd%qTVJfByTV3!QNR<_&xIFMv-lLiWDDksAnyHhE7( zkY+0%N(wT_EN2*A44J1GvV3)KrE$U%)h;43-uuIm^^#?^J)q~EY$)-=P9;8X+~JsZ{ZOQ4hWth}7pAn+4N65uE?IUasRigMMJeJyO3(wcc|mW--*ng9<_ zbo&~~I#{Ti>6nqO(>hFDXFs0~O<-Gba=ci4Kek{gCw()^x0(EvTiHa0DB96&_0u~O z$v~K=(+SI19;cJtnQPMD&fnF&6nkGRmk&}ww_%-_<4I-wY#7`TCrw% z8hs@q4;*un>SY?gXJ&#mx3}*C$vqKHTOY;RZ#x2Lz#~c*`n)d-H63_}AIG+YUvJ4l z_vVc%j3444dFQAV+2@c!NI-o+N!?7;SZl9sf(pY8bW8eX`}wR(mR?Yp&uow;Eld9s z6YF;kl3%m4Ar#bHd?H>X%0d_Lr(I_vPtclYfzH2wf{`rue?;sggIx9p?Yq-2a+X=s zKt9qM!?YYCm=X#*Uk@|26S)CwWqxA@K5V?XTlr7fnxTTzTVSNKC$fgylCXGwV*|U&&BVu1tj^w+Cal$;4#5BPI1;Tt!3K0-%ZXDe`nbV$7*&0N8R%U|`Rt;d_{` zePYoj39BN*i7lqf)t@+rRa=r7(|IJkRY_jSE#ILy#_5&6&QP6xE_Hcr1J?fq{jabF z-MXP4XCT&4pmcq5V84Cue?rF*$=Mv{U@9ye9<5zn7k}}MiEu~>b$u=AUw< zA0^d7Nu8V$bLUKOW9!kY8`LmkE%krNC3hm*y0U+#3CZ?X6nDvsH466B z>tK?yy}Ecg*+1@^Ap`Izp4ouC<{#-27S@({E39pAZr5MmEW2o}x)2(OQy!2LbSJ~W zLd(o;gEMb@Dd`Cf?3Iq=-N|1Y&ujSXyhhl;0jcUzmxt0hY3yf{bb|q6!X^*i`V4Xg zy7`BUyQU$I8*S)p0rx0%j<05mwHI#OnG7j-`^mZHE-q?umBRE9S6w?+{v^%*FRn7Ko%oEJdK-UF7SDu4g7*t7eqrm}O!gSKELk#d zwQo%-9c+S$UU;$b?UT;dzgENd^?TUjXM*;Y9c9V0Jv9y{2`QLh*x{!_LigA)v1LgA z1!h4U0fVuF$=m)5&AZhS(dfK9F&ufxq)_pC8#~m=fS#)h?SOmj{8`oB%}pDj*L(b} z?iYsDyJ*Kef{zqOg~%@W-O8T2PlEbQY>H1-Lqmv4>?%z4v|*OAYAGp1e7;}ary4vk zt~&Kzx9lyk4a5V&>weT0$_&3eKRVx?n>>w8K9jsi2yIy8-!-U3T-d<^uk7a<;Uy9< z<}}Ur%OQ=qh1u@w<}_JjZ!*XAwnHm*u@byft>ig3`5hq@P|t;$OjmAM-SI`=v-pA3 z3!{oyOe8wAe@**_G)zI9Kp~`Oky58bTna3!*Iz)>cWZ};^OEu`>m|7FwM=|`tS~}N zGSh3`ELB1KNG?=5i+Rxd=isyl0pE}U2O9~v3m|zUx|=wT`NUWDzr7t;b9lPm*yn}p zU)0M;vQEMc>sHIC+D2))X^0)Ch3jslX@%dCN}E|D6HkbsG_QTlyo|y@k=!LGBqJPf z&2>c+xow>^`}+0sG4a5tp*O;5hZS24$y&``sH zyewME71Le}mCS#Kot5uZ^KO9GVF1=pL?4tfA?6#i|lR;P6Wc7!9NhC&aT_&^?JkD+%r-|DPqxiK+E@zypuYH z2kHF=uS-5xW9;SM9UohlMjKg_8o#xomg{*@%-g(B@!4qffIC`PZ}bA+`7jsID{u7h zL99F`U#r5z*;%!WIuTaEGb(BZ6mQpA_R;1GDV5qkX8hGMKAL}`+Pjxe(W{am zwuzwM-+WKOMs*ihdIoPYROJ*hYm(03tOO)2{dc4$>2cU$T5d4Q0+Rc8<`ELXiABoy zM;QdOjssz6C9vB%5bBJLh<=I-=S%of=$bAyT^6=8jC9^HEk6bx=YE@DAbPv*^G49a zesE>u#rp@;EG<17r>|T-v{}~7fgt5K#sUlSaFU9Aj1q~2ZhK9q14-3(;vopv3v8yI z!aW~jTt|di1OJogJceA=6-U-N-9lag8VNFc$jEMPzObmIBu3Lq=`>_%u0YaS ze%yIS3dG^9=p$!PDI5YJ7x@t6h$=5m!)e`XADZI0LF<1UY4p+#mS+`4F?zlvg|6XN z#hm*}k0VcBGt9)&2~K!$_I&NsGBSe2r%^5eYH+$PD{A_}u8I4A)dE$g0hO%B&6fDp8 zzto@Sx1v?`o;O1B0A8G<6rw~f(<*A_NPcuqcWSU3MF!&%!CawhKHq6$9W6BV`OltW z`L*WzyinEBC+vE+2J3CwrxiNU(4Iiev|OXU8++pOMq&s2U_W%uGq4A=Ra!Ym+{HAa zCwIC+>4_+3gC<2rf3OY$_`;J}s%7grsu!Rlq}tnsEPkmj`poHjVrw*Tl^c*PXK!D& zpyBN6qINHnb6cOf^&)tt#)zW4-1bBjU1rbaY*=y9PNmNP@SY&rB~@*BXyOW`<{+eL zHJ_gy#kxr02GB0G1D!sA$-OOC0_~}6ZRGgl`Sw?q=K_42VKQ<}d0@5<8+etu8!0+Hc{f<0#TuyCZ^hmJX%#309whVExtH1j zxl86Hp`xLc)sORbU8AX1FGZ;FW0@CG$Po|1Pce8}9ZhO%W8)mT-#I+k`C|2UZdc^~ zJyAf1S}%9^^%YW(UU|+{RBOoA!z8gi{QGtTtEQjUe} z1#d_eW8XFJ%`L`%l;zvR#B`t=$Bwmcl~T?xX^>Tukb#{q?%8eMxbz~N>%k?yh3DIw z4ZpcQcHIwTe73lz!!mm87Jr_E4H*#scRmX}tGup7zZYJxeg&@|2rvqfL*y;c(iZkI zg_{PnNOe-Cf{zzkc(i!>-;Bvwkcc>`&;x?pz_-KGUY}jQt~ABX5}qMF=kzoP#6THT$c;d0edxDZPV2nKF|`EH9S&O| z3{nr^6!xUm>hd+K?ptwsYdyp-t-mC7dSW>hU8C;{RfW~(|8O&9(P^gNqG|9?ZVNp& zBiy>f1Ge{QQR#DppBp_wLgQ_3(JHs*{6aLP|8_6n!Py8a*4Ea3m%|x}rCR6Z%>?RAeoVUG$Q; zDizunLHIdYBZ(hEt#0M?jq47T(9QM8ETf8d&+FD+*Qv%Dd9SU`VCVPrRt|Mfz8!1a(8NG zhv;Gy|BG|>C1=O#$7j9Z{w)sX{nO^Wpf05oha%=WB16E}Tk)!8YdMFTsiA=hk`+S* zM;AteP}|4iQf26=pP%8xSxPHzhdJDQeUTn-6e-MU&U`tizh^4G3ZAb7=SC2wh~78Z zDn#`0E0@peF0r`XTa-XLkjebd*?H^*(8AHlB^_%h3}Mn7wRt<>DiA;HecT5c$xJQ1 zB`RbY*qMn+7|5{er1#6($8|vlrhYxB?u?&1tCoF6#quM)U&TBf0<%oYEV@KLoyO2q zA+CG-o~8a<7Z{dto1;{U>E+jYS)MXhaXw&%k8%c%e-p?W`u?Dyfpufh zx!(Epo|R=YM5ml5nXOpW(d?)RczE#qSOTLZh;WJ7#(&CAt1RRqVNohpVbSEp{YXja z?)Q^1I4nOXjbACY>qVmsRF`tIQe-6)xqzv21H!Eo;BvT4HR8AdG^as-wGP|to~#f^ zQBl&e*KQI2#Yq2m??eBCmcKtS1cpWShm*Nn7OP5B0$wI$RBP!p{l}bpvxxyIw4-aU-9%AgOa!A_%ZU z68;}md4SOMgR{LUy*P6g!~769=C)X>>sqBY%@LTxGFR$uzF*H#gZ(N`I60YaTF-7? zEJbf9$SYd%-rhmLamUttUlx`xNtZ(HvUsF>+U8iNZ?%x~b#YW-c$y$@NM)AE{ZRWIbrNJ@gZ-pCN;-zo-!sxlvL(Da*r zw;WeY#K{isI(ry8dv>@m&xw0QRnCv;p12`Kp(&Im^rV@LBjt7(J3Tu4^|YEF@yn6t zNKRMn44MHhwa@!dmu8zQWAGrEVPj;|X7hV&7E@8BozTzBpuUdsixSPnc!jlA+pvq| z-&WfhTSEkAOD!vN;6`%GMB3KHq=M+nB`?W`#&l1UYE8@g0TIxq8e?(lh_J%n2jwhWl!`P%gGVZNiq5g@#W=F zCd94jK^bvB`mRTP=zdx!Irjay8#yUln&6ayDOt;CAlCPdIq@MrUVtZflP~82mTGtL z=lt)WnY!`9Y~<*Wvy(? zKWv1udhNETK7pFzg=Nyt`buhcs2;YF4M(@|$0ybFq>XYn@Bpjua4S4E!q2|afGY$b z{qcKAGjU`j3L~Rv#D`K1#y{n5VutqZErm%mYqB%bv|1H0KUN&5gx7BXyB$gy)R~}3 z_a$bodRPLBYc}xPJ`D#$rB`y_L!$cMf*kUr|~OG5?hmrW_qKnwtSoa6)wNW|DqTJD0> zg8j+_#%?Cz7w>pXj1;C>&yemd&|?m&vX=L~5}#NP`0+Lx#^;!b$t?dN|I^#-=taVr zX>MLgE*+!vo#Irtv8~i}&))O1Zd^i9OSDWJx8!n&c>ZasoZYn?CnT54)EB@*C2~DecQDxy+uoQNzpa?k}NO(GJ@B2X>r_c=c7q2S^9>PUa_ga z^C8?YnvN`hU^f94Hfe*Xi`=syb;pT5FM$vftQgueOS zz=xNb;&i*_u|4SjB<-Y=#5-T5$Cgv!W0C1Qya#fF{=ACwZT^7#s*IkDbP43+3yO~y z%lih;=XOp&=6CFz)#{2*763&)UaUiiJ@G<)e)by?1&jiFdwWZhm|FI8e%4jbMt`m< zM*`=J^J>9H9#lU!8Cn=Ba!#jaTayFNb|pDFgbx=j{WOqtIiv8^&Ar7_KY;i{mf%!%#0lRggdV0;>5q^|Fu z+cg~`NdP6&j6e_amsyY^2%L(-3k@r_CIruLKp^@L`Ka~&&v68(_<5QMvjAdcCF)K= z^kENqIRSa#(&zOcYHR2Z`y)*Tse}aOfCr^nqCux|R}bjM>$Ch~BMeb*yZQ=CC18XF zGB1R~QW(m3MgNXx*7=#EvZPB)b2dC?#1PFr5L>Nrc}s2dc}Z6~QY&87ogcpF(Sc9$ z>*~%dR~)b03lW?oiR7hrJleZw0v?{r2G8F7|J7QH7fpL8{hDTNAws&c%gd3&dbpZP zbBVr=PBYiC@U$qN!*?p|d+}}#0iox&fv{0b>mzV6BY9<$VP>^_hJSbwVo1>RK;xtj zWeka*22B%ZF|w@H(tx9C=+f0}kHEY#!jjc~Y(Z>%p{D-V{)3Ui%wiVbb2#1ztA{$j z$Iq+YqrwDaMBSZ!v1NoYyPSNm@Vz&z-hOv1+iu^m^uC)$!XW&#-U*!VF5Y36l#rOT_s<`CGEsk= zo?vDA#2UL3#g5tr?u)$=RdZG@#9%91X^>D27(k|kJd9e^KC>EO_d>KWzQH%T)R7Mr#(4>mBEQu4&ZjS5STJ<|a$%E~OF6U*JS# zf!HqU|B+^~CQ2@hwsA)e|44>Yl<9XD*>y*}8t}@ZBisoSEQMYt-1QiY8c@*D(%3B( zoblA`3>!Mi=|lA=x;DAR^1UzQyUuU6WeL0g-H$!ipi zCSg`>ZuVR=X3M6zBY|2thKb2a<{Dz^gsQHnQf~TfjgoA^ORSVyk(&sOS+fEL9Q6!} z+&Dk|iR~oXRpV`I(}QlJaR?D5_Z_uuwKo0)2?3w*aj@jqTMYH$%jic~Zr3}=C;jIg zx2}wyiWk)f+}Q*WbMMEjoKE>OohLgyjP2}n;^IYMvJ;c-f#}6{Yc7Y4EBenH?Gp52 z&*ErPx&S9oUtILZE5@@S!m0OK-Tl8xbor$QJ9^_kI_&=t(`s_w)BHHe`%5)ZYOVIJ zxr@K!Rtxr?|C27{ymym6U`h@EQL0NlC7?wK1gQ|~!EKD$xX8d9kH9|0>Nd1cxKuTN=FXiMc)XrhL5Z@JAUdOWC&{(&TTFQ?Ds-P^Gnk?ywA{hlnr zGeT;=aw^@fG@SCMClj~evw=7B>E?R;Ey@a7&PObd7zodc9kwtpE&uVF4^olZH#kJH zf%BnK`Z-kkvmw~|rOKLmk$&4jj*shIx9E_D!Cm0p;){vhan^Wec?IX?3J-aV_ihE0 zan;&f4s1P2*&Ph5$~I@m6Yy{R{tW*}CbMmX0ISCTT^)Q-fb|Z>$+i~yq4Jzp%YNBx zee@J>XR?9Jwb+M}%ZoPk(rM}_o;MY11UgmmnlDhR0SPQQPAWrE$bzJ4tv=4kkON2u zXWPDF38czRv&IJIj4u*Yock z2g%D&*H+Zr5rUWX&wKwCG$AevB3rGw9RSR1rtg`46Y~en)t*(SZxwsO)niyOX;4rO z81cPO2N&=BbpG#=zaMm+5tjA-Us&9RnZ6$3=Un&$zR^{3!`86fy_!H2aTG<3_H>QO zkRprW>aXWlZ8osCuL0Z8$kF0zn=Y7axaL;WhTY4x=qS+{^efz|*cQa;iw}3(v9K!i zL@n}^hn_#B4=Pf_C0EA}E;$~@q(RGsE}OWGzX)OSbHE=n^ju|liV!#Ur`-yvw=%e_ zVZa|HM0#unJ~~PgIE&TesZ@<4QE}!+RaOiP)Lun#v9q&(3wXKm*v?wQ!;34U$^h$N zu!Sb_@qYh?BahiNBAld#SCsf)P*Td6i}Iw3xpjm-hp7kAD7YW9D$ zKyF|bzTC@g(sR~A>^kTU7?f}I*vQh;suq3C)+ZpVLHyYQ zMy}>yRkzuhw-!@K%=bUan^M%!u^jKcwLw<*qQ3Sv=pULKn}8k}b*Yk6LquP;MSEvw z$vA39dnY1eZN!*pib)qyD^Zn;qOLS#kRO$EN1yNs8RCm)xs>#R`->zz|JDt%bL$V} z_1>Dy{jots;^gEJh^Sg=+G>Xh5}L`pkPt=BRgVq32o?ENb5x}_CA&-NqQfR<=>p~r zo6&YgfYo*HNh6TztBLV(CmX1>T5DK469a3VUg1{Wufw`y!YJ6}-mNY;`NNsh$PE#^ zW8YI?=nKmclbIe$u#R?(7P(`W(D6%X>zn<>bFLFO6J3ZDYmhIz>RXc@=6>S_dfJvGJ8R-CN zl85sxfakTXA*40^y~F*Dwx*Cz<2+6c!yghH`C^>KdeJ(&M^J1o>bkRLV|eSw!l)|_ z?tb@bJ+6?KKpb=-C&c+`axBJ438?;KvWNNHUCU*(zSnNDPNOy8V-sE8`R>>Az4dR^l&y8MXX%@SsM(nW7)+MO zpD`=TSp&}DN&}EHy=H49V8;b_c584=zETZuiTfQPbOsxu~k>;>G^=t+~kht#}rf^wI@1;5kXWUl5_&W2aW z$;Ipz_ZMycnn?;>p^@#--cs&;`P9ae1f5#c35C>XRq0iIU?A%xd=L_-KS(EplWG>T zl_eRgC`QKAR4+uaRcc(swqQJ&GhY*{by5+jWQ3Qe1e8c1jGRl0=E9ox7N?HRet3@z zSJn~v8}l}ICR+PbGF$J9pY-{k93K#2A)=U|q+U$v>coiII*bK{k<8c|ov(Je9?a?! z(DnTvA+Qa!9D7Pe=o5hd{{3U0aF%$P&fuRvL>86?9hQw$05~%@3a8qpROd4Vo;z;6m%Vm2G*4bcp*<@TbvLXA@ zo^^Vpv71o>&5#4hOkDtPUd~5xl&tA)JN}+=Zri9zS*?sIco6aD_^TWuu2Nlj;ctit zl}amobRV6is{-@s(T68LN*Iu}9aaBC=#o8$HBai|V#vil{DC5TM>oZohp6Qn;e7CXzrm4FRkhYRhUov)B&_e}B z$V7^D8YRgCb%VOHUp-W~3Bqb|v8h_k63`DUEJszJ{ z^Y&R36pJiU-eV9UnewaKky^XTp2wG$@_a(dYv3#u+B+`}WZr0Zq>ikJDz)PU7N zQF5kF)SN{?ZsaPPv4$-J#&v!F;=hqUuJxHu7UQw^6r1P}735WLEH=YZoU%FVGe0T~`|1 zJ?B0n9N#UI^0}^AGSk!nsA{vhzLg{t)@HNwYxZTuiy}r}hy1CGS7-^^CzmxDuTHng zAF+UXH-V|N=|5cHJcrv0S`7#f&0>tsHTOrD76qv`b5Q1Q4o*bs7-ZZsKgiSaJjQ@5} zzUA^wb8k)ugQ8=6o3A8|UA-9j$t9XkB@Xv1vFc3UguNCR%b8GAX^-t=3|EqKE3kVJ=bLPucH^~BO4|4(K>PfTD@4E4n zwM&p;c=zTfwvlAU;^H&)a1x?&@T1F{XVcf;=Zr87tE2f^n>*j@(^cKolj{Xy*W*uu zyZuRM+qfV{;*Tsf#YXb{hVmF!THK1WN2OS(fA+EW+!5#JzYCPy+ipZW`qJeLK`FEt+9qJtAyt*l4-p znl5R23VUWPa`7sj(7-@Sjkdjz&zyANp)?9c2?7!l4RiDH3?Sd*af9>2>6+)&%6d>v z*Q-anTwMIYk7Ta$2a62-;en)HlJr04VDs7hZRJt*< zE3ik#{~th0V)LZ$z7!i1-ClT`BDvn_VB!*ynoa=G#E+y#sAEk16o(}#11h7nXQ9X3 z{#zI^Gt)*vJzZXejLbztymERp8!torIoNdg$0J_S6c81#U{TTDDNS_q*y{4H8Ctfos3f1^iT>REnR8*AXI=H!OJC^;4&8R!pZW&U> zt+h+L!3!PX2D7Ru{i0h)&~s79h6S30qv`z7MqM_ z9x8^w2J%po`5P!f{rAS-2{?5jPUjA-}$lWd?Cm2bEGoxpoVE8$Xl! z`K>LsdkN21r$}kXr1$HN0rx^K8>o>a_2>x|=!F&T&C@=3Bx;hBVGU2?;y)5b^uI}# zqfY_F$7!|W&mF#X?iaT>JkbUf>Zgl4I#SHWX^PVj;^eNBB*mMQMO7NI$jR>K8^)YZ zG$YtZGqvxr^JB#z1!cr>sHb7sl$ z2&vNG> z=FjMo@jt#q)rb={B0(tY!_Ui39EL5kyDFmDYnF5gwuHI z_E4hxI3EnmtTl&o+GvL}c^`e)ao3<{+3@-jgAja(g(65)Jxa_T8GjeSbd&J4xa83e z{(@zg+O;kB55l+mGa&%WN z*y%P!^r~$el&+0V95`IEz82M=J3;K0ZjK1SQ<&7m{QM?j9$yd&Ws+SNs>yI9O+UHk z<}ooZ)Yj-qbS~glV_n!bxqo9?v@BP};J8XNWmpIf4OPlTZ&YPURHH+Ua+iw(1*_^) z>CxwvD_|x&K_W*`NLXlQEzamh!f%I|c5|R+`?@6KNVu z`Fxt#dt%a!NR*Vaz}r8vNmAoANWy5bG`9M}2$jt=Ku>0d%vOGzSv>e$@ zOHyfzS(N2Vmru8xT{O~Oys7nAd*NX<-DW(s184xJez zEaBM+s+8U}dCm7e9038n!B zH1uqh&1*$nR^9v5bALv*tV)s^a+%>ST39BEfBqhr=xy$Qx+p)FFgf`o{VW}3H=REe z%(QNUiE-qR)Z+oiUMx^T;1;hWdk{MvM6V||KSV0QzzRAXGJm+g&r@MSBN6>o?RUn= z=lQ`EVC%;C+~ym`pgy^?v0mV{yvD_2PPM29y%#Qb?k9kQ_0d>U|KM8hc%OD1>$k>S zWg1h@8sHbO9_8GF!VU=(o-p)Oq1GI8hFtmW#`Cl8!zZHp&Uhv*Y=Nmd;+CXS7F3N{ z6wAPxs%W0Z*@Z!Qg7_$|fDw;nEnB3sn!HX*Ws)F-XYqgu!la3GdTP7wz+~IxoZfM3 zSJpHflFv4m9jJDW(qlb0b>s*3!){nF!?xd8G?HER$cgSF>y(4*X}hA0jLf}iJUP$J zI+p~S$xJIFr>mUAklT{f)j|7)5{hekK^|sb^Pl-*k+*1$kdg)^CmjsY5#@4AhGkR8 z?dvMs%T*=MTXs=1-Tg2wNH>6I2E5y<*HBJ;_o#`R9YfWw=F`M>rv zg$%5}4G|C(lg-zDe%T#oa}6l#q!Q>`kAq1|EbD8=m!AGwJ%wAOXKiNtr%l1_cXXPn z%0%4>hul~P&sYwmKe7y9c^ut)8m(8wxXe=;DvV%c~%H8|7E75`^-sQpcj zRP!iE&lrm``WLvCu*K_{#ZaH1FQ^2~lV{(>#GMfCV1Eh_KCG*Q=M$Qz#s!v4HtD0| z3cwJijqM2VKJ8It!)FkUps{py?G`6*_rnwtaDjQaSdDrPl4xN$>+1$O4m{qA5MzwY zby;x(Ooh&d2*lv%#j;Z$sR=&?Alh&6g5p6HLJ1fNbJN&wk##M3RdkOlO2W)y(&siI z5J}1sl2-`LZpm|;LsC?!046gMCe4hThBtY%glb(d5Md4 zcU9oNyw<@nM%A1ygxMa;iB$~I$IgDs^*3IL>tW}qSKt*E&m#Wy3ghdBGvHnmX%7{i z<@7QZ#ABmYZu2b8U-GOP=WHbT4RgaLZA$d#sp`swb(Kn-4CWh-}m7VaS zAO>#J^P4rfr($h`3H+0JkRhw8=L;yfz7}T-fK6g z4q~1pe@Qx{$FQrA=is0;2+&X~ivN&v+(Vhu`>aPH2^Vm58rw5|YoCNs3L2UqEXhRO zuM@`}JCMK5m6vwGMY^C_*bT^F^^_cNwnXX3_)ZzTlVR-W%h9|C?nukf-w$Hluuh;B%Lxo-dFTI!`WB zS$XgC-5)e|{hN6ML??iBniL?HBlGyXm^I{bOr*dXu)I8iZ)@q~S37$6Mn~9$9-{a& zw5i9#!vVCV00cW_0)k&OZ-5;mb6aSLNt}Kav002Q+z&aS%m^*kG5b4M-ZYAZ0$WT| zY7Dt(29O7#ey7Q^JO(>7-_1SeBNeqtmCH-o0f*@_KPP8Yb^Dj3e5fD7xfyKeJZnlF zO*9kb6Rk)gv&)QQS(2RdiNXDfDJI_xk~DZ9l=nXmE4KT17)uCbYm#gaJxP!GvlETZ zgVvhL&-n5g4)g}Xr#t43WYrkupNb!tPJR1PmIlQ};DatrzChSqrO3Q|4nAkyuioXJ zuR1ibB>Vr^rpH;ExXiKmvo!naoucQ?ujk-qp(-@gv9zkx=qqKz1F$7cX%}cokIebx@49yj|jp+zgD&#>)j=@-KFtFL1!(;rGtaV1LS=B-3 zm>vt522IlecvgyKYm$@Y{vO?0zs53fj<&}9seaH7qPS! zEHWBDN6Pp3>bbzy?O-loM_HM8xb!$R&Ik}qPU}rxg^amqm4zdkMs+Sd8meYokR7SY zv`}Lc5Dek{;F#H(!w^an=!klhr@0uvQmjfx#Pr zE%y7iv>3IE*;bUlt0O;oQ|`diK{vaFSTlrk)?ar7abv)pVOkW3Mt_{U-@F7ec*^~! zqC+eYLp4zDe|xqaUyHU-37w>~Dl4hn)^hDrv+uO`j-WmtGiP6azTh_@VYh@9jRIT7 zft6|*rJM2NWa7Tj!^gkr?kihOCB>jhl#f#&geV4I&VQQS$r#YP)U!*_kLK6`>3QSw zSrNAx)6)co_(7Td9X;iFJ%|?Q;qaz1sDIr(^)h^spSs%a3h3dy8%i2);q3;}$3BT! ziyZYJ48wa8|0VRPpA2Sf@6ix*)1z^({y||)A|H{FA^k@uE?y6O;WxYUzWJ$?5~lA1 zACeI(Q}cBXL6(R!Hv6Zlnn^PFLe1jJ7@!_rDR`JQd%Y1eZK~(t?hMm)q#?Ln&bQ;Z zx*|e-nJ@u$wpPM>MW}o(T@K6^@`fRcxKI`W4>2-CMVeTSEf61z;Q^`R`$H z=(20i+D_zMK3M(AHo{JDW5~YBMdEDmq2z><6J_2?a9;8|?KHnz*`L!k6r}}PU^(Ym zru#Z(_9-XM>so;Vzv6EqBfj)2H}zR zzJFlv&yhQ?;zf8g;hLDZ@$#%u?FjEcFbOpw6D={1_kt0pfpPlhj@2`NoG}Km))<=D zpQ(f1Niaoy$+VON&9_|!8&C5;_8iOCd~LH^_`2Z_XvE15aW+>0wl_MPwcjs|AfnC3 zE+hc(;rtmIy7U(HmTtS3=@x2cYvoCEEV5iS8u=li{zZW;wfQW?siRe;;wu77!;97DP+o&>w4tGDs$Sw(N9-) z@mOu_CFzn{b))Y4b3T{7D)?cB0lF_~*WvaQxab|y2K-CcJ{G*Qku3xHxFsDkbRI2< z49WZ}Wmbr~0)OcQm1MpvFce z)=Lz6EiEUUY9G^=^5$c`AkpXk1wrRMKKa_sErJ&<%3onDW=!aUgdN%ke`U$*j7aUr zVrpH?OJTNMpgaV@yA+3MZZ$s9n{862fS4RtqKFow%S^9tRvNL(9Qn_yJ#tqJ;pX5S zJ8%48C+}?&oP-vLk>Y%IpE)-ge*@-}e6M7BP@n-JQkw*22s3(L{W8vlpctD+_UG zL=#o;&Ia;T{xv6FLvnj~q??qZH&$Tv8a%Qn(w38!4rc2VH1<0aOr{Y*gbKP8hLrDp z@^G84JCh%BzI^X@yMy<+B`ZqN9DS)TNm;F&pEhOUP+~l4t_2*t?DUt zH)_?KT}ZZ5Kn;Eg=SZ3qesWNFK`Q2fK=IC*0R+i}f!&l9B5#i9S+N@3`Pbn4@ioyL2I+d6Y!%nX2RIYC+GQio&y{;z9^mA zWCU(XCM#{k`ST2M?y2w}8dzgEi11Oa^puzi%TCqs}9xmv(hOq z{c;JXof9EpUYxH(Ct{C>R{oMIDYc}jS9xnu`@j#yo3Tb&Y6oaA?1MN1Glh}#3=Fw28m4K0N`CR6c9O2%VUf~cBSY3AkS9FjfC6gXXNhrG?f9IO-W+DshGS`*M%5qEn(0!cJ7 zJaXyec`L<33tbUPTWmYW8TREny}q4!r(T$ZY(EQ0+Ye4B69CGvhJIyv7lg9wMAl{T zj9FCoIKObe<1{w~S=zQ`0y9-HrZ2`_h7Fg09}G#IHwx9)3jBkiJuKKxGm#wFcSOxk z3zVl}3Aor96NY^7RFFz6By{6-Z#q66i0*Q)bhvS?(M$F0;%*|?Vt=~HKX-XlM?}?f zK4V-t$<6hi{aEPE#m3^!H71g(6{h)BYo$7-_y-j36@LYRcqKVdeMW#%QJOj^*xlmZ zmQ6%Vfb`4d@;GJpT4kjq%K@btGWt8M$$STGOUV#&mnh`#<{h<~F6jHl$HJffs!ee~Y(w`ej^>96y+3wG-F3GK;~yoqc-N$pm_p zKD}?+hF%3BIa^)6)wKZ_8p$K_JR%6g*k~Vu9u}cDmcEshLO#dYr`LNM*vY4~bnhL4;_E3%v}bkJ;N9RbngR5)cJCJKDFXwH5bjRHC> zaC&uR913kg5fqHWpK1!I8(A0Z%VQ3ILk1oEsKICwyAv{b$dw!+Fs|aA$@o)>ceCDN zok1&Z#Vr=vS_|wXj9*IGM~iH6e_D<5L;}O#1Vl=N{eWcsq0lty19T& zwQd`+=2o-IbaTajMs=Cmh0Qn@$wKFg=FEL)_c zG{Qk=FYWdQ(2iY&&0Wc4Fd>=T=~_7{#XH|g6{YrQ2xWc)c?V?@?yj510D1K|C@&IE z(aBIAg~|5LtOJ{T{BR)&r0xISc?rwY)5Wpzc~#`wE%O;R!^WSht+oyby$KJW+b0_y zT(em}X#N*iGP9g~R{YU(lqdx^~P6#htdGQGp?DB?k3e)UbcHnZPfD^K`=g zTo|*&p+rn%3C4`4(vSi35A`-OFEHvD%XPChVP$Ci zyzCw zX))p3k5Ju@=K~>8-KRpl6!YGC+{ig3;hX%eb^R1fuXLYmOvtdWzjCGfu(U`kp<~0^ zX%{Xri_`|8xFbjTnQHAn=yQ5NawtAo#JW>dKPY~QK(|#@s)eI+r0{rzYoLT)&>s9W zVf>DzdrTy*mdUE{qc|0sbB*ehv`4}DgDbC)z90;QA(2X9Xlzh@L3n`alXR(#WQ!~P$WOgm> zhMvyKKwd?$?1ob`*@mm;FVfys!LFYmkhGY$IeSdhqXl>I7xuxQ;6|Rr)R;H=3i5kJc7 zmW+4eq3inQ#6~s)ZZvovyH_ zO%J0aT_ZdH;=X5(Uzc|P>fC0vb7mP6)29;I=LXq?*V+-`%#A&MGtXKCIX+_pO|_T@ zzzpzmnRa2|c+3-mT12_@QYyy+?;EQ%N!`Q#+R;E_ZZ#0y4PSdgrl51wa_*#D=IXcF zpC?XFp{jl`M~LDQMRk}c5B>m>3(wV7%qY)IK?Tmy>=6rPiPWUm;GXSq@2q)9lD{PqKB<}lXC`4 zr8T0HM%EFVS_u~|(+T^9{M=wn469D6eeSYR=?4YCk3M1HUKiUoREgz-}XqAMNm4EM?6FEyd+*&tZwzHr{YALmlt89b>4@O*u^(w{Pt*rKR#mSCk=ZAl5>H3|%nc06N{|dXBEsV-@owq|> z7ZUU+6CMUmcH}FX4@YQ4tKXsg5J8tbwIS?2;VpW2XDE?`;QBQoV>UIeQ=Rv)cRorU z!8M9Ali27^jsrIqRMs|)_0NPHc@V4D!nmOa3J^lmIiQ?h5WEm6ITJITfIme!gLrDQv!~&{(f?F&s{fhTIshS|Y23}!*<1#irP4`GEAl9Z2YDTN@0>fA z8anF-7FL+THG;A7e`5abf(vT~E5+ut@-TCJ zQHOg*5!NL;eT*2CMHQ#f?11PHW8!jdb$Pc4QkJ*0$mR`vI7mkAB9WAa(4H|1AU-i) zs=5SrlBTnA>67@vMDG+Bhu#DhSP`k)=up z?4y6tV2+{O?vqB&9#^_;^Edm6$K?~w{E2g)SyCqVN(0>IhP|*L=Uy(XU6fs=BwBB# zsWoo}s+8UZEivAn6E(Mm%lyow*>ORxI>wiwmRiX{a;*AU=uzptVU{P6vD)Mz~73duc9b^)=X+9wN3` z>Oy!ACM0?}_y@jXffB7v9m?j+#w#4fzOFbnm>ZPIWyLr43u`bs*gI>N)%>k>jQ}gn zO}>gLI(K6i4mb?foY3=0FUx`jND2SelRn{k2w$U&-J-Oi5-v*qt)mW{tPG^8{@xQO znPa$z4dS{>#6HKyd?=bgg(Lz8(bPIM|jq} z8JG&|aL~XgHC612#@9aJN(Om5^&Zrf0TirSp*(cce7Y;47lV(;LUja|crBwyE&tSQ zj-txt_U`imPfTEpBX6;)_0d4*o8*h|zy4kHG*?h(im!#_2lLcPF8NOE^+0T=Hb#5wiv^fK_svzg z0#8o1#eqyo@d;dpmL^XbmAi&Tbvt+zLs*23$;qlDsQpV@O1?mzvfW;t1YuLz?K~V9 z9vdy%$3VH(?&O`VNT|p9%sv+D^tF`dY#l4PJK+}{bzLH0F5bTT4x=L53!n4N+6o`Mr&bvu{K@RXEIAULnKJ``_;ovYM8@1%4yJYR3Ukq&V zxg~>C%i1syHA@>xm-y}!mF;4HR_m7w=i5q3;quS@d(RTGU#Z`lC%Yy~D1B-bC@zgU0H(9ScMo3Fzj~9e`KcO`Iv^bF>`bw8A`cnQ@gp~ukLTT-Al@ZSU<4?k- zEqqbZ=4K^p&Bf!-+7Usbp^x21`(f15ptvC*S|yOGD2!(cf(JsC6QRPWr6z)maIE-H z2g$uLp?!aBCAOo6SHHQ%Gxs_97iq|r<9T`dnRCFv(t?DvztH!hFAXgK1!?_lf^o(c z`@SUHGd2zIb8LoJC;e)aE8UC0o9;v4htpUo;66JyAG^P^nD0NAUtp)tL0iq#yM_NV z?8j8`kNi*28b;;@eo3m;X(*3^4_3aDaDK4>NTd@To>o86Axag}BG2$8DF{}o zknEln4$k0q)9w#CnIR#0qPZ~>XQ#F_KT{oGH>@6afGD(y9d2}MP}9J6qx3<@P9YEz zYpEw`Oe>n&dMd#Dh-}3{m71ia$D-Fi!eU@c;chKgo%Sy_?_nbFOFCVvI(A1D{>|4) z@XKE)Z6p?&=!#0&bf=JxbhCDc4kV9;fpwK12R|&Rq-b_^Da~Xf0dzv1iV9q=hzM&~ z8%6%ow&vLa5IaTk2~T%bhk)pDBM9S_`M+0-`M<9g^!+A`Vsn=LyLWd5PMyW1!_=NK zMSRHH<9FVjLTU5st+VQCJgax;7bhDxzeK_qTHN`S_vY;%pI*d{`Nbm7ak#Uh2*ml1 zIhKs^)0lcclbR6z7U~pge||T<*!&+6lhSZ$^Y`-phcW0fK+vZ=*$C^ z0!i^Hr^O6Tg5qme!7%Rc{*R*JZKuMduLlLAUllsX488 zG3V*Hl38TE`bI|JMf>L%+vnoAmhDkcJYWCHG)v;F_A8{M?XEL!*FZI_(=`8h_vI!j zc$|pSIH(gOe1-E&-gQpYYbnJEt$JJZrrL|AB0wog zs^gB+V_9)8$hS5-&eRE>w^nMah*sieHPh1pGLUGs)%-Z8V|lr!l`1XLDW#0GU|Z$c z{YSO(a%>%)Y7bAm{S~sO=Pm0RRb1qSx9uyD8EM(Az;+QZOg%MEnR{3e3@&A%cG)<| z>su?Ctas5Kwh^cG`V?+OyB-{rqX!aXI3=tKKD)LhYo~m zJ?B51FrmKGAxn?aGuj}b_pLk8U&Y!nPpsR&8m@kuDC17>>*oloI?icu>!6GQyXbit z!nLGV7~-|c01P2i^Nu}hV^^Rszur;|r-o=EczNj5wk?@y>=9X;Ir0HvEXD)=)}LYG z6b!g7cjIIUYEJqtmz3L_d2uGz7JoJVYQ8 z8yNbc*1t}EVrwAmUGhOjGIQUXvENa7TWM#^@i`OiAcWM|i4ee3-!P&`*X|%s{~=+b z%T=q%5=+O}=0Zp_?tAVR&t-r14HIye%!LIFX)(SArrz1s#0quu?6Y@9U2Stj%{;Qir1i-*d+FOX{_<^q=$a0o>I}ak7cWirb!rpJ1CD%olX}?VH z#ImpI_Vv9I?`SK~*1Mk9Nu4S}7EEoZsolYV6tG<)EWyaY?Ofo3KAMnVgH4(*G zfI5yp$)kLg{85a~?{v6Q%ORd)a^`YCZwX_9?R)OE47Id_a9Jl0lOIAk;HA?z!>8dg zx{1(HY>fDLHbYRn8EnaI5CTWvv&_#fi09$wbG>e?!0tEkh6J9NN$#2^@60p5e?;Ap zkdc&6P(d|2|Mj8faSSDJtLzps-w2V+7j36{}4@j~Z{;N`db zNoEz*DKX?$Ec2E?2NI9G;rP>Z2s1^MP$!ImQY-UZEH?%@*!|8_vE%$cy8VhmOuD7z zw-omkd5vdLe71p5Tib_Bz=81o;q3*%e~8SF&sfURCAI8K*#X=dBvcDvJ7J_F1Wn9O z*|8RHHn1`=q)(51(<*_VRiKS^NgrzwgB4Qo4UtSkU2B0G=f=Rw_3(ETqA4K$zU85P z=Q+w@se+?YC5Ktxc+0_j+>4%}jPGtz>beJa-oB8s&Gzkigx$^A$=@fe*zL@Ak>9ZV zJ?(gw_U9OI3g?N0pbR?Jaar|Z^*&)Nc84RNU^Dz=DRq|@Wog`bUOY8nDD~H58_2Ga zFrcE@Brcv57}~?jlNF8TC`cyO9-$6u8G0(@L)lqTf1X)C379j)e)E-vd{$IbP__%e zm$t0NtBOSCxx65DuYF|`0<|eARJ}62<}$cDegF5%_tgNLO<%gv5y*V6@GOWACOA$K zL$?-2J3J9lmCz_G7v+S__ZGe=+l$!nkh{Oi=?8(HwbIBFf&#Iu{*G5?s?dYG4ZN#r zlVdZ_@EARs9)V!WP-wtkaVzaN*n3xe8 z3zlz32-EWZ82Giaw~Xioua=y9(eE#0p8+e14ySWbkK8Gffbr~xtp`?_@0Zaf3ZlP! zA`eh12-<)6 z%h#=C%;TYfd81UJ7BAA|#ej#Z2eOT21MJRRwv+Gzp{f$G|nomFCY02OVEJY7c=GxK%R! zt|-IvFcd3^3ogsNAF$k|GbJBmf|@82zCGR(QF$%Kb~~G$=V_TbVAk1Gd8K*U`*Yi2kypUmp`+hn0+i<&Ejl5l;j9kr=wzEUSi;d|>e87!1CjZrY?Z=%aAK0{^mXFtelDYR?)cVx6gj0ht*(yota zujfGKn^^9Hz}@|@2%Y-p9XE!6r{6fWiBWX!Nots@VP;}eDLhj)W^SV%6~Y%nZn4P0 z)B?Tp6B@g(06XJUn45!6()nz)#F<|j2pke$T+QM$k!T4?dh0Kq(SXFh@XYL6_5T_Z zVjg_Kf9a`8HS${0@@Y;A*d?VglDTxO^gWc>3M65(hFJ(}WpC`}sKYf_5$c5*L1m?6J2k!y2r;4;o^hBw+bAs@b8KU)0tqA`^^-`A12;c$||fY zEp~5|LO{0&X-i-tq}vPvYPMlOaM|6eO7H%w)Q`6-oKu^Im=AzES4UrzI^DkWWN-wE7t^Qu@c@S48{mwoox=zBE4*? zUi+>_wpVp`c)j4P<_|XAl92V(t}c&Z#fBi@{2>nyhNL(Ve5PvuYT z0bWK@{YOUqOO9{n8|hR$|BmBv^*|Zr3M=J}RH8RG1EZznlQg&wh9V+T>K2k_N1UbX z)Y~in{brO{#Kg$Cn68lBNGzLazxfr9hBU1O4+hG#Kcvk8o|XhL35C^WLM0!4nSJ;p z=~P%dgI?Z@{GvSZ>hdk;nPh8;Dw|Z-DenEgDSNtJr}5pTh1k&d56dJc@b_~t8>0Sc z&-Q9f;5*&GL?_}~`QT4|6k8)zpp^m#eW**YSteb4UT-9PDAU|u{hd<$S?dn$8~aI_ zR(AUl?PX-HV;E%mc==GSy1H65!-fx*OpgmZ)w6Rr2q$K&VGo~jaLiB8K~LR2xh6+L zN9cNcW9q7L=^A(+&2~nB01V=0hzN86tAij~f;@NpBZyI){b?v>)a7rP5`HPGC3Vrn zlY2|oS*GnI@LRADBBMi{&!xze9}lDi`LDS$48vUlo*ue^7AXz*!x;IEOP zjeIa2Sm4U!hMM<{b0rA)2TduJU>49!A9zFnuc0Kr#r@&hat&Doov-pYuU&4=%t+<2 zfPjoRr4t1(3ET^8l3n%CCsa$sjUJ0hbapcGPhbg3DK`-k{U6SS^cL3FaM-!}SeO#! zE$pb#l|TQd&nbS(;K`cZbkgC0{d{^Dak^Oh_OA+#Ai zWa}%O_YxU9fp}}%Ab%~Xti4ly(-Fmc+p+^&gs9I86^PRYj=NJE1}=L;yL2ed(%3jX z;0(yeTontBJtSqCA>IL4H`y?X8=H0 zpHmWXVlAxdXvg#N%A*FzrelbKgNZYkAlMi2Eh;XaJgz$Nzvth4 zs3hf~MUhy%u(vB~FKcTngNMMNKRzZu)I^DSPP}$bdCl8=3EJ$;FK=wkbLPLE6mG;F z>UJWh2tB$pX?2^|dBX1-nv%Ft1<76q73JhMwkArgYemp+d;oQ+k1{op{Zd zAhJ(00e5G+h0oqw*(Ahog$90xD$}sHO$Mkn8e`-Y5>F9^FR|W*dwpwZhW2g#cyo! zF8inO6R$7by>6|eYr6^@9(!Fi+u=lB3;M6azvx)DEdv^cRM-qc0*RAA&)0k59nLyh~ZtTu7MKwR|Lrndz zJo|?egv;8A$W&XKuO8TK29FXZ4spj63~Wc*)W1L@skd$mE)Li5G2@E43x{Hm)=nUX z79VBCkPP)&;}drY*G~~O8Hu+FRC&p7(0C-Nso%e@WJw8p)oXM`nCQOK#~yshpAok3 zEQXKwv)Z?|@K943<-T8D^Ixuuemi9EzG`et-+@quCc|rfNJHl4z5o~qIe)0dO#&uT zrb>h3ud)jtUy~JT%jaxIx!y}bCP}3-)OwgCOoa%XZ@4<1$>4W?KgUGhUrb29z7_JKo>GQ7R0p%Fmvq}40NijX9X|SFH;{z5BdmK-@b*QzifA9sCF+YL zvB=Sz#QqwJXHa}6fEGj?I|m{t*5t}$!A<4M%EJb2sB0wA?|-+bi;NgX4VQ*{F5_JC zSixE9+GD~#pi!Gmn3s4{gBLV45_Uf%6n6h&RYhuUGmmb}bxpUh_JT6zew)5|vT%{u z&~6N}QfsamRji=}wM9G7FOHe)*ZTvCRKUEo$6vu{!koeYSS;WW-Gz^|{GnR!&`^*3 zn%4?7Um|0!XVuO6)`53v(Rjl%E_ z)0AADnCym=p>4QT-4x%$lE^*mtDN+4_HS{MU=b8mXY>%+rl!HQRrJJUSp$kr_@0g} zh*x6`h@ONtT%r69tf-o@`5e^o`co2F^wT%D^Rp?Ms`qAh{0^^6CgnIj+GrGV#@|n@ zlX>)iow z$ne+4u(>t6YKKy&eeBh)Y)T7rC#QoLvJVQ}?AICUrwllOyv68=bSMy;KPG%a#V11u z)r!K8wrDakRWj0^ja#(vd@(2@QchD9^G`j~TpxoLRBnWdr%{A|zt&+{OgbnORcH(x z+hV)nNeo9r9x>P5I6IG@lHCbq583O@b==hWpz+LKD*=2jP29^QdITW61!vO`PCa9+ zj`PZS%F{r}K}mczNtbWBPba71_#9ztHXjrofWnXSC$+Lu>bCshCF-BSB`gk*OH;3` zT-$xT$?RPw%Rh;x<)%4m;hy3x!%~+na(zC>Da4-Z3x;qtJ9%z`bl1{LDlU1ir~rOdfd4+2m_!*9@wg#Va~l@Wy};bY3L9ftYEAz(~zVUC&TT(X%(gi+zqvB=R|iK zd6M>7;rz3jbvhsZBAjN#)%PKi_I{&6mRvM8()v|fq0`Lir5F1D&jIq-@mylLG5yYN zWpkTD#Cx4_iZ1(F*vjo8*5Lj7&aSTRHDo1rk@ys|n^((4&_?o*P{(n%=qwH1QJYe8 zG02DsI%%2>Ztkc@3JwrtFfhMEh-eFiWfU-tkVI#bSAcuHEm3as?e%%^vK5S^tr>`GEx5&gEnPaHmQFf6NmAlyADCdLmx` zeJpRn(f*?!miF2f&Zo93(phNe)MkdJwv zn1<0{6`6Q?)OdQ2rL^L8A1uJmbbS81!CT>@+L_iA|ikYmhjJ}jE_~XKmBM<`l6d+r0^F6@M%X4$gR@TAwwl>yk z$JY9IqtV2kKCVWXH*dzk-ior)y&fYa?}>PLX$Ru9oysQOM_HuvLeKUP6crK*b5k`1 z!G}6paC^gQRMlFht6IuvI)s_&N>Ly7S~GN)k_x1pj)*BJZprhqNaY1eP~dI3GY+U~ z7MAN1mwuvU8?Vb->G@U)j6q@@sM-)C_PR6%l92X(=ZvjmTD7ssc4MrT4Z$hsg$C_w zWuR~bg^Om;xaEXwDCIZb??=*|ER`;W5%HS*^8J!xKD<_d#3ZSJ4r(eKQdBkTR`FHf zAfZmfm&-Zrs1y7>n6os@+$|XeuaV`Zj^5jY^TX~6wETC?@E5uEzu4>v%)S-@_r0FU zjR@G=WbS_V1GEEY($Zrbu7;?ViBB*r=q`e?w+<+|EI|xPF!2*0n5w#Ov>EB_)-0do zSZL5OtQe~U9%uZZdB=^zXzhqpk@w?(=AL9?KI*J%xt{D+BY#_LuGt-09^g`?Y4fUB z?v}%f#;^`492x~&llUu+k9&)hiUInKsJ;rHQHKyTGky%;8-En9BNdpFr1?4X7>v5n zMlZ`1um+(>f~2+Y_`%0-gYm+h&3H<{yXu}sYm*q?mt?aNj;u7>6f3K`+3kd)f2X3? z$*a__Jv^IBE2=@u-(*6K2KV2)?)1VT0g(VxfS#*%ABQsAp%S;neSoX(e((GDkIq02 z&YK~4+#$;pisz7+%L|W1Hgb@dNxa^re*~1(mX7jzrl+7j)M^pMoh@fAg5+(JzM$v) zSrdD^-OQGxsH10BBy9%K<)TIk01ueaYb&?XV~u2JCAQKjTr$9q%w2S~KVq^|3(?t{ z90$u%jAAzGc`2$UD5^Baa{c4m_V(=#wNwb)@68$rcj049>jXmHTOvPW$K%182nOXy zQzp1ZY_?=h;K_Z-e)^)_-N{?G>I-K99a|v;&0ET$B-ObY)JkB{jG*vQ{BmF)g1Ifv z$+6tO+olc58I`9QD8p~eFXv4yCEHIn$&(8Gj3B%?W~WPsoUJi^kY<7A#tB_R_3MiO z`;XVhkiq+afBH;+HqqKd{9txY`IIwsw@0x}JqCVFMD8i2{{@O`^oGl-6V^V7Tz;0@ z8sX`)miy^PH%%hFrT? z>AhcO>c=Fj`BN0Dct8nKtq6Y$2NJu1RMmo{9_$YZhJN9i4YqlFD1ppy~>T=)*s1%s)WgUhE^E9=G-)sG1mNYUY=M|tw>Hu&Fk zF7)dCzbIj%K2>W%JF6&0#S5wMI{gmM^VcV`Z=dn9J+q9x`UkqU{C)3&>RIsR@XYSq zQ?vDnQjz6s-bHdIUZx08Nm98Wmob(h#=u9$Z8Mx@6?vcWaeJ-*apM-2PHLAW?(>Yx z#kopz_=$n_F2}^hder7oo0O$s8+!}o@AqCpX*8Xh8@$bdCf2o(>^rKEq`ag#|?`A|T$Bv%3ANz(@Y5DJ_EyG{#v{g zifhpzEycA!@f3m=FRl&lRxA`L1a}FvP~4%o2X}XOiW7>tW6&rViOmlj^Mr#GQ5;t?n_bo!N3Ny%6!x~WRNIoFg&9o4w z^o?;+%JPcGS~aX6{mnjzsOb572C$-E?ZSQr#!xchM0LzRhp}nzSUlR36YTw7E%d!- zsvKWbu$6Pr{i3(Hg`58k55!`hF8y?8n;xn+T^?^G;FJ~?BD^SYht35tim)7e=q43riKWZOO4!OB@vP7R| z2KPE32Az})W5F)05J_BAsv^ZdvBbU!P3_9KWx^Ne`_0aN*q%#RERu+av2v)!#VGiP zY5#eA3y$pV%$=XjjrH|eQObJkpf;+B*L-C3yVkGg5u6+yp|9QtEA9N6r#q9%06}D% zdWow4)C1z>`oOZ4mqAg)5DY>w$NwR&+S_#xO*&su78QSgxcNYdOV5bEpg=8~NDK1a zN!Rn+D2)<-D)gJ$${O>91WIa=cLWEtM04*pR4QihN)O?0|KZ zT4hZ`=hfFGVQZZa)dg7qA#fo6eA!2j4N_1@%FTay>L*>c+8?0ry!`{wE}0MV8tvQJ z`o|MgyIj7_6l`yPp6PoRA;b^b+V0;wHk!VY0BZ`lQGj$j zXR4NRSu=gXduo`6y->$~*s;;gedFL^3N%Qxz@jZ?%Y*}DZ0?zZ`g!Dqtq$bD)>$t`tVXp?;#C8-Js_P}Qq1X|h#&G_u|W?W5glCIj14#72!; z*WSD_v5&3r6a68+WvOu|Vq|diFut$0dl(_bQ@C^#6}cr_ZIBs#<&K(4hH6}S;DXXA`OsEYzlw>Cz5xxDK{j5qnUfA5pS#7yl^Z$MEs-fZLj|2;u+ifeYG(j zW~Y(aqN0hu0I|%U)c#v@1;~m<`>VgKyJ=GZQv=eVDq5PdsMV`RSnJ$TBqk5HpTWVe zVYmfM@UjUR$Mjy0vntQJG0%5vH)}qIR|&MjaD!(2%%t=u@xz^<2;thjt z;L9%|>cVtu6m9*2RnJ*D8^A$|dW(hAxbK^vup$0`RGr?_YF0>L09XiKtk@PVO+t-Z zl~p77MHAd)vn+po8~MW$l00fsq3}$Zc5h*kPvENuMKYE-^7BI)R?dAu1$i-Dr_$|$ z9mZ9T_(OT?LGo->$RCE=S*!qrWBw$#|reFhAM-I-?YfQ=jkm zvzy7OlLEE3t}gL*;fecO)i6d2h=EqI0!`57-;NV;Y^PAX3A4_ZIJISS8G85qkCd>t zGr4Hv!ReKzRJtc^Xc%+GmnjYHC*Eb`E2c zTN#kKgpyHWGEFC!``xr-^!U0N0X$%Y^`x@FC|=!9zB}n7u*$L)W@PZmEHt~(gvr|C zk~|>qhH{zrk!V@K9aDsHrF4@_zs=_(nB3K;NK@3gCw%4WWq2qL8CL)_d;lY>3H`ep zNPm4uN$R%tt{L9HJxv`sxe0X?>;>YHEz*0d3c|M=-X^@EsBe}&d9MKA&+!7 z4Y@u+#iPL{JN?H`<=l{$0+jp-@>L7Ja+s}6`Ir~vORZzg{8?b5hYpJM>N$DcIhW*w zN;9r~r|VI~>;7#}GSXzbLJoTbiNA{#KBwsBXfe*Rg*Iv8UWYOG3i@^ieZRAaTGtQ^ zELL1s5O`i^+--;HOi7gW_$Jhq!Ww^TN{&m8WSGg1$lAL-iIun5`CCR{_npdvaQ;WB z)?|F4vz784=@gr^TcIYUD4bNYrJ4iF(&h+ZG&yAUUa2B8TeC6Y-f*8^bsyrf*pKPMMwQ~8C7_@cytQXx;gRT zNt#70{pq;z@ye)w-GD37sr5}~#?HH={H2D8RHc^rE?Dd^o0es)5Rw*$OXS*vp zOK{<^t<{LjT7(yAMl$9sF)v_$jDN<_XhEkJ9wg^gNISvU{VjNL^kdhh&nsKHwSh(uVv@c_gN{^;a=Vpt39RsbT*#0< zw-%4~M`V}N(a})(ZyuuNz3P;_0kyvC$0U7pk|cKq-8I%?zJ?JgT|L~b5?c%jmE~pT zhl7%la`XOQpH^UWOxYg;pW)OJR|`7qpZMw<&ePC#Ds!BRF%iHkwZ<3;2eR4B*MugU zdUTioo7MWh7%pm&foyla;O!T~{4(1gt1lg&h&cR+(-4IAf`bjWk@kE0g;`{7jpA`F zZ|N2yL9~zU5}Bl@GF|t?I8Oz(tK0&VmklsVzk38IWjg0(nlu#fU@KW$#l0>CDcS|8mq))9{&t8D zC(25#c9rMFb;cEXl{=fbr%suK%&ex-N_rL+lr1L2+Rx0VrbPc7sW?3JDt6l6SN1tv zX^YJ5=eAl!Wi$3`@>9VNu7LoQR-e66e^MUq^5!9$0r?{K7^|}2N=%u8iLZxQE;my? z{vyo&uev&2j{%&OHoc$bIU$G0OHiR<>ZQHB|EXuXdp*gE=HJbn(-xRvD_UERm}^9l z{C3(*Fae@;|A2U6KdmBJiJ8gGmkzJJgfr6gP;|oxd#`wVm_E7HrMr86S@Zl2MLKvbHTLI;q<|P8;AT}QXb?rzCs_qcd>R{hk1V)8KtsAU<~MVPhQ2GD`CN!6*Eh$^+kD*lRWY<6l`H3k2Iw&U9tSRN|j+QIO_J@=>H|Wn+ zh|y#3&c+xYlpqVlxQ5$BZzrH)#p{qJYFXd~n5@gfY0@fMNWE$bGm=I(R~?7xc( z2wscvHUrM5fHhDQ*z{n))#1>saH0B=ut?x=FN>NR#QP+|)B&2JBV({1pAZ%5I`9R1J0bC zX}aA?;^*s9#UBWDQmL=RVs&>a$<{y? z2)*4Mw#KIKb>S}&ISiFH&$q2N zGnm6Pd^dQRcE(%ie|Fojp>9a$a4KpR?cAB~Zehudst;8g-R(X^t);NV+b;Fr4jrS7plK~sk|e0r?XwfDxJR+JoQuX@%s>e>HvS|Y&uT}X88P#$+-%_ME1Wh9H9H8 zLfPN;c}!eYQGoM}Gc)1&oF8mJvfaq8F@L{qyh#_}AQuJ8$q$T^E)Q$k>vs1s&ZpIp z8K1OD;Vr20DXWsH49VqZVnpRu^kA|$Y(ir>J;FAq zmD9Zf9#40UA4%6?q)MtWuW_!H{aVP9(dZ;C?xd73nGzmi&z}=8(IwQJctMK#e9@(o z!&~QtoR|Dep0NM#dVAwX?=ifEaN}|E2MpAz5n;I-{3s%i^@b&Mrw8$a3UzN%Q?s+( zx3$##g|Ud!TUs$VevcFW`5iFcMR^B>cNvYI0ts8}9vC9$6v|URGF5P`01mxBF25up z4(7Mrx#JflVuj&|svEI5kDnO@IMPHv;p29xIIOrYVWp1*4pR110IA0N)%^v0kZff?lp_`^?`o@6}-^iH~Q!#8gcC&wNPDUaf9F);``7OBZo5pu|C(p6z53#~52z&8O}P?uzQpxhw<`PPFnATw zevyo=kqz^#ax&1V-zU6qPh}4q+hekZ)~NP`7(qkLl@P79SLkFc)xZ3e31TG9dcN97 z`}s1G!G=NcjRW0G*NFuddo>J7-@f7I9imBMsm4YXB!T-|^<*Yj~V8r!~Eupf6AR zh0aM4$LXZtIXBO9EW%yMr^%F@jE*18@7UcLu0{)Ke&Y@phR-E0Fz)X%@*&eJwulu) z>5_s3gH*x1WTjfv^(ZR}_R0N9apoq#?|z8Bu!3 z3+Ze(sBJvML@Vvh4|v-*>bh2RJZt~nM7>R=6VMM#%E#`-AH!P@yN)`~l(ilV-d9XB z#?f?xs~AKm;snx}t($CN*5D*w4G)(SwMC>Z8`jhsW%>gHnYJ-xAYzTo%`L5J_WlTJ zH9*mk*ha+5Vb3gN>9Mo-l%(~%=rgY(CEB`{33G%)mq}NP3k!2a8S7T~J)x**6&H)v;dj18j z52+DDJt0aya?705YjQSA3Kk+nMp$oPk)Ul(L3*4|qe4z59@v>)N7CCaq^@TbcDLgA z>6AR7e|z?y8j1nrBIS(kKRV!n5sJ;hGcf5`D2f(clr;?3hXf#)@;`dT zIG47}8Pu@<{%x(M>+7UqEBm@n_pvKIHexs4Cc$q^Qt(ScqG^U~TA>2kyH;b%->=O~ zdSt!s<{Z3PQOZ5aU+uHz)sC#M2P1cMB!%>uesX13Gk6FoC;n(;MKwFd1c-?8PV08z z#lR^5TMuu(3!r=s7GMlR>5*l!BIe7o z1A7a+PH4VV-o#O-#KO^<%#(ICxXou1RS=V~pM9YI-#~{`K&zs^sQ$=Cigx4{howYh z(A13*n8}Iq`+P88XW2B%8|!QO?k@`WFXQJeB5#=IefkQR>5n@iM!?0L@^S#+fOaKy=ynv zP^*c5R+joA4$R3ye(Q!O%&^A?fY1o9`D~Vj1y~5wp7@?cNKUTn4PrEGY>=C&^k5j8 zt-fuZ#1v6~QnCv|jiDX9BwBTSY<;tx&n-Mx;^5dB6)CXf^U*FLiKT`6nOqt(4~%HP z?zV}+UcLgv`%;;a`o~Ko=2W$LM<6CsV>!nh$!sfof1d`z^&`t0Lf9{+he_3VWe?Ju zfl@&Q9?MzugCOzUnm=ZJ^W6oA`{{RUXpAnJZc2y5MNz1t!c{gmr;q;_c-Y3#2KAHxbA&7z3;uZyrBi+8c2@BXz z970RZ^#v7$kG$m#_GO#kar_jgLF1Ea7WMaw6)%JXR?JT={gJp+a=#_R+ZBfnIiG5O zRpX)>tzX;4;<5YEvdI%Ru99WK;(3XGU3;$9WZuCJQ^M_4#V#j&o(}My2!Ydr^D@%* zroVDNC&D1O(BU48*Vw+9AaBAr?yZfHxr5uUWoVwqx-XPS)O0AxA+ z%8}fT-2ovZd2a^y`SlYPOaayA^LifB^AUfIQ-3LCFGR*^WIj+*&zar9CnUabOHV`o z9UmxkiU?OC*@E0sqVUCo$HLJ&4Na(YE-?^#4d7Ys$rOUQmIK!7cCGsC(&r%ql`1P0 zi$uZm>%fzyFtwk1-TGtxFLz6qql${dmDW{=LQw5SG(KOJQ(0R^U30)$-aV&=f?T7c z9L|1_<81maC^qjJr(#dyD=&EYOUaN{Lzk4R~Vxlq~QS|7^{v%`aZ^h%B z)jq6W2t3X!!_-`j4GMha+}W)S>(*ie%QO<0Y=vlS%K1ZI(O`vP6rYTes+SifC+#DV|aaok!a z_2CiLldT0WRk^u>tF?P)xFG+n&=402FwWg=_()P&)5nhl@7XqNSPiuEr&C%Vf0B>+ zTTLEN>qiK`ki6UylHlbF#F$judrCOFeHY@o1KfHK8%Rc{|8FNIBLQZka`yRwlbXj{ zQMgpEw&=9|=h{;gYuhP2T@q*?fja)6sTBzot5GnE+8ucqo7P{Y(Fg=k2ZQfE`t~w` zkpHFI-o2+D*R&Bn`mh>>D@5DD(e@{({F1;?bs$oqKnox}-PVbrGycs{#fDV_Z#epM zf_*#37`|)Jj_wJG@mF0?!kL7intw&Hfdn+Yir$58d`V|Ru0f`Hn&Ec{$8XR@s(WBoT=|LCZG3sCUTw}xYW7SYW>0)HI)knTixgXE8@J~U+{JiPjy`kV_PbS)41 zU%N0~{gPmHzEniWF(=o>X4oxWEF&3`-|DQ94 z3}!h1zr)dxzFP@gul?2+F>K+?*}LRm6y$+cI+YWJADLM@sMOuQNWqW~jA^%&0lG-h z$BuGYF9_LrA|!>XLcVTVRZzBz=)IL;p{ZF=SF9uCKSqy(l|nC`4c=#x!u`8fwY6UD z9kM^(mqdN>kHpYKi0&?S7AwX%MyEff)&zXq?EWbLqZ@9S@fPbX&cY{+WcNEe)t7J< z3?gJ4Gz$q{l>VYQ%R`d(Uq>%-`3JycJDOCTXEz>DX3>L@&p7==-wbcU#WFVd)(nzT zu*8uNLdiS8Jx?5Z{Ije8bE%z$JE~&8NSI+3AAL*pNbFH`&bX~C8Y3Pz!jZEKe(=Lr zshr8k|$iK=UTB4x@HAw!+lR zs#&fgKkq)n>5|{~k#8t@l%fVK>05yvEE>~e_&kLag_$-=-Xh{hvMq=(5s!AZwnM=> zuz@6|hs>+Sky=_L!|}<~vzf&XY2T?84act&tT7IKo%;w1-&6k)E>UVzzA4d7K1NVQ z0{xI!Z=Xg~Q+KbHBKOi8--5Goa!%3Py#my*rwN=?hAaXEij7p`lB!~J6Cn=Dg zb0uZ`g-1Op1p*>*3DA(Xz7fX}KicqiftUd;$n4@~`Kw?=*MZT%c)z~`3GdGdR^+@7 zoNzK*59ga#&y%|9&7P6wta_$s8#jv3%?|&nPbNo5By0=L5hq+UgZ6@=h065<1}Z1g z5yg*dLR)YyAt~SmKRT35!mrrqJo=RZlP6ue4W{?H4GDcX3S!>st^8uG41rLj9a0CV zx;0?xAT0)bHJ>Q_n{oN?f7fn{XWH=~2Z_t|CvkXPoiZ{)usta)Po;zosRI#xZCLLJ zJ?dyV3-;-?yXjd=1d?|qISrrJA}R=ZIc9SYTh>P3>7Tu6YSijOv%QPxQ1zsJ%F%iH zGa9oV>^;Dz=m#QqEBBtEe(}?fV5BmalA_9;+VPC{J`7$NHK1J~j3){?&GBzDKfJ5x zdETbYsP`I`l9p1-d!kq@#4mK?=+hS@T|$&6RX6>)-_D`K;=e1QQM7`vKxh&48pY1G zS790=z}teyuAlzq1wBi0J^8^o^-JiV+nC}d)@Hw=ej_~6(lmhU>c?f+W_Swy^pbDh z*zSJ|9CGrRk^V6U0XM$*@3eoiAdeqbsXgr;a<~FIun8ZCQPOeUgvRJdQ3iP$r1}~;6W>H37Wk!!bmk$7lkAq0`sv&b+B!3=FCejK|G53)4RO?k-qJ$YT6fO$Q zn{UZ>y>BYR@_#VQT)W}>fQ#!Z&fZ$rvN?!*c0+G(p$sWel8FDzbYd(~{t3$6axlDT zj`TmD!(YA$Sn4ndhCWjM&l(wF`QNcIk0=ELY?0WL5TZ;h`CHjou-x$CVHK-5Gkw(` zTnmw^9SmBW6wBB$B<<-%@%%aRaIi)4(x%L*I zx^iA`d3;lZBYUq(K=_crgF>w4C*nMS?3zn}C~U^zSedmhZBhwb;wTaU%zYpMrN7bJ zNd)Qi%8PzJChor&UbWloIJuY+^`XAues8P0-%fr)+jwT53AvEUbH=&-}nZj1c(`aal0y;zquY0_Z-+u0F!*+0{}0${z-*VdL+ty%A$B2 zRs#;`1Csm_m0IWv?9jErL$qWiGG^;=ccf+gQxkg_iBwhXTr@S|4s~0ej$^iUHvx4@ za?s-^gY$VB;zI-OP5m#KNf_|Y%NLcSf4Des{f(lQJlXZ~t2>uhtw6%#b5YWjPXRwY z4W6ViQ6I19L46#p8dQ$3ibP=hoj{$IoY8zXD{vHe_5BTho4#NhNQNa6SQ8h6Y^Xr> zeHr8JX>fuyIkV?+VD@A;pp>IypUm3*en1HXc`Jux{AqM%XYsM+TqM-^HpHNa+De7q zx=KK1RI_GVZkesAhQG!FG(CQOI?*hCF^JY&Kl;C2@$TQQ;1OF&?LSPF!D4>g&_wE1EH>HI_ovM53cm*&I>g=Wu644<2J9#%%>(%0 z3#`^`DA%uqNvZ%gFP-1zqa3AIJQ8#~fnS z&lv5cEOd{u9Yq=RR#LvDvN>jN7={u&S{S?2VD7}fR5z zrfu#qQQz@_77Q=CrgS_r<$IY|~1Y+TOmCjcQHF7m< zqT98)Aw~3-^8r}&r1VStCs;1N+TA37P>`A@nTw|Pg6C9%_6A}5FLUxmQxe%=fj1)2 zgiELurcAmqDHyRCvfGnv?mN6Y`^Y&|0eZcyL&>F3dp&pCnw8Fy1SaP)+ED6Q4*_p{ zCBL&AiT8g4UK#ltV+aB^(c*TMvVPxxSs5~J)-<82bihA#>~B*hPADAiAIFST5%a#R zqcePP=B8lC#`tIN0A0zxUQLU2UZ&0@t3Yn2CBLbk0-eZ^2K#dB@hmhlYs_3&S<56T zzlGl7Z?&)iX#X4RPK}hr?#~8o4t=9JPaB(0U;9YL;|aaWKh6|7A*Z+JC+OSk~LxT$NQbZRch6B_chi+Juk6c&nl!nIQrTl@!x^Vn6ho2)@ExfiYFA<&pP|s4-X|1% z66KEA%i3~v=2S@6RZi~407wZcK4+r*SJOAppuVQhV8Pp$I7c6u0_0N%!gB)@CTrN@LuNiF z37I{(wbf*`>2Zem`*sPvLWVfr-TG^HfbjN5VF~XkwsJT+lXc)WHX3vx!BU>nl>&WkNtd1J@bB}AVx@gRZR4uJ)5;piYaP=`zi=gRBzf z^E5)d4vY@;_;oM2%4Eehe8jz;Rx`@yAUh(aJ0q;kU@{GsEc}0Al>Hw<54cH(6O4p` zLa<%4G?}Thh6Fjf#+Gr~LPVWpUkTVM1Q(PcrEZiWT90?_`b4;6l08m3BHI(plVKsRDYzf60nsyjtYE5`fyy}}n+RR4tNb2!)c zq(r?d)`c=eGePQLOkr{T^1Rn>>VLqbor`;sF5hX4K&~+(VxYO za`gXODU@-R+;-=&W0_GRgDMVOe1Y;o&SCu2P+Gb>i2{d#HaydUX=*El)*C1KHLp^& z#dB@v)k|a7m`k(~Dtg9jOAOQtD&38{j7`2bA#K-55wkxl9gfEh|M)sR#QUH3$RHqe zP;P)U5C|M&Yc-d(d_nnxm=IOZ$SGEw0{#Eb|F^*Z(*hZ;Pp|lg*W82duMPe~J@Ow_ KfaTIAf&T~kzod2m literal 0 HcmV?d00001 diff --git a/static/js/mission.js b/static/js/mission.js new file mode 100644 index 0000000..1e65bac --- /dev/null +++ b/static/js/mission.js @@ -0,0 +1,38 @@ +// mission.js + +document.addEventListener('DOMContentLoaded', function() { + const missionCards = document.querySelectorAll('.mission_card'); + + missionCards.forEach(card => { + card.addEventListener('click', function() { + const isActive = this.classList.contains('active'); + + // 다른 모든 카드 닫기 + missionCards.forEach(c => { + c.classList.remove('active'); + }); + + // 클릭한 카드 토글 + if (!isActive) { + this.classList.add('active'); + } + }); + + // 체크 아이콘 클릭 시 완료 처리 + const checkIcon = card.querySelector('.check_icon'); + checkIcon.addEventListener('click', function(e) { + e.stopPropagation(); // 카드 클릭 이벤트 방지 + + const card = this.closest('.mission_card'); + const isCompleted = card.classList.contains('completed'); + + if (isCompleted) { + card.classList.remove('completed'); + this.src = this.src.replace('check.png', 'nocheck.png'); + } else { + card.classList.add('completed'); + this.src = this.src.replace('nocheck.png', 'check.png'); + } + }); + }); +}); \ No newline at end of file diff --git a/templates/guides/mission.html b/templates/guides/mission.html index e69de29..e380eff 100644 --- a/templates/guides/mission.html +++ b/templates/guides/mission.html @@ -0,0 +1,113 @@ +{% extends 'base.html' %} {% load static %} {% block header %} + +{% endblock %} {% block content %} +

+

DASHBOARD

+

MISSION

+
+
+
+ rocket +
+

{서비스명}의 진척도

+

*파트별로 진행 속도를 맞추는 것을 권장드려요!

+
+
+
+

PM

+
+

+
+
+
+

FE

+
+

+
+
+
+

BE

+
+

+
+
+
+ +
+
+
+
+
+
+
+
+

Github repository 생성하기

+ 체크 +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+

Github repository에 팀원 초대하기

+ 체크 +
+
+

(팀트랙 or 유튜브 제목)

+ +
+
+
+
+ +
+
+
+
+
+
+
+
+

Github branch 파기

+ 체크 +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+

Github repository 생성하기

+ 체크 +
+
+ +
+
+
+
+ + + +{% endblock %} diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index eb202a9..af7b0c2 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -140,7 +140,7 @@

진척도

- + {% endblock %} \ No newline at end of file From a64df1f1e76512597143cf04fced6fbb5c3a8a15 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 14:39:51 +0900 Subject: [PATCH 148/380] =?UTF-8?q?docs:=20=EB=88=84=EB=9D=BD=EB=90=9C=20r?= =?UTF-8?q?equirements.txt=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 6e85486..2990e05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ asgiref==3.11.0 attrs==25.4.0 +bleach==6.3.0 certifi==2026.1.4 cffi==2.0.0 charset-normalizer==3.4.4 @@ -12,6 +13,7 @@ idna==3.11 inflection==0.5.1 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 +Markdown==3.10.1 pillow==12.1.0 psycopg2-binary==2.9.10 pycparser==3.0 @@ -27,3 +29,4 @@ typing_extensions==4.15.0 tzdata==2025.3 uritemplate==4.2.0 urllib3==2.6.3 +webencodings==0.5.1 From 155536efdf5f2d2556b42941af22480769a59c90 Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 14:47:57 +0900 Subject: [PATCH 149/380] =?UTF-8?q?docs:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=9D=B4=EC=8A=88=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b4cc3d1..9cc5ec7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,11 @@ +--- +name: 🐛 Bug Report +about: 기능 오류, API 에러, 로직 이상 등 버그 제보 +title: "[Bug] " +labels: bug +assignees: "" +--- + ## 📌 버그 설명 무엇이 잘못 동작하는지 간단히 작성 From b935df72ea3251f665936edc4e82a79386ff52f7 Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 14:49:45 +0900 Subject: [PATCH 150/380] =?UTF-8?q?fix:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EA=B0=80=20=EC=A0=91=EA=B7=BC=20=EC=95=88=20?= =?UTF-8?q?=EB=90=98=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 5cda072..0677a3b 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -154,7 +154,7 @@ def mypage(request): - 로그인한 사용자의 정보, 역할 레벨, 팀 프로젝트 참여 내역 등을 조회 - 'account/mypage.html' 템플릿을 렌더링 - - 프로젝트 내역은 team_members -> team -> project 경로로 조회한다. + - 프로젝트 내역은 team_memberships -> team -> project 경로로 조회한다. """ user = request.user @@ -162,9 +162,9 @@ def mypage(request): # 역할별 스킬 레벨 (user_role_levels + roles) role_levels = user.role_levels.select_related("role").all() - # 팀 프로젝트 참여 내역 (team_members + role + team + project) + # 팀 프로젝트 참여 내역 (team_memberships + role + team + project) memberships = ( - user.team_members + user.team_memberships .select_related("team__project", "role") .order_by("-joined_at") ) From 740446cc8f6e1b7d7fa4066f06c1a1c7f995731a Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 14:51:41 +0900 Subject: [PATCH 151/380] =?UTF-8?q?Chore:=20Asset=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98,=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20fo?= =?UTF-8?q?rms=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/forms.py | 12 --- .../migrations/0005_retrospectiveasset.py | 77 +++++++++++++++++++ apps/reflections/models.py | 74 ++++++++++++++++-- apps/reflections/views.py | 1 - 4 files changed, 146 insertions(+), 18 deletions(-) delete mode 100644 apps/reflections/forms.py create mode 100644 apps/reflections/migrations/0005_retrospectiveasset.py diff --git a/apps/reflections/forms.py b/apps/reflections/forms.py deleted file mode 100644 index 54fcd2f..0000000 --- a/apps/reflections/forms.py +++ /dev/null @@ -1,12 +0,0 @@ -# reflections/forms.py -from django import forms -from .models import Retrospective - -class RetrospectiveForm(forms.ModelForm): - class Meta: - model = Retrospective - fields = ["project", "title", "content_md", "bookmarked"] - widgets = { - "title": forms.TextInput(attrs={"placeholder": "제목"}), - "content_md": forms.Textarea(attrs={"rows": 16, "placeholder": "마크다운으로 작성하세요"}), - } diff --git a/apps/reflections/migrations/0005_retrospectiveasset.py b/apps/reflections/migrations/0005_retrospectiveasset.py new file mode 100644 index 0000000..76b41f2 --- /dev/null +++ b/apps/reflections/migrations/0005_retrospectiveasset.py @@ -0,0 +1,77 @@ +# Generated by Django 5.2.10 on 2026-02-06 05:36 + +import apps.reflections.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reflections", "0004_retrospective_answers_json_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="RetrospectiveAsset", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "image", + models.ImageField( + help_text="첨부 이미지", + upload_to=apps.reflections.models.retrospective_asset_upload_to, + ), + ), + ( + "alt_text", + models.CharField( + blank=True, + default="", + help_text="마크다운 이미지 alt 텍스트", + max_length=120, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "retrospective", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="assets", + to="reflections.retrospective", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="retrospective_assets", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "retrospective_assets", + "ordering": ["-created_at"], + "indexes": [ + models.Index( + fields=["retrospective", "created_at"], + name="retrospecti_retrosp_f6b2dc_idx", + ), + models.Index( + fields=["user", "created_at"], + name="retrospecti_user_id_382b28_idx", + ), + ], + }, + ), + ] diff --git a/apps/reflections/models.py b/apps/reflections/models.py index 60a596a..bc922f3 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -1,3 +1,7 @@ +# reflections/models.py +import os +import uuid + from django.conf import settings from django.db import models @@ -13,9 +17,8 @@ class Retrospective(models.Model): "projects.Project", on_delete=models.CASCADE, related_name="retrospectives", - # TODO 테스트용 nullable - # 플젝 외의 개인 회고의 목적 있으면 nullable 유지 - null=True, blank=True, + null=True, + blank=True, ) user = models.ForeignKey( @@ -52,8 +55,8 @@ class Retrospective(models.Model): ) bookmarked = models.BooleanField( - default= False, - help_text="찜 여부" + default=False, + help_text="찜 여부", ) created_at = models.DateTimeField(auto_now_add=True) @@ -69,3 +72,64 @@ class Meta: def __str__(self) -> str: return f"{self.user} - {self.project}: {self.title or '회고'}" + + +def retrospective_asset_upload_to(instance: "RetrospectiveAsset", filename: str) -> str: + """ + 저장 경로: + media/retrospectives///. + """ + _, ext = os.path.splitext(filename) + ext = (ext or "").lower() + return f"retrospectives/{instance.user_id}/{instance.retrospective_id}/{uuid.uuid4().hex}{ext}" + + +class RetrospectiveAsset(models.Model): + """ + 회고 첨부 이미지 + - 업로드 후 반환되는 image.url을 md 문법으로 본문에 삽입: ![alt](/media/...) + """ + + retrospective = models.ForeignKey( + Retrospective, + on_delete=models.CASCADE, + related_name="assets", + ) + + # 권한/조회 편의용 (중복이지만 실무에서 유용) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="retrospective_assets", + ) + + image = models.ImageField( + upload_to=retrospective_asset_upload_to, + help_text="첨부 이미지", + ) + + alt_text = models.CharField( + max_length=120, + blank=True, + default="", + help_text="마크다운 이미지 alt 텍스트", + ) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "retrospective_assets" + indexes = [ + models.Index(fields=["retrospective", "created_at"]), + models.Index(fields=["user", "created_at"]), + ] + ordering = ["-created_at"] + + def save(self, *args, **kwargs): + # retrospective.user와 항상 일치시키기 + if self.retrospective_id and (not self.user_id): + self.user = self.retrospective.user + super().save(*args, **kwargs) + + def __str__(self) -> str: + return f"asset:{self.id} retro:{self.retrospective_id} user:{self.user_id}" diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 0ab56fd..18654a3 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -16,7 +16,6 @@ from .models import Retrospective from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer -from .forms import RetrospectiveForm from .services.retrospective_guide import load_guide, build_markdown from apps.projects.models import Project From b73bf53f5f2b9f85fa65f61f584d224728b634aa Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 15:12:40 +0900 Subject: [PATCH 152/380] =?UTF-8?q?Feat:=20=ED=9A=8C=EA=B3=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + apps/reflections/serializers.py | 21 +++- .../templatetags/reflections_extras.py | 42 ++++++++ apps/reflections/views.py | 56 +++++++++- templates/reflections/_note_form.html | 102 +++++++++++++++--- templates/reflections/note_detail.html | 30 +----- 6 files changed, 208 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 7fd024b..fc055a5 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,8 @@ htmlcov/ # 기타 ############################ .cache/ +/media/ + *.pem kitup-key.pem diff --git a/apps/reflections/serializers.py b/apps/reflections/serializers.py index b7a853f..0b157d1 100644 --- a/apps/reflections/serializers.py +++ b/apps/reflections/serializers.py @@ -1,6 +1,6 @@ # reflections/serializers.py from rest_framework import serializers -from .models import Retrospective +from .models import Retrospective, RetrospectiveAsset from .services.retrospective_guide import load_guide, build_markdown @@ -83,4 +83,21 @@ def update(self, instance, validated_data): validated_data["content_md"] = self._rebuild_content_md( validated_data, template_key, answers_json, title ) - return super().update(instance, validated_data) \ No newline at end of file + return super().update(instance, validated_data) + +class RetrospectiveAssetUploadSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField() + md = serializers.SerializerMethodField() + + class Meta: + model = RetrospectiveAsset + fields = ["id", "alt_text", "image", "url", "md", "created_at"] + read_only_fields = ["id", "url", "md", "created_at"] + + def get_url(self, obj): + return obj.image.url if obj.image else "" + + def get_md(self, obj): + alt = obj.alt_text or "image" + url = self.get_url(obj) + return f"![{alt}]({url})" if url else "" \ No newline at end of file diff --git a/apps/reflections/templatetags/reflections_extras.py b/apps/reflections/templatetags/reflections_extras.py index 4a24185..4d39151 100644 --- a/apps/reflections/templatetags/reflections_extras.py +++ b/apps/reflections/templatetags/reflections_extras.py @@ -1,4 +1,8 @@ from django import template +from django.utils.safestring import mark_safe + +import markdown +import bleach register = template.Library() @@ -7,3 +11,41 @@ def get_item(d, key): if not d: return "" return d.get(key, "") + +@register.filter(name="md") +def md(value): + if not value: + return "" + + raw_html = markdown.markdown( + value, + extensions=[ + "fenced_code", # ``` 코드블록 + "tables", + "nl2br", # 줄바꿈 + ], + ) + + allowed_tags = bleach.sanitizer.ALLOWED_TAGS.union({ + "p","br","hr", + "h1","h2","h3","h4","h5","h6", + "pre","code","blockquote", + "ul","ol","li", + "strong","em", + "a","img", + }) + allowed_attrs = { + "a": ["href", "title", "rel", "target"], + "img": ["src", "alt", "title"], + "*": ["class"], + } + + cleaned = bleach.clean( + raw_html, + tags=allowed_tags, + attributes=allowed_attrs, + protocols=["http", "https", "data"], + strip=True, + ) + cleaned = bleach.linkify(cleaned) + return mark_safe(cleaned) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 18654a3..40a5c6a 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -4,6 +4,8 @@ from django.db.models import Q from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.exceptions import PermissionDenied, NotAuthenticated @@ -14,8 +16,12 @@ OpenApiTypes, ) -from .models import Retrospective -from .serializers import RetrospectiveReadSerializer, RetrospectiveWriteSerializer +from .models import Retrospective, RetrospectiveAsset +from .serializers import ( + RetrospectiveReadSerializer, + RetrospectiveWriteSerializer, + RetrospectiveAssetUploadSerializer, +) from .services.retrospective_guide import load_guide, build_markdown from apps.projects.models import Project @@ -346,4 +352,50 @@ def create(self, request, *args, **kwargs): obj = write.save(user=request.user) read = RetrospectiveReadSerializer(obj, context=self.get_serializer_context()) return Response(read.data, status=status.HTTP_201_CREATED) + + @extend_schema( + summary="회고 이미지 업로드", + tags=["Retrospectives"], + request={ + "multipart/form-data": { + "type": "object", + "properties": { + "image": {"type": "string", "format": "binary"}, + "alt_text": {"type": "string"}, + }, + "required": ["image"], + } + }, + responses={201: RetrospectiveAssetUploadSerializer}, + ) + @action( + detail=True, + methods=["post"], + url_path="assets", + parser_classes=[MultiPartParser, FormParser], + ) + def upload_asset(self, request, pk=None): + retro = self.get_object() # 여기서 본인 회고 체크됨 + + f = request.FILES.get("image") + if not f: + return Response({"detail": "image 파일이 필요합니다."}, status=400) + + # 간단한 이미지 타입 체크(추가 안전장치) + ct = (getattr(f, "content_type", "") or "").lower() + if ct and not ct.startswith("image/"): + return Response({"detail": "이미지 파일만 업로드 가능합니다."}, status=400) + + alt_text = (request.data.get("alt_text") or "").strip() + + asset = RetrospectiveAsset.objects.create( + retrospective=retro, + user=request.user, + image=f, + alt_text=alt_text, + ) + + data = RetrospectiveAssetUploadSerializer(asset, context=self.get_serializer_context()).data + return Response(data, status=status.HTTP_201_CREATED) + diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index c5866eb..5590096 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -48,37 +48,109 @@
-
- {{ q.order|default:forloop.counter }}. {{ q.title }} +
+
+ {{ q.order|default:forloop.counter }}. {{ q.title }} +
+ + {# ✅ note가 있을 때만 업로드 가능(생성 페이지는 저장 후 업로드) #} + {% if note %} + + {% else %} + 저장 후 업로드 + {% endif %}
+ {% if q.hint %}
{{ q.hint }}
{% endif %} - {% if q.examples %} - - 예시) - -
    - - {% for ex in q.examples %} -
  • {{ ex }}
  • - {% endfor %} -
- {% endif %} + + ...
{% endfor %} + {# ✅ 숨겨진 파일 인풋 1개만 두고 재사용 #} + {% if note %} + + + + {% endif %}
diff --git a/templates/reflections/note_detail.html b/templates/reflections/note_detail.html index 31e13f5..d04ac86 100644 --- a/templates/reflections/note_detail.html +++ b/templates/reflections/note_detail.html @@ -62,37 +62,15 @@
{% for q in guide.questions %}
- - -
-
- {{ q.order|default:forloop.counter }}. {{ q.title }} -
- - {% if q.hint %} -
- {{ q.hint }} -
- {% endif %} - - {% if q.examples %} -
    - {% for ex in q.examples %} -
  • {{ ex }}
  • - {% endfor %} -
- {% endif %} +
+ {{ q.order|default:forloop.counter }}. {{ q.title }}
-
- + {{ answers|get_item:q.id|md }}
{% endfor %} +
From 4deb82bb88079e427e585ef7b721e9e5d4f0b840 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 17:53:51 +0900 Subject: [PATCH 153/380] =?UTF-8?q?feat=20:=20mission=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mission.css | 52 +++++++++++---- static/images/check.png | Bin 0 -> 2199 bytes static/images/nocheck.png | Bin 0 -> 3614 bytes static/images/now_check.png | Bin 0 -> 2510 bytes static/images/now_nocheck.png | Bin 0 -> 4102 bytes static/js/mission.js | 18 ++++-- templates/guides/mission.html | 115 +++++++++++++++++----------------- 7 files changed, 111 insertions(+), 74 deletions(-) create mode 100644 static/images/check.png create mode 100644 static/images/nocheck.png create mode 100644 static/images/now_check.png create mode 100644 static/images/now_nocheck.png diff --git a/static/css/mission.css b/static/css/mission.css index 4673137..7a4af01 100644 --- a/static/css/mission.css +++ b/static/css/mission.css @@ -41,7 +41,7 @@ background: #eaf0ff; width: 90%; height: 350px; padding: 50px 20px; - margin: 0 auto; + margin: 0 auto 80px; } .mp_title { @@ -81,6 +81,7 @@ background: #DDDDDD; border-radius: 20px; } + .pm_progress > .pm_bar > .pm_real { position: absolute; top: 0; left: 0; @@ -145,36 +146,64 @@ /* 미션 아이템 */ .mission_item { + width: 90%; + margin: 0 auto; display: flex; gap: 20px; - margin-bottom: 20px; } .timeline_dot { display: flex; flex-direction: column; align-items: center; - width: 60px; + width: 80px; flex-shrink: 0; } .circle { - width: 20px; - height: 20px; - background: #4272EF; + width: 30px; height: 30px; + background: #EAF0FF; border-radius: 50%; - border: 4px solid #fff; - box-shadow: 0 0 0 2px #4272EF; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); z-index: 5; position: relative; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; + color: #fff; + transition: all 0.3s ease; +} + +.mission_item.active .circle { + background: #4272EF; + color: #fff; + box-shadow: 0 4px 12px rgba(66, 114, 239, 0.4); + transform: scale(1.1); +} + +.mission_item.completed .circle { + background: #4272EF; + color: #fff; + box-shadow: 0 4px 12px rgba(66, 114, 239, 0.4); } .line { width: 3px; height: 100%; - background: #4272EF; + background: #EAF0FF; flex: 1; - margin-top: -2px; + margin-top: -10px; + transition: background 0.3s ease; +} + +.mission_item.completed .line { + background: #4272EF; +} + +.mission_item:last-child .line { + display: none; } .mission_item:last-child .line { @@ -192,6 +221,7 @@ background: #F8F9FF; border: 2px solid #E0E7FF; border-radius: 20px; + margin-bottom: 25px; padding: 20px 25px; cursor: pointer; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); @@ -226,7 +256,7 @@ transform: scale(1.1); } -/* 카드 내용 (기본 숨김) */ +/* 카드 내용 */ .card_content { max-height: 0; overflow: hidden; diff --git a/static/images/check.png b/static/images/check.png new file mode 100644 index 0000000000000000000000000000000000000000..450598dc8fd20c06e577bf50126215d8589d33ce GIT binary patch literal 2199 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 literal 0 HcmV?d00001 diff --git a/static/images/now_check.png b/static/images/now_check.png new file mode 100644 index 0000000000000000000000000000000000000000..6ab84093574541f828d640e35942008fe5c05c4a GIT binary patch literal 2510 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 Y0E`iD{-qI6%K!iX07*qoM6N<$f)4?I2mk;8 literal 0 HcmV?d00001 diff --git a/static/images/now_nocheck.png b/static/images/now_nocheck.png new file mode 100644 index 0000000000000000000000000000000000000000..88516a2856dc63eca30a41971912355062535049 GIT binary patch literal 4102 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 literal 0 HcmV?d00001 diff --git a/static/js/mission.js b/static/js/mission.js index 1e65bac..992a159 100644 --- a/static/js/mission.js +++ b/static/js/mission.js @@ -1,4 +1,4 @@ -// mission.js +// mission.js - 수정된 버전 document.addEventListener('DOMContentLoaded', function() { const missionCards = document.querySelectorAll('.mission_card'); @@ -6,31 +6,39 @@ document.addEventListener('DOMContentLoaded', function() { missionCards.forEach(card => { card.addEventListener('click', function() { const isActive = this.classList.contains('active'); + const missionItem = this.closest('.mission_item'); - // 다른 모든 카드 닫기 - missionCards.forEach(c => { + // 다른 모든 카드와 mission_item에서 active 제거 + document.querySelectorAll('.mission_card').forEach(c => { c.classList.remove('active'); }); + document.querySelectorAll('.mission_item').forEach(item => { + item.classList.remove('active'); + }); - // 클릭한 카드 토글 + // 클릭한 카드와 mission_item에 active 추가 if (!isActive) { this.classList.add('active'); + missionItem.classList.add('active'); } }); // 체크 아이콘 클릭 시 완료 처리 const checkIcon = card.querySelector('.check_icon'); checkIcon.addEventListener('click', function(e) { - e.stopPropagation(); // 카드 클릭 이벤트 방지 + e.stopPropagation(); const card = this.closest('.mission_card'); + const missionItem = card.closest('.mission_item'); const isCompleted = card.classList.contains('completed'); if (isCompleted) { card.classList.remove('completed'); + missionItem.classList.remove('completed'); this.src = this.src.replace('check.png', 'nocheck.png'); } else { card.classList.add('completed'); + missionItem.classList.add('completed'); this.src = this.src.replace('nocheck.png', 'check.png'); } }); diff --git a/templates/guides/mission.html b/templates/guides/mission.html index e380eff..955b96f 100644 --- a/templates/guides/mission.html +++ b/templates/guides/mission.html @@ -34,80 +34,79 @@

BE

- -
-
-
-
-
-
-
-
-

Github repository 생성하기

- 체크 -
-
- -
+
+
+
1
+
+
+
+
+
+

Github repository 생성하기

+ 체크 +
+
+
+
-
-
-
-
-
-
-
-
-

Github repository에 팀원 초대하기

- 체크 -
-
-

(팀트랙 or 유튜브 제목)

- -
+
+
+
2
+
+
+
+
+
+

Github repository에 팀원 초대하기

+ 체크 +
+
+

(팀트랙 or 유튜브 제목)

+
+
-
-
-
-
-
-
-
-
-

Github branch 파기

- 체크 -
-
- -
+
+
+
3
+
+
+
+
+
+

Github branch 파기

+ 체크 +
+
+
+
-
-
-
-
-
-
-
-
-

Github repository 생성하기

- 체크 -
-
- -
+
+
+
4
+
+
+
+
+
+

Github repository 생성하기

+ 체크 +
+
+
+
{% endblock %} From 2f56ee38def192ac2b49c6f440769959da8a2945 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 17:55:32 +0900 Subject: [PATCH 154/380] =?UTF-8?q?fix=20:=20mission.css=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mission.css | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/static/css/mission.css b/static/css/mission.css index 7a4af01..55c9b6a 100644 --- a/static/css/mission.css +++ b/static/css/mission.css @@ -144,7 +144,7 @@ border-radius: 20px; } -/* 미션 아이템 */ +/* 카드 */ .mission_item { width: 90%; margin: 0 auto; @@ -303,11 +303,6 @@ margin-top: 15px; } -/* 완료된 카드 */ -.mission_card.completed .check_icon { - /* check.png로 변경될 예정 */ -} - .mission_card.completed { opacity: 0.8; } \ No newline at end of file From d64acb44132996a58e019dfe39e9589772947765 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 18:14:20 +0900 Subject: [PATCH 155/380] =?UTF-8?q?Feat:=20=ED=9A=8C=EA=B3=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20+=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20HTML=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/apps.py | 3 ++ apps/reflections/signals.py | 9 ++++ apps/reflections/views.py | 17 +++++++ templates/reflections/_note_form.html | 69 +++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 apps/reflections/signals.py diff --git a/apps/reflections/apps.py b/apps/reflections/apps.py index 5595539..86ab761 100644 --- a/apps/reflections/apps.py +++ b/apps/reflections/apps.py @@ -5,3 +5,6 @@ class ReflectionsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.reflections" label = "reflections" + + def ready(self): + from . import signals # noqa \ No newline at end of file diff --git a/apps/reflections/signals.py b/apps/reflections/signals.py new file mode 100644 index 0000000..a775680 --- /dev/null +++ b/apps/reflections/signals.py @@ -0,0 +1,9 @@ +# reflections/signals.py +from django.db.models.signals import post_delete +from django.dispatch import receiver +from .models import RetrospectiveAsset + +@receiver(post_delete, sender=RetrospectiveAsset) +def delete_asset_file(sender, instance, **kwargs): + if instance.image: + instance.image.delete(save=False) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 40a5c6a..3a25e86 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -398,4 +398,21 @@ def upload_asset(self, request, pk=None): data = RetrospectiveAssetUploadSerializer(asset, context=self.get_serializer_context()).data return Response(data, status=status.HTTP_201_CREATED) + @extend_schema( + summary="회고 이미지 삭제", + tags=["Retrospectives"] + ) + @action(detail=True, methods=["delete"], url_path=r"assets/(?P\d+)") + def delete_asset(self, request, pk=None, asset_id=None): + retro = self.get_object() # 본인 회고인지 포함해서 체크된다고 가정 + + asset = RetrospectiveAsset.objects.filter( + id=asset_id, + retrospective=retro, + user=request.user, + ).first() + if not asset: + return Response({"detail": "asset not found"}, status=404) + asset.delete() # ✅ 여기서 DB 삭제 + (아래 시그널/오버라이드 있으면 파일도 삭제) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index 5590096..5d6e762 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -167,6 +167,73 @@

+ + {% if note and note.assets.all %} +
+
업로드된 이미지
+ + {% for asset in note.assets.all %} +
+
+ {{ asset.alt_text|default:'image' }} + +
+ {{ asset.id }} / {{ asset.image.name|cut:"retrospectives/"|cut:"/" }} +
+
+ + + +
+ {% endfor %} +
+ + {% endif %} +
@@ -179,4 +246,6 @@ 저장
+
+ From 1d0d53c1e4d9d5a45a8388dd5ae1be4f00a4fb0b Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 18:22:36 +0900 Subject: [PATCH 156/380] =?UTF-8?q?feat:=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=98=88=EC=8B=9C=20json=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/fixtures/guides.json | 266 +++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 apps/guides/fixtures/guides.json diff --git a/apps/guides/fixtures/guides.json b/apps/guides/fixtures/guides.json new file mode 100644 index 0000000..ee51672 --- /dev/null +++ b/apps/guides/fixtures/guides.json @@ -0,0 +1,266 @@ +[ + { + "model": "guides.GuideCard", + "pk": 1, + "fields": { + "role": "PM", + "title": "1단계: 팀 소개 및 역할 이해", + "content_md": "# PM의 역할\n\nPM(Product Manager)은 제품의 방향성을 결정하고 팀을 이끌어가는 역할입니다.\n\n## 주요 책임\n- 제품 기획 및 정의\n- 사용자 요구사항 수집\n- 팀 조율 및 의사결정\n\n이 미션에서 팀의 비전과 목표를 함께 정의해봅시다.", + "order_no": 1, + "is_active": true + } + }, + { + "model": "guides.GuideCard", + "pk": 2, + "fields": { + "role": "PM", + "title": "2단계: 요구사항 정의 및 기획", + "content_md": "# 요구사항 정의\n\n프로젝트의 핵심 요구사항을 정의하는 단계입니다.\n\n## 작업 내용\n1. 사용자 페르소나 정의\n2. 주요 기능 목록 작성\n3. 우선순위 결정\n\n팀과 함께 논의하여 요구사항을 문서화합니다.", + "order_no": 2, + "is_active": true + } + }, + { + "model": "guides.GuideCard", + "pk": 3, + "fields": { + "role": "FRONTEND", + "title": "1단계: 개발 환경 설정", + "content_md": "# 프론트엔드 개발 환경\n\n프로젝트 개발을 위한 기본 환경을 설정합니다.\n\n## 설정 사항\n- Node.js 설치 (v18+)\n- npm 패키지 초기화\n- 필요한 라이브러리 설치\n- 프로젝트 구조 생성\n\n팀의 코딩 스탠다드와 개발 규칙을 확인합니다.", + "order_no": 1, + "is_active": true + } + }, + { + "model": "guides.GuideCard", + "pk": 4, + "fields": { + "role": "FRONTEND", + "title": "2단계: 페이지 레이아웃 구현", + "content_md": "# 페이지 레이아웃 구현\n\nUI 디자인을 기반으로 HTML/CSS 레이아웃을 구현합니다.\n\n## 구현 범위\n- 반응형 디자인 적용\n- 기본 스타일링\n- 컴포넌트 구조 설계\n\nPM이 제공한 디자인 스펙을 참고하여 진행합니다.", + "order_no": 2, + "is_active": true + } + }, + { + "model": "guides.GuideCard", + "pk": 5, + "fields": { + "role": "BACKEND", + "title": "1단계: API 설계 및 데이터베이스 구성", + "content_md": "# 백엔드 API 설계\n\n프로젝트의 핵심 API와 데이터베이스를 설계합니다.\n\n## 설계 사항\n- REST API 엔드포인트 정의\n- 데이터베이스 스키마 설계\n- 인증/인가 체계 구축\n- API 문서 작성 (OpenAPI/Swagger)\n\nPM의 요구사항을 기반으로 설계합니다.", + "order_no": 1, + "is_active": true + } + }, + { + "model": "guides.GuideCard", + "pk": 6, + "fields": { + "role": "BACKEND", + "title": "2단계: 핵심 기능 구현", + "content_md": "# 핵심 기능 구현\n\nAPI 설계를 기반으로 실제 기능을 구현합니다.\n\n## 구현 범위\n- 데이터 모델 생성\n- API 엔드포인트 구현\n- 비즈니스 로직 개발\n- 에러 핸들링\n- 테스트 작성\n\n설계된 스펙을 정확하게 구현합니다.", + "order_no": 2, + "is_active": true + } + }, + { + "model": "guides.GuideTask", + "pk": 1, + "fields": { + "card": 1, + "title": "팀 소개 영상 시청", + "description": "팀 협업의 중요성에 대한 10분 영상을 시청합니다.", + "order_no": 1, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 2, + "fields": { + "card": 1, + "title": "팀 목표 논의", + "description": "팀 회의를 통해 프로젝트 목표를 함께 정의합니다.", + "order_no": 2, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 3, + "fields": { + "card": 1, + "title": "역할 할당 및 책임 정의", + "description": "각 팀원의 역할을 명확히 하고 책임을 정의합니다.", + "order_no": 3, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 4, + "fields": { + "card": 2, + "title": "사용자 페르소나 3개 작성", + "description": "프로젝트의 주요 사용자 3명의 페르소나를 정의합니다.", + "order_no": 1, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 5, + "fields": { + "card": 2, + "title": "기능 목록 20개 이상 작성", + "description": "구현할 기능들을 우선순위와 함께 정리합니다.", + "order_no": 2, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 6, + "fields": { + "card": 2, + "title": "1차 릴리스 기능 확정", + "description": "1차 릴리스에 포함될 핵심 기능 5-7개를 확정합니다.", + "order_no": 3, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 7, + "fields": { + "card": 3, + "title": "Node.js 설치", + "description": "공식 사이트에서 Node.js 18 이상을 설치합니다.", + "order_no": 1, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 8, + "fields": { + "card": 3, + "title": "프로젝트 디렉토리 초기화", + "description": "npm init을 통해 package.json 파일을 생성합니다.", + "order_no": 2, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 9, + "fields": { + "card": 3, + "title": "필수 라이브러리 설치", + "description": "React, ESLint, Prettier 등 필수 라이브러리를 설치합니다.", + "order_no": 3, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 10, + "fields": { + "card": 4, + "title": "Header/Footer 컴포넌트 구현", + "description": "모든 페이지에 공통으로 사용될 Header와 Footer를 구현합니다.", + "order_no": 1, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 11, + "fields": { + "card": 4, + "title": "메인 페이지 레이아웃 구현", + "description": "프로젝트의 메인 페이지 레이아웃을 구현합니다.", + "order_no": 2, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 12, + "fields": { + "card": 4, + "title": "모바일 반응형 테스트", + "description": "구현한 페이지가 모바일에서도 제대로 표시되는지 테스트합니다.", + "order_no": 3, + "is_required": false + } + }, + { + "model": "guides.GuideTask", + "pk": 13, + "fields": { + "card": 5, + "title": "데이터베이스 스키마 설계", + "description": "ERD를 그려 데이터베이스 스키마를 설계합니다.", + "order_no": 1, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 14, + "fields": { + "card": 5, + "title": "API 명세서 작성", + "description": "OpenAPI/Swagger 형식으로 API 명세서를 작성합니다.", + "order_no": 2, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 15, + "fields": { + "card": 5, + "title": "인증 시스템 설계", + "description": "JWT 또는 세션 기반 인증 시스템을 설계합니다.", + "order_no": 3, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 16, + "fields": { + "card": 6, + "title": "데이터 모델 구현", + "description": "설계된 스키마를 기반으로 Django 모델을 구현합니다.", + "order_no": 1, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 17, + "fields": { + "card": 6, + "title": "핵심 API 3개 구현", + "description": "가장 중요한 3개의 API 엔드포인트를 구현합니다.", + "order_no": 2, + "is_required": true + } + }, + { + "model": "guides.GuideTask", + "pk": 18, + "fields": { + "card": 6, + "title": "API 테스트 작성", + "description": "구현한 API에 대한 단위 테스트를 작성합니다.", + "order_no": 3, + "is_required": false + } + } +] From c5de531514343965da84b0fc527907cb650c0846 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Fri, 6 Feb 2026 18:27:17 +0900 Subject: [PATCH 157/380] =?UTF-8?q?feat:=20yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ce46934..1b2af42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 From 83ecbe734e4ff456e4d11225be7a23b025bd4710 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Fri, 6 Feb 2026 18:31:30 +0900 Subject: [PATCH 158/380] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1b2af42..91da7a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,9 +14,9 @@ services: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 5s timeout: 5s retries: 5 From 52e6bb30a835fe629a7a02df84da9ded45418364 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Fri, 6 Feb 2026 18:35:03 +0900 Subject: [PATCH 159/380] =?UTF-8?q?feat:=20compose.yml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 91da7a4..8638548 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" - healthcheck: + healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: 5s timeout: 5s From 3a81b5866a0f87efd040ca12b016e91fac438a62 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Fri, 6 Feb 2026 18:44:21 +0900 Subject: [PATCH 160/380] =?UTF-8?q?feat:=20compose.yml=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8638548..c9d9f84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,22 +7,21 @@ services: env_file: - .env environment: - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB:-kitup_db} + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-your_password} volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready"] + test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 web: image: ghcr.io/pirogramming/startlinedev/web:latest - build: . container_name: kitup_web env_file: - .env @@ -40,9 +39,9 @@ services: - "8000:8000" environment: DB_ENGINE: django.db.backends.postgresql - DB_NAME: ${POSTGRES_DB} - DB_USER: ${POSTGRES_USER} - DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB:-kitup_db} + DB_USER: ${POSTGRES_USER:-postgres} + DB_PASSWORD: ${POSTGRES_PASSWORD:-your_password} DB_HOST: db DB_PORT: 5432 REDIS_HOST: redis From d8ed769843edf847635acf0adb093851952601bb Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 18:46:00 +0900 Subject: [PATCH 161/380] =?UTF-8?q?feat:=20guide=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=97=90=20update=5Fat=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0004_guidetask_updated_at.py | 18 ++++++++++++++++++ apps/guides/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 apps/guides/migrations/0004_guidetask_updated_at.py diff --git a/apps/guides/migrations/0004_guidetask_updated_at.py b/apps/guides/migrations/0004_guidetask_updated_at.py new file mode 100644 index 0000000..efbd489 --- /dev/null +++ b/apps/guides/migrations/0004_guidetask_updated_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-02-06 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('guides', '0003_projectprogress_remove_guidecard_stage_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='guidetask', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/apps/guides/models.py b/apps/guides/models.py index b444134..349841c 100644 --- a/apps/guides/models.py +++ b/apps/guides/models.py @@ -99,6 +99,7 @@ class GuideTask(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "guide_tasks" From 94f9c89dd743f45e346508cbfa4382de4fa4bdbd Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 18:46:17 +0900 Subject: [PATCH 162/380] =?UTF-8?q?feat:=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=B3=B5=EC=82=AC=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EB=B6=81=EB=A7=88=ED=81=AC=20=ED=86=A0=EA=B8=80?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(=EC=9E=84=EC=8B=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=84=ED=91=9C=EB=A1=9C=20=EA=B5=AC=ED=98=84,=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B3=80=ED=99=94=20=EC=97=86=EC=9D=8C=20?= =?UTF-8?q?->=20=EC=BD=98=EC=86=94=20=ED=99=95=EC=9D=B8=ED=95=B4=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/reflections/note_detail.html | 50 +++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/templates/reflections/note_detail.html b/templates/reflections/note_detail.html index d04ac86..111e608 100644 --- a/templates/reflections/note_detail.html +++ b/templates/reflections/note_detail.html @@ -36,6 +36,14 @@ style="padding:10px 12px; border:1px solid #111827; border-radius:12px; text-decoration:none; color:#fff; background:#111827;"> 수정하기 + +
@@ -70,7 +78,6 @@
{% endfor %} -
@@ -80,12 +87,53 @@
+
+ +
+ {% endblock %} From b8383dfc498cd840f16329648fcb44b48667c1da Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 18:46:30 +0900 Subject: [PATCH 163/380] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20guide=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/fixtures/guides.json | 204 +++++++++++++++++++------------ 1 file changed, 126 insertions(+), 78 deletions(-) diff --git a/apps/guides/fixtures/guides.json b/apps/guides/fixtures/guides.json index ee51672..ba13690 100644 --- a/apps/guides/fixtures/guides.json +++ b/apps/guides/fixtures/guides.json @@ -3,66 +3,78 @@ "model": "guides.GuideCard", "pk": 1, "fields": { - "role": "PM", - "title": "1단계: 팀 소개 및 역할 이해", - "content_md": "# PM의 역할\n\nPM(Product Manager)은 제품의 방향성을 결정하고 팀을 이끌어가는 역할입니다.\n\n## 주요 책임\n- 제품 기획 및 정의\n- 사용자 요구사항 수집\n- 팀 조율 및 의사결정\n\n이 미션에서 팀의 비전과 목표를 함께 정의해봅시다.", + "role": 1, + "title": "1단계: 팀 소개", + "content_md": "# PM의 역할\n\nPM(Product Manager)은 제품의 방향성을 결정하고 팀을 이끌어가는 역할입니다.", "order_no": 1, - "is_active": true + "is_active": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { "model": "guides.GuideCard", "pk": 2, "fields": { - "role": "PM", - "title": "2단계: 요구사항 정의 및 기획", - "content_md": "# 요구사항 정의\n\n프로젝트의 핵심 요구사항을 정의하는 단계입니다.\n\n## 작업 내용\n1. 사용자 페르소나 정의\n2. 주요 기능 목록 작성\n3. 우선순위 결정\n\n팀과 함께 논의하여 요구사항을 문서화합니다.", + "role": 1, + "title": "2단계: 요구사항", + "content_md": "# 요구사항 정의\n\n프로젝트의 핵심 요구사항을 정의하는 단계입니다.", "order_no": 2, - "is_active": true + "is_active": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { "model": "guides.GuideCard", "pk": 3, "fields": { - "role": "FRONTEND", - "title": "1단계: 개발 환경 설정", - "content_md": "# 프론트엔드 개발 환경\n\n프로젝트 개발을 위한 기본 환경을 설정합니다.\n\n## 설정 사항\n- Node.js 설치 (v18+)\n- npm 패키지 초기화\n- 필요한 라이브러리 설치\n- 프로젝트 구조 생성\n\n팀의 코딩 스탠다드와 개발 규칙을 확인합니다.", + "role": 2, + "title": "1단계: 개발환경", + "content_md": "# 프론트엔드 개발 환경\n\n프로젝트 개발을 위한 기본 환경을 설정합니다.", "order_no": 1, - "is_active": true + "is_active": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { "model": "guides.GuideCard", "pk": 4, "fields": { - "role": "FRONTEND", - "title": "2단계: 페이지 레이아웃 구현", - "content_md": "# 페이지 레이아웃 구현\n\nUI 디자인을 기반으로 HTML/CSS 레이아웃을 구현합니다.\n\n## 구현 범위\n- 반응형 디자인 적용\n- 기본 스타일링\n- 컴포넌트 구조 설계\n\nPM이 제공한 디자인 스펙을 참고하여 진행합니다.", + "role": 2, + "title": "2단계: 레이아웃", + "content_md": "# 페이지 레이아웃 구현\n\nUI 디자인을 기반으로 HTML/CSS 레이아웃을 구현합니다.", "order_no": 2, - "is_active": true + "is_active": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { "model": "guides.GuideCard", "pk": 5, "fields": { - "role": "BACKEND", - "title": "1단계: API 설계 및 데이터베이스 구성", - "content_md": "# 백엔드 API 설계\n\n프로젝트의 핵심 API와 데이터베이스를 설계합니다.\n\n## 설계 사항\n- REST API 엔드포인트 정의\n- 데이터베이스 스키마 설계\n- 인증/인가 체계 구축\n- API 문서 작성 (OpenAPI/Swagger)\n\nPM의 요구사항을 기반으로 설계합니다.", + "role": 3, + "title": "1단계: API설계", + "content_md": "# 백엔드 API 설계\n\n프로젝트의 핵심 API와 데이터베이스를 설계합니다.", "order_no": 1, - "is_active": true + "is_active": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { "model": "guides.GuideCard", "pk": 6, "fields": { - "role": "BACKEND", - "title": "2단계: 핵심 기능 구현", - "content_md": "# 핵심 기능 구현\n\nAPI 설계를 기반으로 실제 기능을 구현합니다.\n\n## 구현 범위\n- 데이터 모델 생성\n- API 엔드포인트 구현\n- 비즈니스 로직 개발\n- 에러 핸들링\n- 테스트 작성\n\n설계된 스펙을 정확하게 구현합니다.", + "role": 3, + "title": "2단계: 기능구현", + "content_md": "# 핵심 기능 구현\n\nAPI 설계를 기반으로 실제 기능을 구현합니다.", "order_no": 2, - "is_active": true + "is_active": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -70,10 +82,12 @@ "pk": 1, "fields": { "card": 1, - "title": "팀 소개 영상 시청", - "description": "팀 협업의 중요성에 대한 10분 영상을 시청합니다.", + "title": "영상시청", + "description": "팀 소개 영상 시청", "order_no": 1, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -81,10 +95,12 @@ "pk": 2, "fields": { "card": 1, - "title": "팀 목표 논의", - "description": "팀 회의를 통해 프로젝트 목표를 함께 정의합니다.", + "title": "팀목표논의", + "description": "팀 회의 진행", "order_no": 2, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -92,10 +108,12 @@ "pk": 3, "fields": { "card": 1, - "title": "역할 할당 및 책임 정의", - "description": "각 팀원의 역할을 명확히 하고 책임을 정의합니다.", + "title": "역할정의", + "description": "역할 할당", "order_no": 3, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -103,10 +121,12 @@ "pk": 4, "fields": { "card": 2, - "title": "사용자 페르소나 3개 작성", - "description": "프로젝트의 주요 사용자 3명의 페르소나를 정의합니다.", + "title": "페르소나작성", + "description": "사용자 페르소나", "order_no": 1, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -114,10 +134,12 @@ "pk": 5, "fields": { "card": 2, - "title": "기능 목록 20개 이상 작성", - "description": "구현할 기능들을 우선순위와 함께 정리합니다.", + "title": "기능목록", + "description": "기능 목록 작성", "order_no": 2, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -125,10 +147,12 @@ "pk": 6, "fields": { "card": 2, - "title": "1차 릴리스 기능 확정", - "description": "1차 릴리스에 포함될 핵심 기능 5-7개를 확정합니다.", + "title": "1차기능확정", + "description": "핵심 기능 확정", "order_no": 3, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -136,10 +160,12 @@ "pk": 7, "fields": { "card": 3, - "title": "Node.js 설치", - "description": "공식 사이트에서 Node.js 18 이상을 설치합니다.", + "title": "Node설치", + "description": "Node.js 설치", "order_no": 1, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -147,10 +173,12 @@ "pk": 8, "fields": { "card": 3, - "title": "프로젝트 디렉토리 초기화", - "description": "npm init을 통해 package.json 파일을 생성합니다.", + "title": "프로젝트초기화", + "description": "npm init 진행", "order_no": 2, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -158,10 +186,12 @@ "pk": 9, "fields": { "card": 3, - "title": "필수 라이브러리 설치", - "description": "React, ESLint, Prettier 등 필수 라이브러리를 설치합니다.", + "title": "라이브러리설치", + "description": "필수 라이브러리", "order_no": 3, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -169,10 +199,12 @@ "pk": 10, "fields": { "card": 4, - "title": "Header/Footer 컴포넌트 구현", - "description": "모든 페이지에 공통으로 사용될 Header와 Footer를 구현합니다.", + "title": "Header/Footer", + "description": "공통 컴포넌트", "order_no": 1, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -180,10 +212,12 @@ "pk": 11, "fields": { "card": 4, - "title": "메인 페이지 레이아웃 구현", - "description": "프로젝트의 메인 페이지 레이아웃을 구현합니다.", + "title": "메인페이지", + "description": "메인 레이아웃", "order_no": 2, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -191,10 +225,12 @@ "pk": 12, "fields": { "card": 4, - "title": "모바일 반응형 테스트", - "description": "구현한 페이지가 모바일에서도 제대로 표시되는지 테스트합니다.", + "title": "반응형테스트", + "description": "모바일 테스트", "order_no": 3, - "is_required": false + "is_required": false, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -202,10 +238,12 @@ "pk": 13, "fields": { "card": 5, - "title": "데이터베이스 스키마 설계", - "description": "ERD를 그려 데이터베이스 스키마를 설계합니다.", + "title": "DB스키마", + "description": "스키마 설계", "order_no": 1, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -213,10 +251,12 @@ "pk": 14, "fields": { "card": 5, - "title": "API 명세서 작성", - "description": "OpenAPI/Swagger 형식으로 API 명세서를 작성합니다.", + "title": "API명세서", + "description": "API 문서 작성", "order_no": 2, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -224,10 +264,12 @@ "pk": 15, "fields": { "card": 5, - "title": "인증 시스템 설계", - "description": "JWT 또는 세션 기반 인증 시스템을 설계합니다.", + "title": "인증시스템", + "description": "인증 설계", "order_no": 3, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -235,10 +277,12 @@ "pk": 16, "fields": { "card": 6, - "title": "데이터 모델 구현", - "description": "설계된 스키마를 기반으로 Django 모델을 구현합니다.", + "title": "모델구현", + "description": "Django 모델", "order_no": 1, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -246,10 +290,12 @@ "pk": 17, "fields": { "card": 6, - "title": "핵심 API 3개 구현", - "description": "가장 중요한 3개의 API 엔드포인트를 구현합니다.", + "title": "API구현", + "description": "API 엔드포인트", "order_no": 2, - "is_required": true + "is_required": true, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } }, { @@ -257,10 +303,12 @@ "pk": 18, "fields": { "card": 6, - "title": "API 테스트 작성", - "description": "구현한 API에 대한 단위 테스트를 작성합니다.", + "title": "테스트작성", + "description": "단위 테스트", "order_no": 3, - "is_required": false + "is_required": false, + "created_at": "2026-02-06T00:00:00Z", + "updated_at": "2026-02-06T00:00:00Z" } } ] From a91c8deb02bb9b8fb918101d08c339019564e4d4 Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 18:47:12 +0900 Subject: [PATCH 164/380] =?UTF-8?q?feat:=20guide=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20import=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/admin.py | 2 +- .../guides/management/commands/load_guides.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 apps/guides/management/commands/load_guides.py diff --git a/apps/guides/admin.py b/apps/guides/admin.py index 7e962f7..2629201 100644 --- a/apps/guides/admin.py +++ b/apps/guides/admin.py @@ -6,7 +6,7 @@ class GuideTaskInline(admin.TabularInline): model = GuideTask extra = 0 - fields = ["title", "order_no", "is_required"] + fields = ["title", "description", "order_no", "is_required"] @admin.register(GuideCard) diff --git a/apps/guides/management/commands/load_guides.py b/apps/guides/management/commands/load_guides.py new file mode 100644 index 0000000..e8e7a7c --- /dev/null +++ b/apps/guides/management/commands/load_guides.py @@ -0,0 +1,31 @@ +import json +from django.core.management.base import BaseCommand +from django.core.management import call_command + + +class Command(BaseCommand): + help = 'JSON 파일에서 가이드 데이터를 로드합니다' + + def add_arguments(self, parser): + parser.add_argument( + '--reset', + action='store_true', + help='기존 가이드 데이터를 먼저 삭제합니다', + ) + + def handle(self, *args, **options): + # 기존 데이터 삭제 옵션 + if options['reset']: + from apps.guides.models import GuideCard, GuideTask + GuideCard.objects.all().delete() + GuideTask.objects.all().delete() + self.stdout.write(self.style.WARNING('기존 가이드 데이터를 삭제했습니다')) + + # fixture 로드 + try: + call_command('loaddata', 'guides') + self.stdout.write( + self.style.SUCCESS('✅ 가이드 데이터를 성공적으로 로드했습니다!') + ) + except Exception as e: + self.stdout.write(self.style.ERROR(f'❌ 오류: {e}')) From 63e6d027e285daf0ac4f04f8d8b05f554f6d2229 Mon Sep 17 00:00:00 2001 From: knana6 Date: Fri, 6 Feb 2026 19:05:55 +0900 Subject: [PATCH 165/380] feat: level_test --- static/css/level_test.css | 263 +++++++++++++++ templates/account/level_test.html | 534 ++++++++++++++++++++++++++++++ 2 files changed, 797 insertions(+) create mode 100644 static/css/level_test.css diff --git a/static/css/level_test.css b/static/css/level_test.css new file mode 100644 index 0000000..b3fb4e3 --- /dev/null +++ b/static/css/level_test.css @@ -0,0 +1,263 @@ +/* level_test.css */ + +.level-test-container { + max-width: 800px; + margin: 0 auto; + padding: 40px 20px; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif; +} + +/* 문항 화면 */ +.questions-screen { + background: #fff; + border-radius: 16px; + padding: 40px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.progress-bar { + width: 100%; + height: 8px; + background: #e5e7eb; + border-radius: 4px; + margin-bottom: 40px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #5b73e8, #7c8fe8); + border-radius: 4px; + transition: width 0.3s ease; +} + +.question-header { + margin-bottom: 32px; +} + +.question-number { + display: inline-block; + background: #f3f4f6; + color: #5b73e8; + font-size: 14px; + font-weight: 600; + padding: 6px 12px; + border-radius: 6px; + margin-bottom: 16px; +} + +.question-title { + font-size: 24px; + font-weight: 700; + color: #1a1a1a; + line-height: 1.4; +} + +.options-container { + margin-bottom: 40px; +} + +.option-item { + background: #f9fafb; + border: 2px solid #e5e7eb; + border-radius: 12px; + padding: 16px 20px; + margin-bottom: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.option-item:hover { + background: #f3f4f6; + border-color: #5b73e8; +} + +.option-item input[type="radio"] { + display: none; +} + +.option-item input[type="radio"]:checked + label { + color: #5b73e8; + font-weight: 600; +} + +.option-item:has(input[type="radio"]:checked) { + background: #eef2ff; + border-color: #5b73e8; +} + +.option-item label { + display: block; + width: 100%; + font-size: 16px; + color: #374151; + cursor: pointer; +} + +.button-container { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.btn-prev, +.btn-next, +.btn-submit { + padding: 14px 32px; + font-size: 16px; + font-weight: 600; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-prev { + background: #f3f4f6; + color: #6b7280; +} + +.btn-prev:hover { + background: #e5e7eb; +} + +.btn-next, +.btn-submit { + background: #5b73e8; + color: #fff; +} + +.btn-next:hover, +.btn-submit:hover { + background: #4a5fc8; +} + +/* 결과 화면 */ +.result-screen { + display: flex; + align-items: center; + justify-content: center; + min-height: 500px; +} + +.result-content { + text-align: center; + max-width: 500px; +} + +.result-icon { + font-size: 64px; + margin-bottom: 24px; +} + +.result-content h2 { + font-size: 28px; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 16px; +} + +.result-content > p { + font-size: 18px; + color: #666; + margin-bottom: 24px; +} + +.result-content > p span { + color: #5b73e8; + font-weight: 600; +} + +.result-level-badge { + display: inline-flex; + align-items: baseline; + gap: 8px; + background: linear-gradient(135deg, #5b73e8, #7c8fe8); + color: #fff; + padding: 24px 48px; + border-radius: 16px; + margin: 24px 0; + box-shadow: 0 8px 24px rgba(91, 115, 232, 0.3); +} + +.level-text { + font-size: 24px; + font-weight: 600; +} + +.level-number { + font-size: 48px; + font-weight: 700; +} + +.result-score { + font-size: 18px; + color: #374151; + font-weight: 600; + margin: 16px 0 24px 0; +} + +.result-score span { + color: #5b73e8; + font-size: 20px; +} + +.result-description { + font-size: 16px; + color: #666; + line-height: 1.6; + margin-bottom: 40px; +} + +.btn-home { + background: #5b73e8; + color: #fff; + border: none; + border-radius: 8px; + padding: 14px 40px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.3s ease; +} + +.btn-home:hover { + background: #4a5fc8; +} + +/* 반응형 */ +@media (max-width: 768px) { + .level-test-container { + padding: 20px 16px; + } + + .questions-screen { + padding: 24px 20px; + } + + .question-title { + font-size: 20px; + } + + .button-container { + flex-direction: column; + } + + .btn-prev, + .btn-next, + .btn-submit { + width: 100%; + } + + .result-level-badge { + padding: 20px 40px; + } + + .level-text { + font-size: 20px; + } + + .level-number { + font-size: 40px; + } +} \ No newline at end of file diff --git a/templates/account/level_test.html b/templates/account/level_test.html index e69de29..f3b2816 100644 --- a/templates/account/level_test.html +++ b/templates/account/level_test.html @@ -0,0 +1,534 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}KITUP - 레벨테스트{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+ + + + + +
+
+
+
+ +
+ 1/10 +

+
+ +
+
+ +
+ +
+ + + +
+
+
+ + + +
+ + +{% endblock %} \ No newline at end of file From ef5d498b77058b539883472fee0d4e7bb03d2aaa Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 19:12:41 +0900 Subject: [PATCH 166/380] =?UTF-8?q?style=20:=20=EB=B0=B0=EA=B2=BD=EC=83=89?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 4 ++-- static/css/mission.css | 10 +++++----- static/css/team.css | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 471be15..6b1c124 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -12,11 +12,11 @@ text-align: center; padding: 15px; text-decoration: none; - background: #eaf0ff; + background: #F6F8FF; } .p_header .p_dashboard { - background: #eaf0ff; + background: #F6F8FF; color: #1d294b; } diff --git a/static/css/mission.css b/static/css/mission.css index 55c9b6a..d95f683 100644 --- a/static/css/mission.css +++ b/static/css/mission.css @@ -12,21 +12,21 @@ text-align: center; padding: 15px; text-decoration: none; - background: #eaf0ff; + background: #F6F8FF; } .p_header .p_dashboard { background: #fff; - color: #cad9ff; + color: #EAF0FF; } .p_header .p_mission { - background: #eaf0ff; + background: #F6F8FF; color: #1d294b; } .p_header .p_dashboard:hover { - background: #eaf0ff; + background: #EAF0FF; color: #1d294b; transition: 0.3s ease-in-out; } @@ -38,7 +38,7 @@ /* 진척도 */ .mission_progress { - background: #eaf0ff; + background: #F6F8FF; width: 90%; height: 350px; padding: 50px 20px; margin: 0 auto 80px; diff --git a/static/css/team.css b/static/css/team.css index a5cbb2e..76885ac 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -1,5 +1,5 @@ body { - background: #EAF0FF; + background: #F6F8FF; } /* 매칭 성공 */ From 31e13917b843bd4bc3ed2af0a0b2eb7922b930e7 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 19:14:24 +0900 Subject: [PATCH 167/380] =?UTF-8?q?fix:=20=EC=83=9D=EC=84=B1=20=EC=A4=91?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/models.py | 6 + apps/reflections/views.py | 68 ++++++- templates/reflections/_note_form.html | 271 ++++++++++++++------------ 3 files changed, 218 insertions(+), 127 deletions(-) diff --git a/apps/reflections/models.py b/apps/reflections/models.py index bc922f3..0b80674 100644 --- a/apps/reflections/models.py +++ b/apps/reflections/models.py @@ -94,6 +94,7 @@ class RetrospectiveAsset(models.Model): Retrospective, on_delete=models.CASCADE, related_name="assets", + null=True, blank=True, ) # 권한/조회 편의용 (중복이지만 실무에서 유용) @@ -103,6 +104,11 @@ class RetrospectiveAsset(models.Model): related_name="retrospective_assets", ) + # 임시 저장용 키 (회고 작성 중 업로드된 이미지 구분용) + draft_key = models.UUIDField( + null=True, blank=True, db_index=True + ) + image = models.ImageField( upload_to=retrospective_asset_upload_to, help_text="첨부 이미지", diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 3a25e86..f019465 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -3,6 +3,7 @@ from django.contrib import messages from django.db.models import Q +import uuid from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.parsers import MultiPartParser, FormParser @@ -122,6 +123,11 @@ def note_create(request): tpl_key = request.GET.get("tpl") or "default" guide = load_guide(tpl_key) + # ✅ draft_key 발급/유지 + if "retro_draft_key" not in request.session: + request.session["retro_draft_key"] = str(uuid.uuid4()) + draft_key = request.session["retro_draft_key"] + if request.method == "POST": title = (request.POST.get("title") or "빈 제목").strip() if not title: @@ -135,18 +141,30 @@ def note_create(request): content_md = build_markdown(guide, answers) - Retrospective.objects.create( + note = Retrospective.objects.create( user= request.user, template_key=tpl_key, title=title, answers_json=answers, content_md = content_md, ) + + # ✅ draft로 업로드된 이미지들을 note에 연결 + RetrospectiveAsset.objects.filter( + user=request.user, + draft_key=draft_key, + retrospective__isnull=True, + ).update(retrospective=note, draft_key=None) + + # ✅ draft_key 정리 + request.session.pop("retro_draft_key", None) + return redirect("reflections:note_list") context = { "guide": guide, "tpl": tpl_key, "answers": {}, + "draft_key": draft_key, } return render(request, "reflections/note_create.html", context) @@ -402,7 +420,11 @@ def upload_asset(self, request, pk=None): summary="회고 이미지 삭제", tags=["Retrospectives"] ) - @action(detail=True, methods=["delete"], url_path=r"assets/(?P\d+)") + @action( + detail=True, + methods=["delete"], + url_path=r"assets/(?P\d+)" + ) def delete_asset(self, request, pk=None, asset_id=None): retro = self.get_object() # 본인 회고인지 포함해서 체크된다고 가정 @@ -416,3 +438,45 @@ def delete_asset(self, request, pk=None, asset_id=None): asset.delete() # ✅ 여기서 DB 삭제 + (아래 시그널/오버라이드 있으면 파일도 삭제) return Response(status=status.HTTP_204_NO_CONTENT) + + @extend_schema( + summary="회고 이미지 임시 업로드", + tags=["Retrospectives"] + ) + @action( + detail=False, + methods=["post"], + url_path="assets/temp", + parser_classes=[MultiPartParser, FormParser], + ) + def upload_temp_asset(self, request): + draft_key = request.data.get("draft_key") + if not draft_key: + return Response({"detail": "draft_key 필요"}, status=400) + + try: + draft_uuid = uuid.UUID(str(draft_key)) + except ValueError: + return Response({"detail": "draft_key 형식 오류"}, status=400) + + f = request.FILES.get("image") + if not f: + return Response({"detail": "image 파일 필요"}, status=400) + + ct = (getattr(f, "content_type", "") or "").lower() + if ct and not ct.startswith("image/"): + return Response({"detail": "이미지 파일만 업로드 가능합니다."}, status=400) + + alt_text = (request.data.get("alt_text") or "").strip() + + asset = RetrospectiveAsset.objects.create( + user=request.user, + draft_key=draft_uuid, + retrospective=None, + image=f, + alt_text=alt_text, + ) + + url = asset.image.url + md = f"![{alt_text or 'image'}]({url})" + return Response({"id": asset.id, "url": url, "md": md}, status=status.HTTP_201_CREATED) \ No newline at end of file diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index 5d6e762..847799e 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -8,6 +8,7 @@ - tpl: template key - answers: dict (qid -> text) (create는 빈 dict 가능) - note: (update에서만 있을 수 있음) +- draft_key: (create에서만 있을 수 있음) ✅ create 즉시 업로드용 - error: (선택) {% endcomment %} @@ -41,11 +42,14 @@ {% endif %}
+ + +
{% for q in guide.questions %}
- +
@@ -53,8 +57,8 @@ {{ q.order|default:forloop.counter }}. {{ q.title }}
- {# ✅ note가 있을 때만 업로드 가능(생성 페이지는 저장 후 업로드) #} - {% if note %} + {# ✅ create에서도 업로드 가능: note 있거나 draft_key 있으면 업로드 버튼 노출 #} + {% if note or draft_key %}
@@ -87,71 +91,92 @@
{% endfor %} - {# ✅ 숨겨진 파일 인풋 1개만 두고 재사용 #} - {% if note %} - - - - {% endif %} -
+ } catch (e) { + console.error(e); + alert("업로드 요청 실패(네트워크)"); + return; + } + + if (!res.ok) { + const t = await res.text().catch(() => ""); + console.error("upload failed:", res.status, t); + alert("업로드 실패"); + return; + } + + const data = await res.json(); + const md = data.md || `![image](${data.url})`; + + const ta = document.getElementById(`ta__${currentQid}`); + if (!ta) return; + + insertAtCursor(ta, (ta.value.endsWith("\n") || ta.value.length === 0) ? md : "\n" + md); + }); + })(); +
@@ -167,74 +192,71 @@

- + + {% if note and note.assets.all %}
업로드된 이미지
- {% for asset in note.assets.all %} -
-
- {{ asset.alt_text|default:'image' }} - -
- {{ asset.id }} / {{ asset.image.name|cut:"retrospectives/"|cut:"/" }} -
+ {% for asset in note.assets.all %} +
+
+ {{ asset.alt_text|default:'image' }} +
+ UUID: {{ asset.id }}
+
+ +
+ {% endfor %} +
- -
- {% endfor %} -
- + }); + })(); + {% endif %} - - From 1d39e0919db781178c64b2a7f2626d0b0e4a37c7 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 19:30:44 +0900 Subject: [PATCH 168/380] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=9A=8C=EA=B3=A0=20=EC=97=90?= =?UTF-8?q?=EC=85=8B=20=EC=9E=90=EB=8F=99=20=EC=82=AD=EC=A0=9C=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EC=96=B4=20=EC=B6=94=EA=B0=80.=20migrations=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/cleanup_temp__assets.py | 47 +++++++++++++++++++ ...6_retrospectiveasset_draft_key_and_more.py | 29 ++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 apps/reflections/management/commands/cleanup_temp__assets.py create mode 100644 apps/reflections/migrations/0006_retrospectiveasset_draft_key_and_more.py diff --git a/apps/reflections/management/commands/cleanup_temp__assets.py b/apps/reflections/management/commands/cleanup_temp__assets.py new file mode 100644 index 0000000..bef1a8a --- /dev/null +++ b/apps/reflections/management/commands/cleanup_temp__assets.py @@ -0,0 +1,47 @@ +# apps/reflections/management/commands/cleanup_temp_assets.py +from datetime import timedelta + +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.conf import settings + +from apps.reflections.models import RetrospectiveAsset + + +class Command(BaseCommand): + help = "Delete temporary retrospective assets (retrospective is NULL) older than TTL." + + def add_arguments(self, parser): + parser.add_argument( + "--hours", + type=int, + default=24, + help="TTL in hours (default: 24). Assets older than this will be deleted.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print what would be deleted without actually deleting.", + ) + + def handle(self, *args, **options): + hours = options["hours"] + dry_run = options["dry_run"] + + cutoff = timezone.now() - timedelta(hours=hours) + + qs = RetrospectiveAsset.objects.filter( + retrospective__isnull=True, + created_at__lt=cutoff, + ) + + count = qs.count() + + if dry_run: + self.stdout.write(self.style.WARNING(f"[DRY RUN] would delete {count} temp assets (hours={hours})")) + return + + # ✅ delete()는 post_delete 시그널이 있으면 파일도 삭제됩니다. + deleted = qs.delete() + # deleted는 (총 삭제 수, {모델: 수}) 형태 + self.stdout.write(self.style.SUCCESS(f"deleted {count} temp assets (hours={hours})")) diff --git a/apps/reflections/migrations/0006_retrospectiveasset_draft_key_and_more.py b/apps/reflections/migrations/0006_retrospectiveasset_draft_key_and_more.py new file mode 100644 index 0000000..5a190c7 --- /dev/null +++ b/apps/reflections/migrations/0006_retrospectiveasset_draft_key_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.10 on 2026-02-06 10:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reflections", "0005_retrospectiveasset"), + ] + + operations = [ + migrations.AddField( + model_name="retrospectiveasset", + name="draft_key", + field=models.UUIDField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name="retrospectiveasset", + name="retrospective", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="assets", + to="reflections.retrospective", + ), + ), + ] From 6e662cad8092627aea92f1594188d86c5cb73fa4 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Fri, 6 Feb 2026 19:34:47 +0900 Subject: [PATCH 169/380] =?UTF-8?q?feat:=20mypage=20html,=20css=201?= =?UTF-8?q?=EC=B0=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mypage.css | 166 ++++++++++++++++++++++++++++++++++ templates/account/mypage.html | 71 +++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 static/css/mypage.css diff --git a/static/css/mypage.css b/static/css/mypage.css new file mode 100644 index 0000000..463dffe --- /dev/null +++ b/static/css/mypage.css @@ -0,0 +1,166 @@ +/* mypage.css */ + +.mypage-wrapper { + background-color: #f2f6ff; /* 전체 배경 연한 파랑 */ + padding: 60px 20px; + min-height: 100vh; + display: flex; + justify-content: center; +} + +.profile-card { + background-color: #ffffff; + width: 1000px; + border-radius: 40px; + display: flex; + padding: 50px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.05); +} + +/* 왼쪽 정보 섹션 */ +.user-info-side { + flex: 0 0 300px; + display: flex; + flex-direction: column; + align-items: center; + border-right: 1px solid #f0f0f0; /* 필요시 구분선 */ + padding-right: 40px; +} + +.avatar-container { + position: relative; + margin-bottom: 15px; +} + +.profile-img { + width: 160px; + height: 160px; + border-radius: 50%; + background-color: #e9e9e9; + object-fit: cover; +} + +.edit-badge { + position: absolute; + right: 5px; + bottom: 5px; + background: #6c6c6c; + width: 34px; + height: 34px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid white; +} + +.level-tag { + background-color: #adc4ff; + color: white; + padding: 6px 24px; + border-radius: 20px; + font-weight: bold; + font-size: 18px; + margin-bottom: 40px; +} + +.info-table { + width: 100%; + margin-bottom: 40px; +} + +.info-row { + display: flex; + justify-content: space-between; + margin-bottom: 18px; + font-size: 16px; +} + +.info-row .label { + font-weight: 800; + color: #333; +} + +.info-row .value { + color: #5c759d; +} + +.edit-button { + background-color: #4470ff; + color: white; + width: 100%; + text-align: center; + padding: 14px 0; + border-radius: 12px; + text-decoration: none; + font-weight: bold; + transition: background 0.2s; +} + +.edit-button:hover { + background-color: #3359d4; +} + +/* 오른쪽 콘텐츠 섹션 */ +.content-side { + flex: 1; + padding-left: 40px; +} + +.tech-stack-box { + border: 2px solid #adc4ff; + border-radius: 25px; + padding: 25px; + margin-bottom: 30px; +} + +.side-title { + font-size: 18px; + font-weight: 800; + margin-bottom: 15px; +} + +.tags { + display: flex; + gap: 12px; +} + +.tag-item { + padding: 8px 18px; + border-radius: 25px; + font-weight: bold; + background: white; + border: 2px solid #ddd; + box-shadow: 0 4px 6px rgba(0,0,0,0.05); +} + +/* 시안의 태그 색상 재현 */ +.tag-javascript { border-color: #00cfc1; color: #00cfc1; } +.tag-reactnative { border-color: #ffc107; color: #ffc107; } +.tag-html { border-color: #00cfc1; color: #00cfc1; } + +/* 프로젝트 그리드 */ +.project-list-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.project-item-card { + background-color: #ebf1ff; + border-radius: 25px; + padding: 30px; + min-height: 140px; +} + +.item-label { + font-size: 16px; + font-weight: 800; + color: #222; + margin-bottom: 10px; +} + +.item-project-title { + color: #4470ff; + font-weight: 600; +} \ No newline at end of file diff --git a/templates/account/mypage.html b/templates/account/mypage.html index e69de29..e7f76cd 100644 --- a/templates/account/mypage.html +++ b/templates/account/mypage.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+ + +
+
+

기술 스택

+
+ {% for rl in role_levels %} + # {{ rl.role.name }} + {% empty %} +

등록된 스택이 없습니다.

+ {% endfor %} +
+
+ +
+ {% for membership in memberships %} +
+

팀 프로젝트 기록

+

{{ membership.team.project.title }}

+
+ {% empty %} +

팀 프로젝트 기록

+

팀 프로젝트 기록

+

팀 프로젝트 기록

+

팀 프로젝트 기록

+ {% endfor %} +
+
+
+
+{% endblock %} \ No newline at end of file From b5bb4a4e7b26871e91fd91a47fa07ee29d156ba8 Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 19:39:07 +0900 Subject: [PATCH 170/380] =?UTF-8?q?feat:=20TechStack=20json=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/fixtures/tech_stacks.json | 227 ++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 apps/accounts/fixtures/tech_stacks.json diff --git a/apps/accounts/fixtures/tech_stacks.json b/apps/accounts/fixtures/tech_stacks.json new file mode 100644 index 0000000..0ecc1c3 --- /dev/null +++ b/apps/accounts/fixtures/tech_stacks.json @@ -0,0 +1,227 @@ +[ + { + "model": "accounts.TechStack", + "pk": 1, + "fields": { + "name": "Python", + "category": "LANGUAGE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 2, + "fields": { + "name": "JavaScript", + "category": "LANGUAGE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 3, + "fields": { + "name": "TypeScript", + "category": "LANGUAGE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 4, + "fields": { + "name": "Java", + "category": "LANGUAGE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 5, + "fields": { + "name": "C++", + "category": "LANGUAGE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 6, + "fields": { + "name": "React", + "category": "FRONTEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 7, + "fields": { + "name": "Vue.js", + "category": "FRONTEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 8, + "fields": { + "name": "Angular", + "category": "FRONTEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 9, + "fields": { + "name": "Next.js", + "category": "FRONTEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 10, + "fields": { + "name": "Svelte", + "category": "FRONTEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 11, + "fields": { + "name": "Django", + "category": "BACKEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 12, + "fields": { + "name": "Flask", + "category": "BACKEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 13, + "fields": { + "name": "FastAPI", + "category": "BACKEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 14, + "fields": { + "name": "Express.js", + "category": "BACKEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 15, + "fields": { + "name": "Spring", + "category": "BACKEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 16, + "fields": { + "name": "Node.js", + "category": "BACKEND", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 17, + "fields": { + "name": "PostgreSQL", + "category": "DATABASE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 18, + "fields": { + "name": "MySQL", + "category": "DATABASE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 19, + "fields": { + "name": "MongoDB", + "category": "DATABASE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 20, + "fields": { + "name": "Redis", + "category": "DATABASE", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 21, + "fields": { + "name": "Docker", + "category": "TOOL", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 22, + "fields": { + "name": "Kubernetes", + "category": "TOOL", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 23, + "fields": { + "name": "Git", + "category": "TOOL", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 24, + "fields": { + "name": "AWS", + "category": "TOOL", + "created_at": "2026-02-06T00:00:00Z" + } + }, + { + "model": "accounts.TechStack", + "pk": 25, + "fields": { + "name": "Figma", + "category": "TOOL", + "created_at": "2026-02-06T00:00:00Z" + } + } +] From 3b8d8e3722559c6f1cd31d6221a4863e6b66242a Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 19:41:01 +0900 Subject: [PATCH 171/380] =?UTF-8?q?feat:=20TechStack=20import=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/commands/load_techstacks.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/accounts/management/commands/load_techstacks.py diff --git a/apps/accounts/management/commands/load_techstacks.py b/apps/accounts/management/commands/load_techstacks.py new file mode 100644 index 0000000..0bc2d02 --- /dev/null +++ b/apps/accounts/management/commands/load_techstacks.py @@ -0,0 +1,29 @@ +from django.core.management.base import BaseCommand +from django.core.management import call_command + + +class Command(BaseCommand): + help = 'JSON 파일에서 기술 스택 데이터를 로드합니다' + + def add_arguments(self, parser): + parser.add_argument( + '--reset', + action='store_true', + help='기존 기술 스택 데이터를 먼저 삭제합니다', + ) + + def handle(self, *args, **options): + # 기존 데이터 삭제 옵션 + if options['reset']: + from apps.accounts.models import TechStack + TechStack.objects.all().delete() + self.stdout.write(self.style.WARNING('기존 기술 스택 데이터를 삭제했습니다')) + + # fixture 로드 + try: + call_command('loaddata', 'tech_stacks') + self.stdout.write( + self.style.SUCCESS('✅ 기술 스택 데이터를 성공적으로 로드했습니다!') + ) + except Exception as e: + self.stdout.write(self.style.ERROR(f'❌ 오류: {e}')) From 3575a8d1521ffcde91024c442a5af5c45ea65e7a Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Fri, 6 Feb 2026 19:45:32 +0900 Subject: [PATCH 172/380] =?UTF-8?q?fix:=20reflection=20=EC=A0=91=EC=86=8D?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/views.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index f019465..6c8b4ae 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -92,12 +92,16 @@ def note_list(request): else: qs = qs.order_by("-created_at") + my_project_ids = ( + TeamMember.objects + .filter(user=request.user) + .values_list("team__project_id", flat=True) + .distinct() + ) + my_projects = ( Project.objects - # Project -> team 에서 멤버에 포함되는지 여부 로직 - # TODO 로직 확인해보기 - .filter(member__user=request.user) - .distinct() + .filter(Q(id__in=my_project_ids) | Q(owner=request.user)) .order_by("title") ) From 8b5addcf6d6914a45125fdd6317b09744712dbe3 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Fri, 6 Feb 2026 19:47:59 +0900 Subject: [PATCH 173/380] fix: mypage.html --- static/css/mypage.css | 1 - templates/account/mypage.html | 24 +++++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/static/css/mypage.css b/static/css/mypage.css index 463dffe..c08a253 100644 --- a/static/css/mypage.css +++ b/static/css/mypage.css @@ -1,4 +1,3 @@ -/* mypage.css */ .mypage-wrapper { background-color: #f2f6ff; /* 전체 배경 연한 파랑 */ diff --git a/templates/account/mypage.html b/templates/account/mypage.html index e7f76cd..77e71f8 100644 --- a/templates/account/mypage.html +++ b/templates/account/mypage.html @@ -1,7 +1,8 @@ {% extends 'base.html' %} {% load static %} -{% block extra_css %} +{% block header %} +{# base.html의 header 블록에 CSS 연결 코드를 삽입합니다 #} {% endblock %} @@ -10,12 +11,13 @@
- - - - - + {% if season.status == 'MATCHING' %} +

팀 매칭 모집이 시작됐어요

+

+ 6주의 기간동안 희망하는 스택으로 프로젝트를 진행하고, 실력을 쌓아보아요. +

+
+
+

WEB 기획

+ {% if user.is_authenticated %} + {% with pm_level=user.get_role_level|add:"PM" %} + {% if pm_level == 1 %} + Level1 +

Lv1

+ {% elif pm_level == 2 %} + Level2 +

Lv2

+ {% elif pm_level == 3 %} + Level3 +

Lv3

+ {% elif pm_level == 4 %} + Level4 +

Lv4

+ + {% else %} + nolevel +

아직 레벨 진단이 완료되지 않았어요!

+ + {% endif %} + {% endwith %} + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+

WEB 프론트엔드

+ {% if user.is_authenticated %} + {% with pm_level=user.get_role_level|add:"PM" %} + {% if pm_level == 1 %} + Level1 +

Lv1

+ {% elif pm_level == 2 %} + Level2 +

Lv2

+ {% elif pm_level == 3 %} + Level3 +

Lv3

+ {% elif pm_level == 4 %} + Level4 +

Lv4

+ + {% else %} + nolevel +

아직 레벨 진단이 완료되지 않았어요!

+ + {% endif %} + {% endwith %} + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+

WEB 백엔드

+ {% if user.is_authenticated %} + + + + + + + + + + + Level4 +

Lv4

+ + {% else %} + nolevel +

로그인 후 레벨을 확인해보세요.

+ + {% endif %} +
+
+ {% elif not season %} +

지금은 팀 매칭 모집 기간이 아니예요.

- - - + {% elif season.status == 'IN_PROJECT' %}

팀 매칭 결과가 발표됐어요.

@@ -47,6 +155,7 @@

팀 매칭 결과가 발표됐어요.

>

결과 보러가기

+ {% endif %}
From bbe913255f331ada1b41f2b495267e23d64b26ca Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 21:18:53 +0900 Subject: [PATCH 200/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EA=B2=80=EC=A6=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/api_urls.py | 1 + apps/accounts/forms.py | 27 ++++++++++++ apps/accounts/views.py | 32 ++++++++++++++ static/css/profile_edit.css | 59 +++++++++++++++++++++++++ templates/account/profile_edit.html | 67 ++++++++++++++++++++++++++++- 5 files changed, 185 insertions(+), 1 deletion(-) diff --git a/apps/accounts/api_urls.py b/apps/accounts/api_urls.py index b683fdd..e020f51 100644 --- a/apps/accounts/api_urls.py +++ b/apps/accounts/api_urls.py @@ -4,4 +4,5 @@ urlpatterns = [ path("check-username/", views.check_username, name="check_username"), path("check-email/", views.check_email, name="check_email"), + path("check-nickname/", views.check_nickname, name="check_nickname"), ] diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 6b6a655..314e8f7 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -1,4 +1,5 @@ from django import forms +import re from .models import User, TechStack @@ -38,8 +39,21 @@ def clean_nickname(self): nick = (self.cleaned_data.get("nickname") or "").strip() if not nick: raise forms.ValidationError("닉네임은 필수입니다.") + + # 길이 검증 + if len(nick) < 2: + raise forms.ValidationError("닉네임은 최소 2자 이상이어야 합니다.") + if len(nick) > 20: + raise forms.ValidationError("닉네임은 최대 20자 이하여야 합니다.") + + # 특수문자 검증 (한글, 영문, 숫자, 밑줄, 하이픈만 허용) + if not re.match(r'^[a-zA-Z0-9가-힣_-]+$', nick): + raise forms.ValidationError("닉네임은 한글, 영문, 숫자, 밑줄(_), 하이픈(-)만 사용 가능합니다.") + + # 중복 확인 if User.objects.filter(nickname=nick).exclude(pk=self.instance.pk).exists(): raise forms.ValidationError("이미 사용 중인 닉네임입니다.") + return nick def clean_github_id(self): @@ -96,8 +110,21 @@ def clean_nickname(self): nick = (self.cleaned_data.get("nickname") or "").strip() if not nick: raise forms.ValidationError("닉네임은 필수입니다.") + + # 길이 검증 + if len(nick) < 2: + raise forms.ValidationError("닉네임은 최소 2자 이상이어야 합니다.") + if len(nick) > 20: + raise forms.ValidationError("닉네임은 최대 20자 이하여야 합니다.") + + # 특수문자 검증 (한글, 영문, 숫자, 밑줄, 하이픈만 허용) + if not re.match(r'^[a-zA-Z0-9가-힣_-]+$', nick): + raise forms.ValidationError("닉네임은 한글, 영문, 숫자, 밑줄(_), 하이픈(-)만 사용 가능합니다.") + + # 중복 확인 if User.objects.filter(nickname=nick).exclude(pk=self.instance.pk).exists(): raise forms.ValidationError("이미 사용 중인 닉네임입니다.") + return nick def clean_github_id(self): diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 0677a3b..0b78672 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -42,6 +42,38 @@ def check_email(request): return JsonResponse({"available": True, "message": "사용 가능한 이메일입니다."}) +@require_GET +def check_nickname(request): + """닉네임 중복 확인 API""" + nickname = request.GET.get("nickname", "").strip() + current_user_id = request.GET.get("user_id") # 프로필 수정 시 자신의 닉네임 제외 + + if not nickname: + return JsonResponse({"available": False, "message": "닉네임을 입력해주세요."}) + + # 길이 검증 (2-20자) + if len(nickname) < 2: + return JsonResponse({"available": False, "message": "닉네임은 최소 2자 이상이어야 합니다."}) + + if len(nickname) > 20: + return JsonResponse({"available": False, "message": "닉네임은 최대 20자 이하여야 합니다."}) + + # 특수문자 검증 (한글, 영문, 숫자, 밑줄, 하이픈만 허용) + import re + if not re.match(r'^[a-zA-Z0-9가-힣_-]+$', nickname): + return JsonResponse({"available": False, "message": "닉네임은 한글, 영문, 숫자, 밑줄(_), 하이픈(-)만 사용 가능합니다."}) + + # 중복 확인 (현재 사용자는 제외) + query = User.objects.filter(nickname=nickname) + if current_user_id: + query = query.exclude(pk=current_user_id) + + if query.exists(): + return JsonResponse({"available": False, "message": "이미 사용 중인 닉네임입니다."}) + + return JsonResponse({"available": True, "message": "사용 가능한 닉네임입니다."}) + + @login_required def level_test(request): """ diff --git a/static/css/profile_edit.css b/static/css/profile_edit.css index 3097a88..210ae26 100644 --- a/static/css/profile_edit.css +++ b/static/css/profile_edit.css @@ -166,6 +166,65 @@ color: #9ca3af; } +/* 닉네임 검증 스타일 */ +.nickname-check-wrapper { + display: flex; + align-items: center; + gap: 10px; + flex: 1; +} + +.nickname-check-wrapper input { + flex: 1; +} + +.check-btn { + padding: 10px 20px; + background-color: #4B7EFF; + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; +} + +.check-btn:hover { + background-color: #3563D9; +} + +.check-btn:active { + background-color: #2548B8; +} + +.check-status { + font-size: 12px; + font-weight: 500; + margin-left: 10px; + white-space: nowrap; +} + +.check-status.success { + color: #10B981; +} + +.check-status.error { + color: #EF4444; +} + +/* 입력 필드 유효성 표시 */ +.input-field.valid { + border: 2px solid #10B981; + background-color: #F0FDF4; +} + +.input-field.invalid { + border: 2px solid #EF4444; + background-color: #FEF2F2; +} + /* 파일 입력 숨기기 */ input[type="file"] { display: none; diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index a582d4d..4e82552 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -28,7 +28,11 @@
- {{ form.nickname }} +
+ {{ form.nickname }} + + +
@@ -80,6 +84,67 @@

기술 스택

const profileInput = document.getElementById('id_profile_image'); const previewImage = document.getElementById('preview-image'); const defaultImageUrl = "{% static 'images/default_profile.png' %}"; + const nicknameInput = document.getElementById('id_nickname'); + const checkNicknameBtn = document.getElementById('check-nickname-btn'); + const checkStatus = document.getElementById('check-status'); + + // 닉네임 검증 + if (checkNicknameBtn && nicknameInput) { + // 중복확인 버튼 클릭 + checkNicknameBtn.addEventListener('click', function(e) { + e.preventDefault(); + checkNicknameDuplicate(); + }); + + // 닉네임 입력 중 실시간 검증 + nicknameInput.addEventListener('input', function() { + checkStatus.textContent = ''; + checkStatus.className = 'check-status'; + nicknameInput.classList.remove('valid', 'invalid'); + }); + + // Enter 키로도 검증 + nicknameInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + checkNicknameDuplicate(); + } + }); + } + + function checkNicknameDuplicate() { + const nickname = nicknameInput.value.trim(); + const userId = "{{ user.id }}"; + + if (!nickname) { + checkStatus.textContent = '닉네임을 입력하세요.'; + checkStatus.className = 'check-status error'; + nicknameInput.classList.add('invalid'); + return; + } + + // API 호출 + fetch(`/api/accounts/check-nickname/?nickname=${encodeURIComponent(nickname)}&user_id=${userId}`) + .then(response => response.json()) + .then(data => { + if (data.available) { + checkStatus.textContent = '✓ ' + data.message; + checkStatus.className = 'check-status success'; + nicknameInput.classList.add('valid'); + nicknameInput.classList.remove('invalid'); + } else { + checkStatus.textContent = '✗ ' + data.message; + checkStatus.className = 'check-status error'; + nicknameInput.classList.add('invalid'); + nicknameInput.classList.remove('valid'); + } + }) + .catch(error => { + console.error('오류:', error); + checkStatus.textContent = '검증 중 오류가 발생했습니다.'; + checkStatus.className = 'check-status error'; + }); + } if (profileInput) { // input 필드 숨기기 From 561ed131978297f59361538f8951886346ffc73c Mon Sep 17 00:00:00 2001 From: plumbestie Date: Fri, 6 Feb 2026 21:24:12 +0900 Subject: [PATCH 201/380] =?UTF-8?q?fix=20:=20main.html=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main.html | 47 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/templates/main.html b/templates/main.html index 3638096..d3ef63e 100644 --- a/templates/main.html +++ b/templates/main.html @@ -71,24 +71,24 @@

WEB 기획

WEB 프론트엔드

{% if user.is_authenticated %} - {% with pm_level=user.get_role_level|add:"PM" %} - {% if pm_level == 1 %} + {% with fe_level=user.get_role_level|add:"FRONTEND" %} + {% if fe_level == 1 %} Level1

Lv1

- {% elif pm_level == 2 %} + {% elif fe_level == 2 %} Level2

Lv2

- {% elif pm_level == 3 %} + {% elif fe_level == 3 %} Level3

Lv3

- {% elif pm_level == 4 %} + {% elif fe_level == 4 %} Level4

Lv4

- + {% else %} nolevel

아직 레벨 진단이 완료되지 않았어요!

- + {% endif %} {% endwith %} {% else %} @@ -106,27 +106,26 @@

WEB 프론트엔드

WEB 백엔드

{% if user.is_authenticated %} - - - - - - - - - - + {% elif be_level == 4 %} Level4

Lv4

- + + {% else %} + nolevel +

아직 레벨 진단이 완료되지 않았어요!

+ + {% endif %} + {% endwith %} {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -211,4 +210,4 @@

서비스명

-->
-{% endblock %} +{% endblock %} \ No newline at end of file From f20cf892068d71d2c1df9444c2a5cf6d0c5f4bac Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 6 Feb 2026 21:31:39 +0900 Subject: [PATCH 202/380] =?UTF-8?q?fix:=20role.json=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B9=A0=EC=A7=84=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/fixtures/roles.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/accounts/fixtures/roles.json b/apps/accounts/fixtures/roles.json index 780f160..b49d321 100644 --- a/apps/accounts/fixtures/roles.json +++ b/apps/accounts/fixtures/roles.json @@ -5,9 +5,7 @@ "fields": { "name": "PM(기획)", "code": "PM", - "description": "프로젝트 기획 및 관리", - "created_at": "2026-02-06T00:00:00Z", - "updated_at": "2026-02-06T00:00:00Z" + "created_at": "2026-02-06T00:00:00Z" } }, { @@ -16,9 +14,7 @@ "fields": { "name": "프론트엔드", "code": "FRONTEND", - "description": "프론트엔드 개발", - "created_at": "2026-02-06T00:00:00Z", - "updated_at": "2026-02-06T00:00:00Z" + "created_at": "2026-02-06T00:00:00Z" } }, { @@ -27,9 +23,7 @@ "fields": { "name": "백엔드", "code": "BACKEND", - "description": "백엔드 개발", - "created_at": "2026-02-06T00:00:00Z", - "updated_at": "2026-02-06T00:00:00Z" + "created_at": "2026-02-06T00:00:00Z" } } ] From d4055e62970c13bb9daebd680bf679078483bbb0 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 10:33:40 +0900 Subject: [PATCH 203/380] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B4=88=EA=B8=B0=ED=99=94=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 1 + static/css/password_reset.css | 278 ++++++++++++++++++ templates/account/password_reset.html | 56 ++++ templates/account/password_reset_done.html | 44 +++ templates/account/password_reset_email.html | 137 +++++++++ templates/account/password_reset_email.txt | 14 + .../account/password_reset_from_key.html | 71 +++++ .../account/password_reset_from_key_done.html | 30 ++ 8 files changed, 631 insertions(+) create mode 100644 static/css/password_reset.css create mode 100644 templates/account/password_reset.html create mode 100644 templates/account/password_reset_done.html create mode 100644 templates/account/password_reset_email.html create mode 100644 templates/account/password_reset_email.txt create mode 100644 templates/account/password_reset_from_key.html create mode 100644 templates/account/password_reset_from_key_done.html diff --git a/config/settings.py b/config/settings.py index 060b228..55dee73 100644 --- a/config/settings.py +++ b/config/settings.py @@ -117,6 +117,7 @@ # 비밀번호 재설정 ACCOUNT_PASSWORD_RESET_ON_CHANGE = False # 비밀번호 변경 시 재로그인 불필요 +PASSWORD_RESET_TIMEOUT = 86400 # 비밀번호 초기화 토큰 유효시간 (초 단위, 24시간) LOGIN_URL = "/accounts/login/" LOGIN_REDIRECT_URL = "/" # 로그인 성공 후 diff --git a/static/css/password_reset.css b/static/css/password_reset.css new file mode 100644 index 0000000..1a0b1c6 --- /dev/null +++ b/static/css/password_reset.css @@ -0,0 +1,278 @@ +/* Password Reset Pages */ + +/* Common Layout */ +.container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px 0; +} + +.auth-card { + background: white; + border-radius: 12px; + padding: 40px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + max-width: 500px; + width: 100%; +} + +.auth-card.success-card { + text-align: center; +} + +/* Header */ +.auth-header { + text-align: center; + margin-bottom: 30px; +} + +.auth-header h1 { + font-size: 28px; + font-weight: bold; + color: #1a1a1a; + margin: 0 0 10px 0; +} + +.auth-header p { + color: #666; + margin: 0; + font-size: 14px; +} + +/* Form Elements */ +.form-label { + font-weight: 500; + color: #333; + margin-bottom: 8px; + display: block; +} + +.form-control { + width: 100%; + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.3s; + box-sizing: border-box; +} + +.form-control:focus { + outline: none; + border-color: #4B7EFF; + box-shadow: 0 0 0 3px rgba(75, 126, 255, 0.1); +} + +.form-group { + margin-bottom: 20px; +} + +.error-message { + color: #dc3545; + font-size: 12px; + margin-top: 5px; +} + +/* Buttons */ +.btn-primary { + width: 100%; + padding: 12px; + background-color: #4B7EFF; + color: white; + border: none; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s; + margin-top: 20px; +} + +.btn-primary:hover { + background-color: #3a63d9; +} + +.btn-block { + width: 100%; +} + +.btn-back { + display: inline-block; + background-color: #4B7EFF; + color: white; + padding: 12px 32px; + text-decoration: none; + border-radius: 6px; + font-size: 15px; + font-weight: 600; + margin-top: 30px; + transition: background-color 0.3s; +} + +.btn-back:hover { + background-color: #3a63d9; +} + +/* Footer Links */ +.auth-footer { + text-align: center; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #eee; +} + +.auth-footer p { + margin: 10px 0; + font-size: 14px; + color: #666; +} + +.auth-footer a { + color: #4B7EFF; + text-decoration: none; + font-weight: 500; +} + +.auth-footer a:hover { + text-decoration: underline; +} + +/* Success Icon */ +.success-icon { + width: 80px; + height: 80px; + background-color: #10B981; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 48px; + margin: 0 auto 30px; +} + +.auth-card h1 { + font-size: 24px; + font-weight: bold; + color: #1a1a1a; + margin: 0 0 15px 0; +} + +.auth-card > p { + color: #666; + margin: 0 0 30px 0; + font-size: 15px; + line-height: 1.6; +} + +/* Info Box */ +.info-box { + background-color: #f0f9ff; + border-left: 4px solid #4B7EFF; + padding: 20px; + border-radius: 6px; + margin: 30px 0; + text-align: left; +} + +.info-box h3 { + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + margin: 0 0 15px 0; +} + +.info-box ol { + margin: 0; + padding-left: 20px; + color: #666; + font-size: 14px; +} + +.info-box li { + margin: 8px 0; +} + +.info-box p { + color: #666; + margin: 0; + font-size: 14px; +} + +/* Warning Box */ +.warning-box { + background-color: #fffbeb; + border-left: 4px solid #f59e0b; + padding: 20px; + border-radius: 6px; + margin: 20px 0; + text-align: left; +} + +.warning-box strong { + color: #d97706; + font-size: 14px; +} + +.warning-box ul { + margin: 10px 0 0 0; + padding-left: 20px; + color: #666; + font-size: 13px; +} + +.warning-box li { + margin: 6px 0; +} + +.warning-box a { + color: #4B7EFF; + text-decoration: none; + font-weight: 500; +} + +.warning-box a:hover { + text-decoration: underline; +} + +/* Password Hint */ +.password-hint { + background-color: #f0f9ff; + border-left: 3px solid #4B7EFF; + padding: 12px; + border-radius: 4px; + margin-top: 10px; + font-size: 12px; + color: #666; +} + +.password-hint strong { + color: #1a1a1a; + display: block; + margin-bottom: 6px; +} + +.password-hint ul { + margin: 0; + padding-left: 18px; +} + +.password-hint li { + margin: 4px 0; +} + +/* Alert Messages */ +.alert { + padding: 12px; + border-radius: 6px; + margin-bottom: 20px; + font-size: 14px; +} + +.alert-danger { + background-color: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} diff --git a/templates/account/password_reset.html b/templates/account/password_reset.html new file mode 100644 index 0000000..c880c97 --- /dev/null +++ b/templates/account/password_reset.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}비밀번호 초기화 - KITUP{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+
+
+
+

비밀번호 초기화

+

등록된 이메일을 입력하면 초기화 링크를 발송합니다

+
+ +
+ {% csrf_token %} + +
+ + {{ form.email }} + {% if form.email.errors %} +
+ {{ form.email.errors }} +
+ {% endif %} +
+ + {% if form.non_field_errors %} + + {% endif %} + + +
+ + +
+
+
+
+{% endblock %} diff --git a/templates/account/password_reset_done.html b/templates/account/password_reset_done.html new file mode 100644 index 0000000..e17840a --- /dev/null +++ b/templates/account/password_reset_done.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}이메일 발송 완료 - KITUP{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+
+
+
+ ✓ +
+

비밀번호 초기화 이메일을 발송했습니다

+

등록하신 이메일 주소로 비밀번호 초기화 링크를 발송했습니다.

+ +
+

다음 단계:

+
    +
  1. 이메일함을 확인하세요
  2. +
  3. KITUP의 비밀번호 초기화 이메일을 찾으세요
  4. +
  5. 이메일의 링크를 클릭하여 새 비밀번호를 설정하세요
  6. +
+
+ +
+ 💡 팁: +
    +
  • 이메일이 보이지 않으면 스팸 폴더를 확인하세요
  • +
  • 초기화 링크는 24시간 동안 유효합니다
  • +
  • 링크 재발송이 필요하면 다시 시도하세요
  • +
+
+ + 로그인으로 돌아가기 +
+
+
+
+{% endblock %} diff --git a/templates/account/password_reset_email.html b/templates/account/password_reset_email.html new file mode 100644 index 0000000..4687a2a --- /dev/null +++ b/templates/account/password_reset_email.html @@ -0,0 +1,137 @@ + + + + + + KITUP 비밀번호 초기화 + + + + + + diff --git a/templates/account/password_reset_email.txt b/templates/account/password_reset_email.txt new file mode 100644 index 0000000..7016444 --- /dev/null +++ b/templates/account/password_reset_email.txt @@ -0,0 +1,14 @@ +{{ user.nickname }}님께, + +{{ site_name }} 계정의 비밀번호 초기화를 요청하셨습니다. + +아래 링크를 클릭하여 새로운 비밀번호를 설정하세요: + +{{ reset_url }} + +이 링크는 24시간 동안 유효합니다. + +혹시 이 요청을 하지 않으셨다면 이 이메일을 무시하셔도 됩니다. + +감사합니다, +{{ site_name }} 팀 diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html new file mode 100644 index 0000000..50b98c7 --- /dev/null +++ b/templates/account/password_reset_from_key.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}새 비밀번호 설정 - KITUP{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+
+
+
+

새 비밀번호 설정

+

안전한 새 비밀번호를 입력하세요

+
+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + +
+ + {{ form.password1 }} + {% if form.password1.errors %} +
+ {{ form.password1.errors }} +
+ {% endif %} +
+ 비밀번호 요구사항: +
    +
  • 최소 8자 이상
  • +
  • 대문자, 소문자, 숫자를 포함해야 합니다
  • +
  • 이전 비밀번호와 다려야 합니다
  • +
+
+
+ +
+ + {{ form.password2 }} + {% if form.password2.errors %} +
+ {{ form.password2.errors }} +
+ {% endif %} +
+ + +
+ + +
+
+
+
+{% endblock %} diff --git a/templates/account/password_reset_from_key_done.html b/templates/account/password_reset_from_key_done.html new file mode 100644 index 0000000..017fc6b --- /dev/null +++ b/templates/account/password_reset_from_key_done.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}비밀번호 변경 완료 - KITUP{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+
+
+
+ ✓ +
+

비밀번호가 변경되었습니다

+

새 비밀번호로 성공적으로 변경되었습니다.

+ +
+

이제 새 비밀번호로 로그인하실 수 있습니다.

+
+ + 로그인 +
+
+
+
+{% endblock %} From 06d7bbbce3ce76f518ee80dda0eec365c55697a6 Mon Sep 17 00:00:00 2001 From: knana6 Date: Sat, 7 Feb 2026 11:11:10 +0900 Subject: [PATCH 204/380] feat: test result --- static/css/test_result.css | 207 +++++++++++++++++++++++++++++ templates/account/test_result.html | 47 +++++++ 2 files changed, 254 insertions(+) create mode 100644 static/css/test_result.css diff --git a/static/css/test_result.css b/static/css/test_result.css new file mode 100644 index 0000000..42add7d --- /dev/null +++ b/static/css/test_result.css @@ -0,0 +1,207 @@ +/* test_result.css */ + +.test-result-container { + min-height: calc(100vh - 100px); + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif; + background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%); +} + +.result-wrapper { + max-width: 600px; + width: 100%; + text-align: center; +} + +/* 타이틀 */ +.result-title { + font-size: 32px; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 40px; + letter-spacing: -0.5px; +} + +/* 레벨 카드 */ +.level-card { + background: #ffffff; + border-radius: 24px; + padding: 60px 40px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08); + margin-bottom: 32px; + position: relative; +} + +/* 레벨 아이콘 */ +.level-icon-wrapper { + margin-bottom: 24px; +} + +.level-icon { + width: 120px; + height: 120px; + margin: 0 auto; + border-radius: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 64px; + position: relative; +} + +.level-icon.level-1 { + background: linear-gradient(135deg, #d4f1d4, #b8e6b8); + box-shadow: 0 8px 24px rgba(184, 230, 184, 0.4); +} + +.level-icon.level-2 { + background: linear-gradient(135deg, #b8e6d5, #8ed1b8); + box-shadow: 0 8px 24px rgba(142, 209, 184, 0.4); +} + +.level-icon.level-3 { + background: linear-gradient(135deg, #8ed1ff, #5b9cff); + box-shadow: 0 8px 24px rgba(91, 156, 255, 0.4); +} + +.level-icon.level-4 { + background: linear-gradient(135deg, #ffd700, #ffb700); + box-shadow: 0 8px 24px rgba(255, 215, 0, 0.5); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +/* 레벨 배지 */ +.level-badge { + display: inline-block; + background: linear-gradient(135deg, #5b73e8, #7c8fe8); + color: #ffffff; + font-size: 28px; + font-weight: 700; + padding: 12px 40px; + border-radius: 50px; + margin-bottom: 32px; + box-shadow: 0 4px 16px rgba(91, 115, 232, 0.3); +} + +/* 메시지 */ +.result-message { + margin-top: 24px; +} + +.user-name { + font-size: 22px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 12px; + line-height: 1.5; +} + +.description { + font-size: 16px; + color: #666; + line-height: 1.6; +} + +/* 홈으로 버튼 */ +.btn-home { + background: #5b73e8; + color: #ffffff; + border: none; + border-radius: 12px; + padding: 16px 48px; + font-size: 18px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 16px rgba(91, 115, 232, 0.3); +} + +.btn-home:hover { + background: #4a5fc8; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(91, 115, 232, 0.4); +} + +.btn-home:active { + transform: translateY(0); +} + +/* 반응형 */ +@media (max-width: 768px) { + .test-result-container { + padding: 24px 16px; + } + + .result-title { + font-size: 24px; + margin-bottom: 32px; + } + + .level-card { + padding: 40px 24px; + border-radius: 20px; + } + + .level-icon { + width: 100px; + height: 100px; + font-size: 52px; + } + + .level-badge { + font-size: 24px; + padding: 10px 32px; + } + + .user-name { + font-size: 18px; + } + + .description { + font-size: 14px; + } + + .btn-home { + width: 100%; + padding: 14px 32px; + font-size: 16px; + } +} + +@media (max-width: 480px) { + .result-title { + font-size: 20px; + } + + .level-card { + padding: 32px 20px; + } + + .level-icon { + width: 80px; + height: 80px; + font-size: 40px; + border-radius: 20px; + } + + .level-badge { + font-size: 20px; + padding: 8px 24px; + } + + .user-name { + font-size: 16px; + } +} \ No newline at end of file diff --git a/templates/account/test_result.html b/templates/account/test_result.html index e69de29..97ec8c5 100644 --- a/templates/account/test_result.html +++ b/templates/account/test_result.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}KITUP - 레벨 진단 결과{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+ +

WEB {{ role_name }} 레벨 진단

+ + +
+ +
+ {% if level == 1 %} +
🌱
+ {% elif level == 2 %} +
🌿
+ {% elif level == 3 %} +
🌳
+ {% else %} +
🏆
+ {% endif %} +
+ + +
Lv{{ level }}
+ + +
+

{{ user.nickname }} 님은 {{ level_description }}입니다.

+

프로젝트를 시작해 보세요!

+
+
+ + + +
+
+{% endblock %} \ No newline at end of file From 8f4d96e115dcedec5b0e157ecc321ab2e6839804 Mon Sep 17 00:00:00 2001 From: knana6 Date: Sat, 7 Feb 2026 11:22:43 +0900 Subject: [PATCH 205/380] feat: passion test --- static/css/passion_test.css | 61 ++++++ templates/teams/passion_test.html | 321 ++++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 static/css/passion_test.css diff --git a/static/css/passion_test.css b/static/css/passion_test.css new file mode 100644 index 0000000..a67369f --- /dev/null +++ b/static/css/passion_test.css @@ -0,0 +1,61 @@ +/* 결과 화면 추가 스타일 */ +.passion-result-message { + font-size: 16px; + color: #666; + margin-bottom: 16px; +} + +/* 열정 레벨 테스트 헤더 */ +.passion-header { + text-align: center; + margin-bottom: 40px; + padding: 32px 24px; + background: linear-gradient(135deg, #f8f9ff, #eef2ff); + border-radius: 16px; +} + +.passion-title { + font-size: 28px; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 16px; +} + +.passion-description { + font-size: 15px; + color: #666; + line-height: 1.8; + margin: 0; +} + +/* 반응형 */ +@media (max-width: 768px) { + .passion-header { + padding: 24px 20px; + margin-bottom: 32px; + } + + .passion-title { + font-size: 22px; + margin-bottom: 12px; + } + + .passion-description { + font-size: 14px; + line-height: 1.6; + } +} + +@media (max-width: 480px) { + .passion-header { + padding: 20px 16px; + } + + .passion-title { + font-size: 20px; + } + + .passion-description { + font-size: 13px; + } +} diff --git a/templates/teams/passion_test.html b/templates/teams/passion_test.html index e69de29..7982128 100644 --- a/templates/teams/passion_test.html +++ b/templates/teams/passion_test.html @@ -0,0 +1,321 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}KITUP - 열정 레벨 진단{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+ + + + +
+ +
+

열정 레벨 판별 설문

+

+ 본 설문은 평가나 선발을 위한 것이 아니라,
+ 비슷한 상황과 몰입도의 팀원을 매칭하기 위한 목적입니다.
+ 현재 본인의 상황에 가장 가까운 항목을 선택해주세요. +

+
+ +
+
+
+ +
+ 1/10 +

+
+ +
+
+ +
+ +
+ + + +
+
+
+ + + +
+ + + +{% endblock %} \ No newline at end of file From 889057d19d03869b91e2285ab990a4928456fe44 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 11:40:09 +0900 Subject: [PATCH 206/380] =?UTF-8?q?fix:=20deploy.yml=20tocken=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 512dde8..1947e8f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -43,11 +43,11 @@ jobs: script: | cd /home/ubuntu/kitup # 서버에서도 GHCR 이미지를 받을 수 있게 로그인 - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin # 최신 이미지 가져오고 컨테이너 재실행 docker-compose pull web - docker-compose up -d --build web + docker-compose up -d web # 후처리 작업 (DB 동기화 및 정적파일 수집) docker-compose exec -T web python manage.py migrate From 4f21be6e34901ca53b03b58708352e82b5261e72 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 11:44:40 +0900 Subject: [PATCH 207/380] =?UTF-8?q?refactor:=20project-season=20=EC=97=B0?= =?UTF-8?q?=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projects/migrations/0005_add_season_fk.py | 19 +++++++++++++++++++ apps/projects/models.py | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/projects/migrations/0005_add_season_fk.py diff --git a/apps/projects/migrations/0005_add_season_fk.py b/apps/projects/migrations/0005_add_season_fk.py new file mode 100644 index 0000000..f5561a3 --- /dev/null +++ b/apps/projects/migrations/0005_add_season_fk.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.10 on 2026-02-07 02:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0004_remove_project_current_stage'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='season', + field=models.ForeignKey(blank=True, help_text='속한 시즌', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='projects.season'), + ), + ] diff --git a/apps/projects/models.py b/apps/projects/models.py index b7e3455..74ca3e1 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -86,6 +86,15 @@ class Status(models.TextChoices): COMPLETED = "COMPLETED", "완료" ARCHIVED = "ARCHIVED", "보관됨" + season = models.ForeignKey( + Season, + on_delete=models.CASCADE, + related_name="projects", + null=True, + blank=True, + help_text="속한 시즌", + ) + owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, From 389f8f622ec2cd03d5f8dd4f41ebb349a35de40d Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 11:49:26 +0900 Subject: [PATCH 208/380] =?UTF-8?q?fix:=20yml=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1947e8f..bbb488b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,7 +41,7 @@ jobs: username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | - cd /home/ubuntu/kitup + cd ~/kitup # 서버에서도 GHCR 이미지를 받을 수 있게 로그인 echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin diff --git a/docker-compose.yml b/docker-compose.yml index c9d9f84..0ae6fc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: python manage.py runserver 0.0.0.0:8000 " volumes: - - .:/app + # - .:/app - static_volume:/app/staticfiles - media_volume:/app/media ports: From 8fe4c019df3e990696dc6451b49d953ca7025bd3 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 11:56:27 +0900 Subject: [PATCH 209/380] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mypage.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/mypage.css b/static/css/mypage.css index e302c3c..0167d53 100644 --- a/static/css/mypage.css +++ b/static/css/mypage.css @@ -8,7 +8,7 @@ } .profile-card { - background-color: #fff; + background-color: #f0f0f0; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); overflow: visible; From 698d2397d855bbb7d43b74309fe5e45d2941e1a9 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 12:18:26 +0900 Subject: [PATCH 210/380] =?UTF-8?q?mypage.html=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mypage.css | 21 ++++++++++++++++++++- templates/account/mypage.html | 4 +--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/static/css/mypage.css b/static/css/mypage.css index 0167d53..a38aa23 100644 --- a/static/css/mypage.css +++ b/static/css/mypage.css @@ -8,7 +8,7 @@ } .profile-card { - background-color: #f0f0f0; + background-color: #ffffff; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); overflow: visible; @@ -193,6 +193,25 @@ border: 2px solid; } +/* 역할별 색상 */ +.tag-pm { + color: #6B8AFF; + border-color: #6B8AFF; + background-color: transparent; +} + +.tag-frontend { + color: #FFC107; + border-color: #FFC107; + background-color: transparent; +} + +.tag-backend { + color: #06D6A0; + border-color: #06D6A0; + background-color: transparent; +} + /* 기술 스택별 색상 */ .tag-javascript { color: #06D6A0; diff --git a/templates/account/mypage.html b/templates/account/mypage.html index 7cb0cfc..535f769 100644 --- a/templates/account/mypage.html +++ b/templates/account/mypage.html @@ -53,9 +53,7 @@

기술 스택

{% for rl in role_levels %} - {% with role_code=rl.role.code|lower %} - # {{ rl.role.name }} - {% endwith %} + # {{ rl.role.name }} {% empty %}

등록된 스택이 없습니다.

{% endfor %} From f38b4ca0fb790840e2ab56c7cc88f287bf2f67e7 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 12:46:59 +0900 Subject: [PATCH 211/380] =?UTF-8?q?fix:=20deploy.yml=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bbb488b..7229b30 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,6 +29,8 @@ jobs: context: . push: true tags: ghcr.io/pirogramming/startlinedev/web:latest + # 빌드 시 캐시로 인한 0바이트 파일 생성을 방지하고 싶다면 아래 옵션을 추가할 수 있습니다. + # no-cache: true deploy: needs: build-and-push @@ -45,10 +47,20 @@ jobs: # 서버에서도 GHCR 이미지를 받을 수 있게 로그인 echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - # 최신 이미지 가져오고 컨테이너 재실행 + # 1. 기존의 태그 이미지(Dangling Images)를 정리하여 용량 확보 및 이미지 꼬임 방지 + docker image prune -f + + # 2. 최신 이미지 가져오기 docker-compose pull web + + # 3. 컨테이너 재실행 docker-compose up -d web - # 후처리 작업 (DB 동기화 및 정적파일 수집) + # 4. 배포 직후 다시 한번 정리 (선택사항이지만 서버를 깨끗하게 유지해줍니다) + docker image prune -f + + # 5. 후처리 작업 (DB 동기화 및 정적파일 수집) + # 컨테이너가 뜰 시간을 잠깐 주기 위해 sleep을 넣는 것도 안정적입니다. + sleep 3 docker-compose exec -T web python manage.py migrate docker-compose exec -T web python manage.py collectstatic --noinput \ No newline at end of file From 3626fd63c7de7bd1f2c9731177ba3eab4f06f014 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 12:48:13 +0900 Subject: [PATCH 212/380] =?UTF-8?q?test:=20=EC=84=9C=EB=B2=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EB=B0=B0=ED=8F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mypage.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/mypage.css b/static/css/mypage.css index a38aa23..56759a9 100644 --- a/static/css/mypage.css +++ b/static/css/mypage.css @@ -8,7 +8,7 @@ } .profile-card { - background-color: #ffffff; + background-color: #f81d1d; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); overflow: visible; From 5af950622c628096fbbdebdd8b49725fa1e2a86b Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 12:52:56 +0900 Subject: [PATCH 213/380] =?UTF-8?q?test:=20css=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=90=EB=8F=99=EB=B0=B0=ED=8F=AC=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mypage.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/mypage.css b/static/css/mypage.css index 56759a9..013a044 100644 --- a/static/css/mypage.css +++ b/static/css/mypage.css @@ -8,7 +8,7 @@ } .profile-card { - background-color: #f81d1d; + background-color: #fff; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); overflow: visible; From 869260769aa477f9b305430486b3d7e3919861f7 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 13:55:59 +0900 Subject: [PATCH 214/380] =?UTF-8?q?fix=20:=20main.html=20KITUP=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main.html | 50 +++++++++++---------------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/templates/main.html b/templates/main.html index d3ef63e..e3ba3c8 100644 --- a/templates/main.html +++ b/templates/main.html @@ -162,52 +162,26 @@

팀 매칭 결과가 발표됐어요.

KITUP 프로젝트

- -

진행된 KITUP 프로젝트가 없습니다.

- - - + {% endfor %} +
+ {% else %} +

진행된 KITUP 프로젝트가 없습니다.

+ {% endif %}
{% endblock %} \ No newline at end of file From 45465305ada0bd058bd9025fe4aa19a5c87bb01a Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Sat, 7 Feb 2026 15:01:56 +0900 Subject: [PATCH 215/380] =?UTF-8?q?feat:=20kitup=5Fdetail.html=20=EC=99=84?= =?UTF-8?q?=EC=84=B1.=20=EC=A6=90=EA=B2=A8=EC=B0=BE=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=EC=99=80=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20TODO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 4 + static/css/kitup.css | 302 ++++++++++++++++++++++----- static/js/kitup.js | 2 + templates/projects/kitup_detail.html | 116 ++++++++++ templates/projects/kitup_list.html | 71 +------ 5 files changed, 381 insertions(+), 114 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index 3f9c801..36b30d9 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -215,12 +215,15 @@ def project_detail(request, project_id): def kitup_list(request): """모든 KITUP 프로젝트 리스트 (완료된 보관 프로젝트)""" # 보관된 프로젝트만 조회 (ARCHIVED 상태) + # TODO 정렬 기능을 위한 GET 설정 + projects = Project.objects.filter( status=Project.Status.ARCHIVED ).select_related('team').order_by('-created_at') context = { "projects": projects, + # TODO 팀 멤버 조회하게 넘겨주기 } return render(request, "projects/kitup_list.html", context) @@ -235,6 +238,7 @@ def kitup_detail(request, project_id): return render(request, "projects/kitup_detail.html", context) +# TODO 즐겨찾기 토글을 위한 POST 뷰 추가 # ================================ # 팀 매칭 관리 API diff --git a/static/css/kitup.css b/static/css/kitup.css index 107404e..f796aee 100644 --- a/static/css/kitup.css +++ b/static/css/kitup.css @@ -1,5 +1,6 @@ /* static/css/kitup.css */ +/* 공통 배경/컨테이너 */ .kitup-page { background: #f6f8fc; min-height: 100vh; @@ -7,19 +8,19 @@ .kitup-container { max-width: 1200px; - width: 85%; margin: 0 auto; padding: 48px 16px 80px; } -/* 제목 */ +/* ========================= + LIST (기존) + ========================= */ .kitup-title { font-size: 28px; font-weight: 700; margin-bottom: 12px; } -/* 정렬 */ .kitup-sort { margin-bottom: 32px; font-size: 14px; @@ -40,14 +41,12 @@ margin: 0 6px; } -/* 카드 그리드 */ .kitup-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; } -/* 카드 */ .kitup-card { display: flex; flex-direction: column; @@ -65,7 +64,6 @@ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08); } -/* 이미지 영역 */ .kitup-card-imgwrap { width: 100%; height: 180px; @@ -78,7 +76,6 @@ object-fit: cover; } -/* 카드 내용 */ .kitup-card-body { padding: 16px 18px 18px; display: flex; @@ -98,56 +95,222 @@ line-height: 1.4; } -/* 좋아요 */ -.kitup-card-like { - margin-top: auto; - display: flex; +.kitup-empty { + grid-column: 1 / -1; + padding: 80px 0; + text-align: center; + color: #9ca3af; + font-size: 15px; +} + +/* ========================= + DETAIL (1번: 카드형 섹션) + ========================= */ +.kitup-detail { + max-width: 980px; +} + +/* 히어로 이미지 카드 */ +.kitup-detail-hero { + margin-top: 8px; +} + +.kitup-detail-imgwrap { + position: relative; + width: 100%; + height: 320px; + border-radius: 18px; + overflow: hidden; + background: #eef2f7; + box-shadow: 0 10px 26px rgba(0, 0, 0, 0.06); +} + +.kitup-detail-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* 우상단 하트 오버레이 */ +.kitup-detail-like { + position: absolute; + top: 14px; + right: 14px; + width: 42px; + height: 42px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.88); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.10); + display: inline-flex; + align-items: center; + justify-content: center; + position: absolute; + top: 14px; + right: 14px; + z-index: 9999; + pointer-events: auto; +} + +/* 제목/설명 */ +.kitup-detail-head { + margin-top: 18px; +} + +.kitup-detail-title { + font-size: 26px; + font-weight: 800; + line-height: 1.2; + margin: 0 0 10px; +} + +.kitup-detail-desc { + margin: 0; + font-size: 15px; + line-height: 1.6; + color: #4b5563; +} + +/* 섹션: 카드형 박스 */ +.kitup-detail-sections { + margin-top: 22px; + display: grid; + grid-template-columns: 1fr; + gap: 16px; +} + +.kitup-detail-section { + background: #ffffff; + border-radius: 16px; + padding: 18px 18px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05); +} + +.kitup-detail-h2 { + margin: 0 0 12px; + font-size: 16px; + font-weight: 700; +} + +/* 팀 메타 */ +.kitup-detail-meta { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 10px; +} + +.kitup-detail-meta li { + display: grid; + grid-template-columns: 90px 1fr; + gap: 12px; align-items: center; - gap: 6px; +} + +.kitup-detail-meta .k { + font-size: 13px; + color: #6b7280; +} + +.kitup-detail-meta .v { font-size: 14px; color: #111827; } -.kitup-heart { - font-size: 16px; +/* 멤버 목록 */ +.kitup-members { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; } -/* empty */ -.kitup-empty { - grid-column: 1 / -1; - padding: 80px 0; - text-align: center; - color: #9ca3af; - font-size: 15px; +.kitup-member { + border: 1px solid #eef2f7; + border-radius: 12px; + padding: 12px 12px; + background: #fbfdff; } -/* 반응형 */ -@media (max-width: 1024px) { - .kitup-grid { - grid-template-columns: repeat(2, 1fr); - } +.kitup-member-role { + font-size: 12px; + color: #2563eb; + font-weight: 700; + margin-bottom: 6px; } -@media (max-width: 640px) { - .kitup-grid { - grid-template-columns: 1fr; - } +.kitup-member-name { + font-size: 14px; + font-weight: 600; + color: #111827; +} - .kitup-title { - font-size: 22px; - } +/* 링크 */ +.kitup-links { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 10px; } +.kitup-links a { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 999px; + background: #eff6ff; + color: #2563eb; + text-decoration: none; + font-size: 13px; + font-weight: 600; +} + +.kitup-links a:hover { + background: #dbeafe; +} + +/* 팀 규칙 */ +.kitup-rules { + margin: 0; + padding: 14px; + border-radius: 12px; + background: #f8fafc; + border: 1px solid #eef2f7; + font-size: 13px; + line-height: 1.6; + color: #111827; + white-space: pre-wrap; +} + +/* ========================= + HEART (SVG toggle) + ========================= */ .kitup-like-btn { - border: 0; - background: transparent; - padding: 0; - cursor: pointer; + appearance: none; + -webkit-appearance: none; + border: none !important; + background: transparent !important; + padding: 0 !important; + margin: 0; line-height: 0; + box-shadow: none !important; display: inline-flex; align-items: center; + justify-content: center; +} + +.kitup-like-btn:focus { + outline: none; } +.kitup-heart-icon { + display: block; + background: transparent; +} + + .kitup-heart-icon { width: 20px; height: 20px; @@ -160,25 +323,66 @@ transform: scale(1.08); } -/* 기본: outline만 보이게 */ -.kitup-heart-icon.is-fill { display: none; } -.kitup-heart-icon.is-outline { display: inline-block; } +.kitup-heart-icon.is-fill { + display: none; +} -/* 눌림: fill만 보이게 + 빨강 */ .kitup-like-btn[aria-pressed="true"] .kitup-heart-icon { color: #ef4444; } -.kitup-like-btn[aria-pressed="true"] .kitup-heart-icon.is-fill { display: inline-block; } -.kitup-like-btn[aria-pressed="true"] .kitup-heart-icon.is-outline { display: none; } - +.kitup-like-btn[aria-pressed="true"] .kitup-heart-icon.is-fill { + display: inline-block; +} +.kitup-like-btn[aria-pressed="true"] .kitup-heart-icon.is-outline { + display: none; +} -/* sr-only (없으면 추가) */ +/* sr-only */ .sr-only { position: absolute; - width: 1px; height: 1px; - padding: 0; margin: -1px; - overflow: hidden; clip: rect(0, 0, 0, 0); - white-space: nowrap; border: 0; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ========================= + Responsive + ========================= */ +@media (max-width: 1024px) { + .kitup-grid { + grid-template-columns: repeat(2, 1fr); + } + + .kitup-members { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .kitup-grid { + grid-template-columns: 1fr; + } + + .kitup-title { + font-size: 22px; + } + + .kitup-detail-imgwrap { + height: 240px; + } + + .kitup-members { + grid-template-columns: 1fr; + } + + .kitup-detail-meta li { + grid-template-columns: 72px 1fr; + } } diff --git a/static/js/kitup.js b/static/js/kitup.js index daa7d73..6ff9d6b 100644 --- a/static/js/kitup.js +++ b/static/js/kitup.js @@ -1,8 +1,10 @@ // static/js/kitup.js + document.addEventListener("click", (e) => { const btn = e.target.closest("[data-like-btn]"); if (!btn) return; + // 카드 클릭 방지 e.preventDefault(); e.stopPropagation(); diff --git a/templates/projects/kitup_detail.html b/templates/projects/kitup_detail.html index e69de29..e6574d1 100644 --- a/templates/projects/kitup_detail.html +++ b/templates/projects/kitup_detail.html @@ -0,0 +1,116 @@ +{# templates/projects/kitup_detail.html #} +{% extends "base.html" %} +{% load static %} + +{% block title %}{{ project.title }}{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} + + +{% endblock %} + diff --git a/templates/projects/kitup_list.html b/templates/projects/kitup_list.html index 87da8a9..f573067 100644 --- a/templates/projects/kitup_list.html +++ b/templates/projects/kitup_list.html @@ -39,7 +39,12 @@

{{ project.title }}

{# 좋아요: 현재 모델/뷰에 “좋아요 개수”가 없음. 일단 UI만 만들어둠 #}
-
From f923261babc5648f5dc9c5c23195c09076439c2e Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 15:57:55 +0900 Subject: [PATCH 216/380] =?UTF-8?q?fix=20:=20dashboard=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EB=B0=8F=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 106 ++++++++++++++++++++------- templates/projects/dashboard.html | 117 ++++++++++++++++-------------- templates/teams/team_apply.html | 1 - 3 files changed, 141 insertions(+), 83 deletions(-) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index fdb9a20..ea9ab85 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -38,6 +38,7 @@ .dashboard { width: 85%; + max-width: 1400px; margin: 0 auto; text-align: center; } @@ -80,7 +81,8 @@ } .d_service > img { - width: 500px; + max-width: 500px; + width: 100%; } .d_service > h3 { @@ -93,6 +95,10 @@ font-size: 24px; } +.project_image { + border-radius: 25px; +} + /* 진행기간 & 즐겨찾기 */ .d_period { display: flex; @@ -202,49 +208,80 @@ hr { margin-right: 5px; } +/* 팀원 카드 컨테이너 - 가로 스크롤 */ .t_member { + width: 100%; margin-top: 20px; + padding: 20px 0; display: flex; - justify-content: center; - gap: 15px + gap: 15px; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; } +/* 스크롤바 스타일링 */ +.t_member::-webkit-scrollbar { + height: 8px; +} + +/* 팀원 카드 */ .t_member > .member { - position: relative; background: #fff; box-shadow: 1px 2px 2px 1px #ccc; padding: 15px 25px; - width: 200px; + min-width: 200px; + max-width: 200px; text-align: center; border-radius: 20px; + flex-shrink: 0; } -.member > .profile { - width: 100px; height: 100px; +.member > .profile_section { + position: relative; + width: 100%; + height: 100px; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 15px; +} + +.member > .profile_section > .profile { + width: 100px; + height: 100px; + border-radius: 50%; + object-fit: cover; } -.member > .level { - width: 35px; height: 35px; +.member > .profile_section > .level { + width: 35px; + height: 35px; position: absolute; - top: 90px; right: 35px; + bottom: 0; + right: calc(50% - 65px); } .member > h3 { margin-top: 15px; margin-bottom: 15px; - font-size: 20px; font-weight: 550; + font-size: 20px; + font-weight: 550; } .member > .info { text-align: start; - font-size: 16px; + font-size: 14px; + word-break: break-all; + overflow-wrap: break-word; } .member > .role_design { font-size: 16px; margin: 15px auto; padding: 5px 0; - width: 70%; height: 30px; + width: 60%; + height: 30px; border: 1px solid #00B9B0; border-radius: 20px; color: #00B9B0; @@ -255,7 +292,8 @@ hr { font-size: 16px; margin: 15px auto; padding: 5px 0; - width: 70%; height: 30px; + width: 70%; + height: 30px; border: 1px solid #FFCE53; border-radius: 20px; color: #FFCE53; @@ -266,7 +304,8 @@ hr { font-size: 16px; margin: 15px auto; padding: 5px 0; - width: 70%; height: 30px; + width: 70%; + height: 30px; border: 1px solid #FF3E88; border-radius: 20px; color: #FF3E88; @@ -285,12 +324,14 @@ hr { } .r_title > img { - width: 40px; height: 40px; + width: 40px; + height: 40px; margin-right: 7px; } .r_title > h3 { - font-weight: 600; font-size: 22px; + font-weight: 600; + font-size: 22px; margin-top: 2px; } @@ -316,12 +357,14 @@ hr { } .l_title > img { - width: 40px; height: 40px; + width: 40px; + height: 40px; margin-right: 7px; } .l_title > h3 { - font-weight: 600; font-size: 22px; + font-weight: 600; + font-size: 22px; margin-top: 2px; } @@ -347,40 +390,49 @@ hr { } .p_title > img { - width: 50px; height: 50px; + width: 50px; + height: 50px; } .p_title > h3 { - font-weight: 600; font-size: 22px; + font-weight: 600; + font-size: 22px; margin-top: 2px; } .p_content { position: relative; - margin: 5px 0 100px 47px ; + margin: 5px 0 100px 47px; } .p_bar { - width: 100%; height: 20px; + width: 100%; + height: 20px; background: #ccc; border-radius: 20px; + position: relative; + overflow: hidden; } .p_real { position: absolute; top: 0; - width: 60%; height: 20px; + left: 0; + height: 100%; background: #5A88FF; border-radius: 20px; + transition: width 0.3s ease; } button { - width: 200px; height: 40px; + width: 200px; + height: 40px; background: #4272EF; border: none; border-radius: 20px; color: #fff; - font-size: 18px; font-weight: 500; + font-size: 18px; + font-weight: 500; text-align: center; margin-bottom: 200px; } @@ -389,4 +441,4 @@ button:hover { cursor: pointer; background: #1F4CC0; transition: 0.3s ease; -} +} \ No newline at end of file diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index a55f49e..b6b2a01 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -16,13 +16,23 @@

진행 중인 프로젝트가 없어요.

+ {% if project.project_image %} + {{ project.title }} + {% else %} 서비스이미지 -

{서비스명}

-

{서비스는 어떤 문제를 해결하기 위한 어떤 서비스입니다.}

+ {% endif %} +

{{ project.title }}

+

{{ project.description|default:"프로젝트 설명이 없습니다." }}

달력이미지 -

진행기간 2026/01/25 ~ 2026/02/19

+

진행기간 + {% if project.starts_at and project.ends_at %} + {{ project.starts_at|date:"Y/m/d" }} ~ {{ project.ends_at|date:"Y/m/d" }} + {% else %} + 미정 + {% endif %} +

@@ -30,9 +40,11 @@

{서비스명}

즐겨찾기

-

즐겨찾기 컨텐츠 1

-

즐겨찾기 컨텐츠 2

-

즐겨찾기 컨텐츠 3

+ {% if project.is_favorite %} +

⭐ 즐겨찾기에 등록됨

+ {% else %} +

즐겨찾기가 없습니다.

+ {% endif %}

@@ -51,63 +63,58 @@

Team

{% if member.role.name == "기획자" %} {{ member.user.username }} {% endif %} - {% endfor %}user1

+ {% endfor %}

FE {% for member in members %} {% if member.role.name == "프론트엔드" %} {{ member.user.username }} {% endif %} - {% endfor %} - user2 user3

+ {% endfor %} +

BE {% for member in members %} {% if member.role.name == "백엔드" %} {{ member.user.username }} {% endif %} - {% endfor %} - user4, user4

+ {% endfor %} +

- + {% for member in members %}
+
+ + {% if member.user.profile_image %} + 프로필사진 + {% else %} 프로필사진 - 레벨사진 -

user1

-

✉️ user1@naver.com

-

🖥️ @user

+ {% endif %} + + {% with level=member.user.get_role_level %} + {% if level == 1 %} + 레벨1 + {% elif level == 2 %} + 레벨2 + {% elif level == 3 %} + 레벨3 + {% elif level == 4 %} + 레벨4 + {% endif %} + {% endwith %} +
+

{{ member.user.nickname|default:member.user.username }}

+

✉️ {{ member.user.email }}

+ {% if member.user.github_id %} +

🖥️ @{{ member.user.github_id }}

+ {% endif %} + {% if member.role.code == "PM" %}

기획자

-
-
- 프로필사진 - 레벨사진 -

user1

-

✉️ user1@naver.com

-

🖥️ @user

+ {% elif member.role.code == "FRONTEND" %}

프론트엔드

-
-
- 프로필사진 - 레벨사진 -

user1

-

✉️ user1@naver.com

-

🖥️ @user

+ {% elif member.role.code == "BACKEND" %}

백엔드

+ {% endif %}
-
- 프로필사진 - 레벨사진 -

user1

-

✉️ user1@naver.com

-

🖥️ @user

-

기획자

-
-
- 프로필사진 - 레벨사진 -

user1

-

✉️ user1@naver.com

-

🖥️ @user

-

기획자

-
+ {% endfor %}
@@ -117,11 +124,11 @@

user1

팀 규칙

-

1. 규칙 1번입니다.

-

2. 규칙 2번입니다.

-

3. 규칙 3번입니다.

-

4. 규칙 4번입니다.

-

5. 규칙 5번입니다.

+ {% if project.team_rules %} + {{ project.team_rules|linebreaks }} + {% else %} +

팀 규칙이 없습니다.

+ {% endif %}
-

Notion :

-

Figma :

-

Github :

+

Notion : {{ project.related_links.notion }}

+

Figma : {{ project.related_links.figma }}

+

Github : {{ project.related_links.github }}

@@ -145,7 +152,7 @@

진척도

- + {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index 5f6d28c..06b4908 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -114,7 +114,6 @@

WEB 백엔드

{% endif %} - {% else %}

From 1b6d643c444df915d2f60c495017132b32c78939 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 16:25:52 +0900 Subject: [PATCH 217/380] =?UTF-8?q?fix=20:=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20views.py=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 11 +++++++++++ static/css/dashboard.css | 16 ++++++++++++---- templates/projects/dashboard.html | 30 ++++++++++++++---------------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index 36b30d9..315b793 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -23,6 +23,16 @@ def _get_project_context(project, user): members = team.members.filter(is_active=True).select_related("user", "role") member_count_by_role = team.get_member_count_by_role() + # 각 멤버에 레벨 정보 추가 + members_with_level = [] + for member in members: + member_data = { + 'member': member, + 'level': member.user.get_role_level(member.role.code) + } + members_with_level.append(member_data) + + # 시즌 정보 season = None active_season = Season.get_active_season() @@ -63,6 +73,7 @@ def _get_project_context(project, user): "project": project, "team": team, "members": members, + "members_with_level": members_with_level, "member_count_by_role": member_count_by_role, "season": season, "guide_progress": guide_progress, diff --git a/static/css/dashboard.css b/static/css/dashboard.css index ea9ab85..a8ee8a9 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -227,11 +227,13 @@ hr { /* 팀원 카드 */ .t_member > .member { + position: relative; background: #fff; box-shadow: 1px 2px 2px 1px #ccc; padding: 15px 25px; min-width: 200px; max-width: 200px; + min-height: 300px; text-align: center; border-radius: 20px; flex-shrink: 0; @@ -259,7 +261,7 @@ hr { height: 35px; position: absolute; bottom: 0; - right: calc(50% - 65px); + right: calc(50% - 55px); } .member > h3 { @@ -277,10 +279,12 @@ hr { } .member > .role_design { + position: absolute; + bottom: 15px; right: 50px; font-size: 16px; margin: 15px auto; padding: 5px 0; - width: 60%; + width: 50%; height: 30px; border: 1px solid #00B9B0; border-radius: 20px; @@ -289,10 +293,12 @@ hr { } .member > .role_frontend { + position: absolute; + bottom: 15px; right: 40px; font-size: 16px; margin: 15px auto; padding: 5px 0; - width: 70%; + width: 60%; height: 30px; border: 1px solid #FFCE53; border-radius: 20px; @@ -301,10 +307,12 @@ hr { } .member > .role_backend { + position: absolute; + bottom: 15px; right: 50px; font-size: 16px; margin: 15px auto; padding: 5px 0; - width: 70%; + width: 50%; height: 30px; border: 1px solid #FF3E88; border-radius: 20px; diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index b6b2a01..aaae27b 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -79,38 +79,36 @@

Team

{% endfor %}

- {% for member in members %} + {% for item in members_with_level %}
- {% if member.user.profile_image %} - 프로필사진 + {% if item.member.user.profile_image %} + 프로필사진 {% else %} 프로필사진 {% endif %} - {% with level=member.user.get_role_level %} - {% if level == 1 %} + {% if item.level == 1 %} 레벨1 - {% elif level == 2 %} + {% elif item.level == 2 %} 레벨2 - {% elif level == 3 %} + {% elif item.level == 3 %} 레벨3 - {% elif level == 4 %} + {% elif item.level == 4 %} 레벨4 {% endif %} - {% endwith %}
-

{{ member.user.nickname|default:member.user.username }}

-

✉️ {{ member.user.email }}

- {% if member.user.github_id %} -

🖥️ @{{ member.user.github_id }}

+

{{ item.member.user.nickname|default:item.member.user.username }}

+

✉️ {{ item.member.user.email }}

+ {% if item.member.user.github_id %} +

🖥️ @{{ item.member.user.github_id }}

{% endif %} - {% if member.role.code == "PM" %} + {% if item.member.role.code == "PM" %}

기획자

- {% elif member.role.code == "FRONTEND" %} + {% elif item.member.role.code == "FRONTEND" %}

프론트엔드

- {% elif member.role.code == "BACKEND" %} + {% elif item.member.role.code == "BACKEND" %}

백엔드

{% endif %}
From 42d17fccbd95fe1a474982d0546328cbcaf76004 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 16:34:53 +0900 Subject: [PATCH 218/380] =?UTF-8?q?fix=20:=20main=20views.py=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/views.py b/config/views.py index fb01c79..5c2d410 100644 --- a/config/views.py +++ b/config/views.py @@ -17,7 +17,10 @@ def main_view(request): """ user = request.user - context = {} + season = Season.get_active_season() + context = { + 'season': season, + } # 로그인 상태만 추가 데이터 조회 if user.is_authenticated: From 0dc48dd5e43cff24700bd4471b25d894ee229cc8 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 17:21:52 +0900 Subject: [PATCH 219/380] =?UTF-8?q?fix=20:=20team.html=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EB=B0=8F=20=EB=B6=84=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team.css | 10 +++- templates/teams/team.html | 100 ++++++++++++++++++-------------- templates/teams/team_apply.html | 2 +- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/static/css/team.css b/static/css/team.css index 76885ac..f53ea50 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -29,11 +29,15 @@ body { box-shadow: 0 2px 2px 0 #999; } -.t_member .profile_img { +.profile_section { width: 120px; height: 120px; } -.t_member .level_img { +.t_member > .profile_section > .profile_img { + width: 120px; height: 120px; +} + +.t_member >.profile_section> .level_img { width: 40px; height: 40px; position: absolute; top: 100px; right: 30px; @@ -44,7 +48,7 @@ body { font-size: 18px; font-weight: 500; } -.t_member .u_stack { +.t_member .u_frontend { margin: 10px auto 0; width: 80%; padding: 5px 10px; diff --git a/templates/teams/team.html b/templates/teams/team.html index f31a444..4c4f963 100644 --- a/templates/teams/team.html +++ b/templates/teams/team.html @@ -8,54 +8,64 @@

내 프로젝트에서 업데이트 된 팀 정보를 확인해보세요.

+ {% for item in team_members %}
- 프로필사진 - level -

user1 (Lv3)

-

프론트엔드

+
+ + {% if item.user.profile_image %} + 프로필사진 + {% else %} + 프로필사진 + {% endif %} + + {% if item.level == 1 %} + 레벨1 + {% elif item.level == 2 %} + 레벨2 + {% elif item.level == 3 %} + 레벨3 + {% elif item.level == 4 %} + 레벨4 + {% endif %} +
+

{{ item.user.nickname }} (Lv{{ item.level }})

+ {% if item.role.code == "PM" %} +

기획자

+ {% elif item.role.code == "FRONTEND" %} +

프론트엔드

+ {% elif item.role.code == "BACKEND" %} +

백엔드

+ {% endif %}
-
- 프로필사진 - level -

user1 (Lv3)

-

프론트엔드

-
-
- 프로필사진 - level -

user1 (Lv3)

-

프론트엔드

-
-
- 프로필사진 - level -

user1 (Lv3)

-

프론트엔드

-
-
- 프로필사진 - level -

user1 (Lv3)

-

프론트엔드

-
+ {% endfor %}
-

내 프로젝트로 이동

+

내 프로젝트로 이동

- + From dbe76bad587e343a9b669c979e91c2b00754c82f Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:12:34 +0900 Subject: [PATCH 223/380] =?UTF-8?q?fix:=20accounts/views.py,=20api=5Furls.?= =?UTF-8?q?py=20=EC=88=98=EC=A0=95=20-=20API=20=EC=95=A4=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EB=AF=B8=EB=93=B1=EB=A1=9D=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/api_urls.py | 1 + apps/accounts/views.py | 51 +++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/apps/accounts/api_urls.py b/apps/accounts/api_urls.py index e020f51..57b906e 100644 --- a/apps/accounts/api_urls.py +++ b/apps/accounts/api_urls.py @@ -5,4 +5,5 @@ path("check-username/", views.check_username, name="check_username"), path("check-email/", views.check_email, name="check_email"), path("check-nickname/", views.check_nickname, name="check_nickname"), + path("level-test/submit/", views.level_submit, name="level_submit"), ] diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 0b78672..fd670dd 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,3 +1,4 @@ +import json from django.urls import reverse from django.utils import timezone from django.contrib.auth.decorators import login_required @@ -5,7 +6,7 @@ from django.contrib.auth import logout from django.contrib import messages from django.http import HttpResponseBadRequest, JsonResponse -from django.views.decorators.http import require_GET +from django.views.decorators.http import require_GET, require_POST from .forms import OnboardingForm, ProfileUpdateForm from .models import Role, User, UserRoleLevel @@ -88,31 +89,39 @@ def level_test(request): return render(request, "account/level_test.html", context) @login_required +@require_POST def level_submit(request): """ - 레벨 테스트 결과 제출 처리 + 레벨 테스트 결과 제출 처리 (API) - - POST 요청으로 역할 코드(role_code)와 레벨(level)을 전달받음 + - POST 요청으로 track(역할 코드), level, total_score, answers를 JSON으로 받음 - UserRoleLevel 모델에 결과 저장 또는 업데이트 - - 제출 후 테스트 결과 페이지로 리다이렉트 + - JSON 응답으로 success 여부 반환 """ - if request.method != "POST": - return HttpResponseBadRequest("잘못된 요청입니다.") - - role_code = request.POST.get("role") - role = get_object_or_404(Role, code=role_code) - level = request.POST.get("level") - - UserRoleLevel.objects.update_or_create( - user=request.user, - role=role, - defaults={ - "level": int(level), - "last_diagnosed_at": timezone.now(), - }, - ) - - return redirect(f"{reverse('accounts:test_result')}?role={role_code}") + try: + data = json.loads(request.body) + role_code = data.get("track") + level = data.get("level") + + if not role_code or level is None: + return JsonResponse({"success": False, "error": "필수 데이터가 없습니다."}) + + role = get_object_or_404(Role, code=role_code) + + UserRoleLevel.objects.update_or_create( + user=request.user, + role=role, + defaults={ + "level": int(level), + "last_diagnosed_at": timezone.now(), + }, + ) + + return JsonResponse({"success": True}) + except json.JSONDecodeError: + return JsonResponse({"success": False, "error": "잘못된 JSON 형식입니다."}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) @login_required def test_result(request): From 69f09f7d0b27ac9030fe83afccde7134e73d4960 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:17:10 +0900 Subject: [PATCH 224/380] =?UTF-8?q?fix:=20level=5Ftest.html=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EC=86=A1=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/account/level_test.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/account/level_test.html b/templates/account/level_test.html index a1c6d93..5ee5951 100644 --- a/templates/account/level_test.html +++ b/templates/account/level_test.html @@ -468,7 +468,7 @@

진단이 완료되었습니다!

} // 서버로 결과 전송 - fetch('/api/level-test/submit/', { + fetch('/api/accounts/level-test/submit/', { method: 'POST', headers: { 'Content-Type': 'application/json', From f126357195bb7d7ff7645df0493a342d913cec01 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:21:20 +0900 Subject: [PATCH 225/380] =?UTF-8?q?fix:=20level=5Ftest=EC=97=90=EC=84=9C?= =?UTF-8?q?=20result-score=20=EC=A0=9C=EA=B1=B0.=20test=EB=A7=8C=20?= =?UTF-8?q?=EB=84=98=EA=B2=A8=EB=8F=84=20=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/account/level_test.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/account/level_test.html b/templates/account/level_test.html index 5ee5951..c6f899a 100644 --- a/templates/account/level_test.html +++ b/templates/account/level_test.html @@ -496,7 +496,6 @@

진단이 완료되었습니다!

document.getElementById('result-track').textContent = trackNames[currentTrack]; document.getElementById('result-level').textContent = level; - document.getElementById('result-score').textContent = totalScore; document.getElementById('result-description').textContent = description; document.getElementById('questions-screen').style.display = 'none'; From dfc11dced7d3a333ac3cb5cdbabf6f2d9df6d01d Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 20:27:26 +0900 Subject: [PATCH 226/380] =?UTF-8?q?fix=20:=20team.css=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 1 - static/css/team.css | 103 +++++++++++++++++++++++++++++++++++--- templates/teams/team.html | 24 +++++++-- 3 files changed, 116 insertions(+), 12 deletions(-) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index a8ee8a9..9fbbb2a 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -220,7 +220,6 @@ hr { scroll-behavior: smooth; } -/* 스크롤바 스타일링 */ .t_member::-webkit-scrollbar { height: 8px; } diff --git a/static/css/team.css b/static/css/team.css index f53ea50..ff87954 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -2,6 +2,53 @@ body { background: #F6F8FF; } +/* 매칭 대기 */ +.team_matching h3 { + font-size: 27px; + font-weight: 600; + margin-bottom: 20px; +} + +.team_matching form { + display: flex; + justify-content: space-between; + align-items: center; + margin: 30px auto 0; + width: 60%; height: 40px; + background: #fff; + border-radius: 25px; + padding: 10px 5px 10px 20px; +} + +.team_matching form:focus-within { + border: 1px solid #4272EF; + box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); +} + +.team_matching form input { + width: 80%; + border: none; + font-size: 15px; + color: #888888; + outline: none; +} + +.team_matching form input::placeholder { + color: #888888; +} + +.team_matching form button { + font-size: 15px; + background: #4272EF; color: #fff; + border: none; border-radius: 20px; + padding: 7px 15px; +} + +.team_matching form button:hover { + background: #1F4CC0; + transition: 0.3s ease; +} + /* 매칭 성공 */ .team_success { margin-top: 50px; @@ -21,7 +68,7 @@ body { .t_member { position: relative; - width: 17%; + width: 16%; height: 250px; padding: 20px; background: #fff; @@ -30,32 +77,72 @@ body { } .profile_section { - width: 120px; height: 120px; + position: relative; + width: 100%; height: 110px; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 15px; } .t_member > .profile_section > .profile_img { - width: 120px; height: 120px; + width: 110px; height: 110px; + border-radius: 50%; } .t_member >.profile_section> .level_img { width: 40px; height: 40px; position: absolute; - top: 100px; right: 30px; + bottom: 0; + right: calc(50% - 60px); } .t_member .u_name { - margin-top: 20px; - font-size: 18px; font-weight: 500; + margin: 10px 0; + font-size: 16px; font-weight: 500; +} + +.t_member .u_name > span { + font-size: 14px; +} + +.t_member .u_design { + width: 45%; + padding: 5px 10px; + color: #00B9B0; + border-radius: 20px; + box-shadow: 1px 2px 2px 1px #00B9B0; + font-size: 16px; + position: absolute; + bottom: 25px; + left: 50%; + transform: translateX(-50%); } .t_member .u_frontend { - margin: 10px auto 0; - width: 80%; + width: 60%; padding: 5px 10px; color: #FFCE53; border-radius: 20px; box-shadow: 1px 2px 2px 1px #FFCE53; font-size: 16px; + position: absolute; + bottom: 25px; + left: 50%; + transform: translateX(-50%); +} + +.t_member .u_backend { + width: 45%; + padding: 5px 10px; + color: #FF3E88; + border-radius: 20px; + box-shadow: 1px 2px 2px 1px #FF3E88; + font-size: 16px; + position: absolute; + bottom: 25px; + left: 50%; + transform: translateX(-50%); } .go_project { diff --git a/templates/teams/team.html b/templates/teams/team.html index 4c4f963..6cee2db 100644 --- a/templates/teams/team.html +++ b/templates/teams/team.html @@ -1,7 +1,21 @@ {% extends 'base.html' %} {% load static %} {% block header %} {% endblock %} {% block content %} + +{% if is_matching_period %} +
+

+ 팀 매칭 신청이 완료되었어요.
+ 팀 매칭 결과가 발표되면 메일로 알려드릴게요. +

+
+ + +
+
+ +{% elif team_matched %}

팀 매칭이 완료 되었어요.
@@ -52,7 +66,8 @@

/> {% endif %}

-

{{ item.user.nickname }} (Lv{{ item.level }})

+

{{ item.user.nickname }}
+ Lv{{ item.level }}

{% if item.role.code == "PM" %}

기획자

{% elif item.role.code == "FRONTEND" %} @@ -67,10 +82,13 @@

>

내 프로젝트로 이동

+ - + +{% endif %} {% endblock %} From 0466f899019c23596425b5a1f92aa48e4f37bcaa Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:27:42 +0900 Subject: [PATCH 227/380] =?UTF-8?q?debug:=20=EB=A0=88=EB=B2=A8=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=EA=B2=B0=EA=B3=BC=20=EC=A0=80=EC=9E=A5=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EB=94=94=EB=B2=84?= =?UTF-8?q?=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/views.py | 15 +++++++++++++-- templates/account/level_test.html | 9 +++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index fd670dd..2d02447 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -103,10 +103,16 @@ def level_submit(request): role_code = data.get("track") level = data.get("level") + print(f"DEBUG: role_code={role_code}, level={level}") # 디버그 로그 + if not role_code or level is None: return JsonResponse({"success": False, "error": "필수 데이터가 없습니다."}) - role = get_object_or_404(Role, code=role_code) + try: + role = Role.objects.get(code=role_code) + print(f"DEBUG: role found - {role}") # 디버그 로그 + except Role.DoesNotExist: + return JsonResponse({"success": False, "error": f"역할을 찾을 수 없습니다: {role_code}"}) UserRoleLevel.objects.update_or_create( user=request.user, @@ -117,10 +123,15 @@ def level_submit(request): }, ) + print(f"DEBUG: 저장 완료 - user={request.user}, role={role}, level={level}") # 디버그 로그 return JsonResponse({"success": True}) - except json.JSONDecodeError: + except json.JSONDecodeError as e: + print(f"DEBUG: JSON 에러 - {e}") # 디버그 로그 return JsonResponse({"success": False, "error": "잘못된 JSON 형식입니다."}) except Exception as e: + print(f"DEBUG: 기타 에러 - {e}") # 디버그 로그 + import traceback + traceback.print_exc() return JsonResponse({"success": False, "error": str(e)}) @login_required diff --git a/templates/account/level_test.html b/templates/account/level_test.html index c6f899a..9be3478 100644 --- a/templates/account/level_test.html +++ b/templates/account/level_test.html @@ -122,7 +122,7 @@

진단이 완료되었습니다!

question: "7. 컴포넌트 단위로 UI를 나누어 만들어본 적이 있나요?", options: [ "컴포넌트 개념이 익숙하지 않다", - "튜토리얼에서 컴포넌트를 사용해봤다", + "강의 예제를 따라 컴포넌트를 만들어본 적이 있다", "재사용 가능한 컴포넌트를 만들어봤다", "프로젝트에서 컴포넌트 구조를 설계해봤다", "확장성과 유지보수를 고려한 컴포넌트 설계가 가능하다" @@ -468,6 +468,7 @@

진단이 완료되었습니다!

} // 서버로 결과 전송 + console.log('제출 데이터:', {track: currentTrack, level: level}); fetch('/api/accounts/level-test/submit/', { method: 'POST', headers: { @@ -481,8 +482,12 @@

진단이 완료되었습니다!

answers: answers }) }) - .then(response => response.json()) + .then(response => { + console.log('응답 상태:', response.status); + return response.json(); + }) .then(data => { + console.log('응답 데이터:', data); if (data.success) { // 트랙 완료 표시 document.getElementById('track-completed').value = 'true'; From d074d53123b85f637165f826d0958b44eee862b2 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 20:35:35 +0900 Subject: [PATCH 228/380] =?UTF-8?q?fix=20:=20team.css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/css/team.css b/static/css/team.css index ff87954..664d000 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -104,6 +104,7 @@ body { .t_member .u_name > span { font-size: 14px; + color: #999; } .t_member .u_design { From 57f440c7cfbbb1ababed8bcd2f492ed7b94e6e72 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:36:31 +0900 Subject: [PATCH 229/380] =?UTF-8?q?fix:=20main.html,=20team=5Fapply.html?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20-=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=20=EB=AC=B8=EB=B2=95=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main.html | 6 +++--- templates/teams/team_apply.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/main.html b/templates/main.html index e3ba3c8..80dd18f 100644 --- a/templates/main.html +++ b/templates/main.html @@ -36,7 +36,7 @@

팀 매칭 모집이 시작됐어요

WEB 기획

{% if user.is_authenticated %} - {% with pm_level=user.get_role_level|add:"PM" %} + {% with pm_level=role_levels.PM %} {% if pm_level == 1 %} Level1

Lv1

@@ -71,7 +71,7 @@

WEB 기획

WEB 프론트엔드

{% if user.is_authenticated %} - {% with fe_level=user.get_role_level|add:"FRONTEND" %} + {% with fe_level=role_levels.FRONTEND %} {% if fe_level == 1 %} Level1

Lv1

@@ -106,7 +106,7 @@

WEB 프론트엔드

WEB 백엔드

{% if user.is_authenticated %} - {% with be_level=user.get_role_level|add:"BACKEND" %} + {% with be_level=role_levels.BACKEND %} {% if be_level == 1 %} Level1

Lv1

diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index 8a18fb5..dc0054e 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -11,7 +11,7 @@

팀 매칭 모집 기간이 시작됐어요!

WEB 기획

{% if user.is_authenticated %} - {% with pm_level=user.get_role_level|add:"PM" %} + {% with pm_level=role_levels.PM %} {% if pm_level == 1 %} Level1

Lv1

@@ -46,7 +46,7 @@

WEB 기획

WEB 프론트엔드

{% if user.is_authenticated %} - {% with fe_level=user.get_role_level|add:"FRONTEND" %} + {% with fe_level=role_levels.FRONTEND %} {% if fe_level == 1 %} Level1

Lv1

@@ -81,7 +81,7 @@

WEB 프론트엔드

WEB 백엔드

{% if user.is_authenticated %} - {% with be_level=user.get_role_level|add:"BACKEND" %} + {% with be_level=role_levels.BACKEND %} {% if be_level == 1 %} Level1

Lv1

From 7968e525608fbf7cbe1a3ef1e2365c9998a0e9fb Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 20:42:02 +0900 Subject: [PATCH 230/380] =?UTF-8?q?feat:=20=ED=8C=80=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/urls.py | 6 ++++++ apps/teams/views.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/apps/teams/urls.py b/apps/teams/urls.py index ba86d5b..d4effd3 100644 --- a/apps/teams/urls.py +++ b/apps/teams/urls.py @@ -13,6 +13,12 @@ # 열정 테스트 (팀플 신청 시) path("passion-test/", views.passion_test, name="passion_test"), # passion_test.html + # 열정 테스트 결과 제출 + path("passion-submit/", views.passion_submit, name="passion_submit"), + + # 팀 매칭 신청 취소 + path("cancel/", views.team_matching_cancel, name="team_matching_cancel"), + # 팀 매칭 결과/대기 화면 path("status/", views.team_status, name="team_status"), # team.html ] diff --git a/apps/teams/views.py b/apps/teams/views.py index 9a50bc6..b1b0852 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -1,6 +1,7 @@ from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, render, redirect from django.contrib.auth.decorators import login_required +from django.contrib import messages from rest_framework import viewsets from django.utils import timezone @@ -112,6 +113,34 @@ def passion_submit(request): return redirect("teams:team_status") +@login_required +def team_matching_cancel(request): + """ + 팀 매칭 신청 취소 + + - 팀 매칭 기간 중에만 취소 가능 + - 프로젝트 기간이면 취소 불가능 + - 사용자의 TeamMember 레코드 삭제 + - passion_level을 NULL로 초기화 (다시 열정 테스트 강제) + """ + if request.method != "POST": + return HttpResponseBadRequest("잘못된 요청입니다.") + + season = Season.get_active_season() + + # 팀 매칭 기간이 아니면 취소 불가능 + if not season or not season.is_matching_period(): + messages.error(request, "❌ 팀 매칭 기간이 아닙니다. 취소할 수 없습니다.") + return redirect("teams:team_status") + + # 열정 레벨 초기화 + request.user.passion_level = None + request.user.save(update_fields=["passion_level"]) + + messages.success(request, "✅ 팀 매칭 신청이 취소되었습니다.") + return redirect("teams:team_apply") + + @login_required def team_status(request): """ From 9c03649311906877657c4548cc3c07bfa3519078 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:42:10 +0900 Subject: [PATCH 231/380] =?UTF-8?q?fix:=20teams/apply=EB=8F=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 2 +- templates/account/level_test.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/teams/views.py b/apps/teams/views.py index 9a50bc6..2943293 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -61,7 +61,7 @@ def team_apply(request): ) role_level_map = { - rl.role.code: rl + rl.role.code: rl.level for rl in role_levels } diff --git a/templates/account/level_test.html b/templates/account/level_test.html index 9be3478..f691c85 100644 --- a/templates/account/level_test.html +++ b/templates/account/level_test.html @@ -376,6 +376,7 @@

진단이 완료되었습니다!

//답변 초기화 window.addEventListener('DOMContentLoaded', function() { currentTrack = document.getElementById('selected-track').value; + console.log('DEBUG: currentTrack 설정됨 -', currentTrack); answers = new Array(10).fill(null); showQuestion(0); From 93c7cf9f122ea0da3e9476428450ea754934a2a0 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 20:49:34 +0900 Subject: [PATCH 232/380] =?UTF-8?q?fix=20:=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/main.html | 9 +++++++++ templates/teams/team_apply.html | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/templates/main.html b/templates/main.html index 80dd18f..aed34ce 100644 --- a/templates/main.html +++ b/templates/main.html @@ -40,12 +40,15 @@

WEB 기획

{% if pm_level == 1 %} Level1

Lv1

+ {% elif pm_level == 2 %} Level2

Lv2

+ {% elif pm_level == 3 %} Level3

Lv3

+ {% elif pm_level == 4 %} Level4

Lv4

@@ -75,12 +78,15 @@

WEB 프론트엔드

{% if fe_level == 1 %} Level1

Lv1

+ {% elif fe_level == 2 %} Level2

Lv2

+ {% elif fe_level == 3 %} Level3

Lv3

+ {% elif fe_level == 4 %} Level4

Lv4

@@ -110,12 +116,15 @@

WEB 백엔드

{% if be_level == 1 %} Level1

Lv1

+ {% elif be_level == 2 %} Level2

Lv2

+ {% elif be_level == 3 %} Level3

Lv3

+ {% elif be_level == 4 %} Level4

Lv4

diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index dc0054e..adb73d4 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -15,12 +15,15 @@

WEB 기획

{% if pm_level == 1 %} Level1

Lv1

+ {% elif pm_level == 2 %} Level2

Lv2

+ {% elif pm_level == 3 %} Level3

Lv3

+ {% elif pm_level == 4 %} Level4

Lv4

@@ -50,12 +53,15 @@

WEB 프론트엔드

{% if fe_level == 1 %} Level1

Lv1

+ {% elif fe_level == 2 %} Level2

Lv2

+ {% elif fe_level == 3 %} Level3

Lv3

+ {% elif fe_level == 4 %} Level4

Lv4

@@ -85,12 +91,15 @@

WEB 백엔드

{% if be_level == 1 %} Level1

Lv1

+ {% elif be_level == 2 %} Level2

Lv2

+ {% elif be_level == 3 %} Level3

Lv3

+ {% elif be_level == 4 %} Level4

Lv4

From 46ab84fc24c5b65e2c1883b97517f776d4155513 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 20:51:47 +0900 Subject: [PATCH 233/380] =?UTF-8?q?fix:=20=EC=97=B4=EC=A0=95=ED=8C=90?= =?UTF-8?q?=EB=B3=84=EC=84=A4=EB=AC=B8=20api=20=EC=95=A4=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/api_urls.py | 3 +- apps/teams/views.py | 54 +++++++++++++++++++++++++++++++ templates/teams/passion_test.html | 9 ++++-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/apps/teams/api_urls.py b/apps/teams/api_urls.py index 641cd09..32b89dd 100644 --- a/apps/teams/api_urls.py +++ b/apps/teams/api_urls.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import TeamViewSet, TeamMemberViewSet +from .views import TeamViewSet, TeamMemberViewSet, passion_submit_api router = DefaultRouter() router.register(r'teams', TeamViewSet, basename='team') @@ -8,4 +8,5 @@ urlpatterns = [ path('', include(router.urls)), + path('passion-test/submit/', passion_submit_api, name='passion_submit_api'), ] diff --git a/apps/teams/views.py b/apps/teams/views.py index 2943293..176c7a7 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -8,6 +8,10 @@ from rest_framework.decorators import action from drf_spectacular.utils import extend_schema, extend_schema_view +import json +from django.http import JsonResponse +from django.views.decorators.http import require_POST + from apps.accounts.models import Role, UserRoleLevel from apps.projects.models import Season @@ -112,6 +116,56 @@ def passion_submit(request): return redirect("teams:team_status") +@login_required +@require_POST +def passion_submit_api(request): + """ + 열정 테스트 결과 제출 처리 (API) + + - POST 요청으로 passion_level을 JSON으로 받음 + - User 모델에 열정 레벨 저장 + - JSON 응답으로 success 여부 반환 + """ + try: + data = json.loads(request.body) + passion_level = data.get("passion_level") + + if passion_level is None: + return JsonResponse({"success": False, "error": "필수 데이터가 없습니다."}) + + request.user.passion_level = int(passion_level) + request.user.save(update_fields=["passion_level"]) + + return JsonResponse({"success": True}) + except json.JSONDecodeError: + return JsonResponse({"success": False, "error": "잘못된 JSON 형식입니다."}) + except Exception as e: + print(f"DEBUG: 열정 테스트 저장 에러 - {e}") + import traceback + traceback.print_exc() + return JsonResponse({"success": False, "error": str(e)}) + + +@login_required +def passion_submit_old(request): + """ + 열정 테스트 결과 제출 처리 + + - POST 요청으로 열정 레벨(passion_level)을 전달받음 + - User 모델에 열정 레벨 저장 + - 제출 후 팀 매칭 결과 페이지로 리다이렉트 + """ + if request.method != "POST": + return HttpResponseBadRequest("잘못된 요청입니다.") + + passion_level = request.POST.get("passion_level") + + request.user.passion_level = int(passion_level) + request.user.save(update_fields=["passion_level"]) + + return redirect("teams:team_status") + + @login_required def team_status(request): """ diff --git a/templates/teams/passion_test.html b/templates/teams/passion_test.html index 7982128..90e1715 100644 --- a/templates/teams/passion_test.html +++ b/templates/teams/passion_test.html @@ -269,7 +269,8 @@

진단이 완료되었습니다!

} // 서버로 결과 전송 - fetch('/api/passion-test/submit/', { + console.log('제출 데이터:', {passion_level: passionLevel, total_score: totalScore}); + fetch('/api/teams/passion-test/submit/', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -282,8 +283,12 @@

진단이 완료되었습니다!

answers: answers }) }) - .then(response => response.json()) + .then(response => { + console.log('응답 상태:', response.status); + return response.json(); + }) .then(data => { + console.log('응답 데이터:', data); if (data.success) { // 결과 표시 (점수는 보이지 않음) document.getElementById('result-level').textContent = passionLevel; From 48cabc5c97044a874c51538679dbfd8eb16d1843 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 21:00:03 +0900 Subject: [PATCH 234/380] =?UTF-8?q?fix:=20=EC=97=B4=EC=A0=95=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=ED=8C=90=EB=B3=84=20=EC=84=A4=EB=AC=B8=20=EC=95=A4?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EB=93=B1=EB=A1=9D,=20?= =?UTF-8?q?=EC=88=98=EC=A2=85=EB=8B=98=EA=B3=BC=EC=9D=98=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/teams/views.py b/apps/teams/views.py index 5b0076f..de1bc25 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -97,6 +97,37 @@ def passion_test(request): return render(request, "teams/passion_test.html") +@login_required +@require_POST +def passion_submit_api(request): + """ + 열정 테스트 결과 제출 처리 (API) + + - POST 요청으로 passion_level을 JSON으로 받음 + - User 모델에 열정 레벨 저장 + - JSON 응답으로 success 여부 반환 + """ + try: + data = json.loads(request.body) + passion_level = data.get("passion_level") + + if passion_level is None: + return JsonResponse({"success": False, "error": "필수 데이터가 없습니다."}) + + request.user.passion_level = int(passion_level) + request.user.save(update_fields=["passion_level"]) + + print(f"DEBUG: 열정 테스트 저장 완료 - user={request.user}, passion_level={passion_level}") + return JsonResponse({"success": True}) + except json.JSONDecodeError: + return JsonResponse({"success": False, "error": "잘못된 JSON 형식입니다."}) + except Exception as e: + print(f"DEBUG: 열정 테스트 저장 에러 - {e}") + import traceback + traceback.print_exc() + return JsonResponse({"success": False, "error": str(e)}) + + @login_required def passion_submit(request): """ From e986dee1e3fc95e70e1f17f328c2b2f6d605d528 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Sat, 7 Feb 2026 21:20:23 +0900 Subject: [PATCH 235/380] =?UTF-8?q?fix:=20teams/views.py=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/teams/views.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/teams/views.py b/apps/teams/views.py index de1bc25..bd296bb 100644 --- a/apps/teams/views.py +++ b/apps/teams/views.py @@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages + from rest_framework import viewsets from django.utils import timezone from rest_framework.response import Response @@ -147,6 +148,33 @@ def passion_submit(request): return redirect("teams:team_status") +@login_required +def team_matching_cancel(request): + """ + 팀 매칭 신청 취소 + + - 팀 매칭 기간 중에만 취소 가능 + - 프로젝트 기간이면 취소 불가능 + - 사용자의 TeamMember 레코드 삭제 + - passion_level을 NULL로 초기화 (다시 열정 테스트 강제) + """ + if request.method != "POST": + return HttpResponseBadRequest("잘못된 요청입니다.") + + season = Season.get_active_season() + + # 팀 매칭 기간이 아니면 취소 불가능 + if not season or not season.is_matching_period(): + messages.error(request, "❌ 팀 매칭 기간이 아닙니다. 취소할 수 없습니다.") + return redirect("teams:team_status") + + # 열정 레벨 초기화 + request.user.passion_level = None + request.user.save(update_fields=["passion_level"]) + + messages.success(request, "✅ 팀 매칭 신청이 취소되었습니다.") + return redirect("teams:team_apply") + @login_required def team_status(request): From c2c4866b5da5c3081fd816a868c8187001ed2e4b Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 21:25:08 +0900 Subject: [PATCH 236/380] =?UTF-8?q?fix=20:=20mission=20=EC=BB=A8=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mission.css | 127 ++++++ static/js/mission.js | 108 ++++- templates/guides/mission.html | 821 ++++++++++++++++++++++++++++++++-- 3 files changed, 1017 insertions(+), 39 deletions(-) diff --git a/static/css/mission.css b/static/css/mission.css index d95f683..aa1e33a 100644 --- a/static/css/mission.css +++ b/static/css/mission.css @@ -305,4 +305,131 @@ .mission_card.completed { opacity: 0.8; +} + +/* 진척도 바 통합 스타일 */ +.role_progress_bar { + margin: 20px 0 15px 100px; + display: flex; + align-items: center; + gap: 15px; +} + +.role_progress_bar > h3 { + width: 30px; + font-size: 20px; + font-weight: 550; +} + +.progress_bar_container { + position: relative; + flex: 1; + height: 15px; + background: #DDDDDD; + border-radius: 20px; + overflow: hidden; +} + +.progress_bar_fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + border-radius: 20px; + transition: width 0.5s ease; +} + +.progress_bar_fill.pm_color { + background: #37D3BF; +} + +.progress_bar_fill.fe_color { + background: #FFDF6E; +} + +.progress_bar_fill.be_color { + background: #FF69A4; +} + +.progress_percent { + min-width: 45px; + font-size: 14px; + font-weight: 600; + color: #4272EF; +} + +/* 미션 설명 스타일 */ +.mission_description { + font-size: 14px; + color: #374151; + line-height: 1.6; + margin-bottom: 15px; +} + +.mission_description ul, +.mission_description ol { + margin-left: 20px; + margin-top: 10px; +} + +.mission_description li { + margin-bottom: 8px; +} + +.mission_description strong { + color: #4272EF; + font-weight: 600; +} + +.mission_description em { + color: #6B7280; + font-style: italic; +} + +/* 태스크 리스트 */ +.task_list { + margin-top: 15px; +} + +.task_item { + display: flex; + align-items: center; + padding: 8px 12px; + margin-bottom: 8px; + background: #F9FAFB; + border-radius: 8px; + transition: all 0.3s ease; +} + +.task_item:hover { + background: #F3F4F6; +} + +.task_item.completed { + opacity: 0.6; +} + +.task_item.completed label { + text-decoration: line-through; + color: #9CA3AF; +} + +.task_checkbox { + width: 18px; + height: 18px; + margin-right: 10px; + cursor: pointer; + accent-color: #4272EF; +} + +.task_item label { + flex: 1; + font-size: 14px; + color: #374151; + cursor: pointer; +} + +/* 기존 mission_input 제거 */ +.mission_input { + display: none; } \ No newline at end of file diff --git a/static/js/mission.js b/static/js/mission.js index 992a159..6d78f9d 100644 --- a/static/js/mission.js +++ b/static/js/mission.js @@ -1,14 +1,22 @@ -// mission.js - 수정된 버전 - document.addEventListener('DOMContentLoaded', function() { const missionCards = document.querySelectorAll('.mission_card'); + const totalMissions = missionCards.length; + + // 페이지 로드 시 진척도 계산 + updateProgress(); missionCards.forEach(card => { - card.addEventListener('click', function() { - const isActive = this.classList.contains('active'); - const missionItem = this.closest('.mission_item'); + const cardHeader = card.querySelector('.card_header'); + + // 카드 클릭 - 펼치기/접기 + cardHeader.addEventListener('click', function(e) { + // 체크 아이콘 클릭은 제외 + if (e.target.classList.contains('check_icon')) return; + + const isActive = card.classList.contains('active'); + const missionItem = card.closest('.mission_item'); - // 다른 모든 카드와 mission_item에서 active 제거 + // 다른 모든 카드 닫기 document.querySelectorAll('.mission_card').forEach(c => { c.classList.remove('active'); }); @@ -16,9 +24,9 @@ document.addEventListener('DOMContentLoaded', function() { item.classList.remove('active'); }); - // 클릭한 카드와 mission_item에 active 추가 + // 클릭한 카드 열기 if (!isActive) { - this.classList.add('active'); + card.classList.add('active'); missionItem.classList.add('active'); } }); @@ -33,14 +41,98 @@ document.addEventListener('DOMContentLoaded', function() { const isCompleted = card.classList.contains('completed'); if (isCompleted) { + // 완료 취소 card.classList.remove('completed'); missionItem.classList.remove('completed'); this.src = this.src.replace('check.png', 'nocheck.png'); } else { + // 완료 처리 card.classList.add('completed'); missionItem.classList.add('completed'); this.src = this.src.replace('nocheck.png', 'check.png'); } + + // 진척도 업데이트 + updateProgress(); + + // 로컬스토리지에 저장 + saveProgress(); }); }); + + // 진척도 계산 및 업데이트 + function updateProgress() { + const completedMissions = document.querySelectorAll('.mission_card.completed').length; + const percent = Math.round((completedMissions / totalMissions) * 100); + + // 현재 사용자 역할에 따라 해당 진척도 바 업데이트 + const userRole = getUserRole(); // PM, FRONTEND, BACKEND + + if (userRole === 'PM') { + updateProgressBar('pm', percent); + } else if (userRole === 'FRONTEND') { + updateProgressBar('fe', percent); + } else if (userRole === 'BACKEND') { + updateProgressBar('be', percent); + } + } + + // 진척도 바 업데이트 + function updateProgressBar(role, percent) { + const progressBar = document.getElementById(`${role}_progress`); + const percentText = document.getElementById(`${role}_percent`); + + if (progressBar && percentText) { + progressBar.style.width = `${percent}%`; + percentText.textContent = `${percent}%`; + } + } + + // 현재 사용자 역할 가져오기 + function getUserRole() { + // HTML에서 역할 정보를 data 속성으로 넣어야 함 + const body = document.body; + return body.dataset.userRole || 'PM'; + } + + // 로컬스토리지에 진행 상황 저장 + function saveProgress() { + const completedMissions = []; + document.querySelectorAll('.mission_card.completed').forEach((card, index) => { + const missionNumber = card.closest('.mission_item').dataset.number; + completedMissions.push(missionNumber); + }); + + const userRole = getUserRole(); + localStorage.setItem(`mission_progress_${userRole}`, JSON.stringify(completedMissions)); + } + + // 로컬스토리지에서 진행 상황 불러오기 + function loadProgress() { + const userRole = getUserRole(); + const saved = localStorage.getItem(`mission_progress_${userRole}`); + + if (saved) { + const completedMissions = JSON.parse(saved); + + completedMissions.forEach(number => { + const missionItem = document.querySelector(`.mission_item[data-number="${number}"]`); + if (missionItem) { + const card = missionItem.querySelector('.mission_card'); + const checkIcon = missionItem.querySelector('.check_icon'); + + card.classList.add('completed'); + missionItem.classList.add('completed'); + if (checkIcon) { + checkIcon.src = checkIcon.src.replace('nocheck.png', 'check.png'); + } + } + }); + + updateProgress(); + } + } + + // 페이지 로드 시 저장된 진행 상황 불러오기 + loadProgress(); }); \ No newline at end of file diff --git a/templates/guides/mission.html b/templates/guides/mission.html index f61d1f4..f8229e3 100644 --- a/templates/guides/mission.html +++ b/templates/guides/mission.html @@ -1,52 +1,76 @@ -{% extends 'base.html' %} {% load static %} {% block header %} +{% extends 'base.html' %} +{% load static %} +{% block header %} -{% endblock %} {% block content %} +{% endblock %} +{% block content %} + +{% if not project %} +
+

진행 중인 프로젝트가 없습니다.

+
+{% else %} +
-

DASHBOARD

+

DASHBOARD

MISSION

+ +
rocket
-

{서비스명}의 진척도

+

{{ project.title }}의 진척도

*파트별로 진행 속도를 맞추는 것을 권장드려요!

-
+ +

PM

-
-

-
+
+
+
+ 0%
-
+ +

FE

-
-

-
+
+
+
+ 0%
-
+ +

BE

-
-

-
+
+
+
+ 0%
+ + +{% if role.code == "PM" %} +
1
-
+
-

Github repository 생성하기

+

팀 커뮤니케이션 채널 개설

체크
- +
    +
  • 1. 디스코드/슬랙 등 팀플용 채널 생성
  • +
  • 2. 팀원 이메일로 초대 링크 공유
  • +
+

(검색 키워드: 디스코드 서버 만들기 / 슬랙 워크스페이스 만들기)

@@ -58,14 +82,17 @@

Github repository 생성하기

-
+
-

Github repository에 팀원 초대하기

+

문서 협업 툴 준비

체크
-

(팀트랙 or 유튜브 제목)

- +
    +
  • 1. 노션/구글독스 계정 생성
  • +
  • 2. 팀 회의록 페이지 생성 및 공유
  • +
+

(검색 키워드: 노션 회의록 템플릿)

@@ -77,13 +104,17 @@

Github repository에 팀원 초대하기

-
+
-

Github branch 파기

+

첫 팀 미팅 진행

체크
- +
    +
  • 1. 자기소개
  • +
  • 2. 프로젝트 참여 목적 공유
  • +
  • 3. 팀 규칙 간단히 정하기
  • +
@@ -95,18 +126,746 @@

Github branch 파기

-
+
+
+

기본 운영 규칙 합의

+ 체크 +
+
+
    +
  • 1. 정기 회의 주기
  • +
  • 2. 스크럼(데일리/주간) 여부
  • +
  • 3. 응답 시간 기준
  • +
  • 4. 연락 불가 일정 미리 공유
  • +
+
+
+
+
+ +
+
+
5
+
+
+
+
+
+

아이디어 공유 미팅 진행

+ 체크 +
+
+
    +
  • 1. 각자 생각한 서비스/프로젝트 아이디어 공유
  • +
  • 2. 현실성/기간 고려해 후보 압축
  • +
+
+
+
+
+ +
+
+
6
+
+
+
+
-

Github repository 생성하기

+

프로젝트 방향 결정

체크
- +
    +
  • 1. 서비스 목표 (학습용 / 포폴 / 출시)
  • +
  • 2. MVP 범위 합의
  • +
+ +
+
+
7
+
+
+
+
+
+

기획 문서 작성

+ 체크 +
+
+
    +
  • 1. 서비스 개요
  • +
  • 2. 주요 기능
  • +
  • 3. 사용자 흐름 정리
  • +
+

(검색 키워드: 서비스 기획서 작성 양식)

+
+
+
+
+
+
8
+
+
+
+
+
+

팀원 피드백 반영

+ 체크 +
+
+
    +
  • 1. 프론트/백 의견 수렴
  • +
  • 2. 기술적으로 어려운 부분 조정
  • +
+
+
+
+
+ +
+
+
9
+
+
+
+
+
+

역할별 업무 범위 정리

+ 체크 +
+
+
    +
  • 1. 프론트 / 백 / 기타 역할 명확화
  • +
  • 2. 누가 무엇을 언제까지 할지 정리
  • +
+
+
+
+
+ +
+
+
10
+
+
+
+
+
+

진행 상황 주기적 체크

+ 체크 +
+
+
    +
  • 1. 일정 밀리는 부분 확인
  • +
  • 2. 병목 발생 시 우선순위 재조정
  • +
+
+
+
+
+ +
+
+
11
+
+
+
+
+
+

팀 분위기 관리

+ 체크 +
+
+
    +
  • 1. 진행 중 어려움 공유
  • +
  • 2. 중간 목표 달성 시 간단한 피드백
  • +
+
+
+
+
+ +
+
+
12
+
+
+
+
+
+

결과물 활용 방향 논의

+ 체크 +
+
+
    +
  • 1. 배포 여부
  • +
  • 2. 포트폴리오 정리
  • +
  • 3. 홍보 방식 논의
  • +
+

(오픈채팅 / SNS / 커뮤니티 등)

+
+
+
+
+ +{% elif role.code == "FRONTEND" %} + + +
+
+
1
+
+
+
+
+
+

팀 커뮤니케이션 채널 참여

+ 체크 +
+
+
    +
  • 1. PM이 공유한 팀플 채널 입장
  • +
+
+
+
+
+ +
+
+
2
+
+
+
+
+
+

기획 내용 숙지

+ 체크 +
+
+
    +
  • 1. 기획 문서 읽고 전체 흐름 파악
  • +
  • 2. 핵심 사용자 화면 정리
  • +
+
+
+
+
+ +
+
+
3
+
+
+
+
+
+

디자인 컨셉 리서치

+ 체크 +
+
+
    +
  • 1. 유사 서비스/디자인 레퍼런스 조사
  • +
+

(검색 키워드: Pinterest UI reference)

+
+
+
+
+ +
+
+
4
+
+
+
+
+
+

레퍼런스 공유 및 합의

+ 체크 +
+
+
    +
  • 1. 팀원들에게 디자인 방향 공유
  • +
  • 2. 피드백 반영해 방향 확정
  • +
+
+
+
+
+ +
+
+
5
+
+
+
+
+
+

디자인 도구 선택

+ 체크 +
+
+
    +
  • 1. Figma / Adobe XD / 기타 도구 선택
  • +
+

(검색 키워드: Figma 사용법)

+
+
+
+
+ +
+
+
6
+
+
+
+
+
+

컬러·폰트 가이드 정리

+ 체크 +
+
+
    +
  • 1. 컬러칩
  • +
  • 2. 기본 폰트
  • +
  • 3. 버튼/텍스트 스타일
  • +
+
+
+
+
+ +
+
+
7
+
+
+
+
+
+

와이어프레임 제작

+ 체크 +
+
+
    +
  • 1. 주요 화면 구조 설계
  • +
  • 2. 사용자 흐름 중심으로 구성
  • +
+
+
+
+
+ +
+
+
8
+
+
+
+
+
+

와이어프레임 리뷰

+ 체크 +
+
+
    +
  • 1. 팀원 피드백 반영
  • +
  • 2. 수정사항 반영
  • +
+
+
+
+
+ +
+
+
9
+
+
+
+
+
+

최종 UI 디자인 완성

+ 체크 +
+
+
    +
  • 1. 색상/아이콘/여백 적용
  • +
+

(검색 키워드: 사용자 편의성 UI 디자인)

+
+
+
+
+ +
+
+
10
+
+
+
+
+
+

퍼블리싱 또는 프론트 구현

+ 체크 +
+
+
    +
  • 1. HTML/CSS 또는 React/Vue 등 선택
  • +
  • 2. 컴포넌트 단위로 구현
  • +
+
+
+
+
+ +
+
+
11
+
+
+
+
+
+

백엔드 연동 고려

+ 체크 +
+
+
    +
  • 1. 데이터 위치/형식 파악
  • +
  • 2. API 연동 포인트 확인
  • +
+
+
+
+
+ +
+
+
12
+
+
+
+
+
+

반응형/기본 UX 점검

+ 체크 +
+
+
    +
  • 1. 모바일/데스크톱 기본 대응
  • +
  • 2. 버튼/폼 동작 확인
  • +
+
+
+
+
+ +
+
+
13
+
+
+
+
+
+

UI 수정 및 정리

+ 체크 +
+
+
    +
  • 1. 실제 사용 시 불편한 부분 개선
  • +
+
+
+
+
+ +{% elif role.code == "BACKEND" %} + + +
+
+
1
+
+
+
+
+
+

기획 및 기능 범위 파악

+ 체크 +
+
+
    +
  • 1. 어떤 기능을 서버에서 담당하는지 확인
  • +
+
+
+
+
+ +
+
+
2
+
+
+
+
+
+

기술 스택 결정

+ 체크 +
+
+
    +
  • 1. Django / Spring / Node 등 선택
  • +
  • 2. DB 종류 선택
  • +
+
+
+
+
+ +
+
+
3
+
+
+
+
+
+

프로젝트 초기 세팅

+ 체크 +
+
+
    +
  • 1. 서버 프로젝트 생성
  • +
  • 2. 기본 폴더 구조 정리
  • +
+
+
+
+
+ +
+
+
4
+
+
+
+
+
+

DB 모델 설계

+ 체크 +
+
+
    +
  • 1. 핵심 엔티티 정의
  • +
  • 2. 관계 설정
  • +
+
+
+
+
+ +
+
+
5
+
+
+
+
+
+

기본 CRUD 설계

+ 체크 +
+
+
    +
  • 1. 생성 / 조회 / 수정 / 삭제 흐름 정리
  • +
+
+
+
+
+ +
+
+
6
+
+
+
+
+
+

API 구조 설계

+ 체크 +
+
+
    +
  • 1. 엔드포인트 네이밍
  • +
  • 2. 요청/응답 형식 정의
  • +
+
+
+
+
+ +
+
+
7
+
+
+
+
+
+

인증/권한 고려

+ 체크 +
+
+
    +
  • 1. 로그인 여부
  • +
  • 2. 사용자별 접근 제한
  • +
+
+
+
+
+ +
+
+
8
+
+
+
+
+
+

API 구현

+ 체크 +
+
+
    +
  • 1. 기능 단위로 구현
  • +
  • 2. 예외 상황 처리
  • +
+
+
+
+
+ +
+
+
9
+
+
+
+
+
+

API 테스트

+ 체크 +
+
+
    +
  • 1. Postman / curl / 테스트 코드로 요청 확인
  • +
  • 2. 정상/에러 케이스 점검
  • +
+
+
+
+
+ +
+
+
10
+
+
+
+
+
+

프론트 연동 지원

+ 체크 +
+
+
    +
  • 1. 프론트 요청 사항 반영
  • +
  • 2. 데이터 형식 조정
  • +
+
+
+
+
+ +
+
+
11
+
+
+
+
+
+

환경 설정 분리

+ 체크 +
+
+
    +
  • 1. 로컬 / 배포 환경 구분
  • +
+
+
+
+
+ +
+
+
12
+
+
+
+
+
+

배포 준비

+ 체크 +
+
+
    +
  • 1. 서버 실행 방식 정리
  • +
  • 2. 도메인/HTTPS 고려
  • +
+
+
+
+
+ +
+
+
13
+
+
+
+
+
+

기능 안정화

+ 체크 +
+
+
    +
  • 1. 에러 로그 확인
  • +
  • 2. 성능/보안 기본 점검
  • +
+
+
+
+
+ +{% endif %} + +{% endif %} + {% endblock %} \ No newline at end of file From a80b1355ed25824ee5d6affcf6d5a0a347dc6842 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 7 Feb 2026 21:33:50 +0900 Subject: [PATCH 237/380] =?UTF-8?q?fix=20:=20mission=20=EC=A7=84=EC=B2=99?= =?UTF-8?q?=EB=8F=84=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/urls.py | 2 + apps/guides/views.py | 89 ++++++++++++++++++++++++++++++++++- static/js/mission.js | 73 +++++++++++++--------------- templates/guides/mission.html | 1 + 4 files changed, 124 insertions(+), 41 deletions(-) diff --git a/apps/guides/urls.py b/apps/guides/urls.py index 010f3bb..7de37b3 100644 --- a/apps/guides/urls.py +++ b/apps/guides/urls.py @@ -6,4 +6,6 @@ urlpatterns = [ # 미션/체크리스트 페이지 path("mission/", views.mission, name="mission"), + # 미션 카드 완료 토글 + path("card//toggle/", views.toggle_card, name="toggle_card"), ] diff --git a/apps/guides/views.py b/apps/guides/views.py index b6d49d3..936867a 100644 --- a/apps/guides/views.py +++ b/apps/guides/views.py @@ -7,6 +7,10 @@ from .models import GuideCard, GuideTask, GuideTaskProgress, ProjectProgress from .services import GuideService +from django.http import JsonResponse +from django.views.decorators.http import require_POST +from django.utils import timezone +import json @login_required def mission(request): @@ -71,7 +75,9 @@ def mission(request): 'is_completed': progress.is_completed if progress else False, 'completed_at': progress.completed_at if progress else None, }) - + + is_card_completed = completed_tasks == len(tasks) and len(tasks) > 0 + mission_data.append({ 'card': card, 'content_html': GuideService.render_markdown(card.content_md), @@ -79,6 +85,7 @@ def mission(request): 'completed_tasks': completed_tasks, 'progress_percent': int((completed_tasks / len(tasks) * 100) if len(tasks) > 0 else 0), 'task_progress_data': task_progress_data, + 'is_completed': is_card_completed, }) # 모든 역할의 진척도 (PM/FE/BE 전부 표시) @@ -93,3 +100,83 @@ def mission(request): 'all_role_progress': all_role_progress, } return render(request, "guides/mission.html", context) + +@login_required +@require_POST +def toggle_card(request, card_id): + """ + 미션 카드 완료 상태 토글 + - 카드의 모든 태스크를 일괄 완료/미완료 처리 + """ + try: + data = json.loads(request.body) + is_completed = data.get('is_completed', False) + + # 현재 프로젝트 조회 + project = Project.objects.filter( + team__members__user=request.user, + team__members__is_active=True + ).first() + + if not project: + return JsonResponse({'success': False, 'error': 'No project'}, status=400) + + # 카드 조회 + card = get_object_or_404(GuideCard, id=card_id) + + # 카드의 모든 태스크 조회 + tasks = card.tasks.all() + + # 모든 태스크 완료 상태 업데이트 + for task in tasks: + progress, created = GuideTaskProgress.objects.update_or_create( + task=task, + project=project, + defaults={ + 'is_completed': is_completed, + 'completed_at': timezone.now() if is_completed else None + } + ) + + # 역할별 진척도 업데이트 + update_project_progress(project, card.role) + + # 업데이트된 진척도 조회 + project_progress = ProjectProgress.objects.get(project=project, role=card.role) + + return JsonResponse({ + 'success': True, + 'progress_percent': project_progress.progress_percent, + 'role_code': card.role.code + }) + + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=500) + + +def update_project_progress(project, role): + """프로젝트의 역할별 진척도 업데이트""" + # 해당 역할의 모든 태스크 조회 + all_tasks = GuideTask.objects.filter( + card__role=role, + card__is_active=True + ) + + total_tasks = all_tasks.count() + + # 완료된 태스크 수 + completed_tasks = GuideTaskProgress.objects.filter( + task__in=all_tasks, + project=project, + is_completed=True + ).count() + + # ProjectProgress 업데이트 또는 생성 + ProjectProgress.objects.update_or_create( + project=project, + role=role, + defaults={ + 'completed_tasks': completed_tasks, + 'total_tasks': total_tasks + } + ) \ No newline at end of file diff --git a/static/js/mission.js b/static/js/mission.js index 6d78f9d..7a44a26 100644 --- a/static/js/mission.js +++ b/static/js/mission.js @@ -1,22 +1,20 @@ document.addEventListener('DOMContentLoaded', function() { const missionCards = document.querySelectorAll('.mission_card'); - const totalMissions = missionCards.length; + const userRole = document.body.dataset.userRole || 'PM'; // PM, FRONTEND, BACKEND - // 페이지 로드 시 진척도 계산 - updateProgress(); + // 페이지 로드 시 저장된 진척도 불러오기 + loadProgress(); missionCards.forEach(card => { const cardHeader = card.querySelector('.card_header'); // 카드 클릭 - 펼치기/접기 cardHeader.addEventListener('click', function(e) { - // 체크 아이콘 클릭은 제외 if (e.target.classList.contains('check_icon')) return; const isActive = card.classList.contains('active'); const missionItem = card.closest('.mission_item'); - // 다른 모든 카드 닫기 document.querySelectorAll('.mission_card').forEach(c => { c.classList.remove('active'); }); @@ -24,20 +22,20 @@ document.addEventListener('DOMContentLoaded', function() { item.classList.remove('active'); }); - // 클릭한 카드 열기 if (!isActive) { card.classList.add('active'); missionItem.classList.add('active'); } }); - // 체크 아이콘 클릭 시 완료 처리 + // 체크 아이콘 클릭 const checkIcon = card.querySelector('.check_icon'); checkIcon.addEventListener('click', function(e) { e.stopPropagation(); const card = this.closest('.mission_card'); const missionItem = card.closest('.mission_item'); + const missionNumber = missionItem.dataset.number; const isCompleted = card.classList.contains('completed'); if (isCompleted) { @@ -52,35 +50,38 @@ document.addEventListener('DOMContentLoaded', function() { this.src = this.src.replace('nocheck.png', 'check.png'); } - // 진척도 업데이트 - updateProgress(); - - // 로컬스토리지에 저장 + // 로컬스토리지에 저장 & 진척도 업데이트 saveProgress(); + updateProgress(); }); }); // 진척도 계산 및 업데이트 function updateProgress() { + const totalMissions = missionCards.length; const completedMissions = document.querySelectorAll('.mission_card.completed').length; const percent = Math.round((completedMissions / totalMissions) * 100); - // 현재 사용자 역할에 따라 해당 진척도 바 업데이트 - const userRole = getUserRole(); // PM, FRONTEND, BACKEND - - if (userRole === 'PM') { - updateProgressBar('pm', percent); - } else if (userRole === 'FRONTEND') { - updateProgressBar('fe', percent); - } else if (userRole === 'BACKEND') { - updateProgressBar('be', percent); - } + updateProgressBar(userRole, percent); } // 진척도 바 업데이트 function updateProgressBar(role, percent) { - const progressBar = document.getElementById(`${role}_progress`); - const percentText = document.getElementById(`${role}_percent`); + let barId, percentId; + + if (role === 'PM') { + barId = 'pm_progress'; + percentId = 'pm_percent'; + } else if (role === 'FRONTEND') { + barId = 'fe_progress'; + percentId = 'fe_percent'; + } else if (role === 'BACKEND') { + barId = 'be_progress'; + percentId = 'be_percent'; + } + + const progressBar = document.getElementById(barId); + const percentText = document.getElementById(percentId); if (progressBar && percentText) { progressBar.style.width = `${percent}%`; @@ -88,28 +89,22 @@ document.addEventListener('DOMContentLoaded', function() { } } - // 현재 사용자 역할 가져오기 - function getUserRole() { - // HTML에서 역할 정보를 data 속성으로 넣어야 함 - const body = document.body; - return body.dataset.userRole || 'PM'; - } - // 로컬스토리지에 진행 상황 저장 function saveProgress() { const completedMissions = []; - document.querySelectorAll('.mission_card.completed').forEach((card, index) => { - const missionNumber = card.closest('.mission_item').dataset.number; - completedMissions.push(missionNumber); + document.querySelectorAll('.mission_item').forEach(item => { + const card = item.querySelector('.mission_card'); + if (card.classList.contains('completed')) { + const missionNumber = item.dataset.number; + completedMissions.push(missionNumber); + } }); - const userRole = getUserRole(); localStorage.setItem(`mission_progress_${userRole}`, JSON.stringify(completedMissions)); } // 로컬스토리지에서 진행 상황 불러오기 function loadProgress() { - const userRole = getUserRole(); const saved = localStorage.getItem(`mission_progress_${userRole}`); if (saved) { @@ -128,11 +123,9 @@ document.addEventListener('DOMContentLoaded', function() { } } }); - - updateProgress(); } + + // 진척도 업데이트 + updateProgress(); } - - // 페이지 로드 시 저장된 진행 상황 불러오기 - loadProgress(); }); \ No newline at end of file diff --git a/templates/guides/mission.html b/templates/guides/mission.html index f8229e3..abae48b 100644 --- a/templates/guides/mission.html +++ b/templates/guides/mission.html @@ -10,6 +10,7 @@

진행 중인 프로젝트가 없습니다.

{% else %} +

DASHBOARD

From 75ed5d8d2af2599933a9b11f40e6c05f0b364c4e Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 21:46:29 +0900 Subject: [PATCH 238/380] =?UTF-8?q?fix:=20=EA=B8=B0=EC=88=A0=20=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=EC=97=90=20role=20=EB=82=98=ED=83=80=EB=82=98?= =?UTF-8?q?=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/account/mypage.html | 4 ++-- templates/account/profile_edit.html | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/account/mypage.html b/templates/account/mypage.html index 535f769..e4f3f75 100644 --- a/templates/account/mypage.html +++ b/templates/account/mypage.html @@ -52,8 +52,8 @@

기술 스택

- {% for rl in role_levels %} - # {{ rl.role.name }} + {% for tech in user_obj.tech_stacks.all %} + # {{ tech.name }} {% empty %}

등록된 스택이 없습니다.

{% endfor %} diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index 4e82552..e569b98 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -66,9 +66,9 @@

기술 스택

- {% for role_level in user.role_levels.all %} -
- # {{ role_level.role.name }} + {% for tech in user.tech_stacks.all %} +
+ # {{ tech.name }}
{% empty %}

등록된 기술 스택이 없습니다.

From d5aa651e793348dd2e58d3ee64d12f69fc6ffc58 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 21:58:25 +0900 Subject: [PATCH 239/380] =?UTF-8?q?feat:=20=ED=8C=80=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=20=EB=B2=84=ED=8A=BC=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team.css | 32 ++++++++++++++++++++++++++++++++ templates/teams/team.html | 6 ++++++ 2 files changed, 38 insertions(+) diff --git a/static/css/team.css b/static/css/team.css index 664d000..80ba68c 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -49,6 +49,38 @@ body { transition: 0.3s ease; } +/* 매칭 취소 */ +.matching_actions { + margin-top: 30px; + display: flex; + justify-content: center; +} + +.cancel_form { + width: 100%; + display: flex; + justify-content: center; +} + +.cancel_button { + width: 150px; + height: 40px; + padding: 10px 20px; + background: #FF6B6B; + color: #fff; + border: none; + border-radius: 20px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: 0.3s ease; +} + +.cancel_button:hover { + background: #E63946; + transition: 0.3s ease; +} + /* 매칭 성공 */ .team_success { margin-top: 50px; diff --git a/templates/teams/team.html b/templates/teams/team.html index 6cee2db..76c6a0b 100644 --- a/templates/teams/team.html +++ b/templates/teams/team.html @@ -12,6 +12,12 @@

+
+
+ {% csrf_token %} + +
+

From 2f8c70759d303c94265c35be299607676e7aa0bc Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 22:25:11 +0900 Subject: [PATCH 240/380] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A8=EB=8D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0006_alter_project_related_links.py | 18 +++++++++++++++++ .../migrations/0007_clean_related_links.py | 20 +++++++++++++++++++ apps/projects/models.py | 6 +++--- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 apps/projects/migrations/0006_alter_project_related_links.py create mode 100644 apps/projects/migrations/0007_clean_related_links.py diff --git a/apps/projects/migrations/0006_alter_project_related_links.py b/apps/projects/migrations/0006_alter_project_related_links.py new file mode 100644 index 0000000..52c248d --- /dev/null +++ b/apps/projects/migrations/0006_alter_project_related_links.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-02-07 13:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0005_add_season_fk'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='related_links', + field=models.TextField(blank=True, help_text='관련 링크 (마크다운)', null=True), + ), + ] diff --git a/apps/projects/migrations/0007_clean_related_links.py b/apps/projects/migrations/0007_clean_related_links.py new file mode 100644 index 0000000..efa5f27 --- /dev/null +++ b/apps/projects/migrations/0007_clean_related_links.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.10 on 2026-02-07 13:23 + +from django.db import migrations + + +def clean_related_links(apps, schema_editor): + """"{}" 형식의 related_links를 None으로 변환""" + Project = apps.get_model('projects', 'Project') + Project.objects.filter(related_links='{}').update(related_links=None) + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0006_alter_project_related_links'), + ] + + operations = [ + migrations.RunPython(clean_related_links), + ] diff --git a/apps/projects/models.py b/apps/projects/models.py index 74ca3e1..f60adf6 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -166,10 +166,10 @@ class Status(models.TextChoices): help_text="팀 규칙 (마크다운)", ) - related_links = models.JSONField( - default=dict, + related_links = models.TextField( + null=True, blank=True, - help_text="관련 링크 (Notion, Figma, GitHub 등)", + help_text="관련 링크 (마크다운)", ) is_favorite = models.BooleanField( From 139cfbbcdf79af8025f486625a5c82db69b00bcd Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 22:25:28 +0900 Subject: [PATCH 241/380] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8F=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/forms.py | 67 +++++------------------- templates/projects/dashboard_update.html | 4 +- 2 files changed, 13 insertions(+), 58 deletions(-) diff --git a/apps/projects/forms.py b/apps/projects/forms.py index 864de7d..34529ac 100644 --- a/apps/projects/forms.py +++ b/apps/projects/forms.py @@ -38,6 +38,11 @@ class Meta: "placeholder": "팀 규칙을 마크다운 형식으로 작성해주세요\n\n예:\n# 회의 규칙\n- 주 1회 수요일 19시\n- 지각 3회 = 경고\n\n# 코드 리뷰\n- PR 생성 후 2시간 내 리뷰\n- 최소 2명 승인 필수", "rows": 6, }), + "related_links": forms.Textarea(attrs={ + "class": "form-control", + "placeholder": "관련 링크를 마크다운 형식으로 작성해주세요\n\n예:\n[Notion](https://notion.so/...)\n[Figma](https://figma.com/...)\n[GitHub](https://github.com/...)", + "rows": 6, + }), "is_favorite": forms.CheckboxInput(attrs={ "class": "form-check-input", }), @@ -50,58 +55,10 @@ def clean_title(self): raise forms.ValidationError("서비스명은 필수입니다.") return title - -class ProjectRelatedLinksForm(forms.Form): - """ - 관련 링크 별도 폼 (AJAX 업데이트용) - """ - - notion_url = forms.URLField( - required=False, - label="Notion", - widget=forms.URLInput(attrs={ - "class": "form-control", - "placeholder": "Notion 링크를 입력하세요", - }), - ) - - figma_url = forms.URLField( - required=False, - label="Figma", - widget=forms.URLInput(attrs={ - "class": "form-control", - "placeholder": "Figma 링크를 입력하세요", - }), - ) - - github_url = forms.URLField( - required=False, - label="GitHub", - widget=forms.URLInput(attrs={ - "class": "form-control", - "placeholder": "GitHub 링크를 입력하세요", - }), - ) - - def clean(self): - """링크가 1개 이상 입력되는지 확인""" - cleaned_data = super().clean() - has_link = any([ - cleaned_data.get("notion_url"), - cleaned_data.get("figma_url"), - cleaned_data.get("github_url"), - ]) - if not has_link: - raise forms.ValidationError("최소 1개 이상의 링크를 입력해주세요.") - return cleaned_data - - def to_dict(self): - """폼 데이터를 딕셔너리로 변환 (JSONField용)""" - if not self.is_valid(): - return {} - - return { - "notion": self.cleaned_data.get("notion_url") or None, - "figma": self.cleaned_data.get("figma_url") or None, - "github": self.cleaned_data.get("github_url") or None, - } + def clean_related_links(self): + """관련 링크 정리 - "{}" 같은 빈 값 제거""" + related_links = self.cleaned_data.get("related_links", "").strip() + # "{}" 또는 빈 문자열이면 None 반환 + if not related_links or related_links == "{}": + return None + return related_links diff --git a/templates/projects/dashboard_update.html b/templates/projects/dashboard_update.html index d7dde56..f23e54c 100644 --- a/templates/projects/dashboard_update.html +++ b/templates/projects/dashboard_update.html @@ -117,9 +117,7 @@

팀 규칙

관련 링크

-

Notion : {{ links_form.notion_url }}

-

Figma : {{ links_form.figma_url }}

-

Github : {{ links_form.github_url }}

+ {{ form.related_links }}
From 62825365907bf03aa36dca94d82acb415df4b22d Mon Sep 17 00:00:00 2001 From: issuejong Date: Sat, 7 Feb 2026 22:26:11 +0900 Subject: [PATCH 242/380] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=B7=B0=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index 315b793..52af7a7 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -6,7 +6,7 @@ from django.views.decorators.http import require_POST, require_http_methods from apps.projects.models import Season, Project -from apps.projects.forms import ProjectDashboardEditForm, ProjectRelatedLinksForm +from apps.projects.forms import ProjectDashboardEditForm from apps.projects.services import TeamMatchingService from apps.teams.models import Team, TeamMember @@ -155,12 +155,9 @@ def dashboard_update(request, project_id): if request.method == "POST": form = ProjectDashboardEditForm(request.POST, request.FILES, instance=project) - links_form = ProjectRelatedLinksForm(request.POST) - if form.is_valid() and links_form.is_valid(): - project = form.save(commit=False) - project.related_links = links_form.to_dict() - project.save() + if form.is_valid(): + form.save() messages.success(request, "✅ 프로젝트 정보가 수정되었습니다.") return redirect("projects:dashboard_detail", project_id=project_id) @@ -168,17 +165,10 @@ def dashboard_update(request, project_id): messages.error(request, "❌ 입력 오류가 있습니다. 다시 확인해주세요.") else: form = ProjectDashboardEditForm(instance=project) - related_links = project.related_links or {} - links_form = ProjectRelatedLinksForm(initial={ - "notion_url": related_links.get("notion"), - "figma_url": related_links.get("figma"), - "github_url": related_links.get("github"), - }) context = { "project": project, "form": form, - "links_form": links_form, } return render(request, "projects/dashboard_update.html", context) From 995f30d4cb1c0ae31dea35f615b00062a682176b Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 8 Feb 2026 01:28:52 +0900 Subject: [PATCH 243/380] =?UTF-8?q?fix=20:=20mission=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/api_urls.py | 15 +- apps/guides/api_views.py | 193 ++------ apps/guides/urls.py | 5 +- apps/guides/views.py | 158 ++----- static/css/mission.css | 248 +++------- static/js/mission.js | 168 +++---- templates/guides/mission.html | 865 ++-------------------------------- 7 files changed, 236 insertions(+), 1416 deletions(-) diff --git a/apps/guides/api_urls.py b/apps/guides/api_urls.py index cf2d6e4..d6204e2 100644 --- a/apps/guides/api_urls.py +++ b/apps/guides/api_urls.py @@ -4,16 +4,5 @@ app_name = "guides_api" urlpatterns = [ - # 태스크 완료/미완료 처리 - path( - "projects//tasks//toggle/", - api_views.toggle_task_completion, - name="toggle_task", - ), - # 프로젝트 역할별 진척도 조회 - path( - "projects//progress/", - api_views.get_project_progress, - name="project_progress", - ), -] + path("card//toggle/", api_views.toggle_card, name="toggle_card"), +] \ No newline at end of file diff --git a/apps/guides/api_views.py b/apps/guides/api_views.py index 041c9a0..d8c1ef5 100644 --- a/apps/guides/api_views.py +++ b/apps/guides/api_views.py @@ -1,179 +1,60 @@ from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required from django.http import JsonResponse -from django.views.decorators.http import require_http_methods -from django.utils import timezone -from django.db.models import Count +from django.views.decorators.http import require_POST import json from apps.projects.models import Project -from apps.teams.models import TeamMember -from .models import GuideTask, GuideTaskProgress, ProjectProgress, GuideCard +from .models import GuideCard, GuideTask, GuideTaskProgress, ProjectProgress @login_required -@require_http_methods(["PATCH"]) -def toggle_task_completion(request, project_id, task_id): - """ - 태스크 완료/미완료 토글 - - Request: - PATCH /api/guides/projects/{project_id}/tasks/{task_id}/toggle/ - { - "is_completed": true - } - """ +@require_POST +def toggle_card(request, card_id): + """카드 완료/미완료 토글""" try: - # JSON body 파싱 - body = json.loads(request.body) - is_completed = body.get('is_completed', False) - - # 권한 확인: 사용자가 이 프로젝트의 팀원인가? - project = get_object_or_404( - Project, - id=project_id, - team__members__user=request.user, - team__members__is_active=True - ) - - task = get_object_or_404(GuideTask, id=task_id) - - # 태스크가 사용자의 역할에 속하는가? - team_member = TeamMember.objects.get( - team=project.team, - user=request.user, - is_active=True - ) - - if task.card.role != team_member.role: - return JsonResponse( - {"error": "이 미션은 당신의 역할이 아닙니다"}, - status=403 + data = json.loads(request.body) + project_id = data.get('project_id') + is_completed = data.get('is_completed') + + project = get_object_or_404(Project, id=project_id) + card = get_object_or_404(GuideCard, id=card_id) + + # 카드의 모든 태스크 완료/미완료 처리 + tasks = card.tasks.all() + for task in tasks: + GuideTaskProgress.objects.update_or_create( + task=task, + project=project, + defaults={'is_completed': is_completed} ) - # GuideTaskProgress 생성 또는 업데이트 - progress, created = GuideTaskProgress.objects.get_or_create( - task=task, - project=project - ) - - # 완료 상태 변경 - if is_completed and not progress.is_completed: - progress.is_completed = True - progress.completed_at = timezone.now() - elif not is_completed and progress.is_completed: - progress.is_completed = False - progress.completed_at = None - - progress.save() - # ProjectProgress 업데이트 - _update_project_progress(project, team_member.role) + update_project_progress(project, card.role) - return JsonResponse({ - "success": True, - "is_completed": progress.is_completed, - "completed_at": progress.completed_at.isoformat() if progress.completed_at else None, - }) - - except json.JSONDecodeError: - return JsonResponse( - {"error": "Invalid JSON"}, - status=400 - ) - except Exception as e: - return JsonResponse( - {"error": str(e)}, - status=400 - ) - - -@login_required -@require_http_methods(["GET"]) -def get_project_progress(request, project_id): - """ - 프로젝트의 역할별 진척도 조회 - - Response: - { - "project": {...}, - "progress": [ - { - "role": "PM", - "completed_tasks": 5, - "total_tasks": 10, - "progress_percent": 50 - }, - ... - ] - } - """ - try: - # 권한 확인 - project = get_object_or_404( - Project, - id=project_id, - team__members__user=request.user, - team__members__is_active=True - ) - - # 모든 역할의 진척도 - progress_data = [] - for role_progress in ProjectProgress.objects.filter(project=project): - progress_data.append({ - "role": role_progress.role.code, - "completed_tasks": role_progress.completed_tasks, - "total_tasks": role_progress.total_tasks, - "progress_percent": role_progress.progress_percent, - }) + return JsonResponse({'success': True}) - return JsonResponse({ - "success": True, - "project_id": project.id, - "project_title": project.title, - "progress": progress_data, - }) - except Exception as e: - return JsonResponse( - {"error": str(e)}, - status=400 - ) + return JsonResponse({'success': False, 'error': str(e)}, status=400) -def _update_project_progress(project, role): - """ - ProjectProgress 업데이트 로직 - 특정 역할의 진척도를 계산하고 저장 - """ - from django.db.models import Count, Q - - # 1. 해당 역할의 모든 태스크 조회 (카드 통해서) - guide_cards = GuideCard.objects.filter( - role=role, - is_active=True - ).prefetch_related('tasks') - - # 2. 모든 task_id 수집 - all_task_ids = [] - for card in guide_cards: - all_task_ids.extend(card.tasks.values_list('id', flat=True)) - - total_tasks = len(all_task_ids) - - # 3. 한 번에 모든 진행 상태 조회 (N+1 해결) - completed_tasks = GuideTaskProgress.objects.filter( - task_id__in=all_task_ids, +def update_project_progress(project, role): + """역할별 진척도 업데이트""" + role_cards = GuideCard.objects.filter(role=role, is_active=True) + all_tasks = GuideTask.objects.filter(card__in=role_cards) + + total = all_tasks.count() + completed = GuideTaskProgress.objects.filter( + task__in=all_tasks, project=project, is_completed=True ).count() - # 4. ProjectProgress 생성 또는 업데이트 - progress, created = ProjectProgress.objects.get_or_create( + ProjectProgress.objects.update_or_create( project=project, - role=role - ) - - progress.total_tasks = total_tasks - progress.completed_tasks = completed_tasks - progress.save() + role=role, + defaults={ + 'total_tasks': total, + 'completed_tasks': completed + } + ) \ No newline at end of file diff --git a/apps/guides/urls.py b/apps/guides/urls.py index 7de37b3..63b1081 100644 --- a/apps/guides/urls.py +++ b/apps/guides/urls.py @@ -4,8 +4,5 @@ app_name = "guides" urlpatterns = [ - # 미션/체크리스트 페이지 path("mission/", views.mission, name="mission"), - # 미션 카드 완료 토글 - path("card//toggle/", views.toggle_card, name="toggle_card"), -] +] \ No newline at end of file diff --git a/apps/guides/views.py b/apps/guides/views.py index 936867a..6158f55 100644 --- a/apps/guides/views.py +++ b/apps/guides/views.py @@ -1,31 +1,22 @@ from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required -from django.db.models import Count, Q from apps.projects.models import Project from apps.teams.models import TeamMember -from .models import GuideCard, GuideTask, GuideTaskProgress, ProjectProgress -from .services import GuideService +from .models import GuideCard, GuideTaskProgress, ProjectProgress -from django.http import JsonResponse -from django.views.decorators.http import require_POST -from django.utils import timezone -import json @login_required def mission(request): - """사용자의 미션 페이지 (현재 프로젝트)""" - # 사용자가 참여한 프로젝트 조회 - project = ( - Project.objects - .filter(team__members__user=request.user, team__members__is_active=True) - .first() - ) + """미션 페이지""" + project = Project.objects.filter( + team__members__user=request.user, + team__members__is_active=True + ).first() if not project: return render(request, "guides/mission.html", {"project": None}) - # 사용자의 역할 조회 team_member = get_object_or_404( TeamMember, team=project.team, @@ -34,61 +25,46 @@ def mission(request): ) role = team_member.role - # 해당 역할의 모든 미션 조회 + # 역할별 미션 카드 guide_cards = GuideCard.objects.filter( role=role, is_active=True - ).prefetch_related('tasks') - - # 1. 모든 task 수집 - all_tasks = [] - cards_tasks_map = {} # card_id -> tasks - for card in guide_cards: - card_tasks = list(card.tasks.all()) - cards_tasks_map[card.id] = card_tasks - all_tasks.extend(card_tasks) - - # 2. 한 번에 모든 progress 조회 (N+1 해결) - progress_map = {} - for progress in GuideTaskProgress.objects.filter( - task__in=all_tasks, - project=project - ): - progress_map[progress.task_id] = progress + ).order_by('order_no').prefetch_related('tasks') - # 각 미션의 진행도 계산 + # 미션 데이터 구성 mission_data = [] for card in guide_cards: - tasks = cards_tasks_map[card.id] - completed_tasks = sum( - 1 for task in tasks - if progress_map.get(task.id, GuideTaskProgress()).is_completed - ) + tasks = card.tasks.all() + + # 완료된 태스크 개수 + completed_count = GuideTaskProgress.objects.filter( + task__in=tasks, + project=project, + is_completed=True + ).count() + + # 카드 완료 여부 + is_card_completed = completed_count == tasks.count() if tasks.count() > 0 else False - # 태스크 데이터 task_progress_data = [] for task in tasks: - progress = progress_map.get(task.id) + progress = GuideTaskProgress.objects.filter( + task=task, + project=project + ).first() task_progress_data.append({ 'task': task, 'is_completed': progress.is_completed if progress else False, - 'completed_at': progress.completed_at if progress else None, }) - - is_card_completed = completed_tasks == len(tasks) and len(tasks) > 0 - + mission_data.append({ 'card': card, - 'content_html': GuideService.render_markdown(card.content_md), - 'total_tasks': len(tasks), - 'completed_tasks': completed_tasks, - 'progress_percent': int((completed_tasks / len(tasks) * 100) if len(tasks) > 0 else 0), 'task_progress_data': task_progress_data, 'is_completed': is_card_completed, }) - # 모든 역할의 진척도 (PM/FE/BE 전부 표시) + # 모든 역할의 진척도 all_role_progress = ProjectProgress.objects.filter( project=project ).select_related('role') @@ -99,84 +75,4 @@ def mission(request): 'mission_data': mission_data, 'all_role_progress': all_role_progress, } - return render(request, "guides/mission.html", context) - -@login_required -@require_POST -def toggle_card(request, card_id): - """ - 미션 카드 완료 상태 토글 - - 카드의 모든 태스크를 일괄 완료/미완료 처리 - """ - try: - data = json.loads(request.body) - is_completed = data.get('is_completed', False) - - # 현재 프로젝트 조회 - project = Project.objects.filter( - team__members__user=request.user, - team__members__is_active=True - ).first() - - if not project: - return JsonResponse({'success': False, 'error': 'No project'}, status=400) - - # 카드 조회 - card = get_object_or_404(GuideCard, id=card_id) - - # 카드의 모든 태스크 조회 - tasks = card.tasks.all() - - # 모든 태스크 완료 상태 업데이트 - for task in tasks: - progress, created = GuideTaskProgress.objects.update_or_create( - task=task, - project=project, - defaults={ - 'is_completed': is_completed, - 'completed_at': timezone.now() if is_completed else None - } - ) - - # 역할별 진척도 업데이트 - update_project_progress(project, card.role) - - # 업데이트된 진척도 조회 - project_progress = ProjectProgress.objects.get(project=project, role=card.role) - - return JsonResponse({ - 'success': True, - 'progress_percent': project_progress.progress_percent, - 'role_code': card.role.code - }) - - except Exception as e: - return JsonResponse({'success': False, 'error': str(e)}, status=500) - - -def update_project_progress(project, role): - """프로젝트의 역할별 진척도 업데이트""" - # 해당 역할의 모든 태스크 조회 - all_tasks = GuideTask.objects.filter( - card__role=role, - card__is_active=True - ) - - total_tasks = all_tasks.count() - - # 완료된 태스크 수 - completed_tasks = GuideTaskProgress.objects.filter( - task__in=all_tasks, - project=project, - is_completed=True - ).count() - - # ProjectProgress 업데이트 또는 생성 - ProjectProgress.objects.update_or_create( - project=project, - role=role, - defaults={ - 'completed_tasks': completed_tasks, - 'total_tasks': total_tasks - } - ) \ No newline at end of file + return render(request, "guides/mission.html", context) \ No newline at end of file diff --git a/static/css/mission.css b/static/css/mission.css index aa1e33a..3230681 100644 --- a/static/css/mission.css +++ b/static/css/mission.css @@ -39,7 +39,8 @@ /* 진척도 */ .mission_progress { background: #F6F8FF; - width: 90%; height: 350px; + width: 90%; + height: 350px; padding: 50px 20px; margin: 0 auto 80px; } @@ -50,11 +51,13 @@ } .mp_title > img { - width: 100px; height: 100px; + width: 100px; + height: 100px; } .mp_title > div > h3 { - font-size: 25px; font-weight: 600; + font-size: 25px; + font-weight: 600; margin-bottom: 15px; } @@ -63,88 +66,58 @@ color: #FF0000; } -.pm_progress { +/* 진척도 바 */ +.role_progress_bar { margin: 20px 0 15px 100px; display: flex; align-items: center; + gap: 15px; } -.pm_progress > h3 { +.role_progress_bar > h3 { width: 30px; - font-size: 20px; font-weight: 550; - margin-right: 15px; + font-size: 20px; + font-weight: 550; } -.pm_progress > .pm_bar { +.progress_bar_container { position: relative; - width: 85%; height: 15px; + flex: 1; + height: 15px; background: #DDDDDD; border-radius: 20px; + overflow: hidden; } -.pm_progress > .pm_bar > .pm_real { +.progress_bar_fill { position: absolute; - top: 0; left: 0; - width: 60%; height: 100%; - background: #37D3BF; + top: 0; + left: 0; + height: 100%; border-radius: 20px; + transition: width 0.5s ease; } -.front_progress { - margin: 20px 0 15px 100px; - display: flex; - align-items: center; -} - -.front_progress > h3 { - width: 30px; - font-size: 20px; font-weight: 550; - margin-right: 15px; -} - -.front_progress > .front_bar { - position: relative; - width: 85%; height: 15px; - background: #DDDDDD; - border-radius: 20px; +.progress_bar_fill.pm_color { + background: #37D3BF; } -.front_progress > .front_bar > .front_real { - position: absolute; - top: 0; left: 0; - width: 50%; height: 100%; +.progress_bar_fill.fe_color { background: #FFDF6E; - border-radius: 20px; -} - -.back_progress { - margin: 20px 0 15px 100px; - display: flex; - align-items: center; } -.back_progress > h3 { - width: 30px; - font-size: 20px; font-weight: 550; - margin-right: 15px; -} - -.back_progress > .back_bar { - position: relative; - width: 85%; height: 15px; - background: #DDDDDD; - border-radius: 20px; +.progress_bar_fill.be_color { + background: #FF69A4; } -.back_progress > .back_bar > .back_real { - position: absolute; - top: 0; left: 0; - width: 70%; height: 100%; - background: #FF69A4; - border-radius: 20px; +.progress_percent { + min-width: 45px; + font-size: 14px; + font-weight: 600; + color: #4272EF; } -/* 카드 */ +/* 미션 아이템 */ .mission_item { width: 90%; margin: 0 auto; @@ -152,6 +125,7 @@ gap: 20px; } +/* 타임라인 */ .timeline_dot { display: flex; flex-direction: column; @@ -161,7 +135,8 @@ } .circle { - width: 30px; height: 30px; + width: 30px; + height: 30px; background: #EAF0FF; border-radius: 50%; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); @@ -172,7 +147,7 @@ justify-content: center; font-size: 16px; font-weight: 700; - color: #fff; + color: #1d294b; transition: all 0.3s ease; } @@ -206,10 +181,6 @@ display: none; } -.mission_item:last-child .line { - display: none; -} - /* 카드 래퍼 */ .card_wrapper { flex: 1; @@ -256,7 +227,7 @@ transform: scale(1.1); } -/* 카드 내용 */ +/* 카드 내용 (기본 숨김) */ .card_content { max-height: 0; overflow: hidden; @@ -265,30 +236,6 @@ margin-top: 0; } -.card_description { - font-size: 14px; - color: #6B7280; - margin: 15px 0 10px 0; -} - -.mission_input { - width: 100%; - padding: 12px 16px; - border: 2px solid #E0E7FF; - border-radius: 12px; - font-size: 15px; - color: #9CA3AF; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.mission_input:focus { - outline: none; - border-color: #4272EF; - color: #1d294b; -} - /* 활성화된 카드 */ .mission_card.active { background: #fff; @@ -298,7 +245,7 @@ } .mission_card.active .card_content { - max-height: 300px; + max-height: 500px; opacity: 1; margin-top: 15px; } @@ -307,73 +254,25 @@ opacity: 0.8; } -/* 진척도 바 통합 스타일 */ -.role_progress_bar { - margin: 20px 0 15px 100px; - display: flex; - align-items: center; - gap: 15px; -} - -.role_progress_bar > h3 { - width: 30px; - font-size: 20px; - font-weight: 550; -} - -.progress_bar_container { - position: relative; - flex: 1; - height: 15px; - background: #DDDDDD; - border-radius: 20px; - overflow: hidden; -} - -.progress_bar_fill { - position: absolute; - top: 0; - left: 0; - height: 100%; - border-radius: 20px; - transition: width 0.5s ease; -} - -.progress_bar_fill.pm_color { - background: #37D3BF; -} - -.progress_bar_fill.fe_color { - background: #FFDF6E; -} - -.progress_bar_fill.be_color { - background: #FF69A4; -} - -.progress_percent { - min-width: 45px; - font-size: 14px; - font-weight: 600; - color: #4272EF; -} - -/* 미션 설명 스타일 */ +/* 마크다운 콘텐츠 스타일 */ .mission_description { font-size: 14px; color: #374151; line-height: 1.6; - margin-bottom: 15px; +} + +.mission_description p { + margin: 10px 0; } .mission_description ul, .mission_description ol { - margin-left: 20px; - margin-top: 10px; + margin: 10px 0 10px 20px; } .mission_description li { margin-bottom: 8px; + line-height: 1.6; } .mission_description strong { @@ -384,52 +283,39 @@ .mission_description em { color: #6B7280; font-style: italic; + font-size: 13px; } -/* 태스크 리스트 */ -.task_list { - margin-top: 15px; -} - -.task_item { - display: flex; - align-items: center; - padding: 8px 12px; - margin-bottom: 8px; - background: #F9FAFB; - border-radius: 8px; - transition: all 0.3s ease; -} - -.task_item:hover { +.mission_description code { background: #F3F4F6; + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; } -.task_item.completed { - opacity: 0.6; +.mission_description p { + margin: 8px 0; + white-space: pre-wrap; } -.task_item.completed label { - text-decoration: line-through; - color: #9CA3AF; +.mission_description strong { + color: #4272EF; + font-weight: 600; } -.task_checkbox { - width: 18px; - height: 18px; - margin-right: 10px; - cursor: pointer; - accent-color: #4272EF; +.mission_description ul { + list-style: disc; + margin-left: 20px; } -.task_item label { - flex: 1; - font-size: 14px; - color: #374151; - cursor: pointer; +.mission_description li { + margin-bottom: 12px; + line-height: 1.6; } -/* 기존 mission_input 제거 */ -.mission_input { - display: none; +.mission_description li em { + display: block; + margin-top: 4px; + color: #6B7280; + font-size: 13px; } \ No newline at end of file diff --git a/static/js/mission.js b/static/js/mission.js index 7a44a26..e0a564e 100644 --- a/static/js/mission.js +++ b/static/js/mission.js @@ -1,131 +1,81 @@ document.addEventListener('DOMContentLoaded', function() { - const missionCards = document.querySelectorAll('.mission_card'); - const userRole = document.body.dataset.userRole || 'PM'; // PM, FRONTEND, BACKEND + if (!PROJECT_ID) return; - // 페이지 로드 시 저장된 진척도 불러오기 - loadProgress(); - - missionCards.forEach(card => { - const cardHeader = card.querySelector('.card_header'); - - // 카드 클릭 - 펼치기/접기 - cardHeader.addEventListener('click', function(e) { - if (e.target.classList.contains('check_icon')) return; - - const isActive = card.classList.contains('active'); - const missionItem = card.closest('.mission_item'); - - document.querySelectorAll('.mission_card').forEach(c => { - c.classList.remove('active'); - }); - document.querySelectorAll('.mission_item').forEach(item => { - item.classList.remove('active'); - }); - - if (!isActive) { - card.classList.add('active'); - missionItem.classList.add('active'); - } - }); - - // 체크 아이콘 클릭 - const checkIcon = card.querySelector('.check_icon'); - checkIcon.addEventListener('click', function(e) { + // 체크 아이콘 클릭 + document.querySelectorAll('.check_icon').forEach(icon => { + icon.addEventListener('click', function(e) { e.stopPropagation(); - const card = this.closest('.mission_card'); - const missionItem = card.closest('.mission_item'); - const missionNumber = missionItem.dataset.number; - const isCompleted = card.classList.contains('completed'); + const missionItem = this.closest('.mission_item'); + const cardId = missionItem.dataset.cardId; + const isCompleted = missionItem.classList.contains('completed'); + // UI 업데이트 if (isCompleted) { - // 완료 취소 - card.classList.remove('completed'); missionItem.classList.remove('completed'); this.src = this.src.replace('check.png', 'nocheck.png'); } else { - // 완료 처리 - card.classList.add('completed'); missionItem.classList.add('completed'); this.src = this.src.replace('nocheck.png', 'check.png'); } - // 로컬스토리지에 저장 & 진척도 업데이트 - saveProgress(); - updateProgress(); + // DB 저장 (API URL 사용) + fetch(`/api/guides/card/${cardId}/toggle/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ + project_id: PROJECT_ID, + is_completed: !isCompleted + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }) + .catch(error => console.error('Error:', error)); }); }); - // 진척도 계산 및 업데이트 - function updateProgress() { - const totalMissions = missionCards.length; - const completedMissions = document.querySelectorAll('.mission_card.completed').length; - const percent = Math.round((completedMissions / totalMissions) * 100); - - updateProgressBar(userRole, percent); - } - - // 진척도 바 업데이트 - function updateProgressBar(role, percent) { - let barId, percentId; - - if (role === 'PM') { - barId = 'pm_progress'; - percentId = 'pm_percent'; - } else if (role === 'FRONTEND') { - barId = 'fe_progress'; - percentId = 'fe_percent'; - } else if (role === 'BACKEND') { - barId = 'be_progress'; - percentId = 'be_percent'; - } - - const progressBar = document.getElementById(barId); - const percentText = document.getElementById(percentId); - - if (progressBar && percentText) { - progressBar.style.width = `${percent}%`; - percentText.textContent = `${percent}%`; - } - } - - // 로컬스토리지에 진행 상황 저장 - function saveProgress() { - const completedMissions = []; - document.querySelectorAll('.mission_item').forEach(item => { - const card = item.querySelector('.mission_card'); - if (card.classList.contains('completed')) { - const missionNumber = item.dataset.number; - completedMissions.push(missionNumber); - } + // 카드 펼치기/접기 + document.querySelectorAll('.card_header').forEach(header => { + header.addEventListener('click', function(e) { + if (e.target.classList.contains('check_icon')) return; + + const card = this.closest('.mission_card'); + const missionItem = this.closest('.mission_item'); + + // 다른 카드 닫기 + document.querySelectorAll('.mission_card').forEach(c => { + c.classList.remove('active'); + }); + document.querySelectorAll('.mission_item').forEach(item => { + item.classList.remove('active'); + }); + + // 현재 카드 토글 + card.classList.toggle('active'); + missionItem.classList.toggle('active'); }); - - localStorage.setItem(`mission_progress_${userRole}`, JSON.stringify(completedMissions)); - } + }); - // 로컬스토리지에서 진행 상황 불러오기 - function loadProgress() { - const saved = localStorage.getItem(`mission_progress_${userRole}`); - - if (saved) { - const completedMissions = JSON.parse(saved); - - completedMissions.forEach(number => { - const missionItem = document.querySelector(`.mission_item[data-number="${number}"]`); - if (missionItem) { - const card = missionItem.querySelector('.mission_card'); - const checkIcon = missionItem.querySelector('.check_icon'); - - card.classList.add('completed'); - missionItem.classList.add('completed'); - if (checkIcon) { - checkIcon.src = checkIcon.src.replace('nocheck.png', 'check.png'); - } + // CSRF 토큰 가져오기 + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; } - }); + } } - - // 진척도 업데이트 - updateProgress(); + return cookieValue; } }); \ No newline at end of file diff --git a/templates/guides/mission.html b/templates/guides/mission.html index abae48b..0159499 100644 --- a/templates/guides/mission.html +++ b/templates/guides/mission.html @@ -1,23 +1,22 @@ {% extends 'base.html' %} {% load static %} + {% block header %} {% endblock %} -{% block content %} +{% block content %} {% if not project %}

진행 중인 프로젝트가 없습니다.

{% else %} -

DASHBOARD

MISSION

-
rocket @@ -27,846 +26,68 @@

{{ project.title }}의 진척도

+ {% for progress in all_role_progress %}
-

PM

-
-
-
- 0% -
- -
-

FE

+

+ {% if progress.role.code == "PM" %}PM + {% elif progress.role.code == "FRONTEND" %}FE + {% elif progress.role.code == "BACKEND" %}BE + {% endif %} +

-
+
+
- 0% + {{ progress.progress_percent }}%
- -
-

BE

-
-
-
- 0% -
-
- - -{% if role.code == "PM" %} - -
-
-
1
-
-
-
-
-
-

팀 커뮤니케이션 채널 개설

- 체크 -
-
-
    -
  • 1. 디스코드/슬랙 등 팀플용 채널 생성
  • -
  • 2. 팀원 이메일로 초대 링크 공유
  • -
-

(검색 키워드: 디스코드 서버 만들기 / 슬랙 워크스페이스 만들기)

-
-
-
-
- -
-
-
2
-
-
-
-
-
-

문서 협업 툴 준비

- 체크 -
-
-
    -
  • 1. 노션/구글독스 계정 생성
  • -
  • 2. 팀 회의록 페이지 생성 및 공유
  • -
-

(검색 키워드: 노션 회의록 템플릿)

-
-
-
-
- -
-
-
3
-
-
-
-
-
-

첫 팀 미팅 진행

- 체크 -
-
-
    -
  • 1. 자기소개
  • -
  • 2. 프로젝트 참여 목적 공유
  • -
  • 3. 팀 규칙 간단히 정하기
  • -
-
-
-
-
- -
-
-
4
-
-
-
-
-
-

기본 운영 규칙 합의

- 체크 -
-
-
    -
  • 1. 정기 회의 주기
  • -
  • 2. 스크럼(데일리/주간) 여부
  • -
  • 3. 응답 시간 기준
  • -
  • 4. 연락 불가 일정 미리 공유
  • -
-
-
-
-
- -
-
-
5
-
-
-
-
-
-

아이디어 공유 미팅 진행

- 체크 -
-
-
    -
  • 1. 각자 생각한 서비스/프로젝트 아이디어 공유
  • -
  • 2. 현실성/기간 고려해 후보 압축
  • -
-
-
-
-
- -
-
-
6
-
-
-
-
-
-

프로젝트 방향 결정

- 체크 -
-
-
    -
  • 1. 서비스 목표 (학습용 / 포폴 / 출시)
  • -
  • 2. MVP 범위 합의
  • -
-
-
-
-
- -
-
-
7
-
-
-
-
-
-

기획 문서 작성

- 체크 -
-
-
    -
  • 1. 서비스 개요
  • -
  • 2. 주요 기능
  • -
  • 3. 사용자 흐름 정리
  • -
-

(검색 키워드: 서비스 기획서 작성 양식)

-
-
-
-
- -
-
-
8
-
-
-
-
-
-

팀원 피드백 반영

- 체크 -
-
-
    -
  • 1. 프론트/백 의견 수렴
  • -
  • 2. 기술적으로 어려운 부분 조정
  • -
-
-
-
-
- -
-
-
9
-
-
-
-
-
-

역할별 업무 범위 정리

- 체크 -
-
-
    -
  • 1. 프론트 / 백 / 기타 역할 명확화
  • -
  • 2. 누가 무엇을 언제까지 할지 정리
  • -
-
-
-
-
- -
-
-
10
-
-
-
-
-
-

진행 상황 주기적 체크

- 체크 -
-
-
    -
  • 1. 일정 밀리는 부분 확인
  • -
  • 2. 병목 발생 시 우선순위 재조정
  • -
-
-
-
-
- -
-
-
11
-
-
-
-
-
-

팀 분위기 관리

- 체크 -
-
-
    -
  • 1. 진행 중 어려움 공유
  • -
  • 2. 중간 목표 달성 시 간단한 피드백
  • -
-
-
-
-
- -
-
-
12
-
-
-
-
-
-

결과물 활용 방향 논의

- 체크 -
-
-
    -
  • 1. 배포 여부
  • -
  • 2. 포트폴리오 정리
  • -
  • 3. 홍보 방식 논의
  • -
-

(오픈채팅 / SNS / 커뮤니티 등)

-
-
-
-
- -{% elif role.code == "FRONTEND" %} - - -
-
-
1
-
-
-
-
-
-

팀 커뮤니케이션 채널 참여

- 체크 -
-
-
    -
  • 1. PM이 공유한 팀플 채널 입장
  • -
-
-
-
-
- -
-
-
2
-
-
-
-
-
-

기획 내용 숙지

- 체크 -
-
-
    -
  • 1. 기획 문서 읽고 전체 흐름 파악
  • -
  • 2. 핵심 사용자 화면 정리
  • -
-
-
-
-
- -
-
-
3
-
-
-
-
-
-

디자인 컨셉 리서치

- 체크 -
-
-
    -
  • 1. 유사 서비스/디자인 레퍼런스 조사
  • -
-

(검색 키워드: Pinterest UI reference)

-
-
-
-
- -
-
-
4
-
-
-
-
-
-

레퍼런스 공유 및 합의

- 체크 -
-
-
    -
  • 1. 팀원들에게 디자인 방향 공유
  • -
  • 2. 피드백 반영해 방향 확정
  • -
-
-
-
-
- -
-
-
5
-
-
-
-
-
-

디자인 도구 선택

- 체크 -
-
-
    -
  • 1. Figma / Adobe XD / 기타 도구 선택
  • -
-

(검색 키워드: Figma 사용법)

-
-
-
+ {% endfor %}
-
+{% for mission in mission_data %} +
-
6
+
{{ forloop.counter }}
-

컬러·폰트 가이드 정리

+

{{ mission.card.title }}

+ {% if mission.is_completed %} + 체크 + {% else %} 체크 + {% endif %}
-
    -
  • 1. 컬러칩
  • -
  • 2. 기본 폰트
  • -
  • 3. 버튼/텍스트 스타일
  • -
+
+
    + {% for task_item in mission.task_progress_data %} +
  • + {{ task_item.task.title }} + {% if task_item.task.description %} +
    {{ task_item.task.description }} + {% endif %} +
  • + {% endfor %} +
+
- -
-
-
7
-
-
-
-
-
-

와이어프레임 제작

- 체크 -
-
-
    -
  • 1. 주요 화면 구조 설계
  • -
  • 2. 사용자 흐름 중심으로 구성
  • -
-
-
-
-
- -
-
-
8
-
-
-
-
-
-

와이어프레임 리뷰

- 체크 -
-
-
    -
  • 1. 팀원 피드백 반영
  • -
  • 2. 수정사항 반영
  • -
-
-
-
-
- -
-
-
9
-
-
-
-
-
-

최종 UI 디자인 완성

- 체크 -
-
-
    -
  • 1. 색상/아이콘/여백 적용
  • -
-

(검색 키워드: 사용자 편의성 UI 디자인)

-
-
-
-
- -
-
-
10
-
-
-
-
-
-

퍼블리싱 또는 프론트 구현

- 체크 -
-
-
    -
  • 1. HTML/CSS 또는 React/Vue 등 선택
  • -
  • 2. 컴포넌트 단위로 구현
  • -
-
-
-
-
- -
-
-
11
-
-
-
-
-
-

백엔드 연동 고려

- 체크 -
-
-
    -
  • 1. 데이터 위치/형식 파악
  • -
  • 2. API 연동 포인트 확인
  • -
-
-
-
-
- -
-
-
12
-
-
-
-
-
-

반응형/기본 UX 점검

- 체크 -
-
-
    -
  • 1. 모바일/데스크톱 기본 대응
  • -
  • 2. 버튼/폼 동작 확인
  • -
-
-
-
-
- -
-
-
13
-
-
-
-
-
-

UI 수정 및 정리

- 체크 -
-
-
    -
  • 1. 실제 사용 시 불편한 부분 개선
  • -
-
-
-
-
- -{% elif role.code == "BACKEND" %} - - -
-
-
1
-
-
-
-
-
-

기획 및 기능 범위 파악

- 체크 -
-
-
    -
  • 1. 어떤 기능을 서버에서 담당하는지 확인
  • -
-
-
-
-
- -
-
-
2
-
-
-
-
-
-

기술 스택 결정

- 체크 -
-
-
    -
  • 1. Django / Spring / Node 등 선택
  • -
  • 2. DB 종류 선택
  • -
-
-
-
-
- -
-
-
3
-
-
-
-
-
-

프로젝트 초기 세팅

- 체크 -
-
-
    -
  • 1. 서버 프로젝트 생성
  • -
  • 2. 기본 폴더 구조 정리
  • -
-
-
-
-
- -
-
-
4
-
-
-
-
-
-

DB 모델 설계

- 체크 -
-
-
    -
  • 1. 핵심 엔티티 정의
  • -
  • 2. 관계 설정
  • -
-
-
-
-
- -
-
-
5
-
-
-
-
-
-

기본 CRUD 설계

- 체크 -
-
-
    -
  • 1. 생성 / 조회 / 수정 / 삭제 흐름 정리
  • -
-
-
-
-
- -
-
-
6
-
-
-
-
-
-

API 구조 설계

- 체크 -
-
-
    -
  • 1. 엔드포인트 네이밍
  • -
  • 2. 요청/응답 형식 정의
  • -
-
-
-
-
- -
-
-
7
-
-
-
-
-
-

인증/권한 고려

- 체크 -
-
-
    -
  • 1. 로그인 여부
  • -
  • 2. 사용자별 접근 제한
  • -
-
-
-
-
- -
-
-
8
-
-
-
-
-
-

API 구현

- 체크 -
-
-
    -
  • 1. 기능 단위로 구현
  • -
  • 2. 예외 상황 처리
  • -
-
-
-
-
- -
-
-
9
-
-
-
-
-
-

API 테스트

- 체크 -
-
-
    -
  • 1. Postman / curl / 테스트 코드로 요청 확인
  • -
  • 2. 정상/에러 케이스 점검
  • -
-
-
-
-
- -
-
-
10
-
-
-
-
-
-

프론트 연동 지원

- 체크 -
-
-
    -
  • 1. 프론트 요청 사항 반영
  • -
  • 2. 데이터 형식 조정
  • -
-
-
-
-
- -
-
-
11
-
-
-
-
-
-

환경 설정 분리

- 체크 -
-
-
    -
  • 1. 로컬 / 배포 환경 구분
  • -
-
-
-
-
- -
-
-
12
-
-
-
-
-
-

배포 준비

- 체크 -
-
-
    -
  • 1. 서버 실행 방식 정리
  • -
  • 2. 도메인/HTTPS 고려
  • -
-
-
-
-
- -
-
-
13
-
-
-
-
-
-

기능 안정화

- 체크 -
-
-
    -
  • 1. 에러 로그 확인
  • -
  • 2. 성능/보안 기본 점검
  • -
-
-
-
-
- -{% endif %} +{% endfor %} {% endif %} + {% endblock %} \ No newline at end of file From 96043bbc86b457bebc4c2c9d848234685de15065 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 8 Feb 2026 03:19:23 +0900 Subject: [PATCH 244/380] =?UTF-8?q?fix=20:=20main=20&=20team=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/views.py | 1 + static/css/main.css | 86 +++++++++++++ static/css/team.css | 34 +----- static/css/team_apply.css | 192 ++++++++++++++++++++++-------- templates/main.html | 18 ++- templates/projects/dashboard.html | 6 +- templates/teams/team.html | 25 +--- templates/teams/team_apply.html | 37 +++++- 8 files changed, 278 insertions(+), 121 deletions(-) diff --git a/config/views.py b/config/views.py index 5c2d410..5519971 100644 --- a/config/views.py +++ b/config/views.py @@ -20,6 +20,7 @@ def main_view(request): season = Season.get_active_season() context = { 'season': season, + 'user_obj': user, } # 로그인 상태만 추가 데이터 조회 diff --git a/static/css/main.css b/static/css/main.css index ce1a1a7..b157440 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -111,6 +111,7 @@ body { font-size: 35px; } +/* 팀 매칭 기간 O 매칭 신청 X */ .main-team .m_team_stack { width: 90%; margin: 40px auto 0; @@ -260,6 +261,91 @@ body { line-height: 30px; } +/* 팀 매칭 O 매칭 결과 X */ +.team_waiting { + margin-top: 120px; + text-align: center; + align-items: center; +} + +.team_waiting > h3 { + font-size: 27px; + font-weight: 600; + margin-bottom: 20px; +} + +.team_waiting > form { + display: flex; + justify-content: space-between; + align-items: center; + margin: 30px auto 0; + width: 60%; height: 40px; + background: #fff; + border-radius: 25px; + padding: 10px 5px 10px 20px; +} + +.team_waiting > form:focus-within { + border: 1px solid #4272EF; + box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); +} + +.team_waiting > form > input { + width: 80%; + border: none; + font-size: 15px; + color: #888888; + outline: none; +} + +.team_waiting > form > input::placeholder { + color: #888888; +} + +.team_waiting > form > button { + font-size: 15px; + background: #4272EF; color: #fff; + border: none; border-radius: 20px; + padding: 7px 15px; +} + +.team_waiting > form > button:hover { + background: #1F4CC0; + transition: 0.3s ease; +} + +/* 매칭 취소 */ +.matching_actions { + margin-top: 30px; + display: flex; + justify-content: center; +} + +.cancel_form { + width: 100%; + display: flex; + justify-content: center; +} + +.cancel_button { + width: 150px; + height: 40px; + padding: 10px 20px; + background: #FF6B6B; + color: #fff; + border: none; + border-radius: 20px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: 0.3s ease; +} + +.cancel_button:hover { + background: #E63946; + transition: 0.3s ease; +} + /* KITUP 프로젝트 */ .main-project { width: 85%; diff --git a/static/css/team.css b/static/css/team.css index 80ba68c..37b5b66 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -2,7 +2,7 @@ body { background: #F6F8FF; } -/* 매칭 대기 */ +/* 매칭 성공 */ .team_matching h3 { font-size: 27px; font-weight: 600; @@ -49,38 +49,6 @@ body { transition: 0.3s ease; } -/* 매칭 취소 */ -.matching_actions { - margin-top: 30px; - display: flex; - justify-content: center; -} - -.cancel_form { - width: 100%; - display: flex; - justify-content: center; -} - -.cancel_button { - width: 150px; - height: 40px; - padding: 10px 20px; - background: #FF6B6B; - color: #fff; - border: none; - border-radius: 20px; - font-size: 15px; - font-weight: 500; - cursor: pointer; - transition: 0.3s ease; -} - -.cancel_button:hover { - background: #E63946; - transition: 0.3s ease; -} - /* 매칭 성공 */ .team_success { margin-top: 50px; diff --git a/static/css/team_apply.css b/static/css/team_apply.css index f34023b..4b75e8f 100644 --- a/static/css/team_apply.css +++ b/static/css/team_apply.css @@ -6,6 +6,146 @@ button:hover { cursor: pointer; } +/* 팀 매칭 기간 X */ +.match_wait { + margin-top: 120px; + text-align: center; + align-items: center; +} + +.match_wait h3 { + font-size: 27px; + font-weight: 600; + margin-bottom: 20px; +} + +.match_wait form { + display: flex; + justify-content: space-between; + align-items: center; + margin: 30px auto 0; + width: 60%; height: 40px; + background: #fff; + border-radius: 25px; + padding: 10px 5px 10px 20px; +} + +.match_wait form:focus-within { + border: 1px solid #4272EF; + box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); +} + +.match_wait form input { + width: 80%; + border: none; + font-size: 15px; + color: #888888; + outline: none; +} + +.match_wait form input::placeholder { + color: #888888; +} + +.match_wait form button { + font-size: 15px; + background: #4272EF; color: #fff; + border: none; border-radius: 20px; + padding: 7px 15px; +} + +.match_wait form button:hover { + background: #1F4CC0; + transition: 0.3s ease; +} + +/* 매칭 대기 */ +.team_waiting { + margin-top: 120px; + text-align: center; + align-items: center; +} + +.team_waiting > h3 { + font-size: 27px; + font-weight: 600; + margin-bottom: 20px; +} + +.team_waiting > form { + display: flex; + justify-content: space-between; + align-items: center; + margin: 30px auto 0; + width: 60%; height: 40px; + background: #fff; + border-radius: 25px; + padding: 10px 5px 10px 20px; +} + +.team_waiting > form:focus-within { + border: 1px solid #4272EF; + box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); +} + +.team_waiting > form > input { + width: 80%; + border: none; + font-size: 15px; + color: #888888; + outline: none; +} + +.team_waiting > form > input::placeholder { + color: #888888; +} + +.team_waiting > form > button { + font-size: 15px; + background: #4272EF; color: #fff; + border: none; border-radius: 20px; + padding: 7px 15px; +} + +.team_waiting > form > button:hover { + background: #1F4CC0; + transition: 0.3s ease; +} + +/* 매칭 취소 */ +.matching_actions { + margin-top: 30px; + display: flex; + justify-content: center; +} + +.cancel_form { + width: 100%; + display: flex; + justify-content: center; +} + +.cancel_button { + width: 150px; + height: 40px; + padding: 10px 20px; + background: #FF6B6B; + color: #fff; + border: none; + border-radius: 20px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: 0.3s ease; +} + +.cancel_button:hover { + background: #E63946; + transition: 0.3s ease; +} + + +/* 팀 매칭 기간 O / 매칭 신청 X */ .team_matching { margin-top: 100px; text-align: center; @@ -162,55 +302,3 @@ button:hover { background: #C03067; transition: 0.5s ease-in-out; } - -.match_wait { - margin-top: 120px; - text-align: center; - align-items: center; -} - -.match_wait h3 { - font-size: 27px; - font-weight: 600; - margin-bottom: 20px; -} - -.match_wait form { - display: flex; - justify-content: space-between; - align-items: center; - margin: 30px auto 0; - width: 60%; height: 40px; - background: #fff; - border-radius: 25px; - padding: 10px 5px 10px 20px; -} - -.match_wait form:focus-within { - border: 1px solid #4272EF; - box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); -} - -.match_wait form input { - width: 80%; - border: none; - font-size: 15px; - color: #888888; - outline: none; -} - -.match_wait form input::placeholder { - color: #888888; -} - -.match_wait form button { - font-size: 15px; - background: #4272EF; color: #fff; - border: none; border-radius: 20px; - padding: 7px 15px; -} - -.match_wait form button:hover { - background: #1F4CC0; - transition: 0.3s ease; -} \ No newline at end of file diff --git a/templates/main.html b/templates/main.html index aed34ce..c5b721a 100644 --- a/templates/main.html +++ b/templates/main.html @@ -27,7 +27,12 @@

오늘의 작업을 기록해보세요.

{% else %} {% endif %}
- {% if season.status == 'MATCHING' %} + + {% if season.status != 'MATCHING' %} +

지금은 팀 매칭 모집 기간이 아니예요.

+ + + {% elif user_obj.passion_level == None %}

팀 매칭 모집이 시작됐어요

6주의 기간동안 희망하는 스택으로 프로젝트를 진행하고, 실력을 쌓아보아요. @@ -148,10 +153,8 @@

WEB 백엔드

{% endif %}
- {% elif not season %} -

지금은 팀 매칭 모집 기간이 아니예요.

- - + + {% elif season.status == 'IN_PROJECT' %}

팀 매칭 결과가 발표됐어요.

@@ -163,6 +166,11 @@

팀 매칭 결과가 발표됐어요.

>

결과 보러가기

+ + + {% else %} +

팀 매칭 신청이 완료되었어요.

+ {% endif %}
diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index aaae27b..2462f44 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -60,20 +60,20 @@

Team

PM {% for member in members %} - {% if member.role.name == "기획자" %} + {% if member.role.code == "PM" %} {{ member.user.username }} {% endif %} {% endfor %}

FE {% for member in members %} - {% if member.role.name == "프론트엔드" %} + {% if member.role.code == "FRONTEND" %} {{ member.user.username }} {% endif %} {% endfor %}

BE {% for member in members %} - {% if member.role.name == "백엔드" %} + {% if member.role.code == "BACKEND" %} {{ member.user.username }} {% endif %} {% endfor %} diff --git a/templates/teams/team.html b/templates/teams/team.html index 76c6a0b..2004cd2 100644 --- a/templates/teams/team.html +++ b/templates/teams/team.html @@ -1,27 +1,8 @@ {% extends 'base.html' %} {% load static %} {% block header %} {% endblock %} {% block content %} - -{% if is_matching_period %} -

-

- 팀 매칭 신청이 완료되었어요.
- 팀 매칭 결과가 발표되면 메일로 알려드릴게요. -

-
- - -
-
-
- {% csrf_token %} - -
-
-
- - -{% elif team_matched %} + +{% if team_matched %}

팀 매칭이 완료 되었어요.
@@ -89,7 +70,7 @@

>

- + {% else %}

팀 매칭에 실패했어요.
diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index adb73d4..936dd82 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -1,7 +1,22 @@ {% extends 'base.html' %} {% load static %} {% block header %} {% endblock %} {% block content %} -{% if season.status == 'MATCHING' %} + + +{% if season.status != 'MATCHING' %} +
+

+ 지금은 팀 매칭 기간이 아니예요.
+ 팀 매칭 기간이 되면 메일로 알려드릴게요. +

+
+ + +
+
+ + +{% elif user_obj.passion_level == None %}

팀 매칭 모집 기간이 시작됐어요!

@@ -123,18 +138,28 @@

WEB 백엔드

{% endif %}

+ +
+ + {% else %} -
+

- 지금은 팀 매칭 기간이 아니예요.
- 팀 매칭 기간이 되면 메일로 알려드릴게요. + 팀 매칭 신청이 완료되었어요.
+ 팀 매칭 결과가 발표되면 메일로 알려드릴게요.

-
+
+
+
+ {% csrf_token %} + +
+
{% endif %} {% endblock %} -
+
From ae9bb5276f94ff85281db67ee65e7ad1eb326dc2 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 8 Feb 2026 03:52:52 +0900 Subject: [PATCH 245/380] =?UTF-8?q?feat=20:=20dashboard=20popup=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 88 ++++++++++++++++++++++++++++++- static/css/main.css | 30 +++++++++-- static/js/dashboard.js | 27 ++++++++++ templates/projects/dashboard.html | 39 +++++++++++++- 4 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 static/js/dashboard.js diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 9fbbb2a..62059c7 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -448,4 +448,90 @@ button:hover { cursor: pointer; background: #1F4CC0; transition: 0.3s ease; -} \ No newline at end of file +} + +/* 모달 배경 */ +.modal-overlay { + display: none; + position: fixed; + top: 0; left: 0; + width: 100vw; height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; + display: none; + justify-content: center; + align-items: center; +} + +/* 팝업 박스 */ +.modal-content { + background: white; + width: 450px; height: 70%; + padding: 30px; + border-radius: 15px; + box-shadow: 0 5px 20px rgba(0,0,0,0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} + +.modal-header h3 { font-size: 20px; font-weight: 700; color: #1d294b; } + +.close-btn { cursor: pointer; font-size: 15px; color: #aaa; } + +/* 멤버 리스트 */ +.member-select-list { + margin: 15px 0; + max-height: 180px; + overflow-y: auto; +} + +.member-option { + display: flex; + align-items: center; + padding: 12px; + border: 1px solid #eee; + border-radius: 10px; + margin-bottom: 10px; + cursor: pointer; +} + +.member-option:hover { background: #f6f8ff; } + +.member-info { + display: flex; align-items: center; gap: 10px; + margin-left: 10px; +} + +.member-info img { + width: 35px; height: 35px; + border-radius: 50%; + object-fit: cover; +} + +/* 입력창 */ +textarea { + width: 100%; height: 50px; + margin-top: 10px; + padding: 12px; + border: 1px solid #ddd; + border-radius: 8px; + resize: none; + box-sizing: border-box; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +.btn-danger { + background: #4272EF; color: white; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; + font-size: 15px; +} +.btn-secondary { background: #f0f0f0; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; } \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css index b157440..000d356 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -96,6 +96,12 @@ body { border-radius: 20px; padding: 7px 30px; } +.m_note_btn:hover { + cursor: pointer; + background: #1A3C97; + transition: 0.3s ease; +} + /* 팀매칭 */ .main-team { margin-top: 90px; @@ -146,7 +152,7 @@ body { font-size: 16px; } -.main-team .m_team_stack .m_design .m_design_btn { +.m_design_btn { margin-top: 15px; padding: 10px 0; width: 250px; @@ -159,6 +165,12 @@ body { font-weight: 550; } +.m_design_btn:hover { + background: #019890; + cursor: pointer; + transition: 0.3s ease; +} + /* WEB 프론트엔드 */ .main-team .m_team_stack .m_frontend { padding: 25px 0; @@ -186,7 +198,7 @@ body { font-size: 16px; } -.main-team .m_team_stack .m_frontend .m_front_btn { +.m_front_btn { margin-top: 15px; padding: 5px 0; width: 250px; @@ -199,6 +211,12 @@ body { font-weight: 550; } +.m_front_btn:hover { + cursor: pointer; + background: #E2BF67; + transition: 0.3s ease; +} + /* WEB 백엔드 */ .main-team .m_team_stack .m_backend { padding: 25px 0; @@ -226,7 +244,7 @@ body { font-size: 16px; } -.main-team .m_team_stack .m_backend .m_back_btn { +m_back_btn { margin-top: 15px; padding: 10px 0; width: 250px; @@ -239,6 +257,12 @@ body { font-weight: 550; } +.m_back_btn:hover { + cursor: pointer; + background: #C03067; + transition: 0.3s ease; +} + /* 팀 매칭 결과 */ .m_team_result { display: block; diff --git a/static/js/dashboard.js b/static/js/dashboard.js new file mode 100644 index 0000000..93686fb --- /dev/null +++ b/static/js/dashboard.js @@ -0,0 +1,27 @@ +document.addEventListener('DOMContentLoaded', function() { + const modal = document.getElementById('reportModal'); + const openBtn = document.querySelector('.btn_report'); + const closeBtns = document.querySelectorAll('.close-btn'); + + // 팝업 열기 + if(openBtn) { + openBtn.addEventListener('click', function(e) { + e.preventDefault(); + modal.style.display = 'flex'; + }); + } + + // 팝업 닫기 + closeBtns.forEach(btn => { + btn.addEventListener('click', () => { + modal.style.display = 'none'; + }); + }); + + // 배경 클릭 시 닫기 + window.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.display = 'none'; + } + }); +}); \ No newline at end of file diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index 2462f44..13e65fd 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -54,7 +54,7 @@

{{ project.title }}

Team

@@ -153,4 +153,41 @@

진척도

{% endif %}
+ + {% endblock %} \ No newline at end of file From baf7a1fa8635eaa9ea578e643e2cb872ec1bdbbb Mon Sep 17 00:00:00 2001 From: knana6 Date: Sun, 8 Feb 2026 16:13:19 +0900 Subject: [PATCH 246/380] fix: signup margin --- static/css/signup.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/css/signup.css b/static/css/signup.css index 08f62ed..92bea7b 100644 --- a/static/css/signup.css +++ b/static/css/signup.css @@ -37,6 +37,8 @@ body { .signup-form { display: flex; flex-direction: column; + margin-left: -35px; + margin-right: -35px; gap: 16px; } From 36b5c370007995216307d55ead5ab8b168b86454 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 8 Feb 2026 17:08:45 +0900 Subject: [PATCH 247/380] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=EC=97=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=95=8C=EB=A6=BC=20=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0=20boolean=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0007_user_email_notifications_enabled.py | 18 ++++++++++++++++++ ...8_alter_user_email_notifications_enabled.py | 18 ++++++++++++++++++ apps/accounts/models.py | 5 +++++ 3 files changed, 41 insertions(+) create mode 100644 apps/accounts/migrations/0007_user_email_notifications_enabled.py create mode 100644 apps/accounts/migrations/0008_alter_user_email_notifications_enabled.py diff --git a/apps/accounts/migrations/0007_user_email_notifications_enabled.py b/apps/accounts/migrations/0007_user_email_notifications_enabled.py new file mode 100644 index 0000000..7139c6f --- /dev/null +++ b/apps/accounts/migrations/0007_user_email_notifications_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-02-08 07:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_techstack_user_tech_stacks'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='email_notifications_enabled', + field=models.BooleanField(default=True, help_text='이메일 알림 수신 여부'), + ), + ] diff --git a/apps/accounts/migrations/0008_alter_user_email_notifications_enabled.py b/apps/accounts/migrations/0008_alter_user_email_notifications_enabled.py new file mode 100644 index 0000000..f3a283b --- /dev/null +++ b/apps/accounts/migrations/0008_alter_user_email_notifications_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-02-08 08:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0007_user_email_notifications_enabled'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email_notifications_enabled', + field=models.BooleanField(default=False, help_text='이메일 알림 수신 여부'), + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 02abf05..060233d 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -97,6 +97,11 @@ class User(AbstractUser): help_text="남은 팀플 참여 금지 횟수", ) + email_notifications_enabled = models.BooleanField( + default=False, + help_text="이메일 알림 수신 여부", + ) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From fdec5828dd5d5685468a37b0bedbcb4e0e99c8c2 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 8 Feb 2026 17:08:46 +0900 Subject: [PATCH 248/380] =?UTF-8?q?fix=20:=20main=20css=20=EA=B9=A8?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/main.css b/static/css/main.css index 000d356..4a33151 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -244,7 +244,7 @@ body { font-size: 16px; } -m_back_btn { +.m_back_btn { margin-top: 15px; padding: 10px 0; width: 250px; From 6acafd0493f2502036e6c80a00c7bd0b30830975 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 8 Feb 2026 17:11:22 +0900 Subject: [PATCH 249/380] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EB=B3=B4=EB=82=B4=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/admin.py | 3 +- apps/projects/admin.py | 43 ++++++++- apps/projects/services.py | 197 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 3 deletions(-) diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index e5f179b..75b52b6 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -15,7 +15,7 @@ class UserRoleLevelInline(admin.TabularInline): @admin.register(User) class UserAdmin(BaseUserAdmin): - list_display = ["id", "username", "nickname", "email", "passion_level", "team_ban_count", "is_staff", "created_at"] + list_display = ["id", "username", "nickname", "email", "passion_level", "team_ban_count", "email_notifications_enabled", "is_staff", "created_at"] list_filter = ["is_staff", "is_active", "created_at", "passion_level"] search_fields = ["username", "nickname", "email"] ordering = ["-created_at"] @@ -24,6 +24,7 @@ class UserAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( ("프로필 정보", {"fields": ("nickname", "profile_image", "bio", "tech_stacks")}), + ("알림 설정", {"fields": ("email_notifications_enabled",)}), ("관리 정보", {"fields": ("passion_level", "team_ban_count")}), ) diff --git a/apps/projects/admin.py b/apps/projects/admin.py index d4bbc96..9e84840 100644 --- a/apps/projects/admin.py +++ b/apps/projects/admin.py @@ -4,7 +4,7 @@ from django.utils.translation import ngettext from .models import Season, Project, ProjectApplication -from .services import TeamMatchingService +from .services import TeamMatchingService, EmailService @admin.register(Season) @@ -13,7 +13,7 @@ class SeasonAdmin(admin.ModelAdmin): list_filter = ["status", "is_active", "created_at"] search_fields = ["name"] ordering = ["-created_at"] - actions = ["activate_season", "deactivate_season", "run_team_matching"] + actions = ["activate_season", "deactivate_season", "run_team_matching", "send_matching_start_email", "send_matching_results_email"] def activate_season(self, request, queryset): """시즌 활성화 (이전 활성 시즌은 자동 비활성화)""" @@ -52,7 +52,46 @@ def run_team_matching(self, request, queryset): f"❌ [{season.name}] 팀 매칭 오류: {str(e)}", messages.ERROR, ) + + def send_matching_results_email(self, request, queryset): + """팀 매칭 결과 이메일 발송""" + for season in queryset: + try: + result = EmailService.send_matching_results(season.id) + self.message_user( + request, + f"📧 [{season.name}] 팀 매칭 결과 이메일 발송 완료: " + f"{result['sent_count']}개 팀 / {result['failed_count']}개 실패", + messages.SUCCESS, + ) + except Exception as e: + self.message_user( + request, + f"❌ [{season.name}] 이메일 발송 오류: {str(e)}", + messages.ERROR, + ) + + def send_matching_start_email(self, request, queryset): + """팀 매칭 기간 시작 알림 이메일 발송""" + for season in queryset: + try: + result = EmailService.send_matching_start_notification(season.id) + self.message_user( + request, + f"📧 [{season.name}] 팀 매칭 시작 알림 이메일 발송 완료: " + f"{result['sent_count']}명 / {result['failed_count']}명 실패", + messages.SUCCESS, + ) + except Exception as e: + self.message_user( + request, + f"❌ [{season.name}] 이메일 발송 오류: {str(e)}", + messages.ERROR, + ) + run_team_matching.short_description = "🤝 팀 매칭 알고리즘 실행" + send_matching_start_email.short_description = "📢 팀 매칭 시작 알림 이메일 발송" + send_matching_results_email.short_description = "📧 팀 매칭 결과 이메일 발송" activate_season.short_description = "✅ 선택된 시즌 활성화" deactivate_season.short_description = "❌ 선택된 시즌 비활성화" diff --git a/apps/projects/services.py b/apps/projects/services.py index 2dea161..e651a3a 100644 --- a/apps/projects/services.py +++ b/apps/projects/services.py @@ -3,6 +3,9 @@ """ from django.db import transaction from django.core.exceptions import ValidationError +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.conf import settings from apps.accounts.models import User, Role, UserRoleLevel from apps.projects.models import Season, Project @@ -176,3 +179,197 @@ def _get_role_candidates(applicants, role_code): candidates.sort(key=lambda x: x['level'], reverse=True) return [c['user'] for c in candidates] + + +class EmailService: + """이메일 발송 서비스""" + + @staticmethod + def send_matching_start_notification(season_id): + """ + 팀 매칭 기간 시작 알림 이메일 발송 + - 모든 사용자에게 매칭 신청 유도 이메일 발송 + + Args: + season_id: Season ID + + Returns: + dict: 발송 결과 통계 + """ + season = Season.objects.get(id=season_id) + + # 이메일 알림 활성화 사용자 조회 + users = User.objects.filter( + email_notifications_enabled=True + ).exclude(email='') + + sent_count = 0 + failed_count = 0 + + for user in users: + try: + EmailService._send_matching_start_email( + user=user, + season=season + ) + sent_count += 1 + except Exception as e: + print(f"❌ 사용자 {user.id} ({user.email}) 이메일 발송 실패: {str(e)}") + failed_count += 1 + + return { + 'season_id': season_id, + 'users_total': users.count(), + 'sent_count': sent_count, + 'failed_count': failed_count, + } + + @staticmethod + def _send_matching_start_email(user, season): + """ + 개별 사용자에게 팀 매칭 기간 시작 알림 발송 + + Args: + user: 수신자 + season: 현재 시즌 + """ + context = { + 'user': user, + 'season': season, + 'matching_start': season.matching_start.strftime('%Y년 %m월 %d일'), + 'matching_end': season.matching_end.strftime('%Y년 %m월 %d일'), + } + + # HTML 템플릿 렌더링 + html_message = render_to_string( + 'emails/matching_start.html', + context + ) + + # 일반 텍스트 버전 + text_message = render_to_string( + 'emails/matching_start.txt', + context + ) + + # 이메일 발송 + send_mail( + subject=f'[KITUP] {season.name} 팀 매칭이 시작되었습니다', + message=text_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[user.email], + html_message=html_message, + fail_silently=False, + ) + + @staticmethod + def send_matching_results(season_id): + """ + 팀 매칭 결과 이메일 발송 + - 매칭된 팀의 멤버들에게만 발송 + - 발송 완료 후 email_notifications_enabled = False로 변경 + + Args: + season_id: Season ID + + Returns: + dict: 발송 결과 통계 + """ + season = Season.objects.get(id=season_id) + + # 현재 시즌의 모든 팀 조회 + teams = Team.objects.filter( + project__season=season + ).prefetch_related( + 'members__user', + 'members__role', + 'project' + ).distinct() + + sent_count = 0 + failed_count = 0 + notified_users = [] + + for team in teams: + try: + # 각 팀의 모든 멤버에게 매칭 결과 이메일 발송 + for member in team.members.all(): + if member.user.email_notifications_enabled: + EmailService._send_team_matching_email( + user=member.user, + team=team, + season=season + ) + notified_users.append(member.user.id) + sent_count += 1 + except Exception as e: + print(f"❌ 팀 {team.id} 이메일 발송 실패: {str(e)}") + failed_count += 1 + + # 이메일을 받은 사용자들의 email_notifications_enabled를 False로 변경 + if notified_users: + User.objects.filter(id__in=notified_users).update(email_notifications_enabled=False) + + return { + 'season_id': season_id, + 'teams_total': teams.count(), + 'sent_count': sent_count, + 'failed_count': failed_count, + 'notified_users': len(notified_users), + } + + @staticmethod + def _send_team_matching_email(user, team, season): + """ + 개별 사용자에게 팀 매칭 결과 이메일 발송 + + Args: + user: 수신자 + team: 할당된 팀 + season: 현재 시즌 + """ + # 팀원 정보 수집 + team_members = [] + for member in team.members.all(): + role_level = UserRoleLevel.objects.filter( + user=member.user, + role=member.role + ).first() + + team_members.append({ + 'user': member.user, + 'role': member.role, + 'level': role_level.level if role_level else None, + }) + + # 이메일 컨텍스트 + context = { + 'user': user, + 'team': team, + 'team_members': team_members, + 'season': season, + 'project_start': season.project_start.strftime('%Y년 %m월 %d일'), + 'project_end': season.project_end.strftime('%Y년 %m월 %d일'), + } + + # HTML 템플릿 렌더링 + html_message = render_to_string( + 'emails/matching_result.html', + context + ) + + # 일반 텍스트 버전 + text_message = render_to_string( + 'emails/matching_result.txt', + context + ) + + # 이메일 발송 + send_mail( + subject=f'[KITUP] {season.name} 팀 매칭 완료', + message=text_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[user.email], + html_message=html_message, + fail_silently=False, + ) From 9cc0332f3a1dc44fd245610e3e2e71ca6b469e08 Mon Sep 17 00:00:00 2001 From: issuejong Date: Sun, 8 Feb 2026 17:31:14 +0900 Subject: [PATCH 250/380] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=BC=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/email-matching-result.css | 159 ++++++++++++++++++++++++++ templates/emails/matching_result.html | 74 ++++++++++++ templates/emails/matching_result.txt | 27 +++++ templates/emails/matching_start.html | 56 +++++++++ templates/emails/matching_start.txt | 32 ++++++ 5 files changed, 348 insertions(+) create mode 100644 static/css/email-matching-result.css create mode 100644 templates/emails/matching_result.html create mode 100644 templates/emails/matching_result.txt create mode 100644 templates/emails/matching_start.html create mode 100644 templates/emails/matching_start.txt diff --git a/static/css/email-matching-result.css b/static/css/email-matching-result.css new file mode 100644 index 0000000..00ddb0d --- /dev/null +++ b/static/css/email-matching-result.css @@ -0,0 +1,159 @@ +/* Email Template Styles - Matching Result */ + +/* Base Styles */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif; + line-height: 1.6; + color: #333; + background-color: #f5f5f5; +} + +.container { + max-width: 600px; + margin: 0 auto; + background-color: #ffffff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* Header Section */ +.header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px 20px; + text-align: center; +} + +.header h1 { + margin: 0; + font-size: 28px; + font-weight: 600; +} + +/* Content Section */ +.content { + padding: 30px 20px; +} + +.greeting { + font-size: 18px; + color: #333; + margin-bottom: 20px; +} + +.greeting strong { + color: #667eea; +} + +/* Period Info Box */ +.period-info { + background-color: #f0f4ff; + border-left: 4px solid #667eea; + padding: 15px; + margin: 20px 0; + border-radius: 4px; +} + +.period-info p { + margin: 5px 0; + font-size: 14px; +} + +/* Team Section */ +.team-section { + margin: 30px 0; +} + +.team-section h2 { + font-size: 18px; + color: #667eea; + margin-bottom: 20px; + border-bottom: 2px solid #f0f0f0; + padding-bottom: 10px; +} + +/* Team Members Table */ +.team-members { + width: 100%; + border-collapse: collapse; + margin: 20px 0; +} + +.team-members thead { + background-color: #667eea; + color: white; +} + +.team-members th { + padding: 16px 12px; + text-align: left; + font-weight: 600; + font-size: 14px; +} + +.team-members td { + padding: 20px 12px; + border-bottom: 1px solid #e0e0e0; + vertical-align: middle; +} + +.team-members tbody tr:hover { + background-color: #f9f9f9; +} + +.team-members tbody tr:last-child td { + border-bottom: none; +} + +/* Member Details */ +.member-name { + font-weight: 600; + color: #333; + font-size: 15px; +} + +.role-badge { + display: inline-block; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + white-space: nowrap; +} + +.role-pm { + background-color: #FFE5B4; + color: #FF8C00; +} + +.role-fe { + background-color: #B4E5FF; + color: #0066CC; +} + +.role-be { + background-color: #B4FFB4; + color: #006600; +} + +/* Footer Section */ +.footer { + background-color: #f5f5f5; + padding: 20px; + text-align: center; + border-top: 1px solid #e0e0e0; + font-size: 12px; + color: #666; +} + +.cta-button { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 12px 30px; + text-decoration: none; + border-radius: 25px; + font-weight: 600; + margin-top: 20px; +} diff --git a/templates/emails/matching_result.html b/templates/emails/matching_result.html new file mode 100644 index 0000000..7477168 --- /dev/null +++ b/templates/emails/matching_result.html @@ -0,0 +1,74 @@ + + + + + + + +
+ +
+

🎉 팀 매칭 완료

+
+ + +
+
+ 안녕하세요, {{ user.nickname }}님! 👋 +
+ +

+ {{ season.name }} 팀 매칭이 완료되었습니다!
+ 함께할 팀원들을 소개합니다. +

+ + +
+

📅 프로젝트 기간

+

{{ project_start }} ~ {{ project_end }}

+
+ + +
+

👥 당신의 팀

+ + + + + + + + + + {% for member in team_members %} + + + + + + {% endfor %} + +
닉네임역할레벨
{{ member.user.nickname }} + {% if member.role.code == "PM" %} + 기획자 (PM) + {% elif member.role.code == "FRONTEND" %} + 프론트엔드 (FE) + {% elif member.role.code == "BACKEND" %} + 백엔드 (BE) + {% endif %} + Lv{{ member.level }}
+
+ +

+ KITUP 대시보드에서 팀 정보와 프로젝트 세부사항을 확인할 수 있습니다. +

+
+ + + +
+ + diff --git a/templates/emails/matching_result.txt b/templates/emails/matching_result.txt new file mode 100644 index 0000000..1c3405d --- /dev/null +++ b/templates/emails/matching_result.txt @@ -0,0 +1,27 @@ +팀 매칭 완료 + +안녕하세요, {{ user.nickname }}님! + +{{ season.name }} 팀 매칭이 완료되었습니다. +함께할 팀원들을 소개합니다. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📅 프로젝트 기간 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +{{ project_start }} ~ {{ project_end }} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +👥 당신의 팀 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +[이름] [역할] [레벨] +{% for member in team_members %} +{{ member.user.nickname|ljust:18 }} {% if member.role.code == "PM" %}기획자 (PM){% elif member.role.code == "FRONTEND" %}프론트엔드 (FE){% elif member.role.code == "BACKEND" %}백엔드 (BE){% endif %}{% if member.role.code == "PM" %} {% elif member.role.code == "FRONTEND" %} {% elif member.role.code == "BACKEND" %} {% endif %} Lv{{ member.level }} +{% endfor %} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +KITUP 대시보드에서 팀 정보와 프로젝트 세부사항을 확인할 수 있습니다. + +이 이메일은 KITUP에서 자동으로 발송된 메일입니다. +© 2026 KITUP. All rights reserved. diff --git a/templates/emails/matching_start.html b/templates/emails/matching_start.html new file mode 100644 index 0000000..bd2725a --- /dev/null +++ b/templates/emails/matching_start.html @@ -0,0 +1,56 @@ + + + + + + + +
+ +
+

🎯 팀 매칭 기간이 시작되었습니다!

+
+ + +
+
+ 안녕하세요, {{ user.nickname }}님! 👋 +
+ +

+ {{ season.name }} 팀 매칭 기간이 시작되었습니다!
+ 이제 당신과 함께 프로젝트를 할 팀원들을 찾을 수 있습니다. +

+ + +
+

📅 팀 매칭 기간

+

{{ matching_start }} ~ {{ matching_end }}

+
+ + +
+

💡 팀 매칭 신청 방법

+
    +
  1. KITUP 앱에 접속하세요
  2. +
  3. 열정 레벨 테스트를 작성하세요
  4. +
  5. 원하는 역할을 선택하세요
  6. +
  7. 매칭이 완료될 때까지 기다려주세요
  8. +
+
+ +

+ ⚠️ 주의사항:
+ 열정 레벨 테스트를 작성하지 않으면 팀 매칭에 참여할 수 없습니다.
+ 기간 내에 신청을 완료해주세요! +

+
+ + + +
+ + diff --git a/templates/emails/matching_start.txt b/templates/emails/matching_start.txt new file mode 100644 index 0000000..690a5ef --- /dev/null +++ b/templates/emails/matching_start.txt @@ -0,0 +1,32 @@ +팀 매칭 기간이 시작되었습니다! + +안녕하세요, {{ user.nickname }}님! + +{{ season.name }} 팀 매칭 기간이 시작되었습니다. +이제 당신과 함께 프로젝트를 할 팀원들을 찾을 수 있습니다. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📅 팀 매칭 기간 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +{{ matching_start }} ~ {{ matching_end }} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +💡 팀 매칭 신청 방법 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. KITUP 앱에 접속하세요 +2. 열정 레벨 테스트를 작성하세요 +3. 원하는 역할을 선택하세요 +4. 매칭이 완료될 때까지 기다려주세요 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⚠️ 주의사항 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +열정 레벨 테스트를 작성하지 않으면 팀 매칭에 참여할 수 없습니다. +기간 내에 신청을 완료해주세요! + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +이 이메일은 KITUP에서 자동으로 발송된 메일입니다. +© 2026 KITUP. All rights reserved. From fca8013958f2b2c859995cdbd9fd11fef7f9dca7 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Sun, 8 Feb 2026 17:51:52 +0900 Subject: [PATCH 251/380] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B3=A0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20AJAX?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/api_urls.py | 1 + apps/accounts/serializers.py | 9 +++++ apps/accounts/views.py | 51 +++++++++++++++++++++++++++- static/js/dashboard.js | 56 +++++++++++++++++++++++++++++++ templates/projects/dashboard.html | 1 + 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 apps/accounts/serializers.py diff --git a/apps/accounts/api_urls.py b/apps/accounts/api_urls.py index 57b906e..8593cd5 100644 --- a/apps/accounts/api_urls.py +++ b/apps/accounts/api_urls.py @@ -6,4 +6,5 @@ path("check-email/", views.check_email, name="check_email"), path("check-nickname/", views.check_nickname, name="check_nickname"), path("level-test/submit/", views.level_submit, name="level_submit"), + path("report/create", views.create_report, name="create_report"), ] diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py new file mode 100644 index 0000000..b5203f4 --- /dev/null +++ b/apps/accounts/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +class ReportCreateRequestSerializer(serializers.Serializer): + reported_user_id = serializers.IntegerField() + reason = serializers.CharField(min_length=1, max_length=2000) + +class ReportCreateResponseSerializer(serializers.Serializer): + ok = serializers.BooleanField() + report_id = serializers.IntegerField() diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 2d02447..6e2e813 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -8,8 +8,15 @@ from django.http import HttpResponseBadRequest, JsonResponse from django.views.decorators.http import require_GET, require_POST +from drf_spectacular.utils import extend_schema +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 .forms import OnboardingForm, ProfileUpdateForm -from .models import Role, User, UserRoleLevel +from .models import Role, User, UserRoleLevel, Report +from .serializers import ReportCreateRequestSerializer, ReportCreateResponseSerializer @require_GET @@ -240,3 +247,45 @@ def withdraw(request): return redirect("/") return render(request, "account/withdraw.html") + +@extend_schema( + tags=["Accounts"], + summary="유저 신고 생성", + request=ReportCreateRequestSerializer, + responses={201: ReportCreateResponseSerializer, 400: None, 409: None}, +) +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def create_report(request): + ser = ReportCreateRequestSerializer(data=request.data) + ser.is_valid(raise_exception=True) + + reported_user_id = ser.validated_data["reported_user_id"] + reason = ser.validated_data["reason"] + + if not reported_user_id: + return JsonResponse({"ok": False, "error": "reported_user_id is required"}, status=400) + if not reason: + return JsonResponse({"ok": False, "error": "reason is required"}, status=400) + + reported_user = get_object_or_404(User, pk=reported_user_id) + + # 자기 자신 신고 방지 + if reported_user.id == request.user.id: + return JsonResponse({"ok": False, "error": "cannot report yourself"}, status=400) + + # (선택) 동일 대상 중복 신고 방지: 대기중(PENDING) 하나만 허용 같은 정책 + if Report.objects.filter( + reporter=request.user, + reported_user=reported_user, + status=Report.Status.PENDING, + ).exists(): + return JsonResponse({"ok": False, "error": "already reported (pending)"}, status=409) + + report = Report.objects.create( + reporter=request.user, + reported_user=reported_user, + reason=reason, + ) + + return JsonResponse({"ok": True, "report_id": report.id}, status=201) \ No newline at end of file diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 93686fb..5f4517c 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -2,6 +2,15 @@ document.addEventListener('DOMContentLoaded', function() { const modal = document.getElementById('reportModal'); const openBtn = document.querySelector('.btn_report'); const closeBtns = document.querySelectorAll('.close-btn'); + const form = document.getElementById("reportForm"); + const textarea = form.querySelector("textarea[name='reason']"); + + + /* CSRF */ + function getCookie(name) { + const v = document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)"); + return v ? v.pop() : ""; + } // 팝업 열기 if(openBtn) { @@ -15,6 +24,7 @@ document.addEventListener('DOMContentLoaded', function() { closeBtns.forEach(btn => { btn.addEventListener('click', () => { modal.style.display = 'none'; + form.reset(); }); }); @@ -24,4 +34,50 @@ document.addEventListener('DOMContentLoaded', function() { modal.style.display = 'none'; } }); + + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const selected = form.querySelector( + "input[name='reported_user']:checked" + ); + const reason = textarea.value.trim(); + + if (!selected) { + alert("신고할 팀원을 선택해주세요."); + return; + } + if (!reason) { + alert("신고 사유를 입력해주세요."); + return; + } + + try { + const res = await fetch("/api/accounts/report/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCookie("csrftoken"), + }, + credentials: "same-origin", + body: JSON.stringify({ + reported_user_id: selected.value, + reason: reason, + }), + }); + + const data = await res.json(); + + if (!res.ok) { + alert(data.error || "신고에 실패했습니다."); + return; + } + + alert("신고가 접수되었습니다."); + modal.style.display = "none"; + form.reset(); + } catch (err) { + alert("네트워크 오류가 발생했습니다."); + } + }); }); \ No newline at end of file diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index 13e65fd..c118bf6 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -161,6 +161,7 @@

버스 탑승자 신고하기

+ {% csrf_token %} -

로그인

+

로그인

{% if form.errors %} diff --git a/templates/account/signup.html b/templates/account/signup.html index d6e0388..23f53fb 100644 --- a/templates/account/signup.html +++ b/templates/account/signup.html @@ -58,7 +58,7 @@

회원가입

-
From 2f8dbc704c8f72474c49f0115ca51a779c8f0443 Mon Sep 17 00:00:00 2001 From: knana6 Date: Sun, 8 Feb 2026 19:08:40 +0900 Subject: [PATCH 254/380] fix: social login layout --- static/css/login.css | 6 +++--- static/css/signup.css | 28 ++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/static/css/login.css b/static/css/login.css index d7c27f4..fc26daa 100644 --- a/static/css/login.css +++ b/static/css/login.css @@ -131,14 +131,14 @@ body { } .social-icon { - width: 48px; - height: 48px; + width: 44px; + height: 44px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: white; - border: 1px solid #e0e0e0; + border: 0.5px solid #e0e0e0; transition: all 0.3s ease; cursor: pointer; } diff --git a/static/css/signup.css b/static/css/signup.css index 42eb060..8895fe8 100644 --- a/static/css/signup.css +++ b/static/css/signup.css @@ -113,7 +113,8 @@ body { } .social-login { - + justify-content: center; + gap: 16px; margin-top: 32px; padding-top: 32px; border-top: 1px solid #e0e0e0; @@ -133,14 +134,14 @@ body { } .social-icon { - width: 48px; - height: 48px; + width: 44px; + height: 44px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: white; - border: 1px solid #e0e0e0; + border: 0.5px solid #e0e0e0; transition: all 0.3s ease; cursor: pointer; } @@ -155,4 +156,23 @@ body { width: 100%; height: 100%; border-radius: 50%; +} +@media (max-width: 480px) { + .login-box { + padding: 32px 24px; + } + + .login-title { + font-size: 20px; + } + + .social-icon { + width: 44px; + height: 44px; + } + + .social-icon img { + width: 20px; + height: 20px; + } } \ No newline at end of file From bc42b3e039243893683d64ff5dcc8668cafdf254 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Sun, 8 Feb 2026 20:37:40 +0900 Subject: [PATCH 255/380] =?UTF-8?q?fix:=20=ED=9A=8C=EA=B3=A0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8,=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EC=84=A0=ED=83=9D=EC=9D=84=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=9C=EA=B3=B5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reflections/views.py | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 6c8b4ae..6f1aca2 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -29,6 +29,38 @@ from apps.teams.models import TeamMember +def _get_my_projects_and_roles(user): + """ + 내 프로젝트와 프로젝트에서의 내 역할 찾기 + """ + my_project_ids = ( + TeamMember.objects + .filter(user=user) + .values_list("team__project_id", flat=True) + .distinct() + ) + + my_projects = ( + Project.objects + .filter(Q(id__in=my_project_ids) | Q(owner=user)) + .order_by("title") + ) + + # 프로젝트별 내 role 코드(TeamMember.role.code) 매핑 + role_map = {} + tm_qs = ( + TeamMember.objects + .filter(user=user, team__project__in=my_projects) + .select_related("role", "team__project") + ) + for tm in tm_qs: + pid = tm.team.project_id + # 같은 프로젝트에 팀멤버가 여러개면(이상 케이스) 첫 값 유지 + role_map.setdefault(pid, getattr(tm.role, "code", None)) + + return my_projects, role_map + + @login_required def note_list(request): """ @@ -132,12 +164,30 @@ def note_create(request): request.session["retro_draft_key"] = str(uuid.uuid4()) draft_key = request.session["retro_draft_key"] + my_projects, my_role_map = _get_my_projects_and_roles(request.user) + if request.method == "POST": title = (request.POST.get("title") or "빈 제목").strip() + project_id_raw = (request.POST.get("project_id") or "").strip() + role_code = (request.POST.get("role") or "").strip() + if not title: context = {"guide": guide, "tpl": tpl_key, "error": "제목은 필수입니다."} return render(request, "reflections/note_create.html", context) + project = None + if project_id_raw: + project = get_object_or_404(Project, id=project_id_raw) + + # 보안/권한: 내 프로젝트가 아니면 막기 + if project not in my_projects: + messages.error(request, "내 프로젝트만 선택할 수 있습니다.") + return redirect("reflections:note_create") + + # role 자동 채움 정책: role이 비어있으면 프로젝트 기준 role_map에서 채움 + if not role_code: + role_code = my_role_map.get(project.id) or "" + answers = dict() # qid: "답변 내용" 형식 for q in guide["questions"]: qid = q["id"] @@ -147,6 +197,8 @@ def note_create(request): note = Retrospective.objects.create( user= request.user, + project=project, + role=role_code or None, template_key=tpl_key, title=title, answers_json=answers, @@ -169,6 +221,8 @@ def note_create(request): "tpl": tpl_key, "answers": {}, "draft_key": draft_key, + "my_projects": my_projects, + "my_role_map": my_role_map, } return render(request, "reflections/note_create.html", context) @@ -197,8 +251,13 @@ def note_update(request, note_id): # 기존 답변(answers_json)로 textarea 기본값 채우기 existing_answers = note.answers_json or {} + my_projects, my_role_map = _get_my_projects_and_roles(request.user) + if request.method == "POST": title = (request.POST.get("title") or "빈 제목").strip() + project_id_raw = (request.POST.get("project_id") or "").strip() + role_code = (request.POST.get("role") or "").strip() + if not title: context = { "note": note, @@ -208,6 +267,17 @@ def note_update(request, note_id): "error": "제목은 필수입니다.", } return render(request, "reflections/note_update.html", context) + + project = None + if project_id_raw: + project = get_object_or_404(Project, id=project_id_raw) + if project not in my_projects: + messages.error(request, "내 프로젝트만 선택할 수 있습니다.") + return redirect("reflections:note_update", note_id=note.id) + + # role 비어있으면 자동, 있으면 사용자가 고른 값 존중 + if not role_code: + role_code = my_role_map.get(project.id) or "" answers = {} for q in guide["questions"]: @@ -217,6 +287,8 @@ def note_update(request, note_id): content_md = build_markdown(guide, answers) note.title = title + note.project = project + note.role = role_code or None note.answers_json = answers note.content_md = content_md note.save(update_fields=["title", "answers_json", "content_md", "updated_at"]) @@ -228,6 +300,8 @@ def note_update(request, note_id): "guide": guide, "tpl": tpl, "answers": existing_answers, + "my_projects": my_projects, + "my_role_map": my_role_map, } return render(request, "reflections/note_update.html", context) From 7dcabaa8015b5836ea03f88dba0f8c7625487b51 Mon Sep 17 00:00:00 2001 From: knana6 Date: Sun, 8 Feb 2026 22:02:51 +0900 Subject: [PATCH 256/380] fix: email certify css --- static/css/profile_edit.css | 561 ++++++++++++++++-------------------- 1 file changed, 244 insertions(+), 317 deletions(-) diff --git a/static/css/profile_edit.css b/static/css/profile_edit.css index 210ae26..8800c64 100644 --- a/static/css/profile_edit.css +++ b/static/css/profile_edit.css @@ -1,424 +1,351 @@ -/* profile_edit.css */ - -.profile-edit-wrapper { - max-width: 1400px; - margin: 0 auto; - padding: 40px 20px; - background-color: #fff; - min-height: 100vh; +* { + margin: 0; + padding: 0; + box-sizing: border-box; } -.edit-card { - background-color: #fff; - border-radius: 20px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); - overflow: visible; - position: relative; +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Apple SD Gothic Neo', sans-serif; + background-color: #f5f5f5; display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; + margin: 0; } -/* 상단 배너 영역 */ -.edit-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 180px; - background-color: #EAF0FF; - border-radius: 20px 20px 0 0; - z-index: 0; +.container { + width: 100%; + max-width: 445px; } -/* 왼쪽 프로필 영역 - 70% */ -.user-info-side { - flex: 0 0 70%; - display: flex; - flex-direction: column; - align-items: center; - padding: 60px 50px 10px 10px; - position: relative; - z-index: 1; +.row { + width: 100%; } -/* 프로필 이미지 */ -.avatar-container { - position: relative; - width: 180px; - height: 180px; - margin-bottom: 20px; - margin-top: 40px; +.col-lg-6, +.col-md-8 { + width: 100%; } -.profile-img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - background: linear-gradient(180deg, #E8F0FF 0%, #6B8AFF 100%); - border: 6px solid #fff; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.edit-badge { - position: absolute; - bottom: 10px; - right: 10px; - width: 42px; - height: 42px; - background-color: #6b7280; - border-radius: 50%; +.justify-content-center { display: flex; - align-items: center; justify-content: center; - cursor: pointer; - border: 3px solid #fff; - transition: background-color 0.2s; } -.edit-badge:hover { - background-color: #4b5563; +/* 카드 스타일 */ +.auth-card { + background: white; + border-radius: 16px; + padding: 48px 40px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + width: 100%; } -.edit-badge img { - width: 20px; - height: 20px; - filter: brightness(0) invert(1); +/* 헤더 */ +.auth-header { + text-align: center; + margin-bottom: 32px; } -/* 레벨 태그 */ -.level-tag { - background-color: #A4BDFD; - color: white; - padding: 10px 35px; - border-radius: 25px; - font-size: 18px; +.auth-header h1 { + font-size: 24px; font-weight: 600; - margin-bottom: 30px; + color: #333; + margin-bottom: 12px; } -/* 폼 영역 */ -.edit-form { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; +.auth-header p { + font-size: 14px; + color: #666; + line-height: 1.5; } -/* 정보 입력 테이블 */ -.info-table { - width: 100%; - max-width: 550px; - display: flex; - flex-direction: column; - gap: 20px; +/* 성공 카드 */ +.success-card { + text-align: center; } -.info-row { +.success-icon { + width: 80px; + height: 80px; + background: #22c55e; + border-radius: 50%; display: flex; align-items: center; - gap: 20px; + justify-content: center; + margin: 0 auto 24px; + font-size: 48px; + color: white; + font-weight: bold; } -.info-row .label { - font-weight: 700; - font-size: 16px; - color: #000; - min-width: 140px; - text-align: left; +.success-card h1 { + font-size: 24px; + font-weight: 600; + color: #333; + margin-bottom: 12px; +} + +.success-card > p { + font-size: 14px; + color: #666; + margin-bottom: 24px; } -/* input wrapper 추가 */ -.input-wrapper { - flex: 1; +/* 폼 그룹 */ +.form-group { + margin-bottom: 20px; } -/* Django form 필드 스타일링 - 모든 input에 파란 배경 적용 */ -.info-row input[type="text"], -.info-row input[type="email"], -.info-row input[type="password"], -.input-field { +.form-label { + display: block; + font-size: 14px; + color: #666; + font-weight: 500; + margin-bottom: 8px; +} + +/* 입력 필드 */ +input[type="email"], +input[type="password"], +input[type="text"] { width: 100%; - padding: 12px 20px; - border: none; - background-color: #E8EFFF; - border-radius: 10px; + padding: 14px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; font-size: 15px; - color: #4b5563; - transition: background-color 0.2s; - box-sizing: border-box; + transition: all 0.3s ease; + background-color: #fafafa; } -.info-row input:focus, -.input-field:focus { +input[type="email"]:focus, +input[type="password"]:focus, +input[type="text"]:focus { outline: none; - background-color: #DDE7FF; + border-color: #4285f4; + background-color: white; +} + +input[type="email"]::placeholder, +input[type="password"]::placeholder, +input[type="text"]::placeholder { + color: #999; } -/* readonly 필드만 회색 배경 */ -.input-field.readonly { - background-color: #F3F4F6 !important; - color: #9ca3af; - cursor: not-allowed; +/* 버튼 */ +.btn { + width: 100%; + padding: 14px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.3s ease; } -.info-row input::placeholder { - color: #9ca3af; +.btn-primary { + background: #4285f4; + color: white; } -/* 닉네임 검증 스타일 */ -.nickname-check-wrapper { - display: flex; - align-items: center; - gap: 10px; - flex: 1; +.btn-primary:hover { + background: #3367d6; +} + +.btn-primary:active { + background: #2851a3; } -.nickname-check-wrapper input { - flex: 1; +.btn-block { + width: 100%; + margin-top: 8px; } -.check-btn { - padding: 10px 20px; - background-color: #4B7EFF; +.btn-back { + display: inline-block; + padding: 14px 32px; + background: #4285f4; color: white; - border: none; + text-decoration: none; border-radius: 8px; - font-size: 14px; + font-size: 16px; font-weight: 600; - cursor: pointer; - transition: background-color 0.2s; - white-space: nowrap; + margin-top: 24px; + transition: background 0.3s ease; } -.check-btn:hover { - background-color: #3563D9; +.btn-back:hover { + background: #3367d6; } -.check-btn:active { - background-color: #2548B8; +/* 정보 박스 */ +.info-box { + background: #e3f2fd; + border-left: 4px solid #4285f4; + padding: 20px; + border-radius: 8px; + margin: 24px 0; + text-align: left; } -.check-status { - font-size: 12px; - font-weight: 500; - margin-left: 10px; - white-space: nowrap; +.info-box h3 { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; } -.check-status.success { - color: #10B981; +.info-box p { + font-size: 14px; + color: #555; + margin: 0; } -.check-status.error { - color: #EF4444; +.info-box ol { + margin: 0; + padding-left: 20px; } -/* 입력 필드 유효성 표시 */ -.input-field.valid { - border: 2px solid #10B981; - background-color: #F0FDF4; +.info-box ol li { + font-size: 14px; + color: #555; + margin-bottom: 8px; + line-height: 1.5; } -.input-field.invalid { - border: 2px solid #EF4444; - background-color: #FEF2F2; +/* 경고 박스 */ +.warning-box { + background: #fffbeb; + border-left: 4px solid #f59e0b; + padding: 20px; + border-radius: 8px; + margin: 24px 0; + text-align: left; } -/* 파일 입력 숨기기 */ -input[type="file"] { - display: none; +.warning-box strong { + font-size: 14px; + color: #333; + display: block; + margin-bottom: 8px; } -/* 에러 메시지 */ -.error-messages { - width: 100%; - max-width: 550px; - margin-top: 15px; +.warning-box ul { + margin: 0; + padding-left: 20px; } -.error-text { - color: #dc2626; +.warning-box ul li { font-size: 14px; - margin: 5px 0; + color: #555; + margin-bottom: 6px; + line-height: 1.5; } -/* 수정 완료 버튼 */ -.save-button { - margin-top: 35px; - width: 100%; - max-width: 240px; - padding: 14px 0; - background-color: #4F75FF; - color: white; - border: none; - border-radius: 10px; - font-size: 17px; - font-weight: 700; - cursor: pointer; - transition: background-color 0.2s; +.warning-box a { + color: #4285f4; + text-decoration: none; } -.save-button:hover { - background-color: #3d5dd1; +.warning-box a:hover { + text-decoration: underline; } -/* 오른쪽 콘텐츠 영역 - 30% */ -.content-side { - flex: 0 0 30%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; /* 왼쪽 정렬로 변경 */ - padding: 40px 60px 40px 10px; /* 왼쪽 패딩 줄임 */ - position: relative; - z-index: 1; +/* 비밀번호 힌트 */ +.password-hint { + background: #e3f2fd; + border-left: 4px solid #4285f4; + padding: 16px; + border-radius: 8px; + margin-top: 12px; } -/* 기술 스택 박스 */ -.tech-stack-box { - background-color: white; - border: 2px solid #A4BDFD; - border-radius: 30px; - padding: 45px; - width: 100%; - max-width: 400px; /* 최대 너비 줄임 */ +.password-hint strong { + font-size: 14px; + color: #333; + display: block; + margin-bottom: 8px; } -.side-title { - font-size: 22px; - font-weight: 700; - color: #000; - margin: 0 0 30px 0; - text-align: center; +.password-hint ul { + margin: 0; + padding-left: 20px; } -.tags { - display: flex; - flex-direction: column; - align-items: center; - gap: 18px; +.password-hint ul li { + font-size: 13px; + color: #555; + margin-bottom: 4px; } -.tag-item { - padding: 14px 32px; - border-radius: 30px; - font-size: 18px; - font-weight: 600; - border: 2px solid; - background-color: transparent; - min-width: 220px; - text-align: center; +/* 에러 메시지 */ +.error-message { + color: #ef4444; + font-size: 13px; + margin-top: 6px; } -/* 기술 스택별 색상 */ -.tag-javascript, -.tag-js { - color: #06D6A0; - border-color: #06D6A0; +.alert { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 20px; + font-size: 14px; } -.tag-react-native, -.tag-react, -.tag-reactnative, -.tag-rn { - color: #FFC107; - border-color: #FFC107; +.alert-danger { + background: #fef2f2; + color: #ef4444; + border: 1px solid #fecaca; } -.tag-html { - color: #00B4D8; - border-color: #00B4D8; +/* 푸터 */ +.auth-footer { + text-align: center; + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #e0e0e0; } -.empty-msg { - color: #9ca3af; - font-size: 15px; - text-align: center; +.auth-footer p { + font-size: 14px; + color: #666; + margin-bottom: 8px; } -/* 반응형 */ -@media (max-width: 1024px) { - .user-info-side { - flex: 0 0 65%; - padding: 40px 30px; - } - - .content-side { - flex: 0 0 35%; - padding: 40px 20px; - } - - .info-table { - max-width: 100%; - } - - .info-row .label { - min-width: 120px; - font-size: 14px; - } +.auth-footer a { + color: #4285f4; + text-decoration: none; + transition: color 0.3s ease; } -@media (max-width: 768px) { - .profile-edit-wrapper { - padding: 20px 10px; - } +.auth-footer a:hover { + color: #3367d6; + text-decoration: underline; +} - .edit-card { - flex-direction: column; - } - - .edit-card::before { - height: 140px; +/* 반응형 디자인 */ +@media (max-width: 480px) { + .auth-card { + padding: 32px 24px; } - .user-info-side { - flex: none; - width: 100%; - padding: 30px 20px 40px; - } - - .avatar-container { - width: 140px; - height: 140px; - margin-top: 20px; + .auth-header h1, + .success-card h1 { + font-size: 20px; } - .content-side { - flex: none; - width: 100%; - padding: 20px; - align-items: center; /* 모바일에서는 가운데 정렬 */ - } - - .tech-stack-box { - max-width: 100%; - padding: 30px 20px; - } - - .info-row { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .info-row .label { - min-width: auto; - font-size: 14px; + .success-icon { + width: 64px; + height: 64px; + font-size: 36px; } - - .input-wrapper { - width: 100%; - } - - .info-row input, - .input-field { - width: 100%; - } - - .tag-item { - min-width: 180px; - font-size: 16px; - padding: 12px 24px; + + .info-box, + .warning-box, + .password-hint { + padding: 16px; } } \ No newline at end of file From 29134da829e043a4003e4eadd2f8d85965ca1edb Mon Sep 17 00:00:00 2001 From: knana6 Date: Sun, 8 Feb 2026 22:54:28 +0900 Subject: [PATCH 257/380] fix: password reset css --- static/css/password_reset.css | 385 ++++++++++++++++++++-------------- 1 file changed, 229 insertions(+), 156 deletions(-) diff --git a/static/css/password_reset.css b/static/css/password_reset.css index 1a0b1c6..8800c64 100644 --- a/static/css/password_reset.css +++ b/static/css/password_reset.css @@ -1,278 +1,351 @@ -/* Password Reset Pages */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} -/* Common Layout */ -.container { - min-height: 100vh; +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Apple SD Gothic Neo', sans-serif; + background-color: #f5f5f5; display: flex; + justify-content: center; align-items: center; + min-height: 100vh; + padding: 20px; + margin: 0; +} + +.container { + width: 100%; + max-width: 445px; +} + +.row { + width: 100%; +} + +.col-lg-6, +.col-md-8 { + width: 100%; +} + +.justify-content-center { + display: flex; justify-content: center; - padding: 20px 0; } +/* 카드 스타일 */ .auth-card { background: white; - border-radius: 12px; - padding: 40px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - max-width: 500px; + border-radius: 16px; + padding: 48px 40px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); width: 100%; } -.auth-card.success-card { - text-align: center; -} - -/* Header */ +/* 헤더 */ .auth-header { text-align: center; - margin-bottom: 30px; + margin-bottom: 32px; } .auth-header h1 { - font-size: 28px; - font-weight: bold; - color: #1a1a1a; - margin: 0 0 10px 0; + font-size: 24px; + font-weight: 600; + color: #333; + margin-bottom: 12px; } .auth-header p { + font-size: 14px; color: #666; - margin: 0; + line-height: 1.5; +} + +/* 성공 카드 */ +.success-card { + text-align: center; +} + +.success-icon { + width: 80px; + height: 80px; + background: #22c55e; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 24px; + font-size: 48px; + color: white; + font-weight: bold; +} + +.success-card h1 { + font-size: 24px; + font-weight: 600; + color: #333; + margin-bottom: 12px; +} + +.success-card > p { font-size: 14px; + color: #666; + margin-bottom: 24px; +} + +/* 폼 그룹 */ +.form-group { + margin-bottom: 20px; } -/* Form Elements */ .form-label { + display: block; + font-size: 14px; + color: #666; font-weight: 500; - color: #333; margin-bottom: 8px; - display: block; } -.form-control { +/* 입력 필드 */ +input[type="email"], +input[type="password"], +input[type="text"] { width: 100%; - padding: 10px 12px; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 14px; - transition: border-color 0.3s; - box-sizing: border-box; + padding: 14px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 15px; + transition: all 0.3s ease; + background-color: #fafafa; } -.form-control:focus { +input[type="email"]:focus, +input[type="password"]:focus, +input[type="text"]:focus { outline: none; - border-color: #4B7EFF; - box-shadow: 0 0 0 3px rgba(75, 126, 255, 0.1); -} - -.form-group { - margin-bottom: 20px; + border-color: #4285f4; + background-color: white; } -.error-message { - color: #dc3545; - font-size: 12px; - margin-top: 5px; +input[type="email"]::placeholder, +input[type="password"]::placeholder, +input[type="text"]::placeholder { + color: #999; } -/* Buttons */ -.btn-primary { +/* 버튼 */ +.btn { width: 100%; - padding: 12px; - background-color: #4B7EFF; - color: white; + padding: 14px; border: none; - border-radius: 6px; + border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; - transition: background-color 0.3s; - margin-top: 20px; + transition: background 0.3s ease; +} + +.btn-primary { + background: #4285f4; + color: white; } .btn-primary:hover { - background-color: #3a63d9; + background: #3367d6; +} + +.btn-primary:active { + background: #2851a3; } .btn-block { width: 100%; + margin-top: 8px; } .btn-back { display: inline-block; - background-color: #4B7EFF; + padding: 14px 32px; + background: #4285f4; color: white; - padding: 12px 32px; text-decoration: none; - border-radius: 6px; - font-size: 15px; + border-radius: 8px; + font-size: 16px; font-weight: 600; - margin-top: 30px; - transition: background-color 0.3s; + margin-top: 24px; + transition: background 0.3s ease; } .btn-back:hover { - background-color: #3a63d9; + background: #3367d6; } -/* Footer Links */ -.auth-footer { - text-align: center; - margin-top: 20px; - padding-top: 20px; - border-top: 1px solid #eee; -} - -.auth-footer p { - margin: 10px 0; - font-size: 14px; - color: #666; -} - -.auth-footer a { - color: #4B7EFF; - text-decoration: none; - font-weight: 500; -} - -.auth-footer a:hover { - text-decoration: underline; -} - -/* Success Icon */ -.success-icon { - width: 80px; - height: 80px; - background-color: #10B981; - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 48px; - margin: 0 auto 30px; -} - -.auth-card h1 { - font-size: 24px; - font-weight: bold; - color: #1a1a1a; - margin: 0 0 15px 0; -} - -.auth-card > p { - color: #666; - margin: 0 0 30px 0; - font-size: 15px; - line-height: 1.6; -} - -/* Info Box */ +/* 정보 박스 */ .info-box { - background-color: #f0f9ff; - border-left: 4px solid #4B7EFF; + background: #e3f2fd; + border-left: 4px solid #4285f4; padding: 20px; - border-radius: 6px; - margin: 30px 0; + border-radius: 8px; + margin: 24px 0; text-align: left; } .info-box h3 { font-size: 16px; font-weight: 600; - color: #1a1a1a; - margin: 0 0 15px 0; + color: #333; + margin-bottom: 12px; } -.info-box ol { - margin: 0; - padding-left: 20px; - color: #666; +.info-box p { font-size: 14px; + color: #555; + margin: 0; } -.info-box li { - margin: 8px 0; +.info-box ol { + margin: 0; + padding-left: 20px; } -.info-box p { - color: #666; - margin: 0; +.info-box ol li { font-size: 14px; + color: #555; + margin-bottom: 8px; + line-height: 1.5; } -/* Warning Box */ +/* 경고 박스 */ .warning-box { - background-color: #fffbeb; + background: #fffbeb; border-left: 4px solid #f59e0b; padding: 20px; - border-radius: 6px; - margin: 20px 0; + border-radius: 8px; + margin: 24px 0; text-align: left; } .warning-box strong { - color: #d97706; font-size: 14px; + color: #333; + display: block; + margin-bottom: 8px; } .warning-box ul { - margin: 10px 0 0 0; + margin: 0; padding-left: 20px; - color: #666; - font-size: 13px; } -.warning-box li { - margin: 6px 0; +.warning-box ul li { + font-size: 14px; + color: #555; + margin-bottom: 6px; + line-height: 1.5; } .warning-box a { - color: #4B7EFF; + color: #4285f4; text-decoration: none; - font-weight: 500; } .warning-box a:hover { text-decoration: underline; } -/* Password Hint */ +/* 비밀번호 힌트 */ .password-hint { - background-color: #f0f9ff; - border-left: 3px solid #4B7EFF; - padding: 12px; - border-radius: 4px; - margin-top: 10px; - font-size: 12px; - color: #666; + background: #e3f2fd; + border-left: 4px solid #4285f4; + padding: 16px; + border-radius: 8px; + margin-top: 12px; } .password-hint strong { - color: #1a1a1a; + font-size: 14px; + color: #333; display: block; - margin-bottom: 6px; + margin-bottom: 8px; } .password-hint ul { margin: 0; - padding-left: 18px; + padding-left: 20px; +} + +.password-hint ul li { + font-size: 13px; + color: #555; + margin-bottom: 4px; } -.password-hint li { - margin: 4px 0; +/* 에러 메시지 */ +.error-message { + color: #ef4444; + font-size: 13px; + margin-top: 6px; } -/* Alert Messages */ .alert { - padding: 12px; - border-radius: 6px; + padding: 12px 16px; + border-radius: 8px; margin-bottom: 20px; font-size: 14px; } .alert-danger { - background-color: #f8d7da; - border: 1px solid #f5c6cb; - color: #721c24; + background: #fef2f2; + color: #ef4444; + border: 1px solid #fecaca; +} + +/* 푸터 */ +.auth-footer { + text-align: center; + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #e0e0e0; } + +.auth-footer p { + font-size: 14px; + color: #666; + margin-bottom: 8px; +} + +.auth-footer a { + color: #4285f4; + text-decoration: none; + transition: color 0.3s ease; +} + +.auth-footer a:hover { + color: #3367d6; + text-decoration: underline; +} + +/* 반응형 디자인 */ +@media (max-width: 480px) { + .auth-card { + padding: 32px 24px; + } + + .auth-header h1, + .success-card h1 { + font-size: 20px; + } + + .success-icon { + width: 64px; + height: 64px; + font-size: 36px; + } + + .info-box, + .warning-box, + .password-hint { + padding: 16px; + } +} \ No newline at end of file From b7da6a4264d1e3d12a0babad9980c802011ffcd4 Mon Sep 17 00:00:00 2001 From: issuejong Date: Mon, 9 Feb 2026 16:15:59 +0900 Subject: [PATCH 258/380] =?UTF-8?q?refactor:=20TechStack=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/fixtures/tech_stacks.json | 30 +++++++++---------- .../0009_alter_techstack_category.py | 18 +++++++++++ apps/accounts/models.py | 4 +-- 3 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 apps/accounts/migrations/0009_alter_techstack_category.py diff --git a/apps/accounts/fixtures/tech_stacks.json b/apps/accounts/fixtures/tech_stacks.json index dedaeb6..1ecc28f 100644 --- a/apps/accounts/fixtures/tech_stacks.json +++ b/apps/accounts/fixtures/tech_stacks.json @@ -4,7 +4,7 @@ "pk": 1, "fields": { "name": "Python", - "category": "LANGUAGE", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -13,7 +13,7 @@ "pk": 2, "fields": { "name": "JavaScript", - "category": "LANGUAGE", + "category": "FRONTEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -22,7 +22,7 @@ "pk": 3, "fields": { "name": "TypeScript", - "category": "LANGUAGE", + "category": "FRONTEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -31,7 +31,7 @@ "pk": 4, "fields": { "name": "Java", - "category": "LANGUAGE", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -39,8 +39,8 @@ "model": "accounts.TechStack", "pk": 5, "fields": { - "name": "C/C++", - "category": "LANGUAGE", + "name": "Notion", + "category": "PM", "created_at": "2026-02-06T00:00:00Z" } }, @@ -94,7 +94,7 @@ "pk": 11, "fields": { "name": "PostgreSQL", - "category": "DATABASE", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -103,7 +103,7 @@ "pk": 12, "fields": { "name": "MySQL", - "category": "DATABASE", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -112,7 +112,7 @@ "pk": 13, "fields": { "name": "MongoDB", - "category": "DATABASE", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -121,7 +121,7 @@ "pk": 14, "fields": { "name": "Redis", - "category": "DATABASE", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -130,7 +130,7 @@ "pk": 15, "fields": { "name": "Docker", - "category": "TOOL", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -139,7 +139,7 @@ "pk": 16, "fields": { "name": "Git", - "category": "TOOL", + "category": "PM", "created_at": "2026-02-06T00:00:00Z" } }, @@ -148,7 +148,7 @@ "pk": 17, "fields": { "name": "AWS", - "category": "TOOL", + "category": "BACKEND", "created_at": "2026-02-06T00:00:00Z" } }, @@ -157,7 +157,7 @@ "pk": 18, "fields": { "name": "Figma", - "category": "TOOL", + "category": "PM", "created_at": "2026-02-06T00:00:00Z" } }, @@ -166,7 +166,7 @@ "pk": 19, "fields": { "name": "HTML/CSS", - "category": "LANGUAGE", + "category": "FRONTEND", "created_at": "2026-02-06T00:00:00Z" } } diff --git a/apps/accounts/migrations/0009_alter_techstack_category.py b/apps/accounts/migrations/0009_alter_techstack_category.py new file mode 100644 index 0000000..606f4bc --- /dev/null +++ b/apps/accounts/migrations/0009_alter_techstack_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.10 on 2026-02-09 07:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0008_alter_user_email_notifications_enabled'), + ] + + operations = [ + migrations.AlterField( + model_name='techstack', + name='category', + field=models.CharField(choices=[('FRONTEND', '프론트엔드'), ('BACKEND', '백엔드'), ('PM', '기획')], help_text='기술 카테고리', max_length=20), + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 060233d..5e6b8fd 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -11,11 +11,9 @@ class TechStack(models.Model): """ class Category(models.TextChoices): - LANGUAGE = "LANGUAGE", "프로그래밍 언어" FRONTEND = "FRONTEND", "프론트엔드" BACKEND = "BACKEND", "백엔드" - DATABASE = "DATABASE", "데이터베이스" - TOOL = "TOOL", "개발 도구" + PM = "PM", "기획" name = models.CharField( max_length=50, From 5f70fab0322df9074e655de30c3333b734c034f3 Mon Sep 17 00:00:00 2001 From: knana6 Date: Mon, 9 Feb 2026 18:08:56 +0900 Subject: [PATCH 259/380] fix: guide word reset --- templates/account/password_reset_from_key.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html index 50b98c7..9fc9cdc 100644 --- a/templates/account/password_reset_from_key.html +++ b/templates/account/password_reset_from_key.html @@ -39,7 +39,7 @@

새 비밀번호 설정

  • 최소 8자 이상
  • 대문자, 소문자, 숫자를 포함해야 합니다
  • -
  • 이전 비밀번호와 다려야 합니다
  • +
  • 이전 비밀번호와 달라야 합니다
From 0b25f2d0c25ca6b3be51c067884fa624c6640c68 Mon Sep 17 00:00:00 2001 From: knana6 Date: Mon, 9 Feb 2026 18:14:05 +0900 Subject: [PATCH 260/380] fix: profile edit layout --- templates/account/profile_edit.html | 538 ++++++++++++++++++---------- 1 file changed, 348 insertions(+), 190 deletions(-) diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index e569b98..f6cd6ea 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -1,194 +1,352 @@ -{% extends 'base.html' %} -{% load static %} - -{% block header %} - -{% endblock %} - -{% block content %} -
-
- - -
-
-

기술 스택

-
- {% for tech in user.tech_stacks.all %} -
- # {{ tech.name }} -
- {% empty %} -

등록된 기술 스택이 없습니다.

- {% endfor %} -
-
-
-
-
- - -{% endblock %} \ No newline at end of file +} \ No newline at end of file From bcf43f63f347f860a168768cb0ec2dee2732529e Mon Sep 17 00:00:00 2001 From: knana6 Date: Mon, 9 Feb 2026 18:19:07 +0900 Subject: [PATCH 261/380] fix: diff file changed error --- static/css/profile_edit.css | 457 +++++++++++------------ templates/account/profile_edit.html | 538 ++++++++++------------------ 2 files changed, 419 insertions(+), 576 deletions(-) diff --git a/static/css/profile_edit.css b/static/css/profile_edit.css index 8800c64..f6cd6ea 100644 --- a/static/css/profile_edit.css +++ b/static/css/profile_edit.css @@ -7,345 +7,346 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Apple SD Gothic Neo', sans-serif; background-color: #f5f5f5; - display: flex; - justify-content: center; - align-items: center; min-height: 100vh; - padding: 20px; - margin: 0; + padding: 40px 20px; } -.container { - width: 100%; - max-width: 445px; +.profile-edit-wrapper { + max-width: 1200px; + margin: 0 auto; } -.row { - width: 100%; +.edit-card { + background: white; + border-radius: 20px; + padding: 60px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + display: grid; + grid-template-columns: 400px 1fr; + gap: 80px; } -.col-lg-6, -.col-md-8 { +/* 왼쪽: 사용자 정보 */ +.user-info-side { + display: flex; + flex-direction: column; + align-items: center; +} + +/* 프로필 이미지 */ +.avatar-container { + position: relative; + width: 180px; + height: 180px; + margin-bottom: 20px; +} + +.profile-img { width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } -.justify-content-center { +.edit-badge { + position: absolute; + bottom: 0; + right: 0; + width: 44px; + height: 44px; + background: #333; + border-radius: 50%; display: flex; + align-items: center; justify-content: center; + cursor: pointer; + transition: transform 0.2s ease; } -/* 카드 스타일 */ -.auth-card { - background: white; - border-radius: 16px; - padding: 48px 40px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - width: 100%; +.edit-badge:hover { + transform: scale(1.1); } -/* 헤더 */ -.auth-header { - text-align: center; - margin-bottom: 32px; +.edit-badge img { + width: 20px; + height: 20px; + filter: invert(1); } -.auth-header h1 { - font-size: 24px; +/* 레벨 태그 */ +.level-tag { + background: #667eea; + color: white; + padding: 8px 24px; + border-radius: 20px; + font-size: 16px; font-weight: 600; - color: #333; - margin-bottom: 12px; + margin-bottom: 40px; } -.auth-header p { - font-size: 14px; - color: #666; - line-height: 1.5; +/* 폼 */ +.edit-form { + width: 100%; } -/* 성공 카드 */ -.success-card { - text-align: center; +/* 정보 테이블 */ +.info-table { + width: 100%; + margin-bottom: 30px; } -.success-icon { - width: 80px; - height: 80px; - background: #22c55e; - border-radius: 50%; +.info-row { display: flex; - align-items: center; - justify-content: center; - margin: 0 auto 24px; - font-size: 48px; - color: white; - font-weight: bold; + flex-direction: column; + margin-bottom: 24px; } -.success-card h1 { - font-size: 24px; +.label { + font-size: 15px; font-weight: 600; color: #333; - margin-bottom: 12px; + margin-bottom: 10px; } -.success-card > p { - font-size: 14px; - color: #666; - margin-bottom: 24px; -} - -/* 폼 그룹 */ -.form-group { - margin-bottom: 20px; -} - -.form-label { - display: block; - font-size: 14px; - color: #666; - font-weight: 500; - margin-bottom: 8px; -} - -/* 입력 필드 */ -input[type="email"], -input[type="password"], -input[type="text"] { +.input-field { width: 100%; - padding: 14px 16px; - border: 1px solid #e0e0e0; - border-radius: 8px; + padding: 14px 18px; + border: none; + background-color: #f0f2f7; + border-radius: 12px; font-size: 15px; + color: #666; transition: all 0.3s ease; - background-color: #fafafa; } -input[type="email"]:focus, -input[type="password"]:focus, -input[type="text"]:focus { +.input-field:focus { outline: none; - border-color: #4285f4; - background-color: white; + background-color: #e8eaf0; } -input[type="email"]::placeholder, -input[type="password"]::placeholder, -input[type="text"]::placeholder { +.input-field.readonly { + background-color: #f0f2f7; color: #999; + cursor: not-allowed; } -/* 버튼 */ -.btn { - width: 100%; - padding: 14px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: background 0.3s ease; +/* 닉네임 중복확인 */ +.nickname-check-wrapper { + display: flex; + flex-direction: column; + gap: 8px; } -.btn-primary { - background: #4285f4; - color: white; +.nickname-check-wrapper input { + width: 100%; + padding: 14px 18px; + border: none; + background-color: #f0f2f7; + border-radius: 12px; + font-size: 15px; + color: #333; + transition: all 0.3s ease; } -.btn-primary:hover { - background: #3367d6; +.nickname-check-wrapper input:focus { + outline: none; + background-color: #e8eaf0; } -.btn-primary:active { - background: #2851a3; +.nickname-check-wrapper input.valid { + background-color: #f0fdf4; + border: 2px solid #22c55e; } -.btn-block { - width: 100%; - margin-top: 8px; +.nickname-check-wrapper input.invalid { + background-color: #fef2f2; + border: 2px solid #ef4444; } -.btn-back { - display: inline-block; - padding: 14px 32px; - background: #4285f4; +.check-btn { + padding: 10px 20px; + background-color: #667eea; color: white; - text-decoration: none; + border: none; border-radius: 8px; - font-size: 16px; + font-size: 13px; font-weight: 600; - margin-top: 24px; + cursor: pointer; transition: background 0.3s ease; + align-self: flex-start; } -.btn-back:hover { - background: #3367d6; +.check-btn:hover { + background-color: #5568d3; } -/* 정보 박스 */ -.info-box { - background: #e3f2fd; - border-left: 4px solid #4285f4; - padding: 20px; - border-radius: 8px; - margin: 24px 0; - text-align: left; +.check-status { + font-size: 13px; + margin-top: 4px; } -.info-box h3 { - font-size: 16px; - font-weight: 600; - color: #333; - margin-bottom: 12px; +.check-status.success { + color: #22c55e; } -.info-box p { - font-size: 14px; - color: #555; - margin: 0; +.check-status.error { + color: #ef4444; } -.info-box ol { - margin: 0; - padding-left: 20px; +/* 에러 메시지 */ +.error-messages { + margin-bottom: 20px; } -.info-box ol li { +.error-text { + color: #ef4444; font-size: 14px; - color: #555; margin-bottom: 8px; - line-height: 1.5; } -/* 경고 박스 */ -.warning-box { - background: #fffbeb; - border-left: 4px solid #f59e0b; - padding: 20px; - border-radius: 8px; - margin: 24px 0; - text-align: left; +/* 수정 완료 버튼 */ +.save-button { + width: 100%; + padding: 16px; + background: #4285f4; + color: white; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.3s ease; + margin-top: 10px; } -.warning-box strong { - font-size: 14px; - color: #333; - display: block; - margin-bottom: 8px; +.save-button:hover { + background: #3367d6; } -.warning-box ul { - margin: 0; - padding-left: 20px; +.save-button:active { + background: #2851a3; } -.warning-box ul li { - font-size: 14px; - color: #555; - margin-bottom: 6px; - line-height: 1.5; +/* 오른쪽: 기술 스택 */ +.content-side { + display: flex; + flex-direction: column; } -.warning-box a { - color: #4285f4; - text-decoration: none; +.tech-stack-box { + background: #f8f9fa; + border: 2px solid #e9ecef; + border-radius: 20px; + padding: 40px; + min-height: 400px; } -.warning-box a:hover { - text-decoration: underline; +.side-title { + font-size: 20px; + font-weight: 700; + color: #333; + text-align: center; + margin-bottom: 30px; } -/* 비밀번호 힌트 */ -.password-hint { - background: #e3f2fd; - border-left: 4px solid #4285f4; - padding: 16px; - border-radius: 8px; - margin-top: 12px; +.tags { + display: flex; + flex-wrap: wrap; + gap: 12px; } -.password-hint strong { - font-size: 14px; - color: #333; - display: block; - margin-bottom: 8px; +.tag-item { + background: white; + border: 2px solid #e9ecef; + border-radius: 20px; + padding: 10px 20px; + font-size: 15px; + font-weight: 600; + transition: all 0.3s ease; } -.password-hint ul { - margin: 0; - padding-left: 20px; +.tag-item:nth-child(3n+1) { + color: #06b6d4; + border-color: #06b6d4; } -.password-hint ul li { - font-size: 13px; - color: #555; - margin-bottom: 4px; +.tag-item:nth-child(3n+2) { + color: #f59e0b; + border-color: #f59e0b; } -/* 에러 메시지 */ -.error-message { - color: #ef4444; - font-size: 13px; - margin-top: 6px; +.tag-item:nth-child(3n+3) { + color: #10b981; + border-color: #10b981; } -.alert { - padding: 12px 16px; - border-radius: 8px; - margin-bottom: 20px; +.empty-msg { + text-align: center; + color: #999; font-size: 14px; + padding: 40px 0; } -.alert-danger { - background: #fef2f2; - color: #ef4444; - border: 1px solid #fecaca; +/* 반응형 디자인 */ +@media (max-width: 1024px) { + .edit-card { + grid-template-columns: 1fr; + gap: 40px; + padding: 40px; + } } -/* 푸터 */ -.auth-footer { - text-align: center; - margin-top: 24px; - padding-top: 24px; - border-top: 1px solid #e0e0e0; -} +@media (max-width: 768px) { + .edit-card { + padding: 30px 20px; + } -.auth-footer p { - font-size: 14px; - color: #666; - margin-bottom: 8px; -} + .avatar-container { + width: 140px; + height: 140px; + } -.auth-footer a { - color: #4285f4; - text-decoration: none; - transition: color 0.3s ease; -} + .edit-badge { + width: 36px; + height: 36px; + } + + .edit-badge img { + width: 16px; + height: 16px; + } -.auth-footer a:hover { - color: #3367d6; - text-decoration: underline; + .tech-stack-box { + padding: 30px 20px; + } } -/* 반응형 디자인 */ @media (max-width: 480px) { - .auth-card { - padding: 32px 24px; + body { + padding: 20px 10px; + } + + .edit-card { + padding: 20px 15px; + } + + .avatar-container { + width: 120px; + height: 120px; } - .auth-header h1, - .success-card h1 { - font-size: 20px; + .level-tag { + font-size: 14px; + padding: 6px 20px; } - .success-icon { - width: 64px; - height: 64px; - font-size: 36px; + .side-title { + font-size: 18px; } - .info-box, - .warning-box, - .password-hint { - padding: 16px; + .tag-item { + font-size: 14px; + padding: 8px 16px; } } \ No newline at end of file diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index f6cd6ea..e569b98 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -1,352 +1,194 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Apple SD Gothic Neo', sans-serif; - background-color: #f5f5f5; - min-height: 100vh; - padding: 40px 20px; -} - -.profile-edit-wrapper { - max-width: 1200px; - margin: 0 auto; -} - -.edit-card { - background: white; - border-radius: 20px; - padding: 60px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - display: grid; - grid-template-columns: 400px 1fr; - gap: 80px; -} - -/* 왼쪽: 사용자 정보 */ -.user-info-side { - display: flex; - flex-direction: column; - align-items: center; -} - -/* 프로필 이미지 */ -.avatar-container { - position: relative; - width: 180px; - height: 180px; - margin-bottom: 20px; -} - -.profile-img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} - -.edit-badge { - position: absolute; - bottom: 0; - right: 0; - width: 44px; - height: 44px; - background: #333; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: transform 0.2s ease; -} - -.edit-badge:hover { - transform: scale(1.1); -} - -.edit-badge img { - width: 20px; - height: 20px; - filter: invert(1); -} - -/* 레벨 태그 */ -.level-tag { - background: #667eea; - color: white; - padding: 8px 24px; - border-radius: 20px; - font-size: 16px; - font-weight: 600; - margin-bottom: 40px; -} - -/* 폼 */ -.edit-form { - width: 100%; -} - -/* 정보 테이블 */ -.info-table { - width: 100%; - margin-bottom: 30px; -} - -.info-row { - display: flex; - flex-direction: column; - margin-bottom: 24px; -} - -.label { - font-size: 15px; - font-weight: 600; - color: #333; - margin-bottom: 10px; -} - -.input-field { - width: 100%; - padding: 14px 18px; - border: none; - background-color: #f0f2f7; - border-radius: 12px; - font-size: 15px; - color: #666; - transition: all 0.3s ease; -} - -.input-field:focus { - outline: none; - background-color: #e8eaf0; -} - -.input-field.readonly { - background-color: #f0f2f7; - color: #999; - cursor: not-allowed; -} - -/* 닉네임 중복확인 */ -.nickname-check-wrapper { - display: flex; - flex-direction: column; - gap: 8px; -} - -.nickname-check-wrapper input { - width: 100%; - padding: 14px 18px; - border: none; - background-color: #f0f2f7; - border-radius: 12px; - font-size: 15px; - color: #333; - transition: all 0.3s ease; -} - -.nickname-check-wrapper input:focus { - outline: none; - background-color: #e8eaf0; -} - -.nickname-check-wrapper input.valid { - background-color: #f0fdf4; - border: 2px solid #22c55e; -} - -.nickname-check-wrapper input.invalid { - background-color: #fef2f2; - border: 2px solid #ef4444; -} - -.check-btn { - padding: 10px 20px; - background-color: #667eea; - color: white; - border: none; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: background 0.3s ease; - align-self: flex-start; -} - -.check-btn:hover { - background-color: #5568d3; -} - -.check-status { - font-size: 13px; - margin-top: 4px; -} - -.check-status.success { - color: #22c55e; -} - -.check-status.error { - color: #ef4444; -} - -/* 에러 메시지 */ -.error-messages { - margin-bottom: 20px; -} - -.error-text { - color: #ef4444; - font-size: 14px; - margin-bottom: 8px; -} - -/* 수정 완료 버튼 */ -.save-button { - width: 100%; - padding: 16px; - background: #4285f4; - color: white; - border: none; - border-radius: 12px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: background 0.3s ease; - margin-top: 10px; -} - -.save-button:hover { - background: #3367d6; -} - -.save-button:active { - background: #2851a3; -} - -/* 오른쪽: 기술 스택 */ -.content-side { - display: flex; - flex-direction: column; -} - -.tech-stack-box { - background: #f8f9fa; - border: 2px solid #e9ecef; - border-radius: 20px; - padding: 40px; - min-height: 400px; -} - -.side-title { - font-size: 20px; - font-weight: 700; - color: #333; - text-align: center; - margin-bottom: 30px; -} - -.tags { - display: flex; - flex-wrap: wrap; - gap: 12px; -} - -.tag-item { - background: white; - border: 2px solid #e9ecef; - border-radius: 20px; - padding: 10px 20px; - font-size: 15px; - font-weight: 600; - transition: all 0.3s ease; -} - -.tag-item:nth-child(3n+1) { - color: #06b6d4; - border-color: #06b6d4; -} - -.tag-item:nth-child(3n+2) { - color: #f59e0b; - border-color: #f59e0b; -} - -.tag-item:nth-child(3n+3) { - color: #10b981; - border-color: #10b981; -} - -.empty-msg { - text-align: center; - color: #999; - font-size: 14px; - padding: 40px 0; -} - -/* 반응형 디자인 */ -@media (max-width: 1024px) { - .edit-card { - grid-template-columns: 1fr; - gap: 40px; - padding: 40px; - } -} - -@media (max-width: 768px) { - .edit-card { - padding: 30px 20px; - } - - .avatar-container { - width: 140px; - height: 140px; +{% extends 'base.html' %} +{% load static %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+ + +
+
+

기술 스택

+
+ {% for tech in user.tech_stacks.all %} +
+ # {{ tech.name }} +
+ {% empty %} +

등록된 기술 스택이 없습니다.

+ {% endfor %} +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file From 89ba517d39b1cd1a311abef468bfde9f0b3a26de Mon Sep 17 00:00:00 2001 From: knana6 Date: Mon, 9 Feb 2026 18:28:24 +0900 Subject: [PATCH 262/380] fix: double check field layout --- static/css/profile_edit.css | 26 ++++++++++++++++---------- templates/account/profile_edit.html | 6 ++++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/static/css/profile_edit.css b/static/css/profile_edit.css index f6cd6ea..d4cb2bd 100644 --- a/static/css/profile_edit.css +++ b/static/css/profile_edit.css @@ -138,8 +138,14 @@ body { gap: 8px; } -.nickname-check-wrapper input { - width: 100%; +.input-button-group { + display: flex; + gap: 10px; + align-items: center; +} + +.input-button-group input { + flex: 1; padding: 14px 18px; border: none; background-color: #f0f2f7; @@ -149,32 +155,33 @@ body { transition: all 0.3s ease; } -.nickname-check-wrapper input:focus { +.input-button-group input:focus { outline: none; background-color: #e8eaf0; } -.nickname-check-wrapper input.valid { +.input-button-group input.valid { background-color: #f0fdf4; border: 2px solid #22c55e; } -.nickname-check-wrapper input.invalid { +.input-button-group input.invalid { background-color: #fef2f2; border: 2px solid #ef4444; } .check-btn { - padding: 10px 20px; + padding: 14px 24px; background-color: #667eea; color: white; border: none; - border-radius: 8px; - font-size: 13px; + border-radius: 12px; + font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.3s ease; - align-self: flex-start; + white-space: nowrap; + flex-shrink: 0; } .check-btn:hover { @@ -183,7 +190,6 @@ body { .check-status { font-size: 13px; - margin-top: 4px; } .check-status.success { diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index e569b98..825c4f3 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -29,8 +29,10 @@
- {{ form.nickname }} - +
+ {{ form.nickname }} + +
From 399bd08c4c00996318fc170fd1c4b95d0a17c8ea Mon Sep 17 00:00:00 2001 From: plumbestie Date: Mon, 9 Feb 2026 21:31:36 +0900 Subject: [PATCH 263/380] =?UTF-8?q?fix=20:=20team=5Fapply=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=EB=B0=9B=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team_apply.css | 57 ++++++++++++--------------------- templates/teams/team_apply.html | 8 ++--- 2 files changed, 24 insertions(+), 41 deletions(-) diff --git a/static/css/team_apply.css b/static/css/team_apply.css index 4b75e8f..c7762ce 100644 --- a/static/css/team_apply.css +++ b/static/css/team_apply.css @@ -61,7 +61,7 @@ button:hover { /* 매칭 대기 */ .team_waiting { - margin-top: 120px; + margin-top: 200px; text-align: center; align-items: center; } @@ -72,55 +72,38 @@ button:hover { margin-bottom: 20px; } -.team_waiting > form { +/* 매칭 취소 */ +.matching_actions { + margin-top: 30px; display: flex; - justify-content: space-between; - align-items: center; - margin: 30px auto 0; - width: 60%; height: 40px; - background: #fff; - border-radius: 25px; - padding: 10px 5px 10px 20px; -} - -.team_waiting > form:focus-within { - border: 1px solid #4272EF; - box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); + justify-content: center; + gap: 50px; } -.team_waiting > form > input { - width: 80%; - border: none; - font-size: 15px; - color: #888888; - outline: none; -} +.matching_actions > form { -.team_waiting > form > input::placeholder { - color: #888888; } -.team_waiting > form > button { +.noti_button { + width: 150px; + height: 40px; + padding: 10px 20px; + background: #4272EF; + color: #fff; + border: none; + border-radius: 20px; font-size: 15px; - background: #4272EF; color: #fff; - border: none; border-radius: 20px; - padding: 7px 15px; -} - -.team_waiting > form > button:hover { - background: #1F4CC0; + font-weight: 500; + cursor: pointer; transition: 0.3s ease; } -/* 매칭 취소 */ -.matching_actions { - margin-top: 30px; - display: flex; - justify-content: center; +.noti_button:hover { + background: #1A3C97; + transition: 0.3s ease; } .cancel_form { - width: 100%; display: flex; justify-content: center; } diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index 936dd82..3ccd8fa 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -148,11 +148,11 @@

팀 매칭 신청이 완료되었어요.
팀 매칭 결과가 발표되면 메일로 알려드릴게요.

-
- - -
+
+ {% csrf_token %} + +
{% csrf_token %} From 06785253f71bca09407473ad8335441ffb7a3cb3 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Mon, 9 Feb 2026 22:10:30 +0900 Subject: [PATCH 264/380] =?UTF-8?q?feat:=20note=5Flist,=20create,=20update?= =?UTF-8?q?,=20form=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?bookmark=20AJAX=20=EC=B2=98=EB=A6=AC,=20html=20=EB=82=B4=20JS?= =?UTF-8?q?=20=EB=B3=84=EB=8F=84=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/admin.py | 2 + apps/reflections/views.py | 30 +- static/css/reflections.css | 710 +++++++++++++++++++++++++ static/js/reflections.js | 332 ++++++++++++ templates/reflections/_note_form.html | 377 +++++-------- templates/reflections/note_create.html | 16 +- templates/reflections/note_list.html | 167 ++++++ templates/reflections/note_update.html | 16 +- 8 files changed, 1387 insertions(+), 263 deletions(-) create mode 100644 static/css/reflections.css create mode 100644 static/js/reflections.js diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index 75b52b6..8504c7d 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -117,6 +117,8 @@ def approve_and_ban(self, request, queryset): for report in pending_reports: # 피신고자에게 팀 밴 2회 추가 report.reported_user.team_ban_count += 2 + # TODO 팀에서 피신고자 제거하기 + report.reported_user.save() # 신고 상태 업데이트 diff --git a/apps/reflections/views.py b/apps/reflections/views.py index 6f1aca2..d35761e 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -89,26 +89,20 @@ def note_list(request): ) # 스택 필터 - role_codes = request.GET.getlist("roles") - if role_codes : - get_personnal_retro = "none" in role_codes + role = (request.GET.get("roles") or "").strip() + if role == "none": + qs = qs.filter(project__isnull=True) + + elif role in ("PM", "FRONTEND", "BACKEND"): role_project_ids = ( TeamMember.objects - .filter(user=request.user, role__code__in=role_codes ) + .filter(user=request.user, role__code=role) .values_list("team__project_id", flat=True) .distinct() ) - - if get_personnal_retro and role_project_ids: - qs = qs.filter( - Q(project__isnull=True) | - Q(project_id__in = role_project_ids) - ) - elif get_personnal_retro: - qs = qs.filter(project__isnull=True) - elif role_project_ids: - qs = qs.filter(project_id__in = role_project_ids) + # ✅ 매칭 프로젝트가 없으면 결과 0개가 맞음 + qs = qs.filter(project_id__in=role_project_ids) # 북마크 필터 bookmarked = request.GET.get("bookmarked") @@ -140,6 +134,7 @@ def note_list(request): context = { "notes" : qs, "my_projects": my_projects, # 내 프로젝트 조회 -> 필터에 보여주기 + "role": role, "q" : q, "bookmarked": bookmarked, "sort": sort, @@ -198,7 +193,6 @@ def note_create(request): note = Retrospective.objects.create( user= request.user, project=project, - role=role_code or None, template_key=tpl_key, title=title, answers_json=answers, @@ -223,6 +217,7 @@ def note_create(request): "draft_key": draft_key, "my_projects": my_projects, "my_role_map": my_role_map, + "note": None } return render(request, "reflections/note_create.html", context) @@ -288,7 +283,6 @@ def note_update(request, note_id): note.title = title note.project = project - note.role = role_code or None note.answers_json = answers note.content_md = content_md note.save(update_fields=["title", "answers_json", "content_md", "updated_at"]) @@ -300,8 +294,8 @@ def note_update(request, note_id): "guide": guide, "tpl": tpl, "answers": existing_answers, - "my_projects": my_projects, - "my_role_map": my_role_map, + "my_projects": my_projects or {}, + "my_role_map": my_role_map or [], } return render(request, "reflections/note_update.html", context) diff --git a/static/css/reflections.css b/static/css/reflections.css new file mode 100644 index 0000000..d8d2fb0 --- /dev/null +++ b/static/css/reflections.css @@ -0,0 +1,710 @@ +/* reflections.css (note_list) */ + +/* ========================= + Color Tokens (피그마 제공 + 그레이스케일 추출/근사) + - 제공한 Blue/Role 색상은 그대로 + - 텍스트 그레이는 피그마 스샷 기준으로 "어두운 제목/중간 본문/연한 날짜" 톤으로 맞춤 +========================= */ +:root { + /* Blue scale */ + --blue-0: #F6F8FF; + --blue-5: #EAF0FF; + --blue-20: #A4BDFD; + --blue-40: #4272EF; + --blue-50: #1F4CC0; + --blue-70: #1D294B; + + /* Role colors */ + --pm: #00B9B0; + --pm-sub: #37D3BF; + --backend: #FF3E88; + --backend-sub: #FF69A4; + --frontend: #FFCE53; + --frontend-sub: #FFDF6E; + + /* UI states */ + --heart: #F24C4C; + --danger: #F15C5C; + + /* Grayscale (피그마 텍스트 톤 맞춤) */ + --gray-900: #2B2F3A; /* 제목급(거의 블랙) */ + --gray-700: #4B5563; /* 본문 1 */ + --gray-600: #6B7280; /* 본문 2 */ + --gray-500: #8B95A1; /* 날짜/보조 */ + --gray-300: #D7DDE8; /* 경계 */ +} +body { + font-family: inherit; /* 이미 있으면 생략 */ +} + +/* ========================= + Page +========================= */ +.ref-page { + background: var(--blue-0); + padding: 32px 0 96px; +} + +.ref-controls, +.ref-list { + max-width: 960px; + margin: 0 auto; + padding: 0 16px; +} + +/* ========================= + Search (피그마처럼 길게, 버튼은 오른쪽 pill) +========================= */ +.ref-controls { + margin-bottom: 18px; +} + +.ref-search { + display: flex; + flex-direction: column; + gap: 14px; +} + +.ref-searchbar { + display: flex; + align-items: center; + gap: 10px; + background: #fff; + border-radius: 999px; + padding: 14px 16px; + box-shadow: 0 0 0 1px var(--blue-5); +} + +.ref-searchicon { + color: var(--blue-40); + display: inline-flex; + align-items: center; +} + +.ref-searchinput { + flex: 1; + border: 0; + outline: 0; + font-size: 16px; + color: var(--gray-900); +} + +.ref-searchinput::placeholder { + color: var(--gray-500); +} + +.ref-searchbtn { + border: 0; + cursor: pointer; + border-radius: 999px; + padding: 10px 18px; + font-size: 14px; + font-weight: 600; + background: var(--blue-70); /* 피그마처럼 진한 버튼 */ + color: #fff; +} + +/* ========================= + Filter row (왼쪽 2개, 오른쪽 글쓰기) +========================= */ +.ref-filters { + display: flex; + align-items: center; + gap: 12px; +} + +.ref-select-container { + position: relative; + display: inline-flex; + align-items: center; +} + +.ref-selectbox { + appearance: none; + border: 1px solid var(--blue-20); + background: #fff; + border-radius: 999px; + padding: 12px 44px 12px 18px; + font-size: 14px; + font-weight: 600; + color: var(--gray-900); + min-width: 180px; +} + +.ref-selectchev { + position: absolute; + right: 14px; + color: var(--blue-40); + pointer-events: none; + display: inline-flex; + align-items: center; +} + +.ref-writebtn { + margin-left: auto; + text-decoration: none; + border-radius: 999px; + padding: 12px 22px; + font-size: 14px; + font-weight: 700; + background: var(--blue-40); + color: #fff; +} + +.bookmark-filter-btn { + display: inline-flex; + align-items: center; + gap: 6px; + + padding: 8px 14px; + border-radius: 999px; + + background: #ffffff; /* OFF: 흰 배경 */ + color: var(--gray-800); /* OFF: 어두운 글씨 */ + + border: 1px solid var(--blue-20); + cursor: pointer; + + font-size: 14px; + font-weight: 700; +} + +.bookmark-filter-btn .bookmark-icon { + width: 16px; + height: 16px; + fill: currentColor; +} + +/* ON 상태 */ +.bookmark-filter-btn.is-active { + background: var(--blue-70); /* ON: 어두운 배경 */ + color: #ffffff; /* ON: 밝은 글씨 */ + border-color: var(--blue-70); +} + + +/* ========================= + List / Card (피그마처럼 넓고 둥글고, 테두리+은은한 그림자) +========================= */ +.ref-list { + display: flex; + flex-direction: column; + gap: 18px; +} + +.note-card { + position: relative; + background: #fff; + border-radius: 22px; + padding: 22px 22px; + box-shadow: 0 0 0 1px var(--blue-5); + display: flex; + align-items: flex-start; + gap: 14px; +} + +.note-main { + flex: 1; + text-decoration: none; + color: inherit; + min-width: 0; +} + +/* 북마크(하트 대신) */ +.bookmark-btn { + border: 0; + background: transparent; + cursor: pointer; + padding: 2px; + color: var(--gray-300); /* 기본은 연한 그레이 */ + line-height: 0; +} + +.bookmark-btn.is-active .bookmark-icon{ + color: var(--blue-40); +} + +.bookmark-icon { + width: 20px; + height: 20px; + display: block; + fill: currentColor; + color: var(--gray-500) +} + +.bookmark-on { + display: none; +} + +.bookmark-btn.is-active .bookmark-on { + display: block; +} + +.bookmark-btn.is-active .bookmark-off { + display: none; +} + + +/* 제목 줄 */ +.note-topline { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.note-title { + font-size: 20px; /* 피그마처럼 큼 */ + font-weight: 800; + color: var(--gray-900); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 태그 pill */ +.note-tag { + font-size: 12px; + font-weight: 700; + padding: 6px 12px; + border-radius: 999px; + white-space: nowrap; + border: 1px solid transparent; +} + +/* 역할별 태그(피그마처럼 라인+연한 배경 느낌) */ +.tag-pm { + background: color-mix(in srgb, var(--pm-sub) 18%, #fff); + border-color: color-mix(in srgb, var(--pm-sub) 55%, #fff); + color: var(--pm); +} + +.tag-backend { + background: color-mix(in srgb, var(--backend-sub) 18%, #fff); + border-color: color-mix(in srgb, var(--backend-sub) 55%, #fff); + color: var(--backend); +} + +.tag-frontend { + background: color-mix(in srgb, var(--frontend-sub) 22%, #fff); + border-color: color-mix(in srgb, var(--frontend-sub) 60%, #fff); + color: #B88700; +} + +/* 본문 프리뷰 */ +.note-preview { + margin: 10px 0 0; + font-size: 16px; + line-height: 1.4; + color: var(--gray-600); + display: -webkit-box; + + /* 표준 (린터용, 미래 대비) */ + line-clamp: 2; + + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* 날짜 */ +.note-date { + display: inline-block; + margin-top: 10px; + font-size: 14px; + font-weight: 600; + color: var(--gray-500); +} + +/* ========================= + More menu (오른쪽 케밥 + 드롭다운) +========================= */ +.note-morewrap { + position: relative; + margin-left: 6px; +} + +.note-morebtn { + border: 0; + background: transparent; + cursor: pointer; + padding: 6px; + border-radius: 10px; + color: var(--blue-70); +} + +.note-morebtn:hover { + background: var(--blue-5); +} + +.note-menu { + position: absolute; + right: 0; + top: 38px; + width: 220px; /* 피그마처럼 넓게 */ + padding: 6px 0; + background: #fff; + border-radius: 18px; + box-shadow: 0 10px 24px rgba(29, 41, 75, 0.12); + overflow: hidden; + z-index: 20; +} + +.note-menuitem { + display: block; + width: 100%; + padding: 7px 14px; + font-size: 14px; /* 피그마 스샷처럼 크게 보이게 */ + font-weight: 800; + letter-spacing: -0.02em; /* 한글 크게 보이는 느낌 보정 */ + color: var(--blue-40); /* 피그마처럼 수정은 블루 계열 */ + text-decoration: none; + background: #fff; + border: 0; + text-align: left; + cursor: pointer; +} + +.note-menuitem:hover { + background: var(--blue-5); +} + +.note-menuitem.danger { + color: var(--danger); +} + +.note-menuform { + margin: 0; +} + +/* ========================= + Empty +========================= */ +.ref-empty { + text-align: center; + padding: 84px 0; +} + +.ref-empty-title { + color: var(--gray-700); + font-weight: 800; + margin: 0 0 14px; +} + +.ref-empty-cta { + display: inline-block; + text-decoration: none; + border-radius: 999px; + padding: 12px 20px; + background: var(--blue-40); + color: #fff; + font-weight: 800; +} + +/* ========================= + Responsive (너무 찌그러지지 않게) +========================= */ +@media (max-width: 720px) { + .ref-searchbtn { + padding: 10px 14px; + } + + .ref-selectbox { + min-width: 140px; + } + + .note-title { + font-size: 18px; + } + + .note-preview { + font-size: 15px; + } +} + +/* ========================= + Note Form (create/update) +========================= */ +.ref-form-wrap { + max-width: 1020px; + margin: 32px auto 120px; + padding: 44px 44px 34px; + background: #fff; + border-radius: 26px; + box-shadow: 0 10px 30px rgba(29, 41, 75, 0.08); +} + +.ref-alert { + margin-bottom: 14px; + padding: 12px 14px; + border: 1px solid #fca5a5; + background: #fef2f2; + border-radius: 12px; + font-weight: 700; +} + +.ref-form-head { + display: flex; + align-items: center; + gap: 18px; +} + +.ref-title { + flex: 1; + border: 0; + outline: 0; + font-size: 34px; + font-weight: 900; + color: var(--gray-900); + padding: 8px 0; +} + +.ref-head-right { + display: flex; + align-items: center; + gap: 12px; +} + +.ref-divider { + height: 2px; + background: var(--blue-20); + opacity: 0.5; + margin: 18px 0 26px; + border-radius: 999px; +} + +/* meta */ +.ref-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px 22px; + margin-bottom: 22px; +} + +.ref-meta-row { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ref-label { + font-size: 13px; + font-weight: 800; + color: var(--gray-700); +} + +.ref-select { + width: 100%; + appearance: none; + border: 1px solid var(--blue-20); + background: #fff; + border-radius: 14px; + padding: 12px 14px; + font-size: 14px; + font-weight: 800; + color: var(--gray-900); + +} + +.ref-selectwrap{ + position: relative; +} + +.ref-form-selectchev { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; /* 클릭은 select로 */ + display: inline-flex; + align-items: center; + justify-content: center; + + width: 28px; + height: 28px; + border-radius: 10px; + + color: var(--blue-50); /* 필요하면 바꾸세요 */ + opacity: 0.9; +} + +.ref-form-selectchev svg { + width: 18px; + height: 18px; + display: block; +} + +.ref-meta-hint { + font-size: 12px; + color: var(--gray-500); + font-weight: 700; +} + +/* questions */ +.ref-q-list { + display: flex; + flex-direction: column; + gap: 18px; +} + +.ref-q-card { + border: 1px solid var(--blue-20); + border-radius: 18px; + overflow: hidden; + background: #fff; +} + +.ref-q-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 14px 16px; + background: color-mix(in srgb, var(--blue-5) 80%, #fff); +} + +.ref-q-title { + display: flex; + align-items: center; + gap: 10px; + font-weight: 900; + color: var(--gray-900); +} + +.ref-q-num { + width: 22px; + height: 22px; + border-radius: 50%; + background: #2f2f2f; + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 900; +} + +.ref-q-actions { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.ref-iconbtn { + border: 0; + background: transparent; + cursor: pointer; + padding: 6px; + border-radius: 12px; + color: var(--blue-50); +} + +.ref-iconbtn:hover { + background: var(--blue-5); +} + +.ref-iconbtn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.ref-q-body { + padding: 16px; +} + +.ref-textarea { + width: 100%; + border: 0; + outline: 0; + resize: vertical; + min-height: 140px; + font-size: 16px; + line-height: 1.55; + color: var(--gray-700); +} + +/* assets */ +.ref-assets { + margin-top: 18px; + padding-top: 16px; + border-top: 1px solid var(--blue-5); +} + +.ref-hidden-file { + display: none !important; +} + +.ref-assets-title { + font-weight: 900; + color: var(--gray-900); + margin-bottom: 10px; +} + +.ref-assets-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.ref-asset { + border: 1px solid var(--blue-5); + border-radius: 14px; + overflow: hidden; + background: #fff; +} + +.ref-asset-img { + width: 100%; + height: 140px; + object-fit: cover; + display: block; +} + +.ref-asset-del { + width: 100%; + padding: 10px 12px; + border: 0; + background: #fff; + cursor: pointer; + font-weight: 900; + color: var(--danger); +} + +/* bottom actions */ +.ref-form-actions { + margin-top: 22px; + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.ref-btn { + border-radius: 999px; + padding: 12px 18px; + font-weight: 900; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.ref-btn-ghost { + border: 1px solid var(--blue-20); + background: #fff; + color: var(--gray-900); +} + +.ref-btn-primary { + border: 0; + background: var(--blue-40); + color: #fff; +} + +/* responsive */ +@media (max-width: 900px) { + .ref-form-wrap { padding: 28px 18px; } + .ref-title { font-size: 26px; } + .ref-meta { grid-template-columns: 1fr; } + .ref-assets-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} diff --git a/static/js/reflections.js b/static/js/reflections.js new file mode 100644 index 0000000..da12c40 --- /dev/null +++ b/static/js/reflections.js @@ -0,0 +1,332 @@ +// static/js/reflections.js +(() => { + /** --------------------------- + * Helpers + * -------------------------- */ + const qs = (sel, el = document) => el.querySelector(sel); + const qsa = (sel, el = document) => Array.from(el.querySelectorAll(sel)); + + const closeAllMenus = (exceptWrap = null) => { + qsa("[data-more-wrap]").forEach((wrap) => { + if (exceptWrap && wrap === exceptWrap) return; + const btn = qs("[data-more-btn]", wrap); + const menu = qs("[data-more-menu]", wrap); + if (!btn || !menu) return; + btn.setAttribute("aria-expanded", "false"); + menu.hidden = true; + }); + }; + + /** --------------------------- + * 1) Kebab menu (수정/삭제) + * - 버튼 클릭: 해당 메뉴 토글 + * - 바깥 클릭: 모두 닫기 + * - ESC: 닫기 + * -------------------------- */ + const bindMenus = () => { + qsa("[data-more-wrap]").forEach((wrap) => { + const btn = qs("[data-more-btn]", wrap); + const menu = qs("[data-more-menu]", wrap); + if (!btn || !menu) return; + + btn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + + const isOpen = btn.getAttribute("aria-expanded") === "true"; + closeAllMenus(wrap); + + // 토글 + btn.setAttribute("aria-expanded", String(!isOpen)); + menu.hidden = isOpen; + + // 열릴 때만 포커스 이동(접근성) + if (!isOpen) { + const firstItem = qs(".note-menuitem", menu); + if (firstItem) firstItem.focus?.(); + } + }); + + // 메뉴 내부 클릭은 바깥 클릭 닫기 막기 + menu.addEventListener("click", (e) => e.stopPropagation()); + }); + + // 바깥 클릭하면 닫기 + document.addEventListener("click", () => closeAllMenus(null)); + + // ESC 누르면 닫기 + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeAllMenus(null); + }); + }; + + /** --------------------------- + * 2) Bookmark toggle (AJAX 포함) + * - 기본값 false로 시작 (서버값 무시) + * - 클릭 시 UI만 토글 + * - aria-pressed, class 동기화 + * -------------------------- */ + const getCSRFToken = () => + (document.querySelector("[name=csrfmiddlewaretoken]") || {}).value || + (document.cookie.match(/(?:^|;\s*)csrftoken=([^;]+)/) || [])[1] || + ""; + + const bindBookmarkAjax = () => { + document.querySelectorAll("[data-bookmark-btn]").forEach((btn) => { + btn.addEventListener("click", async (e) => { + console.log("bookmark btn clicked"); + e.preventDefault(); + e.stopPropagation(); + + // note id 추출 (A안/B안 모두 대응) + const noteId = + btn.dataset.noteId || + btn.closest("[data-note-id]")?.dataset.noteId; + + if (!noteId) { + console.error("noteId not found for bookmark button"); + return; + } + + // 현재 상태 + const wasActive = btn.classList.contains("is-active"); + const next = !wasActive; + + // 옵티미스틱 UI + btn.classList.toggle("is-active", next); + btn.setAttribute("aria-pressed", next ? "true" : "false"); + btn.disabled = true; + + try { + const res = await fetch(`/api/reflections/retrospectives/${noteId}/`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCSRFToken(), + }, + body: JSON.stringify({ bookmarked: next }), + }); + + if (!res.ok) { + const t = await res.text().catch(() => ""); + throw new Error(`PATCH failed: ${res.status} ${t}`); + } + + // 응답이 bookmarked를 내려주면 동기화(선택) + const data = await res.json().catch(() => null); + if (data?.bookmarked !== undefined) { + btn.classList.toggle("is-active", !!data.bookmarked); + btn.setAttribute("aria-pressed", data.bookmarked ? "true" : "false"); + } + + } catch (err) { + console.error(err); + + // 실패 시 롤백 + btn.classList.toggle("is-active", wasActive); + btn.setAttribute("aria-pressed", wasActive ? "true" : "false"); + + alert("북마크 변경 실패"); + } finally { + btn.disabled = false; + } + }); + }); + }; + + + + const bindProjectRoleAutofill = () => { + const projectSel = document.querySelector("[data-project-select]"); + const roleSel = document.querySelector("[data-role-select]"); + const roleMapEl = document.getElementById("roleMapJson"); + + if (!projectSel || !roleSel || !roleMapEl) return; + + let roleMap = {}; + try { + roleMap = JSON.parse(roleMapEl.textContent || "{}"); + } catch { + roleMap = {}; + } + + projectSel.addEventListener("change", () => { + const pid = projectSel.value; + if (!pid) return; + + const autoRole = roleMap[pid]; + if (autoRole) { + roleSel.value = autoRole; // 사용자가 원하면 다시 바꿀 수 있음 + } + }); + }; + + const bindAssetUpload = () => { + const fileInput = document.getElementById("assetFileInput"); + if (!fileInput) return; + + const wrap = document.querySelector(".ref-form-wrap"); + if (!wrap) return; + + const NOTE_ID = (wrap.dataset.noteId || "").trim(); + const DRAFT_KEY = (wrap.dataset.draftKey || "").trim(); + let currentQid = null; + + const csrfToken = (document.querySelector("[name=csrfmiddlewaretoken]") || {}).value || ""; + + // ✅ URL name은 프로젝트에 맞춰야 함 (기존 _note_form.html에 있던 이름 그대로) + const uploadUrlWithNote = NOTE_ID ? wrap.dataset.uploadNoteUrl : ""; + const uploadUrlTemp = wrap.dataset.uploadTempUrl; + + // 위 2개를 템플릿에서 data로 주는 방식이 제일 안전함. + // (아래 "data-upload-..." 주는 방법 참고) + + const insertAtCursor = (textarea, text) => { + const start = textarea.selectionStart ?? textarea.value.length; + const end = textarea.selectionEnd ?? textarea.value.length; + const before = textarea.value.slice(0, start); + const after = textarea.value.slice(end); + textarea.value = before + text + after; + const pos = start + text.length; + textarea.setSelectionRange(pos, pos); + textarea.focus(); + }; + + document.querySelectorAll("[data-asset-btn]").forEach((btn) => { + btn.addEventListener("click", () => { + currentQid = btn.dataset.qid; + fileInput.value = ""; + fileInput.click(); + }); + }); + + fileInput.addEventListener("change", async () => { + if (!fileInput.files || !fileInput.files[0] || !currentQid) return; + + if (!NOTE_ID && !DRAFT_KEY) { + alert("draft_key가 없어 업로드할 수 없습니다."); + return; + } + + const url = NOTE_ID ? uploadUrlWithNote : uploadUrlTemp; + if (!url) { + alert("업로드 URL이 설정되지 않았습니다."); + return; + } + + const fd = new FormData(); + fd.append("image", fileInput.files[0]); + fd.append("alt_text", "image"); + if (!NOTE_ID) fd.append("draft_key", DRAFT_KEY); + + let res; + try { + res = await fetch(url, { + method: "POST", + headers: { "X-CSRFToken": csrfToken }, + body: fd, + }); + } catch (e) { + console.error(e); + alert("업로드 요청 실패(네트워크)"); + return; + } + + if (!res.ok) { + const t = await res.text().catch(() => ""); + console.error("upload failed:", res.status, t); + alert("업로드 실패"); + return; + } + + const data = await res.json(); + const md = data.md || `![image](${data.url})`; + + const ta = document.getElementById(`ta__${currentQid}`); + if (!ta) return; + + insertAtCursor(ta, (ta.value.endsWith("\n") || ta.value.length === 0) ? md : "\n" + md); + }); + }; + + const bindAssetDelete = () => { + const wrap = document.querySelector(".ref-form-wrap"); + if (!wrap) return; + + const NOTE_ID = (wrap.dataset.noteId || "").trim(); + if (!NOTE_ID) return; + + const csrfToken = (document.querySelector("[name=csrfmiddlewaretoken]") || {}).value || ""; + + document.querySelectorAll("[data-asset-delete]").forEach((btn) => { + btn.addEventListener("click", async () => { + if (!confirm("이미지를 삭제할까요?")) return; + + const assetId = btn.dataset.assetId; + const urlTpl = wrap.dataset.deleteAssetUrlTpl; // 예: "/retrospectives/12/assets/0/" 형태 + if (!urlTpl) return; + + const url = urlTpl.replace("/0/", `/${assetId}/`); + + const res = await fetch(url, { + method: "DELETE", + headers: { "X-CSRFToken": csrfToken }, + }); + + if (!res.ok) { + alert("삭제 실패"); + return; + } + + const row = document.querySelector(`[data-asset-row="${assetId}"]`); + if (row) row.remove(); + }); + }); + }; + const bindAutoSubmit = () => { + document.querySelectorAll("[data-auto-submit]").forEach((el) => { + el.addEventListener("change", () => { + el.form?.submit(); + }); + }); + }; + + const bindBookmarkFilter = () => { + const btn = document.querySelector("[data-bookmark-filter]"); + if (!btn) return; + + btn.addEventListener("click", () => { + const url = new URL(window.location.href); + const isOn = url.searchParams.get("bookmarked"); + + if (isOn) { + url.searchParams.delete("bookmarked"); + } else { + url.searchParams.set("bookmarked", "1"); + } + + window.location.href = url.toString(); + }); + }; + + + + /** --------------------------- + * Init + * -------------------------- */ + const init = () => { + bindMenus(); + bindBookmarkAjax(); + bindProjectRoleAutofill(); + bindAssetUpload(); + bindAssetDelete(); + bindAutoSubmit(); + bindBookmarkFilter(); + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index 847799e..9cc8b8f 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -1,272 +1,179 @@ - - +{# templates/reflections/_note_form.html #} {% load reflections_extras %} +{% load static %} + + -{% comment %} -필수 context: -- guide: {"title": "...", "questions":[{"id":"q1", "label":"..."}, ...]} -- tpl: template key -- answers: dict (qid -> text) (create는 빈 dict 가능) -- note: (update에서만 있을 수 있음) -- draft_key: (create에서만 있을 수 있음) ✅ create 즉시 업로드용 -- error: (선택) -{% endcomment %} +
-
{% if error %} -
- {{ error }} -
+
{{ error }}
{% endif %} - -
+ {# 상단 타이틀/태그/메뉴 라인 #} +
-
- -
- - template: {{ tpl }} - - {% if note %} - - {{ note.created_at|date:"Y.m.d (D)" }} - - {% endif %} +
+ {% if note and note.role %} + + {{ note.role }} + + {% endif %} + +
+ + + +
+
- - +
+ + {# 프로젝트/역할 선택(원하신 기능) #} +
+
+ +
+ + +
+
- -
- {% for q in guide.questions %} -
+
+ +
+ + +
+ 프로젝트 선택 시 역할이 자동 입력됩니다. (원하면 직접 변경 가능) +
+
- -
-
-
- {{ q.order|default:forloop.counter }}. {{ q.title }} -
+ {# 파일 인풋 1개 재사용 #} + - {# ✅ create에서도 업로드 가능: note 있거나 draft_key 있으면 업로드 버튼 노출 #} - {% if note or draft_key %} - - {% else %} - 현재 이미지 업로드가 불가능합니다. - {% endif %} + {# 질문 카드들 #} +
+ {% for q in guide.questions %} +
+
+
+ {{ q.order|default:forloop.counter }} + {{ q.title }}
- {% if q.hint %} -
- {{ q.hint }} -
- {% endif %} - - ... +
+ {# note 있거나 draft_key 있으면 업로드 버튼 활성 #} + + + +
-
+
{% endfor %}
- - - - -
- 내보내기용 마크다운 보기 -
- -

- * content_md는 저장/내보내기용이며, 작성 UI의 메인은 answers_json 입니다. -

-
-
- - + {# 업로드된 이미지 리스트(note 있을 때만) #} {% if note and note.assets.all %} -
-
업로드된 이미지
- - {% for asset in note.assets.all %} -
-
- {{ asset.alt_text|default:'image' }} -
- UUID: {{ asset.id }} +
+
업로드된 이미지
+
+ {% for asset in note.assets.all %} +
+ {{ asset.alt_text|default:'image' }} +
-
- - + {% endfor %}
- {% endfor %} -
- - +
{% endif %} - -
- - 목록 - - + {# 하단 버튼 #} +
+ 내보내기 +
-
+ +{# role_map JSON 전달 (Django json_script) #} +{{ my_role_map|json_script:"roleMapJson" }} diff --git a/templates/reflections/note_create.html b/templates/reflections/note_create.html index f2b10d5..1bdf9a0 100644 --- a/templates/reflections/note_create.html +++ b/templates/reflections/note_create.html @@ -1,11 +1,17 @@ - - {% extends "base.html" %} -{% load reflections_extras %} +{% load static %} + +{% block title %}회고 작성{% endblock %} + +{% block header %} + +{% endblock %} {% block content %} -
+ {% csrf_token %} - {% include "reflections/_note_form.html" with guide=guide tpl=tpl answers=answers %} + {% include "reflections/_note_form.html" %}
+ + {% endblock %} diff --git a/templates/reflections/note_list.html b/templates/reflections/note_list.html index e69de29..f12addf 100644 --- a/templates/reflections/note_list.html +++ b/templates/reflections/note_list.html @@ -0,0 +1,167 @@ +{# templates/reflections/note_list.html #} +{% extends "base.html" %} +{% load static %} + +{% block title %}회고{% endblock %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+ + {# 상단 컨트롤 영역 #} +
+ +
+ + {# 리스트 영역 #} +
+ {% if notes %} + {% for note in notes %} + + {% endfor %} + {% else %} +
+

작성된 회고가 없습니다.

+ 첫 회고 작성하기 +
+ {% endif %} +
+ +
+ + +{% endblock %} diff --git a/templates/reflections/note_update.html b/templates/reflections/note_update.html index 3a711b3..15c31eb 100644 --- a/templates/reflections/note_update.html +++ b/templates/reflections/note_update.html @@ -1,11 +1,17 @@ - - {% extends "base.html" %} -{% load reflections_extras %} +{% load static %} + +{% block title %}회고 수정{% endblock %} + +{% block header %} + +{% endblock %} {% block content %} -
+ {% csrf_token %} - {% include "reflections/_note_form.html" with note=note guide=guide tpl=tpl answers=answers %} + {% include "reflections/_note_form.html" %}
+ + {% endblock %} From 7f4921d5b75644cb7905351dadc989862dddc073 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Mon, 9 Feb 2026 23:15:14 +0900 Subject: [PATCH 265/380] =?UTF-8?q?feat:=20=ED=91=9C=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5,=20=EC=82=BD=EC=9E=85=ED=95=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=81=AC=EA=B8=B0=20=EC=A0=81?= =?UTF-8?q?=EC=A0=88=ED=9E=88=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templatetags/reflections_extras.py | 61 ++++++++++++++++ static/css/reflections.css | 73 +++++++++++++++++++ static/js/reflections.js | 45 ++++++++++++ templates/reflections/_note_form.html | 8 +- templates/reflections/note_detail.html | 5 ++ 5 files changed, 191 insertions(+), 1 deletion(-) diff --git a/apps/reflections/templatetags/reflections_extras.py b/apps/reflections/templatetags/reflections_extras.py index 4d39151..5ceba27 100644 --- a/apps/reflections/templatetags/reflections_extras.py +++ b/apps/reflections/templatetags/reflections_extras.py @@ -3,9 +3,67 @@ import markdown import bleach +import re register = template.Library() +def _ensure_class(html: str, tag: str, cls: str) -> str: + # 1) class가 이미 있는 경우: 기존 class 뒤에 추가(중복 방지) + def repl_with_class(m): + before, classes, after = m.group(1), m.group(2), m.group(3) + class_list = classes.split() + if cls not in class_list: + class_list.append(cls) + return f'<{tag}{before}class="{" ".join(class_list)}"{after}' + + html = re.sub( + rf"<{tag}([^>]*?)class=\"([^\"]*)\"([^>]*?)>", + repl_with_class, + html, + flags=re.IGNORECASE, + ) + + # 2) class가 없는 경우: 새로 추가 + html = re.sub( + rf"<{tag}(\s|>)", + rf'<{tag} class="{cls}"\1', + html, + flags=re.IGNORECASE, + ) + return html + +def _add_classes(html: str) -> str: + # table + html = _ensure_class(html, "table", "md-table") + html = _ensure_class(html, "thead", "md-thead") + html = _ensure_class(html, "tbody", "md-tbody") + html = _ensure_class(html, "tr", "md-tr") + html = _ensure_class(html, "th", "md-th") + html = _ensure_class(html, "td", "md-td") + + # images + html = _ensure_class(html, "img", "md-img") + + # code blocks + html = _ensure_class(html, "pre", "md-pre") + html = _ensure_class(html, "code", "md-code") + + # blockquote / lists + html = _ensure_class(html, "blockquote", "md-quote") + html = _ensure_class(html, "ul", "md-ul") + html = _ensure_class(html, "ol", "md-ol") + html = _ensure_class(html, "li", "md-li") + + # headings + html = _ensure_class(html, "h1", "md-h1") + html = _ensure_class(html, "h2", "md-h2") + html = _ensure_class(html, "h3", "md-h3") + + # paragraphs + html = _ensure_class(html, "p", "md-p") + + return html + @register.filter(name="get_item") def get_item(d, key): if not d: @@ -33,6 +91,8 @@ def md(value): "ul","ol","li", "strong","em", "a","img", + # 테이블 관련 태그 허용 + "table","thead","tbody","tr","th","td", }) allowed_attrs = { "a": ["href", "title", "rel", "target"], @@ -48,4 +108,5 @@ def md(value): strip=True, ) cleaned = bleach.linkify(cleaned) + cleaned = _add_classes(cleaned) return mark_safe(cleaned) diff --git a/static/css/reflections.css b/static/css/reflections.css index d8d2fb0..72ba452 100644 --- a/static/css/reflections.css +++ b/static/css/reflections.css @@ -708,3 +708,76 @@ body { .ref-meta { grid-template-columns: 1fr; } .ref-assets-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } + +/* ========================= + Markdown-like Table (Notion/GitHub-ish) + ========================= */ + +/* ===== table ===== */ +.md-table{ + width:100%; + border-collapse:separate; + border-spacing:0; + table-layout:fixed; + font-size:14px; + line-height:1.6; + color:#111827; + background:#fff; + border:1px solid #e5e7eb; + border-radius:12px; + overflow:hidden; + display:block; /* 좁은 화면에서 스크롤 */ + overflow-x:auto; + -webkit-overflow-scrolling:touch; +} + +.md-thead{ background:#f9fafb; } +.md-th,.md-td{ + padding:10px 12px; + border-right:1px solid #e5e7eb; + border-bottom:1px solid #e5e7eb; + vertical-align:top; + word-break:break-word; +} +.md-th{ font-weight:600; } +.md-tr:last-child .md-td{ border-bottom:0; } +.md-tr .md-th:last-child, +.md-tr .md-td:last-child{ border-right:0; } + +.md-tbody .md-tr:nth-child(even){ background:#fcfcfd; } +.md-tbody .md-tr:hover{ background:#f3f4f6; } + +/* ===== image ===== */ +.md-img{ + display:block; + max-width:100%; + height:auto; + max-height:420px; /* 여기로 “일정 크기 이하” 제한 */ + object-fit:contain; + border:1px solid #e5e7eb; + border-radius:12px; + background:#fff; +} + +/* (선택) markdown 기본 요소도 깔끔하게 */ +.md-pre{ + padding:12px; + border:1px solid #e5e7eb; + border-radius:12px; + overflow:auto; + background:#0b1020; +} +.md-code{ + font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size:13px; +} +.md-quote{ + margin:10px 0; + padding:10px 12px; + border-left:4px solid #e5e7eb; + background:#f9fafb; + border-radius:10px; +} +.md-p{ margin:8px 0; } +.md-ul,.md-ol{ padding-left:20px; margin:8px 0; } +.md-li{ margin:4px 0; } diff --git a/static/js/reflections.js b/static/js/reflections.js index da12c40..4701475 100644 --- a/static/js/reflections.js +++ b/static/js/reflections.js @@ -291,6 +291,50 @@ }); }; + const insertTableAtCursor = (textarea, rows = 2, cols = 2) => { + console.log("insertTableAtCursor", { rows, cols }); + const headerCells = Array.from({ length: cols }, (_, i) => `헤더${i + 1}`); + const dividerCells = Array.from({ length: cols }, () => "---"); + const bodyRows = Array.from({ length: rows }, () => + Array.from({ length: cols }, () => "내용") + ); + + const lines = [ + `| ${headerCells.join(" | ")} |`, + `| ${dividerCells.join(" | ")} |`, + ...bodyRows.map((row) => `| ${row.join(" | ")} |`), + "", // 마지막 줄바꿈 + ]; + + const table = `\n${lines.join("\n")}`; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + + textarea.value = value.slice(0, start) + table + value.slice(end); + + const newPos = start + table.length; + textarea.selectionStart = textarea.selectionEnd = newPos; + textarea.focus(); + }; + + const bindInsertTable = () => { + document.addEventListener("click", (e) => { + const btn = e.target.closest("[data-table-btn]"); + if (!btn) return; + + const qid = btn.dataset.qid; + if (!qid) return; + + const textarea = document.getElementById(`ta__${qid}`); + if (!textarea) return; + + insertTableAtCursor(textarea, 2, 2); + }); + }; + + const bindBookmarkFilter = () => { const btn = document.querySelector("[data-bookmark-filter]"); if (!btn) return; @@ -322,6 +366,7 @@ bindAssetDelete(); bindAutoSubmit(); bindBookmarkFilter(); + bindInsertTable() }; if (document.readyState === "loading") { diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index 9cc8b8f..f302749 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -132,7 +132,13 @@ - -{% endblock %} \ No newline at end of file + + {% endblock %} \ No newline at end of file From bce03ab4261761330112cae3f84ae029ffbb0a83 Mon Sep 17 00:00:00 2001 From: bimvocado Date: Tue, 10 Feb 2026 01:26:30 +0900 Subject: [PATCH 270/380] fix: tech stack --- static/css/onboarding_profile.css | 14 ++++++++++++ templates/account/onboarding_profile.html | 28 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/static/css/onboarding_profile.css b/static/css/onboarding_profile.css index 4edf2e4..363a2c1 100644 --- a/static/css/onboarding_profile.css +++ b/static/css/onboarding_profile.css @@ -305,6 +305,20 @@ form > div:last-of-type label:has(input[type="checkbox"]:checked)::before { font-weight: bold; } +/* JS fallback class when :has is unsupported */ +form > div:last-of-type label.is-checked { + background-color: var(--primary-blue); + border-color: var(--primary-blue); + color: var(--white); + font-weight: 600; +} + +form > div:last-of-type label.is-checked::before { + content: '✓'; + margin-right: 6px; + font-weight: bold; +} + /* Help text for tech stacks */ form > div:last-of-type small { display: block; diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html index b1ef5c3..5d925bd 100644 --- a/templates/account/onboarding_profile.html +++ b/templates/account/onboarding_profile.html @@ -62,5 +62,33 @@

{% if user.nickname %}프로필 수정{% else %}프로필 설정{% endif %}< reader.readAsDataURL(f); }); })(); + + // Tech stack checkbox visual fallback (for browsers without :has) + (function() { + const container = document.querySelector('form > div:last-of-type'); + if (!container) return; + // find all checkbox inputs related to tech_stacks + const inputs = container.querySelectorAll('input[type="checkbox"]'); + if (!inputs || inputs.length === 0) return; + + function updateLabelForInput(input) { + // find label by for= or closest wrapping label + let label = container.querySelector(`label[for="${input.id}"]`); + if (!label) label = input.closest('label'); + if (!label) return; + if (input.checked) label.classList.add('is-checked'); + else label.classList.remove('is-checked'); + } + + inputs.forEach((inp) => { + // initial sync + updateLabelForInput(inp); + // listen change + inp.addEventListener('change', () => updateLabelForInput(inp)); + // also ensure clicking label toggles class if label used without wrapping + const labelRef = container.querySelector(`label[for="${inp.id}"]`); + if (labelRef) labelRef.addEventListener('click', () => setTimeout(() => updateLabelForInput(inp), 0)); + }); + })(); {% endblock %} \ No newline at end of file From 3f3daab80f53d82b547ff3cfe1dd19c5421a343d Mon Sep 17 00:00:00 2001 From: bimvocado Date: Tue, 10 Feb 2026 01:42:20 +0900 Subject: [PATCH 271/380] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EB=B0=8F=20=ED=85=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/forms.py | 6 +- templates/account/onboarding_profile.html | 74 ++++++++++++++--------- templates/account/profile_edit.html | 9 +++ 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 314e8f7..13f3d81 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -65,7 +65,8 @@ def clean_github_id(self): def save(self, commit=True): user = super().save(commit) if commit: - user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) + if "tech_stacks" in self.cleaned_data: + user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) return user @@ -136,5 +137,6 @@ def clean_github_id(self): def save(self, commit=True): user = super().save(commit) if commit: - user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) + if "tech_stacks" in self.cleaned_data: + user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) return user diff --git a/templates/account/onboarding_profile.html b/templates/account/onboarding_profile.html index 5d925bd..6498c53 100644 --- a/templates/account/onboarding_profile.html +++ b/templates/account/onboarding_profile.html @@ -37,23 +37,31 @@

{% if user.nickname %}프로필 수정{% else %}프로필 설정{% endif %}< {% endblock %} \ No newline at end of file diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index 825c4f3..80334e3 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -50,6 +50,15 @@

+
+ {{ form.tech_stacks.errors }} + +
+ {{ form.tech_stacks }} +
+ {{ form.tech_stacks.help_text }} +
+ {% if form.errors %}
{% for field, errors in form.errors.items %} From dc8abeebed34d550c9c2d603979e293d94b1fe0f Mon Sep 17 00:00:00 2001 From: bimvocado Date: Tue, 10 Feb 2026 01:58:06 +0900 Subject: [PATCH 272/380] fix: profile img --- apps/accounts/forms.py | 9 ++--- static/css/profile_edit.css | 55 +++++++++++++++++++++++++++++ templates/account/profile_edit.html | 21 +++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 13f3d81..2c0e67a 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -63,10 +63,11 @@ def clean_github_id(self): return github_id or None def save(self, commit=True): - user = super().save(commit) - if commit: - if "tech_stacks" in self.cleaned_data: - user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) + user = super().save(commit=False) + # Always save user record first (with profile_image) + user.save() + if "tech_stacks" in self.cleaned_data: + user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) return user diff --git a/static/css/profile_edit.css b/static/css/profile_edit.css index d4cb2bd..e401982 100644 --- a/static/css/profile_edit.css +++ b/static/css/profile_edit.css @@ -294,6 +294,61 @@ body { padding: 40px 0; } +/* Tech stack form field styling */ +.info-row div[style*="max-height"] { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-start; + align-content: flex-start; +} + +.info-row div[style*="max-height"] fieldset { + border: none; + padding: 0; + margin: 0; + display: contents; +} + +.info-row div[style*="max-height"] input[type="checkbox"] { + display: none; +} + +.info-row div[style*="max-height"] label { + display: inline-flex; + align-items: center; + padding: 8px 16px; + background-color: #f0f2f7; + border: 2px solid #e9ecef; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + color: #333; + cursor: pointer; + transition: all 0.2s; + margin: 0; + user-select: none; +} + +.info-row div[style*="max-height"] label:hover { + background-color: #e8eaf0; + border-color: #667eea; + transform: translateY(-1px); +} + +.info-row div[style*="max-height"] label.is-checked { + background-color: #667eea; + border-color: #667eea; + color: white; + font-weight: 600; +} + +.info-row div[style*="max-height"] label.is-checked::before { + content: '✓'; + margin-right: 6px; + font-weight: bold; +} + /* 반응형 디자인 */ @media (max-width: 1024px) { .edit-card { diff --git a/templates/account/profile_edit.html b/templates/account/profile_edit.html index 80334e3..e3609e6 100644 --- a/templates/account/profile_edit.html +++ b/templates/account/profile_edit.html @@ -200,6 +200,27 @@

기술 스택

input.classList.add('input-field'); } }); + + // Tech stack checkbox visual handler (for profile edit) + (function() { + const form = document.querySelector('.edit-form'); + if (!form) return; + const inputs = form.querySelectorAll('input[type="checkbox"]'); + if (!inputs || inputs.length === 0) return; + + function updateLabelForInput(input) { + let label = form.querySelector(`label[for="${input.id}"]`); + if (!label) label = input.closest('label'); + if (!label) return; + if (input.checked) label.classList.add('is-checked'); + else label.classList.remove('is-checked'); + } + + inputs.forEach((inp) => { + updateLabelForInput(inp); + inp.addEventListener('change', () => updateLabelForInput(inp)); + }); + })(); }); {% endblock %} \ No newline at end of file From 1e99cbc8c50360df8cfc571dbb6fb41e2adf407e Mon Sep 17 00:00:00 2001 From: bimvocado Date: Tue, 10 Feb 2026 02:03:33 +0900 Subject: [PATCH 273/380] fix: mypage --- apps/accounts/forms.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 2c0e67a..2c77201 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -136,8 +136,10 @@ def clean_github_id(self): return github_id or None def save(self, commit=True): - user = super().save(commit) - if commit: - if "tech_stacks" in self.cleaned_data: - user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) + user = super().save(commit=False) + # Always save user record first (includes profile_image file upload) + user.save() + # Handle ManyToMany field separately + if "tech_stacks" in self.cleaned_data: + user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) return user From af87cc712eda354bc856a4cc47656f558844f287 Mon Sep 17 00:00:00 2001 From: knana6 Date: Tue, 10 Feb 2026 12:51:34 +0900 Subject: [PATCH 274/380] fix: pencil icon --- static/images/pencil_icon.png | Bin 0 -> 1130 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/pencil_icon.png diff --git a/static/images/pencil_icon.png b/static/images/pencil_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e8c7ff7692eb67e4ac67523c3bf1c8e01baaaa68 GIT binary patch literal 1130 zcmeAS@N?(olHy`uVBq!ia0vp^DImGZx^prwfgF}}M_)$E)e-c?47?}Tix;TbZFutAJ8+zM7ph3S@RzdoT^aNfH z^99l=EM^V73CwN^a~0BU+Px0ceJuZ_{JuRcZ?=9DFkd5AV{+Ds(DW)lo z3XdkmnTdppXm_f0d@t~o9cgrnFjmM^D znHgDA`lDCO3^J;E{`u+@*(+X|l6#ByRi69GoAp=-DEQ4Zw$wCw`jm^-`@5Ix2XIQ5 zpZDN8YyRV7&FdSfv8@w4B=@}MyUg!iZ@Eo3T|Xt|>DR(>7!!Lj?6fc#Hg9^ul98k zbFreUli^~KFW32&HCj$EnmEI;%Qo$~VM%!AT1tY&QQ)V#@3W>6~7U2(UJG-Z^x0pUfFtY8@Q|&eu$%(0& zar@qQOcIP-_UP&9HW#3PcHF)v9@7k4KdHK=ayZcdIe$8OP2;iSDiG~hmap>t0S!^9bE3AFKdtL7HvFk_;|X76`S;ziFY4+R?EinFO8r`t@wujV zKaXptxTMxF{mnjY=cpEPjPr$3zD?(jMxGTt#tZ5-->?ZSHj=3jnp~3`IrZTIwc|&{ zgccdu08O~zpcLZ9P;#+<-jNocOHSu5FF5tVS$toy$}?WKre$Gw7XH7j7s{6Q!v2ln zPRW`NC&DCc_uaaF@%;J=Je}7Q_I>GkuFZSI@O^r)oL$Qr+uv{P{~zRfa3ucbcXNYz zt5OZ*oz}_zmeF+7<&0=v>A6S$|D0W(E8~vEGg;lb&}T7C>3{M}mYU~}pT01-{CVTb zL%v~?ejat$QTfaB;|kv^|2Is?KEbcPcWK4L@JSokpQ>=1Ox|}z_6uVV!@pIpbU7^= zgjl#8CC)SY2p=i0Tg~+7q1tNJ|29vIb!Y6JA0Pde{nA0x6y9gTa?c;6e4hLN-oMZ1 vwG@mq+V)vni#)YtumAMq+NmI}Pwev+&z^F&bP0l+XkKsv`0r literal 0 HcmV?d00001 From e27c8558f3c21339fadd3fb35cd140b22f493e29 Mon Sep 17 00:00:00 2001 From: knana6 Date: Tue, 10 Feb 2026 12:55:09 +0900 Subject: [PATCH 275/380] fix: default image --- static/images/default_profile.png | Bin 0 -> 144134 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/default_profile.png diff --git a/static/images/default_profile.png b/static/images/default_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5c3a589569ea6325fb442100e894fdc267074c GIT binary patch literal 144134 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 literal 0 HcmV?d00001 From f00cabdcbeb3e0c1478f8b93ca3d43a0a52c0599 Mon Sep 17 00:00:00 2001 From: knana6 Date: Tue, 10 Feb 2026 13:16:14 +0900 Subject: [PATCH 276/380] fix: onboarding forms.py submit --- apps/accounts/forms.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py index 2c77201..63dccdf 100644 --- a/apps/accounts/forms.py +++ b/apps/accounts/forms.py @@ -135,11 +135,12 @@ def clean_github_id(self): raise forms.ValidationError("이미 등록된 GitHub 아이디입니다.") return github_id or None - def save(self, commit=True): - user = super().save(commit=False) - # Always save user record first (includes profile_image file upload) +def save(self, commit=True): + user = super().save(commit=False) + + if commit: user.save() - # Handle ManyToMany field separately if "tech_stacks" in self.cleaned_data: user.tech_stacks.set(self.cleaned_data.get("tech_stacks", [])) - return user + return user + From 54a47554a8a312405f32edc7e5b1ab6ecee650d2 Mon Sep 17 00:00:00 2001 From: knana6 Date: Tue, 10 Feb 2026 13:44:54 +0900 Subject: [PATCH 277/380] feat: note detail --- static/css/reflections.css | 223 +++++++++++++++++++++++++ templates/reflections/note_detail.html | 207 +++++++++++------------ 2 files changed, 321 insertions(+), 109 deletions(-) diff --git a/static/css/reflections.css b/static/css/reflections.css index 72ba452..a30cfef 100644 --- a/static/css/reflections.css +++ b/static/css/reflections.css @@ -781,3 +781,226 @@ body { .md-p{ margin:8px 0; } .md-ul,.md-ol{ padding-left:20px; margin:8px 0; } .md-li{ margin:4px 0; } + +/* ========================= + Page +========================= */ +.ref-page { + background: var(--blue-0); + padding: 32px 0 96px; +} + +.ref-controls, +.ref-list { + max-width: 960px; + margin: 0 auto; + padding: 0 16px; +} + +/* ... (기존 CSS 내용 생략) ... */ + +/* ========================= + Note Detail +========================= */ +.ref-detail-wrap { + max-width: 900px; + margin: 0 auto; + padding: 20px 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* 상단 바 */ +.ref-detail-topbar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.ref-detail-title-section { + flex: 1; +} + +.ref-detail-title { + font-size: 20px; + font-weight: 800; + line-height: 1.2; + color: var(--gray-900); + margin: 0; +} + +.ref-detail-meta-text { + margin-top: 6px; + font-size: 13px; + color: var(--gray-600); +} + +.ref-detail-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +/* 메타 카드 */ +.ref-detail-meta-card { + border: 1px solid #e5e7eb; + border-radius: 18px; + overflow: hidden; + background: #fff; +} + +.ref-detail-meta-tags { + padding: 14px; + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} + +.ref-meta-tag { + font-size: 12px; + padding: 6px 10px; + border-radius: 999px; + background: #f3f4f6; + color: #374151; + font-weight: 600; +} + +.ref-meta-tag-project { + background: #eef2ff; + color: #3730a3; +} + +.ref-meta-tag-personal { + background: #ecfeff; + color: #155e75; +} + +/* 질문 리스트 */ +.ref-detail-qa-list { + display: flex; + flex-direction: column; + gap: 18px; +} + +.ref-detail-qa-card { + border: 1px solid #e5e7eb; + border-radius: 18px; + overflow: hidden; + background: #fff; +} + +.ref-detail-qa-header { + background: #eef2ff; + padding: 12px 14px; + font-weight: 700; + color: var(--gray-900); + display: flex; + align-items: center; + gap: 10px; +} + +.ref-detail-qa-title { + flex: 1; +} + +.ref-detail-qa-body { + padding: 14px; + font-size: 16px; + line-height: 1.7; + color: var(--gray-700); +} + +/* 마크다운 내보내기 */ +.ref-detail-export { + border: 1px solid #e5e7eb; + border-radius: 18px; + background: #fff; + overflow: hidden; +} + +.ref-detail-export-summary { + cursor: pointer; + padding: 12px 14px; + font-weight: 700; + background: #f3f4f6; + color: var(--gray-900); + user-select: none; +} + +.ref-detail-export-summary:hover { + background: #e5e7eb; +} + +.ref-detail-export-body { + padding: 14px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.ref-detail-export-textarea { + width: 100%; + resize: vertical; + border: 1px solid #e5e7eb; + border-radius: 14px; + padding: 12px; + outline: none; + background: #f9fafb; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + color: var(--gray-900); +} + +.ref-detail-export-textarea:focus { + border-color: var(--blue-40); + background: #fff; +} + +/* 반응형 - Detail */ +@media (max-width: 768px) { + .ref-detail-topbar { + flex-direction: column; + align-items: stretch; + } + + .ref-detail-actions { + justify-content: flex-start; + } + + .ref-detail-title { + font-size: 18px; + } + + .ref-detail-meta-text { + font-size: 12px; + } + + .ref-detail-qa-header { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 480px) { + .ref-detail-wrap { + padding: 16px 12px; + } + + .ref-detail-title { + font-size: 16px; + } + + .ref-detail-qa-body { + font-size: 15px; + } + + .ref-meta-tag { + font-size: 11px; + padding: 5px 8px; + } +} diff --git a/templates/reflections/note_detail.html b/templates/reflections/note_detail.html index 8cfcde4..659c466 100644 --- a/templates/reflections/note_detail.html +++ b/templates/reflections/note_detail.html @@ -1,133 +1,123 @@ - - - {% extends "base.html" %} -{% load reflections_extras %} {% load static %} +{% load reflections_extras %} -{% block title %}회고 상세{% endblock %} +{% block title %}{{ note.title }} - 회고{% endblock %} {% block header %} {% endblock %} {% block content %} -
- - -
-
-
- {{ note.title }} -
-
- 템플릿: {{ note.template_key }} · - 생성: {{ note.created_at|date:"Y-m-d H:i" }} · - 수정: {{ note.updated_at|date:"Y-m-d H:i" }} +
+ - -
-
- - bookmarked: {{ note.bookmarked|yesno:"true,false" }} - - - {% if note.project %} - - project: {{ note.project.title }} - - {% else %} - - 개인 회고 + {# 메타 카드 #} +
+
+ + bookmarked: {{ note.bookmarked|yesno:"true,false" }} - {% endif %} -
-
- - -
- {% for q in guide.questions %} -
-
- {{ q.order|default:forloop.counter }}. {{ q.title }} -
-
- {{ answers|get_item:q.id|md }} -
-
- {% endfor %} -
- -
- - content_md 보기(내보내기용) - -
- - + {% if note.project %} + + project: {{ note.project.title }} + + {% else %} + + 개인 회고 + + {% endif %} + + {% if note.role %} + + {{ note.role }} + + {% endif %} +
+
+ + {# 질문 리스트(보기 전용) #} +
+ {% for q in guide.questions %} +
+
+ {{ q.order|default:forloop.counter }} + {{ q.title }} +
+
+ {{ answers|get_item:q.id|render_markdown }} +
+
+ {% endfor %}
- - + {# 마크다운 내보내기(접기) #} +
+ + content_md 보기(내보내기용) + +
+ + +
+
+
- + -{% endblock %} +{% endblock %} \ No newline at end of file From ae6d3e4d776bf377ff07db6c93925fcab44c56a4 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 16:51:45 +0900 Subject: [PATCH 278/380] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=ED=8A=B8=EC=9A=A9=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/migrations/0008_projectlike.py | 30 ++++++++++++ apps/projects/models.py | 51 ++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 apps/projects/migrations/0008_projectlike.py diff --git a/apps/projects/migrations/0008_projectlike.py b/apps/projects/migrations/0008_projectlike.py new file mode 100644 index 0000000..d866f4c --- /dev/null +++ b/apps/projects/migrations/0008_projectlike.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.10 on 2026-02-10 07:11 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0007_clean_related_links'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ProjectLike', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('project', models.ForeignKey(help_text='좋아요 받은 프로젝트', on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='projects.project')), + ('user', models.ForeignKey(help_text='좋아요 누른 사용자', on_delete=django.db.models.deletion.CASCADE, related_name='project_likes', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'project_likes', + 'indexes': [models.Index(fields=['project'], name='project_lik_project_a701ec_idx'), models.Index(fields=['user'], name='project_lik_user_id_47c108_idx')], + 'unique_together': {('user', 'project')}, + }, + ), + ] diff --git a/apps/projects/models.py b/apps/projects/models.py index f60adf6..a45ad9a 100644 --- a/apps/projects/models.py +++ b/apps/projects/models.py @@ -189,6 +189,23 @@ class Meta: def __str__(self) -> str: return self.title + + def get_like_count(self) -> int: + """좋아요 개수 반환""" + return self.likes.count() + + def is_liked_by(self, user) -> bool: + """특정 사용자가 좋아요를 눌렀는지 확인""" + if not user or user.is_anonymous: + return False + return self.likes.filter(user=user).exists() + + def toggle_like(self, user): + """사용자의 좋아요 상태 토글""" + like_obj, created = self.likes.get_or_create(user=user) + if not created: + like_obj.delete() + return created # True: 좋아요 추가, False: 좋아요 제거 class ProjectApplication(models.Model): @@ -257,3 +274,37 @@ class Meta: def __str__(self) -> str: return f"{self.user} → {self.project} ({self.role.code})" + + +class ProjectLike(models.Model): + """ + 프로젝트 좋아요 + - 사용자가 프로젝트에 좋아요를 누를 수 있음 + - 중복 좋아요 방지 (User + Project 유니크) + """ + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="project_likes", + help_text="좋아요 누른 사용자", + ) + + project = models.ForeignKey( + 'Project', + on_delete=models.CASCADE, + related_name="likes", + help_text="좋아요 받은 프로젝트", + ) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "project_likes" + unique_together = ("user", "project") + indexes = [ + models.Index(fields=["project"]), + models.Index(fields=["user"]), + ] + + def __str__(self) -> str: + return f"{self.user} ❤️ {self.project}" From 51a4e774e9c538a2ede95a16532eb9f5755462ec Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 16:52:18 +0900 Subject: [PATCH 279/380] =?UTF-8?q?feat:=20ajax=20=EB=B0=8F=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=EB=A5=BC=20=EC=9C=84=ED=95=9C=20url=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/api_urls.py | 4 +++- apps/projects/urls.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/projects/api_urls.py b/apps/projects/api_urls.py index 8dadb0d..1d43b7c 100644 --- a/apps/projects/api_urls.py +++ b/apps/projects/api_urls.py @@ -1,5 +1,7 @@ from django.urls import path +from . import views urlpatterns = [ - # 프로젝트 API URL은 추후 추가 + # 프로젝트 좋아요 토글 + path("/like/", views.toggle_project_like, name="api_toggle_project_like"), ] diff --git a/apps/projects/urls.py b/apps/projects/urls.py index 47b301b..43cf0f5 100644 --- a/apps/projects/urls.py +++ b/apps/projects/urls.py @@ -16,6 +16,7 @@ # KITUP 프로젝트 (모든 프로젝트) path("all/", views.kitup_list, name="kitup_list"), # kitup_list.html path("all//", views.kitup_detail, name="kitup_detail"), # kitup_detail.html + path("all//like/", views.toggle_project_like, name="toggle_project_like"), # 좋아요 API # 팀 매칭 관리 path("matching//run/", views.run_team_matching, name="run_team_matching"), # API From cb6bf4486992faac1f912547934eb485412bb12e Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 16:52:32 +0900 Subject: [PATCH 280/380] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index 52af7a7..8f8c30d 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -216,15 +216,24 @@ def project_detail(request, project_id): def kitup_list(request): """모든 KITUP 프로젝트 리스트 (완료된 보관 프로젝트)""" # 보관된 프로젝트만 조회 (ARCHIVED 상태) - # TODO 정렬 기능을 위한 GET 설정 - projects = Project.objects.filter( status=Project.Status.ARCHIVED - ).select_related('team').order_by('-created_at') + ).select_related('team') + + # 정렬 처리 + sort = request.GET.get('sort', 'popular') + if sort == 'latest': + projects = projects.order_by('-created_at') + elif sort == 'oldest': + projects = projects.order_by('created_at') + else: # popular (기본값) + # annotate로 좋아요 개수 추가하여 정렬 + from django.db.models import Count + projects = projects.annotate(like_count=Count('likes')).order_by('-like_count', '-created_at') context = { "projects": projects, - # TODO 팀 멤버 조회하게 넘겨주기 + "sort": sort, } return render(request, "projects/kitup_list.html", context) @@ -239,7 +248,27 @@ def kitup_detail(request, project_id): return render(request, "projects/kitup_detail.html", context) -# TODO 즐겨찾기 토글을 위한 POST 뷰 추가 + +@login_required +@require_POST +@login_required +@require_POST +def toggle_project_like(request, project_id): + """프로젝트 좋아요 토글 API""" + from django.http import JsonResponse + + project = get_object_or_404(Project, id=project_id) + + # 좋아요 토글 + is_liked = project.toggle_like(request.user) + like_count = project.get_like_count() + + return JsonResponse({ + 'success': True, + 'is_liked': is_liked, + 'like_count': like_count, + }) + # ================================ # 팀 매칭 관리 API From d98a1e74bf9bb22fa00da71eb11f3c9ec16cff8c Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 16:53:02 +0900 Subject: [PATCH 281/380] =?UTF-8?q?feat:=20ajax=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/kitup.css | 24 ++++++- static/js/kitup.js | 65 +++++++++++++++-- templates/projects/kitup_detail.html | 82 +++++++++++++++++++-- templates/projects/kitup_list.html | 104 ++++++++++++++++++++++----- 4 files changed, 243 insertions(+), 32 deletions(-) diff --git a/static/css/kitup.css b/static/css/kitup.css index f796aee..e554f2d 100644 --- a/static/css/kitup.css +++ b/static/css/kitup.css @@ -81,6 +81,7 @@ display: flex; flex-direction: column; gap: 8px; + flex: 1; } .kitup-card-title { @@ -103,6 +104,20 @@ font-size: 15px; } +/* 카드 하단 좋아요 영역 */ +.kitup-card-like { + display: flex; + align-items: center; + gap: 6px; + margin-top: auto; + padding-top: 8px; +} + +.kitup-like-count { + font-size: 14px; + color: #6b7280; +} + /* ========================= DETAIL (1번: 카드형 섹션) ========================= */ @@ -144,11 +159,14 @@ display: inline-flex; align-items: center; justify-content: center; - position: absolute; - top: 14px; - right: 14px; z-index: 9999; pointer-events: auto; + cursor: pointer; +} + +.kitup-detail-like .kitup-heart-icon { + width: 28px; + height: 28px; } /* 제목/설명 */ diff --git a/static/js/kitup.js b/static/js/kitup.js index 6ff9d6b..3a5e040 100644 --- a/static/js/kitup.js +++ b/static/js/kitup.js @@ -1,13 +1,66 @@ // static/js/kitup.js -document.addEventListener("click", (e) => { - const btn = e.target.closest("[data-like-btn]"); +// CSRF 토큰 가져오기 +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +// 하트 버튼 클릭 핸들러 +document.addEventListener("click", async (e) => { + const btn = e.target.closest(".kitup-like-btn"); if (!btn) return; - // 카드 클릭 방지 + // 이벤트 전파 방지 e.preventDefault(); e.stopPropagation(); + e.stopImmediatePropagation(); + + const projectId = btn.dataset.projectId; + + console.log('좋아요 클릭:', projectId); + + try { + // 좋아요 API 호출 + const response = await fetch(`/projects/all/${projectId}/like/`, { + method: 'POST', + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + }, + }); + + console.log('API 응답:', response.status); + + if (!response.ok) { + throw new Error(`API 오류: ${response.status}`); + } + + const data = await response.json(); + console.log('응답 데이터:', data); + + // UI 업데이트 + btn.dataset.liked = String(data.is_liked); + btn.setAttribute("aria-pressed", String(data.is_liked)); + + // 좋아요 수 업데이트 + let countEl = btn.nextElementSibling; + if (countEl && countEl.classList.contains('kitup-like-count')) { + countEl.textContent = data.like_count; + console.log('좋아요 수 업데이트:', data.like_count); + } + } catch (error) { + console.error('좋아요 처리 중 오류:', error); + alert('좋아요 처리 중 오류가 발생했습니다: ' + error.message); + } +}, true); // 캡처 단계에서 처리 - const pressed = btn.getAttribute("aria-pressed") === "true"; - btn.setAttribute("aria-pressed", String(!pressed)); -}); diff --git a/templates/projects/kitup_detail.html b/templates/projects/kitup_detail.html index e6574d1..b90014a 100644 --- a/templates/projects/kitup_detail.html +++ b/templates/projects/kitup_detail.html @@ -20,12 +20,13 @@ {{ project.title }} {% endif %} - {# 하트: UI만 (나중에 AJAX 연결) #} + {# 하트 #} + {{ project.get_like_count }}
@@ -111,6 +113,78 @@

팀 규칙

- + {% endblock %} diff --git a/templates/projects/kitup_list.html b/templates/projects/kitup_list.html index f573067..abf0b87 100644 --- a/templates/projects/kitup_list.html +++ b/templates/projects/kitup_list.html @@ -9,7 +9,7 @@

KITUP에 진행된 프로젝트

- {# 정렬 UI: 지금 view는 정렬 파라미터를 안 씀. 일단 링크만 만들어두고 추후 view에서 반영 #} + {# 정렬 UI #} {% with sort=request.GET.sort|default:"popular" %}
인기순 @@ -36,29 +36,28 @@

{{ project.title }}

{{ project.description|default:""|truncatechars:60 }}

- - {# 좋아요: 현재 모델/뷰에 “좋아요 개수”가 없음. 일단 UI만 만들어둠 #}
+ + + + + 좋아요 + + {{ project.get_like_count }}
- + {% empty %}
아직 공개된 프로젝트가 없습니다. @@ -68,6 +67,73 @@

{{ project.title }}

- + + +{% endblock %} From 4a218ee4f6c718990e7cae354497e4ad49da2a5e Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 16:55:32 +0900 Subject: [PATCH 282/380] =?UTF-8?q?fix=20:=20dashboard=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EA=B8=B0=20=EB=B2=84=ED=8A=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/projects/dashboard.html | 6 +++++- templates/projects/dashboard_update.html | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index c118bf6..2fbc0ec 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -150,7 +150,11 @@

진척도

- + {% if is_team_member %} + + + + {% endif %} {% endif %}
diff --git a/templates/projects/dashboard_update.html b/templates/projects/dashboard_update.html index 2f432f8..19c2252 100644 --- a/templates/projects/dashboard_update.html +++ b/templates/projects/dashboard_update.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load static %} {% block header %} - + {% endblock %} {% block content %}
@@ -15,18 +15,23 @@
{% if project.project_image %} - 서비스이미지 + 서비스이미지 {% else %} 서비스이미지 {% endif %} -

{{ project.title }}

-

{{ project.description }}

+ +
달력이미지 -

진행기간 {{ season.project_start|date:"Y/m/d" }} ~ {{ season.project_end|date:"Y/m/d" }}

+

진행기간 + {% if project.starts_at and project.ends_at %} + {{ project.starts_at|date:"Y/m/d" }} ~ {{ project.ends_at|date:"Y/m/d" }} + {% else %} + 미정 + {% endif %}

@@ -74,28 +79,40 @@

Team

{% endif %} {% endfor %}

-
- {% for member in members %} -
- {% if member.user.profile_image %} - 프로필사진 +
+ {% for item in members_with_level %} +
+
+ {% if item.member.user.profile_image %} + 프로필사진 {% else %} - 프로필사진 + 프로필사진 {% endif %} - 레벨사진 -

{{ member.user.username }}

-

✉️ {{ member.user.email }}

-

🖥️ @{{ member.user.username }}

- {% if member.role.name == "기획자" %} -

기획자

- {% elif member.role.name == "프론트엔드" %} -

프론트엔드

- {% elif member.role.name == "백엔드" %} -

백엔드

+ + {% if item.level == 1 %} + 레벨1 + {% elif item.level == 2 %} + 레벨2 + {% elif item.level == 3 %} + 레벨3 + {% elif item.level == 4 %} + 레벨4 {% endif %}
- {% endfor %} + +

{{ item.member.user.username }}

+

✉️ {{ item.member.user.email }}

+

🖥️ @{{ item.member.user.username }}

+ {% if item.member.role.code == "PM" %} +

기획자

+ {% elif item.member.role.name == "프론트엔드" %} +

프론트엔드

+ {% elif item.member.role.name == "백엔드" %} +

백엔드

+ {% endif %}
+ {% endfor %} +
From cfa336d6f73c3da6576b3a0995334f2efcf22b43 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 10 Feb 2026 19:19:07 +0900 Subject: [PATCH 284/380] =?UTF-8?q?feat:=20=ED=91=9C=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20=EB=AA=A8=EB=8B=AC=20=EB=9D=84=EC=9B=8C?= =?UTF-8?q?=EC=84=9C=20=ED=91=9C=20=ED=81=AC=EA=B8=B0=20=EC=A0=95=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/reflections.css | 91 +++++++++++++++++++++----- static/js/reflections.js | 73 +++++++++++++++++++-- templates/reflections/_note_form.html | 24 ++++++- templates/reflections/note_detail.html | 2 +- 4 files changed, 167 insertions(+), 23 deletions(-) diff --git a/static/css/reflections.css b/static/css/reflections.css index a30cfef..d8056de 100644 --- a/static/css/reflections.css +++ b/static/css/reflections.css @@ -687,15 +687,29 @@ body { display: inline-flex; align-items: center; justify-content: center; + min-height: 44px; + line-height: 1; } +button.ref-btn{ + appearance: none; + -webkit-appearance: none; + border: 0; + font: inherit; + font-weight: 900; + /* color: inherit; */ + background: transparent; +} + + + .ref-btn-ghost { border: 1px solid var(--blue-20); background: #fff; color: var(--gray-900); } -.ref-btn-primary { +.ref-btn.ref-btn-primary { border: 0; background: var(--blue-40); color: #fff; @@ -709,6 +723,66 @@ body { .ref-assets-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +/* 테이블 삽입 modal */ +.ref-table-picker-backdrop{ + position: fixed; inset: 0; + background: rgba(0,0,0,.4); + z-index: 1000; +} + +/* hidden 아닐 때만 표시 */ +.ref-table-picker-backdrop:not([hidden]){ + display: flex; + align-items: center; + justify-content: center; +} + + +.ref-table-picker-modal{ + background:#fff; + border-radius:16px; + padding:18px; + width:230px; +} + +.ref-table-picker-title{ + font-weight:900; + margin-bottom:12px; +} + +.ref-table-picker-grid{ + aspect-ratio: 1 / 1; + display:grid; + grid-template-columns:repeat(5,1fr); + gap:4px; + margin-bottom:12px; +} + +.ref-table-cell{ + width:36px; height:36px; + border:1px solid #d1d5db; + border-radius: 8px; + cursor:pointer; +} + +.ref-table-cell.active{ + background:#2563eb; +} + +.ref-table-picker-footer{ + display:flex; + justify-content:space-between; + align-items:center; +} + +.ref-table-picker-cancel{ + border:0; + background:transparent; + color:var(--gray-600); + font-weight:700; + cursor:pointer; +} + /* ========================= Markdown-like Table (Notion/GitHub-ish) ========================= */ @@ -782,22 +856,7 @@ body { .md-ul,.md-ol{ padding-left:20px; margin:8px 0; } .md-li{ margin:4px 0; } -/* ========================= - Page -========================= */ -.ref-page { - background: var(--blue-0); - padding: 32px 0 96px; -} - -.ref-controls, -.ref-list { - max-width: 960px; - margin: 0 auto; - padding: 0 16px; -} -/* ... (기존 CSS 내용 생략) ... */ /* ========================= Note Detail diff --git a/static/js/reflections.js b/static/js/reflections.js index 4701475..85d7278 100644 --- a/static/js/reflections.js +++ b/static/js/reflections.js @@ -320,20 +320,83 @@ }; const bindInsertTable = () => { + const backdrop = document.querySelector(".ref-table-picker-backdrop"); + if (!backdrop) return; + + const sizeText = backdrop.querySelector(".ref-table-picker-size"); + const cancelBtn = backdrop.querySelector(".ref-table-picker-cancel"); + const cells = Array.from(backdrop.querySelectorAll(".ref-table-cell")); + + if (!sizeText || !cancelBtn || cells.length === 0) return; + + let targetTextarea = null; + let hoverRows = 0, hoverCols = 0; + + const reset = () => { + hoverRows = 0; hoverCols = 0; + sizeText.textContent = "0 × 0"; + cells.forEach(c => c.classList.remove("active")); + }; + + const close = () => { + backdrop.hidden = true; + reset(); + targetTextarea = null; + }; + + // 표 버튼 클릭 -> 모달 오픈 document.addEventListener("click", (e) => { const btn = e.target.closest("[data-table-btn]"); if (!btn) return; const qid = btn.dataset.qid; - if (!qid) return; + const ta = document.getElementById(`ta__${qid}`); + if (!ta) return; + + targetTextarea = ta; + reset(); + backdrop.hidden = false; + }); + + // hover -> 미리보기(하이라이트) + cells.forEach((cell) => { + cell.addEventListener("mouseenter", () => { + hoverRows = +cell.dataset.rows; + hoverCols = +cell.dataset.cols; + + cells.forEach((c) => { + c.classList.toggle( + "active", + +c.dataset.rows <= hoverRows && +c.dataset.cols <= hoverCols + ); + }); + + sizeText.textContent = `${hoverRows} × ${hoverCols}`; + }); - const textarea = document.getElementById(`ta__${qid}`); - if (!textarea) return; + // ✅ click -> 즉시 삽입 + cell.addEventListener("click", () => { + const r = +cell.dataset.rows; + const c = +cell.dataset.cols; + if (!targetTextarea || !r || !c) return; - insertTableAtCursor(textarea, 2, 2); + insertTableAtCursor(targetTextarea, r, c); + close(); + }); }); - }; + // 취소/바깥/ESC 닫기 + cancelBtn.addEventListener("click", close); + + backdrop.addEventListener("click", (e) => { + if (e.target === backdrop) close(); + }); + + document.addEventListener("keydown", (e) => { + if (!backdrop.hidden && e.key === "Escape") close(); + }); + }; + const bindBookmarkFilter = () => { const btn = document.querySelector("[data-bookmark-filter]"); diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index f302749..e34495a 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -176,9 +176,31 @@ {# 하단 버튼 #}
- 내보내기 + 돌아가기
+ + + +
{# role_map JSON 전달 (Django json_script) #} diff --git a/templates/reflections/note_detail.html b/templates/reflections/note_detail.html index 659c466..3ff1eb6 100644 --- a/templates/reflections/note_detail.html +++ b/templates/reflections/note_detail.html @@ -80,7 +80,7 @@

{{ note.title }}

{{ q.title }}
- {{ answers|get_item:q.id|render_markdown }} + {{ answers|get_item:q.id|md }}
{% endfor %} From 08d5f4609f24e5389818b2d004a4c7d3f75c7357 Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 10 Feb 2026 19:23:02 +0900 Subject: [PATCH 285/380] =?UTF-8?q?fix:=20=EB=B2=84=ED=8A=BC=EC=97=90=20?= =?UTF-8?q?=EC=BB=A4=EC=84=9C=20=EC=98=AC=EB=A6=B4=20=EB=95=8C=20=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=20=EC=95=88=20=EB=B0=94=EB=80=8C=EB=8A=94=20?= =?UTF-8?q?=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/reflections.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/css/reflections.css b/static/css/reflections.css index d8056de..23d0f18 100644 --- a/static/css/reflections.css +++ b/static/css/reflections.css @@ -36,7 +36,9 @@ body { font-family: inherit; /* 이미 있으면 생략 */ } - +button { + cursor: pointer; +} /* ========================= Page ========================= */ From 32f2568a2fa5f883fb3688db2dd94697c2832e0d Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 10 Feb 2026 19:27:36 +0900 Subject: [PATCH 286/380] =?UTF-8?q?fix:=20=EB=8F=8C=EC=95=84=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정으로 들어오면 -> detail로 작성으로 들어오면 -> list로 --- templates/reflections/_note_form.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/reflections/_note_form.html b/templates/reflections/_note_form.html index e34495a..c12c0f3 100644 --- a/templates/reflections/_note_form.html +++ b/templates/reflections/_note_form.html @@ -176,7 +176,11 @@ {# 하단 버튼 #}
- 돌아가기 + {% if note and note.id %} + 돌아가기 + {% else %} + 돌아가기 + {% endif %}
From ade7535e38ca4e552354f2da79e0f3a5a233a9c1 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 19:32:38 +0900 Subject: [PATCH 287/380] =?UTF-8?q?fix=20:=20main=20=ED=9A=8C=EA=B3=A0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=B6=84=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/views.py | 4 + static/css/main.css | 16 +++- templates/main.html | 216 +++++++++++++++++++++++++++++++++----------- 3 files changed, 178 insertions(+), 58 deletions(-) diff --git a/config/views.py b/config/views.py index 5519971..2d1bc1e 100644 --- a/config/views.py +++ b/config/views.py @@ -26,7 +26,11 @@ def main_view(request): # 로그인 상태만 추가 데이터 조회 if user.is_authenticated: """회고 부분""" + recent_reflections = Retrospective.objects.filter( + user=user + ).order_by('-created_at')[:4] + context["recent_reflections"] = recent_reflections """팀 매칭 부분""" season = Season.get_active_season() is_matching_period = season and season.is_matching_period() if season else False diff --git a/static/css/main.css b/static/css/main.css index 4a33151..956c5d0 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -59,22 +59,30 @@ body { border-radius: 20px; } -.main-note .main-note-content .login_content { +.reflection_list { + display: flex; + flex-direction: column; + gap: 15px; + overflow-x: auto; + padding-bottom: 10px; +} + +.login_content { display: flex; align-items: center; gap: 20px; } -.main-note .main-note-content .login_content .m_title { +.login_content .m_title { width: 20%; font-weight: 600; } -.main-note .main-note-content .login_content .m_content { +.login_content .m_content { width: 70%; } -.main-note .main-note-content .login_content .m_date { +.login_content .m_date { width: 10%; font-size: 16px; color: #999999; diff --git a/templates/main.html b/templates/main.html index c5b721a..fd49980 100644 --- a/templates/main.html +++ b/templates/main.html @@ -11,20 +11,35 @@

오늘의 작업을 기록해보세요.

{% if user.is_authenticated %} - + {% if recent_reflections %} +
+ {% for reflection in recent_reflections %} + + {% endfor %} +
+ {% else %} +

작성된 기록이 없습니다.

+ {% endif %} {% else %} -

로그인 후 이용해보세요.

+

로그인 후 이용해보세요.

{% endif %}
{% if user.is_authenticated %} - {% else %} {% endif %} + {% else %} + {% endif %}
@@ -40,30 +55,60 @@

팀 매칭 모집이 시작됐어요

WEB 기획

- {% if user.is_authenticated %} - {% with pm_level=role_levels.PM %} + {% if user.is_authenticated %} + {% with pm_level=role_levels.PM %} {% if pm_level == 1 %} - Level1 -

Lv1

- + Level1 +

Lv1

+ {% elif pm_level == 2 %} - Level2 -

Lv2

- + Level2 +

Lv2

+ {% elif pm_level == 3 %} - Level3 -

Lv3

- + Level3 +

Lv3

+ {% elif pm_level == 4 %} - Level4 -

Lv4

- + Level4 +

Lv4

+ {% else %} nolevel

아직 레벨 진단이 완료되지 않았어요!

- - {% endif %} - {% endwith %} + + {% endif %} + {% endwith %} {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -78,30 +123,60 @@

WEB 기획

WEB 프론트엔드

- {% if user.is_authenticated %} - {% with fe_level=role_levels.FRONTEND %} + {% if user.is_authenticated %} + {% with fe_level=role_levels.FRONTEND%} {% if fe_level == 1 %} Level1

Lv1

- + {% elif fe_level == 2 %} - Level2 -

Lv2

- + Level2 +

Lv2

+ {% elif fe_level == 3 %} - Level3 -

Lv3

- + Level3 +

Lv3

+ {% elif fe_level == 4 %} - Level4 -

Lv4

- + Level4 +

Lv4

+ {% else %} nolevel

아직 레벨 진단이 완료되지 않았어요!

- - {% endif %} - {% endwith %} + + {% endif %} + {% endwith %} {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -116,30 +191,60 @@

WEB 프론트엔드

WEB 백엔드

- {% if user.is_authenticated %} - {% with be_level=role_levels.BACKEND %} + {% if user.is_authenticated %} + {% with be_level=role_levels.BACKEND%} {% if be_level == 1 %} Level1

Lv1

- + {% elif be_level == 2 %} - Level2 -

Lv2

- + Level2 +

Lv2

+ {% elif be_level == 3 %} - Level3 -

Lv3

- + Level3 +

Lv3

+ {% elif be_level == 4 %} Level4

Lv4

- + {% else %} nolevel

아직 레벨 진단이 완료되지 않았어요!

- - {% endif %} - {% endwith %} + + {% endif %} + {% endwith %} {% else %} nolevel

로그인 후 레벨을 확인해보세요.

@@ -184,7 +289,10 @@

KITUP 프로젝트

{% for project in archived_projects %}
{% if project.project_image %} - {{ project.title }} + {{ project.title }} {% else %} 진행된 KITUP 프로젝트가 없습니다. {% endif %}
-{% endblock %} \ No newline at end of file +{% endblock %} From d5552dcc299e302ffac6410b1dd46e2c8c39d2b9 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 20:07:41 +0900 Subject: [PATCH 288/380] =?UTF-8?q?main=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/main.css | 307 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/static/css/main.css b/static/css/main.css index 956c5d0..30ef296 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -454,3 +454,310 @@ footer { width: 100%; height: 150px; } + +/* ======================================== + 반응형 미디어 쿼리 +======================================== */ + +/* 데스크탑 (1200px 이상) - 기본 스타일 유지 */ +@media (min-width: 1200px) { + .main-container, + .main-note, + .main-project { + width: 85%; + } +} + +/* 노트북 (768px ~ 1199px) */ +@media (max-width: 1199px) { + .main-container, + .main-note, + .main-project { + width: 90%; + } + + .main-note .main-note-title h3 { + font-size: 22px; + } + + .main-team h3 { + font-size: 30px; + } + + .main-team .m_team_stack .m_design h3, + .main-team .m_team_stack .m_frontend h3, + .main-team .m_team_stack .m_backend h3 { + font-size: 22px; + } + + .m_design_btn, + .m_front_btn, + .m_back_btn { + width: 80%; + font-size: 16px; + } + + .m_note_btn { + width: 25%; + } + + .m_team_result { + width: 22%; + } +} + +/* 태블릿 (481px ~ 767px) */ +@media (max-width: 767px) { + .main-container, + .main-note, + .main-project { + width: 92%; + } + + .main-note .main-note-title img, + .main-project .m_project_title img { + width: 10%; + } + + .main-note .main-note-title h3, + .main-project .m_project_title h3 { + font-size: 20px; + } + + .main-note .main-note-content { + padding: 25px 30px; + } + + .login_content { + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 15px 0; + border-bottom: 1px solid #e0e0e0; + } + + .login_content .m_title, + .login_content .m_content, + .login_content .m_date { + width: 100%; + } + + .login_content .m_date { + font-size: 14px; + } + + .m_note_btn { + width: 40%; + font-size: 15px; + } + + .main-team { + padding: 70px 0; + } + + .main-team h3 { + font-size: 24px; + padding: 0 20px; + } + + .main-team p { + padding: 0 20px; + font-size: 15px; + } + + .main-team .m_team_stack { + flex-direction: column; + width: 85%; + gap: 25px; + } + + .main-team .m_team_stack .m_design, + .main-team .m_team_stack .m_frontend, + .main-team .m_team_stack .m_backend { + width: 100%; + padding: 30px 20px; + } + + .main-team .m_team_stack .m_design h3, + .main-team .m_team_stack .m_frontend h3, + .main-team .m_team_stack .m_backend h3 { + font-size: 22px; + } + + .m_design_btn, + .m_front_btn, + .m_back_btn { + width: 80%; + max-width: 280px; + } + + .m_team_result { + width: 45%; + } + + .team_waiting > form { + width: 85%; + } + + .main-project .m_project_content .m_project > div { + flex: 0 0 260px; + height: 280px; + } +} + +/* 모바일 (480px 이하) */ +@media (max-width: 480px) { + .main-container, + .main-note, + .main-project { + width: 95%; + } + + .main-container img { + margin-top: 20px; + } + + .main-note { + margin-top: 60px; + } + + .main-note .main-note-title img, + .main-project .m_project_title img { + width: 12%; + } + + .main-note .main-note-title h3, + .main-project .m_project_title h3 { + font-size: 18px; + } + + .main-note .main-note-content { + padding: 20px 20px; + margin-top: 20px; + } + + .login_content .m_title { + font-size: 15px; + } + + .login_content .m_content { + font-size: 14px; + } + + .main-note .main-note-content .unlogin_content { + font-size: 16px; + } + + .m_note_btn { + width: 50%; + font-size: 14px; + padding: 8px 20px; + } + + .main-team { + margin-top: 60px; + padding: 50px 0; + } + + .main-team h3 { + font-size: 20px; + padding: 0 15px; + margin-bottom: 15px; + } + + .main-team p { + padding: 0 15px; + font-size: 14px; + } + + .main-team .m_team_stack { + width: 90%; + gap: 20px; + margin-top: 30px; + } + + .main-team .m_team_stack .m_design, + .main-team .m_team_stack .m_frontend, + .main-team .m_team_stack .m_backend { + padding: 25px 15px; + border-radius: 30px; + } + + .main-team .m_team_stack .m_design h3, + .main-team .m_team_stack .m_frontend h3, + .main-team .m_team_stack .m_backend h3 { + font-size: 19px; + } + + .main-team .m_team_stack .m_nolevel { + font-size: 14px; + } + + .m_design_btn, + .m_front_btn, + .m_back_btn { + width: 90%; + max-width: 250px; + font-size: 16px; + height: 42px; + } + + .m_team_result { + width: 60%; + font-size: 16px; + } + + .m_team_result p { + font-size: 16px; + } + + .team_waiting > h3 { + font-size: 20px; + padding: 0 15px; + } + + .team_waiting > form { + width: 90%; + } + + .cancel_button { + width: 130px; + font-size: 14px; + } + + .main-project { + margin-bottom: 30px; + } + + .main-project .m_project_title { + margin-top: 60px; + } + + .main-project .m_project_content { + margin-top: 20px; + } + + .main-project .m_project_content .m_noproject { + font-size: 16px; + padding: 25px 20px; + } + + .main-project .m_project_content .m_project > div { + flex: 0 0 240px; + height: 260px; + padding: 15px; + } + + .main-project .m_project_content .m_project div h3 { + font-size: 19px; + } + + .main-project .m_project_content .m_project div p { + font-size: 14px; + } + + footer { + margin-top: 100px; + height: 120px; + } +} \ No newline at end of file From 865f385074befd757cca7e1ed556e7a9d98c3496 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 20:12:33 +0900 Subject: [PATCH 289/380] =?UTF-8?q?fix=20:=20dashboard=5Fupdate=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A7=81=ED=81=AC=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(=EC=A7=84=EC=B2=99=EB=8F=84=20=EB=AF=B8?= =?UTF-8?q?=EC=99=84=EB=A3=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/projects/dashboard.html | 25 ++++++----------------- templates/projects/dashboard_update.html | 26 +++++++----------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index bbb20df..fd5fe9e 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -34,17 +34,13 @@

{{ project.title }}

{% endif %}

-
-
- 북마크이미지 -

즐겨찾기

+
-
progress diff --git a/templates/projects/dashboard_update.html b/templates/projects/dashboard_update.html index 19c2252..c3a4e90 100644 --- a/templates/projects/dashboard_update.html +++ b/templates/projects/dashboard_update.html @@ -34,17 +34,16 @@ {% endif %}

- -
-
- 북마크이미지 -

즐겨찾기

+ +
- - -
From 804d2c44dcc1598855f92c3f9fdd1d1ac93b6dd2 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 20:25:09 +0900 Subject: [PATCH 290/380] =?UTF-8?q?fix=20:=20team=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team.css | 363 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 314 insertions(+), 49 deletions(-) diff --git a/static/css/team.css b/static/css/team.css index 37b5b66..7ef92c5 100644 --- a/static/css/team.css +++ b/static/css/team.css @@ -2,56 +2,9 @@ body { background: #F6F8FF; } -/* 매칭 성공 */ -.team_matching h3 { - font-size: 27px; - font-weight: 600; - margin-bottom: 20px; -} - -.team_matching form { - display: flex; - justify-content: space-between; - align-items: center; - margin: 30px auto 0; - width: 60%; height: 40px; - background: #fff; - border-radius: 25px; - padding: 10px 5px 10px 20px; -} - -.team_matching form:focus-within { - border: 1px solid #4272EF; - box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); -} - -.team_matching form input { - width: 80%; - border: none; - font-size: 15px; - color: #888888; - outline: none; -} - -.team_matching form input::placeholder { - color: #888888; -} - -.team_matching form button { - font-size: 15px; - background: #4272EF; color: #fff; - border: none; border-radius: 20px; - padding: 7px 15px; -} - -.team_matching form button:hover { - background: #1F4CC0; - transition: 0.3s ease; -} - /* 매칭 성공 */ .team_success { - margin-top: 50px; + margin-top: 15%; text-align: center; align-items: center; } @@ -61,7 +14,6 @@ body { } .team_member { - margin-top: 30px; display: flex; justify-content: center; gap: 20px; } @@ -193,4 +145,317 @@ body { .team_fail a:hover { background: #4272EF; transition: 0.3s ease; +} + +/* ======================================== + 반응형 미디어 쿼리 +======================================== */ + +/* 데스크탑 (1200px 이상) - 기본 스타일 유지 */ +@media (min-width: 1200px) { + .team_member { + flex-wrap: wrap; + } + + .t_member { + width: 16%; + } +} + +/* 노트북 (768px ~ 1199px) */ +@media (max-width: 1199px) { + .team_success { + margin-top: 25%; + } + + .team_success h3 { + font-size: 23px; + } + + .team_member { + flex-wrap: wrap; + gap: 18px; + width: 90%; + margin-left: auto; + margin-right: auto; + } + + .t_member { + width: 22%; + height: 240px; + } + + .profile_section { + height: 100px; + } + + .t_member > .profile_section > .profile_img { + width: 100px; + height: 100px; + } + + .t_member > .profile_section > .level_img { + width: 35px; + height: 35px; + right: calc(50% - 55px); + } + + .t_member .u_name { + font-size: 15px; + } + + .t_member .u_design, + .t_member .u_backend { + width: 50%; + font-size: 15px; + } + + .t_member .u_frontend { + width: 65%; + font-size: 15px; + } + + .go_project { + width: 25%; + } + + .team_fail { + margin-top: 25%; + } + + .team_fail h3 { + font-size: 27px; + } + + .team_fail a { + width: 25%; + } + + .team_matching form { + width: 70%; + } +} + +/* 태블릿 (481px ~ 767px) */ +@media (max-width: 767px) { + .team_success { + margin-top: 30%; + } + + .team_success h3 { + font-size: 20px; + padding: 0 20px; + } + + .team_member { + flex-direction: column; + align-items: center; + width: 85%; + gap: 20px; + margin-top: 25px; + } + + .t_member { + width: 70%; + height: 250px; + padding: 25px 20px; + } + + .profile_section { + height: 110px; + } + + .t_member > .profile_section > .profile_img { + width: 110px; + height: 110px; + } + + .t_member > .profile_section > .level_img { + width: 40px; + height: 40px; + right: calc(50% - 60px); + } + + .t_member .u_name { + font-size: 17px; + margin: 15px 0; + } + + .t_member .u_design { + width: 35%; + font-size: 16px; + } + + .t_member .u_frontend { + width: 45%; + font-size: 16px; + } + + .t_member .u_backend { + width: 35%; + font-size: 16px; + } + + .go_project { + width: 45%; + margin-top: 40px; + } + + .go_project p { + font-size: 16px; + } + + .team_fail { + margin-top: 30%; + } + + .team_fail h3 { + font-size: 24px; + padding: 0 20px; + } + + .team_fail a { + width: 45%; + } + + .team_fail a p { + font-size: 16px; + } + + .team_matching h3 { + font-size: 24px; + padding: 0 20px; + } + + .team_matching form { + width: 80%; + } + + .team_matching form input { + font-size: 14px; + } + + .team_matching form button { + font-size: 14px; + } +} + +/* 모바일 (480px 이하) */ +@media (max-width: 480px) { + .team_success { + margin-top: 45%; + } + + .team_success h3 { + font-size: 18px; + padding: 0 15px; + line-height: 1.4; + } + + .team_member { + width: 90%; + gap: 15px; + margin-top: 25px; + } + + .t_member { + width: 85%; + height: 240px; + padding: 20px 15px; + } + + .profile_section { + height: 100px; + margin-bottom: 10px; + } + + .t_member > .profile_section > .profile_img { + width: 100px; + height: 100px; + } + + .t_member > .profile_section > .level_img { + width: 35px; + height: 35px; + right: calc(50% - 55px); + } + + .t_member .u_name { + font-size: 16px; + margin: 12px 0; + } + + .t_member .u_name > span { + font-size: 13px; + } + + .t_member .u_design { + width: 40%; + font-size: 15px; + padding: 4px 8px; + } + + .t_member .u_frontend { + width: 50%; + font-size: 15px; + padding: 4px 8px; + } + + .t_member .u_backend { + width: 40%; + font-size: 15px; + padding: 4px 8px; + } + + .go_project { + width: 60%; + height: 38px; + margin-top: 35px; + padding: 8px 10px; + } + + .go_project p { + font-size: 15px; + } + + .team_fail { + margin-top: 45%; + } + + .team_fail h3 { + font-size: 20px; + padding: 0 15px; + line-height: 1.4; + } + + .team_fail a { + width: 60%; + height: 38px; + margin-top: 40px; + padding: 8px 10px; + } + + .team_fail a p { + font-size: 15px; + } + + .team_matching h3 { + font-size: 20px; + padding: 0 15px; + } + + .team_matching form { + width: 90%; + height: 38px; + padding: 8px 5px 8px 15px; + } + + .team_matching form input { + font-size: 13px; + } + + .team_matching form button { + font-size: 13px; + padding: 6px 12px; + } } \ No newline at end of file From bdd1b1d25cd8a692a33538743e5496d5bacf8dff Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 20:45:09 +0900 Subject: [PATCH 291/380] =?UTF-8?q?fix=20:=20team=5Fapply=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/team_apply.css | 339 +++++++++++++++++++++++++++----- templates/teams/team_apply.html | 6 +- 2 files changed, 298 insertions(+), 47 deletions(-) diff --git a/static/css/team_apply.css b/static/css/team_apply.css index c7762ce..3894c0b 100644 --- a/static/css/team_apply.css +++ b/static/css/team_apply.css @@ -8,7 +8,7 @@ button:hover { /* 팀 매칭 기간 X */ .match_wait { - margin-top: 120px; + margin-top: 15%; text-align: center; align-items: center; } @@ -19,49 +19,13 @@ button:hover { margin-bottom: 20px; } -.match_wait form { - display: flex; - justify-content: space-between; - align-items: center; +.match_wait .noti { margin: 30px auto 0; - width: 60%; height: 40px; - background: #fff; - border-radius: 25px; - padding: 10px 5px 10px 20px; -} - -.match_wait form:focus-within { - border: 1px solid #4272EF; - box-shadow: 0 2px 15px rgba(66, 114, 239, 0.2); -} - -.match_wait form input { - width: 80%; - border: none; - font-size: 15px; - color: #888888; - outline: none; -} - -.match_wait form input::placeholder { - color: #888888; -} - -.match_wait form button { - font-size: 15px; - background: #4272EF; color: #fff; - border: none; border-radius: 20px; - padding: 7px 15px; -} - -.match_wait form button:hover { - background: #1F4CC0; - transition: 0.3s ease; } /* 매칭 대기 */ .team_waiting { - margin-top: 200px; + margin-top: 15%; text-align: center; align-items: center; } @@ -80,10 +44,6 @@ button:hover { gap: 50px; } -.matching_actions > form { - -} - .noti_button { width: 150px; height: 40px; @@ -130,7 +90,7 @@ button:hover { /* 팀 매칭 기간 O / 매칭 신청 X */ .team_matching { - margin-top: 100px; + margin-top: 10%; text-align: center; } @@ -285,3 +245,294 @@ button:hover { background: #C03067; transition: 0.5s ease-in-out; } + +/* ======================================== + 반응형 미디어 쿼리 +======================================== */ + +/* 데스크탑 (1200px 이상) - 기본 스타일 유지 */ +@media (min-width: 1200px) { + .t_stack { + width: 90%; + } + + .t_stack .t_design, + .t_stack .t_frontend, + .t_stack .t_backend { + width: 27%; + } +} + +/* 노트북 (768px ~ 1199px) */ +@media (max-width: 1199px) { + .match_wait { + margin-top: 18%; + } + + .match_wait h3 { + font-size: 25px; + } + + .team_waiting { + margin-top: 18%; + } + + .team_waiting > h3 { + font-size: 25px; + } + + .matching_actions { + gap: 40px; + } + + .noti_button, + .cancel_button { + width: 140px; + font-size: 14px; + } + + .team_matching { + margin-top: 12%; + } + + .team_matching h3 { + font-size: 27px; + } + + .t_stack { + width: 92%; + gap: 30px; + } + + .t_stack .t_design, + .t_stack .t_frontend, + .t_stack .t_backend { + width: 30%; + padding: 18px 0; + } + + .t_stack .t_design h3, + .t_stack .t_frontend h3, + .t_stack .t_backend h3 { + font-size: 19px; + } + + .t_stack .t_design img, + .t_stack .t_frontend img, + .t_stack .t_backend img { + width: 38px; + height: 38px; + } + + .t_stack .t_nolevel, + .t_stack .t_frontend .t_level, + .t_stack .t_backend .t_level { + font-size: 13px; + } + + .t_stack .t_design .t_design_btn, + .t_stack .t_frontend .t_front_btn, + .t_stack .t_backend .t_back_btn { + width: 75%; + font-size: 14px; + height: 32px; + } +} + +/* 태블릿 (481px ~ 767px) */ +@media (max-width: 767px) { + .match_wait { + margin-top: 25%; + } + + .match_wait h3 { + font-size: 22px; + padding: 0 20px; + line-height: 1.4; + } + + .team_waiting { + margin-top: 25%; + } + + .team_waiting > h3 { + font-size: 22px; + padding: 0 20px; + line-height: 1.4; + } + + .matching_actions { + flex-direction: column; + align-items: center; + gap: 20px; + } + + .noti_button, + .cancel_button { + width: 200px; + height: 42px; + font-size: 15px; + } + + .team_matching { + margin-top: 15%; + } + + .team_matching h3 { + font-size: 24px; + padding: 0 20px; + } + + .team_matching p { + padding: 0 20px; + font-size: 14px; + } + + .t_stack { + flex-direction: column; + align-items: center; + width: 85%; + gap: 25px; + margin-top: 35px; + } + + .t_stack .t_design, + .t_stack .t_frontend, + .t_stack .t_backend { + width: 75%; + padding: 25px 20px; + } + + .t_stack .t_design h3, + .t_stack .t_frontend h3, + .t_stack .t_backend h3 { + font-size: 20px; + margin-bottom: 10px; + } + + .t_stack .t_design img, + .t_stack .t_frontend img, + .t_stack .t_backend img { + width: 45px; + height: 45px; + } + + .t_stack .t_nolevel, + .t_stack .t_frontend .t_level, + .t_stack .t_backend .t_level { + font-size: 15px; + margin-top: 8px; + } + + .t_stack .t_design .t_design_btn, + .t_stack .t_frontend .t_front_btn, + .t_stack .t_backend .t_back_btn { + width: 75%; + font-size: 16px; + height: 38px; + margin-top: 18px; + } +} + +/* 모바일 (480px 이하) */ +@media (max-width: 480px) { + .match_wait { + margin-top: 35%; + } + + .match_wait h3 { + font-size: 19px; + padding: 0 15px; + line-height: 1.5; + } + + .match_wait .noti { + margin-top: 25px; + } + + .team_waiting { + margin-top: 35%; + } + + .team_waiting > h3 { + font-size: 19px; + padding: 0 15px; + line-height: 1.5; + } + + .matching_actions { + flex-direction: column; + align-items: center; + gap: 15px; + margin-top: 25px; + } + + .noti_button, + .cancel_button { + width: 180px; + height: 40px; + font-size: 14px; + } + + .team_matching { + margin-top: 20%; + } + + .team_matching h3 { + font-size: 20px; + padding: 0 15px; + line-height: 1.4; + } + + .team_matching p { + padding: 0 15px; + font-size: 13px; + line-height: 1.5; + } + + .t_stack { + flex-direction: column; + align-items: center; + width: 90%; + gap: 20px; + margin-top: 30px; + } + + .t_stack .t_design, + .t_stack .t_frontend, + .t_stack .t_backend { + width: 85%; + padding: 22px 15px; + border-radius: 30px; + } + + .t_stack .t_design h3, + .t_stack .t_frontend h3, + .t_stack .t_backend h3 { + font-size: 18px; + margin-bottom: 8px; + } + + .t_stack .t_design img, + .t_stack .t_frontend img, + .t_stack .t_backend img { + width: 40px; + height: 40px; + } + + .t_stack .t_nolevel, + .t_stack .t_frontend .t_level, + .t_stack .t_backend .t_level { + font-size: 13px; + margin-top: 6px; + } + + .t_stack .t_design .t_design_btn, + .t_stack .t_frontend .t_front_btn, + .t_stack .t_backend .t_back_btn { + width: 85%; + font-size: 14px; + height: 36px; + margin-top: 15px; + } +} \ No newline at end of file diff --git a/templates/teams/team_apply.html b/templates/teams/team_apply.html index 3ccd8fa..48143bc 100644 --- a/templates/teams/team_apply.html +++ b/templates/teams/team_apply.html @@ -9,9 +9,9 @@

지금은 팀 매칭 기간이 아니예요.
팀 매칭 기간이 되면 메일로 알려드릴게요.

-
- - + + {% csrf_token %} +
From d7d60b01fdf905a703e980089e7ddccab1be59fd Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 20:55:52 +0900 Subject: [PATCH 292/380] =?UTF-8?q?mission=20=EB=B0=98=EC=9D=91=ED=98=95?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/mission.css | 411 ++++++++++++++++++++++++++++++++++ templates/guides/mission.html | 4 + 2 files changed, 415 insertions(+) diff --git a/static/css/mission.css b/static/css/mission.css index 3230681..8a826c7 100644 --- a/static/css/mission.css +++ b/static/css/mission.css @@ -318,4 +318,415 @@ margin-top: 4px; color: #6B7280; font-size: 13px; +} + +.m_footer { + width: 90%; + margin: 10% auto; + text-align: center; + font-size: 25px; font-weight: 550; +} + +.m_footer > span { + font-weight: 600; + color: #4272EF; +} + +/* ======================================== + 반응형 미디어 쿼리 +======================================== */ + +/* 데스크탑 (1200px 이상) - 기본 스타일 유지 */ +@media (min-width: 1200px) { + .p_header { + width: 90%; + } + + .mission_progress { + width: 90%; + } + + .mission_item { + width: 90%; + } +} + +/* 노트북 (768px ~ 1199px) */ +@media (max-width: 1199px) { + .p_header { + width: 92%; + } + + .p_header a p { + font-size: 17px; + } + + .mission_progress { + width: 92%; + height: auto; + padding: 40px 20px; + } + + .mp_title > img { + width: 90px; + height: 90px; + } + + .mp_title > div > h3 { + font-size: 23px; + } + + .mp_title > div > p { + font-size: 13px; + } + + .role_progress_bar { + margin: 18px 0 12px 90px; + } + + .role_progress_bar > h3 { + font-size: 19px; + } + + .progress_bar_container { + height: 14px; + } + + .progress_percent { + font-size: 13px; + } + + .mission_item { + width: 92%; + } + + .timeline_dot { + width: 70px; + } + + .circle { + width: 28px; + height: 28px; + font-size: 15px; + } + + .mission_card { + padding: 18px 22px; + } + + .card_header h3 { + font-size: 17px; + } + + .check_icon { + width: 26px; + height: 26px; + } + + .mission_description { + font-size: 13px; + } + + .m_footer { + margin: 8% auto; + font-size: 23px; + } +} + +/* 태블릿 (481px ~ 767px) */ +@media (max-width: 767px) { + .p_header { + width: 95%; + height: 45px; + } + + .p_header a { + padding: 12px; + } + + .p_header a p { + font-size: 16px; + } + + .mission_progress { + width: 95%; + padding: 35px 15px; + margin-bottom: 60px; + } + + .mp_title { + flex-direction: column; + align-items: flex-start; + } + + .mp_title > img { + width: 70px; + height: 70px; + margin-bottom: 15px; + } + + .mp_title > div > h3 { + font-size: 20px; + margin-bottom: 10px; + } + + .mp_title > div > p { + font-size: 12px; + } + + .role_progress_bar { + margin: 15px 0 10px 0; + gap: 10px; + } + + .role_progress_bar > h3 { + width: 25px; + font-size: 17px; + } + + .progress_bar_container { + height: 12px; + } + + .progress_percent { + min-width: 40px; + font-size: 12px; + } + + .mission_item { + width: 95%; + gap: 15px; + } + + .timeline_dot { + width: 60px; + } + + .circle { + width: 26px; + height: 26px; + font-size: 14px; + } + + .line { + width: 2px; + } + + .mission_card { + padding: 16px 18px; + margin-bottom: 20px; + border-radius: 15px; + } + + .card_header h3 { + font-size: 16px; + } + + .check_icon { + width: 24px; + height: 24px; + } + + .mission_description { + font-size: 13px; + } + + .mission_description li { + margin-bottom: 10px; + } + + .mission_description em { + font-size: 12px; + } + + .mission_card.active .card_content { + max-height: 600px; + } + + .m_footer { + margin: 15% auto; + font-size: 19px; + padding: 0 20px; + line-height: 1.5; + } +} + +/* 모바일 (480px 이하) */ +@media (max-width: 480px) { + .p_header { + width: 100%; + height: 40px; + } + + .p_header a { + padding: 10px; + } + + .p_header a p { + font-size: 14px; + } + + .mission_progress { + width: 95%; + padding: 25px 12px; + margin-bottom: 50px; + } + + .mp_title { + flex-direction: column; + align-items: flex-start; + } + + .mp_title > img { + width: 60px; + height: 60px; + margin-bottom: 12px; + } + + .mp_title > div > h3 { + font-size: 18px; + margin-bottom: 8px; + } + + .mp_title > div > p { + font-size: 11px; + line-height: 1.4; + } + + .role_progress_bar { + margin: 12px 0 8px 0; + gap: 8px; + } + + .role_progress_bar > h3 { + width: 23px; + font-size: 15px; + } + + .progress_bar_container { + height: 10px; + } + + .progress_percent { + min-width: 38px; + font-size: 11px; + } + + .mission_item { + width: 95%; + gap: 12px; + } + + .timeline_dot { + width: 50px; + } + + .circle { + width: 24px; + height: 24px; + font-size: 13px; + } + + .line { + width: 2px; + margin-top: -8px; + } + + .mission_card { + padding: 14px 15px; + margin-bottom: 18px; + border-radius: 12px; + } + + .mission_card:hover { + transform: none; + } + + .mission_card.active { + transform: none; + } + + .card_header { + align-items: flex-start; + } + + .card_header h3 { + font-size: 15px; + line-height: 1.4; + padding-right: 5px; + } + + .check_icon { + width: 22px; + height: 22px; + flex-shrink: 0; + } + + .mission_card.active .card_content { + max-height: 700px; + margin-top: 12px; + } + + .mission_description { + font-size: 12px; + } + + .mission_description p { + margin: 6px 0; + } + + .mission_description ul, + .mission_description ol { + margin: 8px 0 8px 15px; + } + + .mission_description li { + margin-bottom: 8px; + line-height: 1.5; + } + + .mission_description li em { + font-size: 11px; + } + + .mission_description code { + font-size: 11px; + padding: 1px 4px; + } + + .m_footer { + margin: 20% auto; + font-size: 16px; + padding: 0 15px; + line-height: 1.6; + } + + .no_project { + margin-top: 40%; + text-align: center; + } + + .no_project h3 { + font-size: 18px; + padding: 0 15px; + } +} + +@media (max-width: 767px) { + .mission_card { + -webkit-tap-highlight-color: transparent; + } + + .check_icon { + padding: 5px; + margin: -5px; + } + + .mission_card:hover { + box-shadow: 0 2px 8px rgba(66, 114, 239, 0.08); + } + + .mission_card:active { + box-shadow: 0 4px 12px rgba(66, 114, 239, 0.15); + } } \ No newline at end of file diff --git a/templates/guides/mission.html b/templates/guides/mission.html index 0159499..b974f28 100644 --- a/templates/guides/mission.html +++ b/templates/guides/mission.html @@ -85,6 +85,10 @@

{{ mission.card.title }}

{% endif %} + + \ No newline at end of file From 2430aa44d4b0b0839fd0f57e8305d1b26992105a Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 21:29:04 +0900 Subject: [PATCH 294/380] =?UTF-8?q?refactor:=20dashboard=20form=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/forms.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/projects/forms.py b/apps/projects/forms.py index 34529ac..dc16cf3 100644 --- a/apps/projects/forms.py +++ b/apps/projects/forms.py @@ -16,7 +16,6 @@ class Meta: "project_image", # 프로젝트 프로필 사진 "team_rules", # 팀 규칙 "related_links", # 관련 링크 - "is_favorite", # 즐겨찾기 ] widgets = { "title": forms.TextInput(attrs={ @@ -40,12 +39,9 @@ class Meta: }), "related_links": forms.Textarea(attrs={ "class": "form-control", - "placeholder": "관련 링크를 마크다운 형식으로 작성해주세요\n\n예:\n[Notion](https://notion.so/...)\n[Figma](https://figma.com/...)\n[GitHub](https://github.com/...)", + "placeholder": "관련 링크를 줄바꿈으로 구분하여 입력해주세요\n\n예:\nhttps://notion.so/...\nhttps://figma.com/...\nhttps://github.com/...", "rows": 6, }), - "is_favorite": forms.CheckboxInput(attrs={ - "class": "form-check-input", - }), } def clean_title(self): From 7df8b20c7ae6cff4e9430cddd3f4b9ff2f8f8357 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 21:29:22 +0900 Subject: [PATCH 295/380] =?UTF-8?q?feat:=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/templatetags/__init__.py | 0 apps/projects/templatetags/markdown_tags.py | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 apps/projects/templatetags/__init__.py create mode 100644 apps/projects/templatetags/markdown_tags.py diff --git a/apps/projects/templatetags/__init__.py b/apps/projects/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/projects/templatetags/markdown_tags.py b/apps/projects/templatetags/markdown_tags.py new file mode 100644 index 0000000..7d4bdaf --- /dev/null +++ b/apps/projects/templatetags/markdown_tags.py @@ -0,0 +1,12 @@ +from django import template +import markdown as md + +register = template.Library() + + +@register.filter +def markdown(text): + """마크다운을 HTML로 변환""" + if not text: + return "" + return md.markdown(text) From 3b2fab8c38c5ae970b531c41df429ad49bdb5c5e Mon Sep 17 00:00:00 2001 From: plumbestie Date: Tue, 10 Feb 2026 21:35:08 +0900 Subject: [PATCH 296/380] =?UTF-8?q?fix=20:=20dashboard=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 502 +++++++++++++++++++++++++++++++- static/css/dashboard_update.css | 454 ++++++++++++++++++++++++++++- 2 files changed, 954 insertions(+), 2 deletions(-) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index c0d0780..05f8dab 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -541,4 +541,504 @@ textarea { background: #4272EF; color: white; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; font-size: 15px; } -.btn-secondary { background: #f0f0f0; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; } \ No newline at end of file +.btn-secondary { background: #f0f0f0; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; } + +/* =================================== + 반응형 미디어 쿼리 + =================================== */ + +/* 노트북 (1024px ~ 1400px) */ +@media (max-width: 1400px) { + .dashboard { + width: 90%; + } +} + +/* 태블릿 (768px ~ 1024px) */ +@media (max-width: 1024px) { + .dashboard { + width: 95%; + } + + .p_header a p { + font-size: 16px; + } + + .d_service > img { + max-width: 400px; + } + + .d_service > h3 { + font-size: 28px; + } + + .d_service > p { + font-size: 20px; + } + + .d_period > img { + width: 35px; + height: 35px; + } + + .d_period > p { + font-size: 16px; + } + + .d_period > p > .period_title { + font-size: 20px; + } + + .t_title > img, + .b_title > img, + .r_title > img, + .l_title > img { + width: 35px; + height: 35px; + } + + .t_title > h3, + .b_title > p, + .r_title > h3, + .l_title > h3 { + font-size: 20px; + } + + .p_title > img { + width: 45px; + height: 45px; + } + + .t_member > .member { + max-width: 180px; + height: 280px; + padding: 12px 20px; + } + + .member > .profile_section > .profile { + width: 90px; + height: 90px; + } + + .member > .profile_section > .level { + width: 32px; + height: 32px; + right: calc(50% - 50px); + } + + .member > h3 { + font-size: 18px; + } + + .member > .info { + font-size: 13px; + } + + .r_content > p { + font-size: 16px; + line-height: 20px; + } + + .l_content { + font-size: 16px; + } + + .l_content > p > a { + font-size: 16px; + line-height: 20px; + word-break: break-all; + } + + .modal-content { + width: 400px; + height: 65%; + } +} + +/* 모바일 (~ 768px) */ +@media (max-width: 768px) { + .dashboard { + width: 95%; + } + + .p_header { + width: 95%; + height: 45px; + } + + .p_header a { + padding: 12px; + } + + .p_header a p { + font-size: 14px; + } + + .no_project > h3 { + font-size: 28px; + margin: 100px auto 20px; + } + + .no_project > a { + width: 180px; + height: 35px; + } + + .no_project > a > p { + font-size: 16px; + line-height: 35px; + } + + .d_service { + margin: 40px auto 0; + } + + .d_service > img { + max-width: 300px; + } + + .d_service > h3 { + font-size: 24px; + margin-top: 15px; + } + + .d_service > p { + font-size: 16px; + margin-top: 12px; + padding: 0 10px; + } + + .d_period { + margin-top: 50px; + flex-direction: column; + align-items: flex-start; + } + + .d_period > img { + width: 30px; + height: 30px; + margin-bottom: 5px; + } + + .d_period > p { + font-size: 14px; + margin-left: 0; + } + + .d_period > p > .period_title { + font-size: 16px; + } + + .b_title > img { + width: 30px; + height: 30px; + } + + .b_title > p { + font-size: 18px; + } + + .b_content { + margin-left: 37px; + } + + hr { + margin-top: 35px; + } + + .d_team, + .d_rule, + .d_link, + .d_progress { + margin-top: 35px; + } + + .t_title > img, + .r_title > img, + .l_title > img { + width: 30px; + height: 30px; + } + + .t_title > h3, + .r_title > h3, + .l_title > h3 { + font-size: 18px; + } + + .p_title > img { + width: 40px; + height: 40px; + } + + .p_title > h3 { + font-size: 18px; + } + + .t_report > img { + width: 18px; + height: 18px; + } + + .t_report a { + font-size: 12px; + } + + .d_team > .t_content > p { + margin-left: 37px; + font-size: 16px; + } + + .d_team > .t_content > p > .t_role { + font-size: 17px; + } + + .t_member { + margin-top: 15px; + padding: 15px 0; + gap: 12px; + } + + .t_member > .member { + max-width: 160px; + height: 260px; + padding: 10px 18px; + } + + .member > .profile_section { + height: 80px; + margin-bottom: 12px; + } + + .member > .profile_section > .profile { + width: 80px; + height: 80px; + } + + .member > .profile_section > .level { + width: 28px; + height: 28px; + right: calc(50% - 45px); + } + + .member > h3 { + font-size: 16px; + margin-top: 12px; + margin-bottom: 12px; + } + + .member > .info { + font-size: 12px; + } + + .member > .role_design, + .member > .role_frontend, + .member > .role_backend { + font-size: 14px; + height: 28px; + padding: 4px 0; + bottom: 10px; + } + + .member > .role_design { + right: 40px; + } + + .member > .role_frontend { + right: 30px; + } + + .member > .role_backend { + right: 40px; + } + + .r_content { + margin-left: 37px; + } + + .r_content > p { + font-size: 14px; + line-height: 20px; + } + + .l_content { + margin-left: 37px; + font-size: 14px; + word-wrap: break-word; + overflow-wrap: break-word; + } + + .l_content > p { + word-break: break-all; + white-space: pre-wrap; + } + + .l_content > p > a { + font-size: 14px; + line-height: 20px; + word-break: break-all; + display: inline-block; + max-width: 100%; + } + + .p_content { + margin: 5px 0 80px 37px; + } + + .p_bar { + height: 18px; + } + + button { + width: 180px; + height: 38px; + font-size: 16px; + margin-bottom: 150px; + } + + .modal-content { + width: 90%; + max-width: 350px; + height: 60%; + padding: 25px; + } + + .modal-header h3 { + font-size: 18px; + } + + .member-select-list { + max-height: 150px; + } + + .member-option { + padding: 10px; + } + + .member-info img { + width: 30px; + height: 30px; + } + + textarea { + height: 45px; + font-size: 13px; + } +} + +/* 소형 모바일 (~ 480px) */ +@media (max-width: 480px) { + .p_header a p { + font-size: 13px; + } + + .d_service > img { + max-width: 250px; + } + + .d_service > h3 { + font-size: 20px; + } + + .d_service > p { + font-size: 14px; + padding: 0 5px; + } + + .d_period > p { + font-size: 13px; + } + + .d_period > p > .period_title { + font-size: 15px; + } + + .t_member > .member { + max-width: 140px; + height: 240px; + padding: 10px 15px; + } + + .member > .profile_section { + height: 70px; + } + + .member > .profile_section > .profile { + width: 70px; + height: 70px; + } + + .member > .profile_section > .level { + width: 25px; + height: 25px; + right: calc(50% - 40px); + } + + .member > h3 { + font-size: 15px; + } + + .member > .info { + font-size: 11px; + } + + .member > .role_design, + .member > .role_frontend, + .member > .role_backend { + font-size: 13px; + height: 26px; + } + + .r_content { + margin-left: 30px; + } + + .r_content > p { + font-size: 13px; + line-height: 18px; + } + + .l_content { + margin-left: 30px; + font-size: 13px; + padding-right: 5px; + } + + .l_content > p > a { + font-size: 13px; + line-height: 18px; + word-break: break-all; + overflow-wrap: anywhere; + } + + .p_content { + margin-left: 30px; + } + + button { + width: 160px; + height: 35px; + font-size: 15px; + } +} + + +@media (max-width: 360px) { + .l_content { + margin-left: 25px; + font-size: 12px; + } + + .l_content > p > a { + font-size: 12px; + line-height: 16px; + } + + .r_content { + margin-left: 25px; + } + + .r_content > p { + font-size: 12px; + } +} \ No newline at end of file diff --git a/static/css/dashboard_update.css b/static/css/dashboard_update.css index 446fcd4..3165b01 100644 --- a/static/css/dashboard_update.css +++ b/static/css/dashboard_update.css @@ -565,4 +565,456 @@ textarea { background: #4272EF; color: white; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; font-size: 15px; } -.btn-secondary { background: #f0f0f0; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; } \ No newline at end of file +.btn-secondary { background: #f0f0f0; border: none; padding: 5px 10px; border-radius: 8px; cursor: pointer; } + +/* =================================== + 반응형 미디어 쿼리 + =================================== */ + +/* 노트북 (1024px ~ 1400px) */ +@media (max-width: 1400px) { + .dashboard { + width: 90%; + } + + .d_service > input { + width: 40%; + } + + .d_service > textarea { + width: 70%; + } + + .r_content > textarea, + .l_content > textarea { + width: 70%; + } +} + +/* 태블릿 (768px ~ 1024px) */ +@media (max-width: 1024px) { + .dashboard { + width: 95%; + } + + .p_header a p { + font-size: 16px; + } + + .d_service > img { + max-width: 400px; + } + + .d_service > input { + width: 50%; + font-size: 24px; + height: 35px; + } + + .d_service > textarea { + width: 80%; + height: 70px; + font-size: 15px; + } + + .d_period > img { + width: 35px; + height: 35px; + } + + .d_period > p { + font-size: 16px; + } + + .d_period > p > .period_title { + font-size: 20px; + } + + .t_title > img, + .b_title > img, + .r_title > img, + .l_title > img { + width: 35px; + height: 35px; + } + + .t_title > h3, + .b_title > p, + .r_title > h3, + .l_title > h3 { + font-size: 20px; + } + + .p_title > img { + width: 45px; + height: 45px; + } + + .t_member > .member { + max-width: 180px; + height: 280px; + padding: 12px 20px; + } + + .member > .profile_section > .profile { + width: 90px; + height: 90px; + } + + .member > .profile_section > .level { + width: 32px; + height: 32px; + right: calc(50% - 50px); + } + + .member > h3 { + font-size: 18px; + } + + .member > .info { + font-size: 13px; + } + + .r_content > textarea, + .l_content > textarea { + width: 80%; + height: 100px; + font-size: 13px; + } + + .modal-content { + width: 400px; + height: 65%; + } +} + +/* 모바일 (~ 768px) */ +@media (max-width: 768px) { + .dashboard { + width: 95%; + } + + .p_header { + width: 95%; + height: 45px; + } + + .p_header a { + padding: 12px; + } + + .p_header a p { + font-size: 14px; + } + + .no_project > h3 { + font-size: 28px; + margin: 100px auto 20px; + } + + .no_project > a { + width: 180px; + height: 35px; + } + + .no_project > a > p { + font-size: 16px; + line-height: 35px; + } + + .d_service { + margin: 40px auto 0; + } + + .d_service > img { + max-width: 300px; + } + + .d_service > input { + width: 70%; + font-size: 20px; + height: 32px; + } + + .d_service > textarea { + width: 90%; + height: 60px; + font-size: 14px; + } + + .d_period { + margin-top: 50px; + flex-direction: column; + align-items: flex-start; + } + + .d_period > img { + width: 30px; + height: 30px; + margin-bottom: 5px; + } + + .d_period > p { + font-size: 14px; + margin-left: 0; + } + + .d_period > p > .period_title { + font-size: 16px; + } + + .b_title > img { + width: 30px; + height: 30px; + } + + .b_title > p { + font-size: 18px; + } + + .b_content { + margin-left: 37px; + } + + hr { + margin-top: 35px; + } + + .d_team, + .d_rule, + .d_link, + .d_progress { + margin-top: 35px; + } + + .t_title > img, + .r_title > img, + .l_title > img { + width: 30px; + height: 30px; + } + + .t_title > h3, + .r_title > h3, + .l_title > h3 { + font-size: 18px; + } + + .p_title > img { + width: 40px; + height: 40px; + } + + .p_title > h3 { + font-size: 18px; + } + + .t_report > img { + width: 18px; + height: 18px; + } + + .t_report a { + font-size: 12px; + } + + .d_team > .t_content > p { + margin-left: 37px; + font-size: 16px; + } + + .d_team > .t_content > p > .t_role { + font-size: 17px; + } + + .t_member { + margin-top: 15px; + padding: 15px 0; + gap: 12px; + } + + .t_member > .member { + max-width: 160px; + height: 260px; + padding: 10px 18px; + } + + .member > .profile_section { + height: 80px; + margin-bottom: 12px; + } + + .member > .profile_section > .profile { + width: 80px; + height: 80px; + } + + .member > .profile_section > .level { + width: 28px; + height: 28px; + right: calc(50% - 45px); + } + + .member > h3 { + font-size: 16px; + margin-top: 12px; + margin-bottom: 12px; + } + + .member > .info { + font-size: 12px; + } + + .member > .role_design, + .member > .role_frontend, + .member > .role_backend { + font-size: 14px; + height: 28px; + padding: 4px 0; + bottom: 10px; + } + + .member > .role_design { + right: 40px; + } + + .member > .role_frontend { + right: 30px; + } + + .member > .role_backend { + right: 40px; + } + + .r_content, + .l_content { + margin-left: 37px; + } + + .r_content > textarea, + .l_content > textarea { + width: 90%; + height: 90px; + font-size: 13px; + line-height: 20px; + } + + .p_content { + margin: 5px 0 80px 37px; + } + + .p_bar { + height: 18px; + } + + button { + width: 180px; + height: 38px; + font-size: 16px; + margin-bottom: 150px; + } + + .modal-content { + width: 90%; + max-width: 350px; + height: 60%; + padding: 25px; + } + + .modal-header h3 { + font-size: 18px; + } + + .member-select-list { + max-height: 150px; + } + + .member-option { + padding: 10px; + } + + .member-info img { + width: 30px; + height: 30px; + } + + textarea { + height: 45px; + font-size: 13px; + } +} + +/* 소형 모바일 (~ 480px) */ +@media (max-width: 480px) { + .p_header a p { + font-size: 13px; + } + + .d_service > img { + max-width: 250px; + } + + .d_service > input { + width: 85%; + font-size: 18px; + height: 30px; + } + + .d_service > textarea { + width: 95%; + height: 55px; + font-size: 13px; + } + + .d_period > p { + font-size: 13px; + } + + .t_member > .member { + max-width: 140px; + height: 240px; + padding: 10px 15px; + } + + .member > .profile_section { + height: 70px; + } + + .member > .profile_section > .profile { + width: 70px; + height: 70px; + } + + .member > .profile_section > .level { + width: 25px; + height: 25px; + right: calc(50% - 40px); + } + + .member > h3 { + font-size: 15px; + } + + .member > .info { + font-size: 11px; + } + + .member > .role_design, + .member > .role_frontend, + .member > .role_backend { + font-size: 13px; + height: 26px; + } + + .r_content > textarea, + .l_content > textarea { + width: 95%; + font-size: 12px; + } + + button { + width: 160px; + height: 35px; + font-size: 15px; + } +} \ No newline at end of file From c7a7638e59faa5bee2df9d143a6e94f8b3ea1cde Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 10 Feb 2026 21:44:15 +0900 Subject: [PATCH 297/380] =?UTF-8?q?feat:=20note=5Flist=EC=97=90=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/reflections.css | 95 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/static/css/reflections.css b/static/css/reflections.css index 23d0f18..a8be99a 100644 --- a/static/css/reflections.css +++ b/static/css/reflections.css @@ -40,7 +40,7 @@ button { cursor: pointer; } /* ========================= - Page + Note List Page ========================= */ .ref-page { background: var(--blue-0); @@ -412,9 +412,9 @@ button { padding: 10px 14px; } - .ref-selectbox { + /* .ref-selectbox { min-width: 140px; - } + } */ .note-title { font-size: 18px; @@ -425,6 +425,95 @@ button { } } +/* ========================= + Mobile Responsive +========================= */ +@media (max-width: 640px) { + + .note-card { + flex-direction: column; + gap: 12px; + padding: 18px; + } + + /* 상단 액션 고정 */ + .bookmark-btn { + position: absolute; + top: 14px; + left: 14px; + } + + .note-morewrap { + position: absolute; + top: 10px; + right: 10px; + } + + /* 본문 영역 여백 확보 */ + .note-main { + padding-top: 32px; + } + + /* 제목 / 태그 세로 정렬 */ + .note-topline { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .note-title { + font-size: 17px; + white-space: normal; + line-height: 1.3; + } + + .note-tag { + font-size: 11px; + padding: 5px 10px; + } + + .note-preview { + font-size: 14px; + line-clamp: 3; + -webkit-line-clamp: 3; + } + + .note-date { + font-size: 13px; + } + + .ref-filters { + display: flex; + flex-wrap: wrap; + gap: 10px 12px; + } + + /* ===== 위 줄: 정렬 / 역할 ===== */ + .ref-select-container { + flex: 0 0 calc(50% - 6px); + min-width: 0; + } + + .ref-selectbox { + width: 100%; + min-width: 0; /* ← 핵심 */ + padding-right: 44px; /* 화살표 공간 */ + } + + .ref-selectchev { + right: 14px; + } + + /* ===== 아래 줄: 북마크 / 글쓰기 ===== */ + .bookmark-filter-btn, + .ref-writebtn { + flex: 0 0 calc(50% - 6px); + justify-content: center; + text-align: center; + } +} + + /* ========================= Note Form (create/update) ========================= */ From 30defa0fe18878e725a8528652fd6ba13612ec1f Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 21:44:35 +0900 Subject: [PATCH 298/380] =?UTF-8?q?feat:=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EC=9C=BC=EB=A1=9C=20=EC=9E=85=EB=A0=A5=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/forms.py | 4 ++-- templates/projects/dashboard.html | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/projects/forms.py b/apps/projects/forms.py index dc16cf3..e15ac87 100644 --- a/apps/projects/forms.py +++ b/apps/projects/forms.py @@ -34,12 +34,12 @@ class Meta: }), "team_rules": forms.Textarea(attrs={ "class": "form-control", - "placeholder": "팀 규칙을 마크다운 형식으로 작성해주세요\n\n예:\n# 회의 규칙\n- 주 1회 수요일 19시\n- 지각 3회 = 경고\n\n# 코드 리뷰\n- PR 생성 후 2시간 내 리뷰\n- 최소 2명 승인 필수", + "placeholder": "팀 규칙을 마크다운으로 작성해주세요\n\n예:\n# 회의\n- 주 1회 수요일 19시\n- 지각 3회 = 경고\n\n# 코드 리뷰\n- PR 2시간 내 리뷰\n- 2명 승인 필수", "rows": 6, }), "related_links": forms.Textarea(attrs={ "class": "form-control", - "placeholder": "관련 링크를 줄바꿈으로 구분하여 입력해주세요\n\n예:\nhttps://notion.so/...\nhttps://figma.com/...\nhttps://github.com/...", + "placeholder": "관련 링크를 마크다운으로 입력해주세요\n\n예:\n[Notion](https://notion.so/...)\n[Figma](https://figma.com/...)\n[GitHub](https://github.com/...)", "rows": 6, }), } diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index fd5fe9e..0fc67d8 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% load static %} +{% load markdown_tags %} {% block header %} {% endblock %} @@ -40,7 +41,7 @@

{{ project.title }}

관련 링크

-

{{ project.related_links|urlize|linebreaksbr }}

+

{{ project.related_links|markdown|safe }}


@@ -119,7 +120,7 @@

팀 규칙

{% if project.team_rules %} - {{ project.team_rules|linebreaks }} + {{ project.team_rules|markdown|safe }} {% else %}

팀 규칙이 없습니다.

{% endif %} From 87294d277a83dd91f7b3247c91b9ff25af27652a Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 21:52:40 +0900 Subject: [PATCH 299/380] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=97=AD=ED=95=A0?= =?UTF-8?q?=EB=B3=84=20=EC=A7=84=EC=B2=99=EB=8F=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/guides/views.py | 49 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/apps/guides/views.py b/apps/guides/views.py index 6158f55..8ddbe30 100644 --- a/apps/guides/views.py +++ b/apps/guides/views.py @@ -3,6 +3,7 @@ from apps.projects.models import Project from apps.teams.models import TeamMember +from apps.accounts.models import Role from .models import GuideCard, GuideTaskProgress, ProjectProgress @@ -64,10 +65,50 @@ def mission(request): 'is_completed': is_card_completed, }) - # 모든 역할의 진척도 - all_role_progress = ProjectProgress.objects.filter( - project=project - ).select_related('role') + # 모든 역할의 진척도 계산 + all_role_progress = [] + + # 프로젝트에 속한 모든 팀원의 역할 가져오기 + team_members_data = TeamMember.objects.filter( + team=project.team, + is_active=True + ).values('role').distinct() + + team_role_ids = [member['role'] for member in team_members_data] + team_roles = Role.objects.filter(id__in=team_role_ids) + + for team_role in team_roles: + # 해당 역할의 모든 미션 카드 + cards = GuideCard.objects.filter( + role=team_role, + is_active=True + ).prefetch_related('tasks') + + # 전체 태스크 수 및 완료된 태스크 수 + total_tasks = 0 + completed_tasks = 0 + + for card in cards: + for task in card.tasks.all(): + total_tasks += 1 + # 이 태스크가 프로젝트에서 완료되었는지 확인 + is_completed = GuideTaskProgress.objects.filter( + task=task, + project=project, + is_completed=True + ).exists() + + if is_completed: + completed_tasks += 1 + + progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) + + all_role_progress.append({ + 'role': team_role, + 'total_tasks': total_tasks, + 'completed_tasks': completed_tasks, + 'progress_percent': progress_percent, + }) context = { 'project': project, From 36272b85fb6c9c05f686f0e738ab98046900351a Mon Sep 17 00:00:00 2001 From: Tonyjoo11 Date: Tue, 10 Feb 2026 22:20:13 +0900 Subject: [PATCH 300/380] =?UTF-8?q?fix:=20=ED=9A=8C=EA=B3=A0=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=20=EA=B0=84=EC=86=8C=ED=99=94=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9D=84=20=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../retrospective_compact.json | 10 ++++++++++ apps/reflections/serializers.py | 4 ++-- apps/reflections/views.py | 4 ++-- static/css/reflections.css | 19 ++++++++++++++++--- 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 apps/reflections/guide_templates/retrospective_compact.json diff --git a/apps/reflections/guide_templates/retrospective_compact.json b/apps/reflections/guide_templates/retrospective_compact.json new file mode 100644 index 0000000..17968b0 --- /dev/null +++ b/apps/reflections/guide_templates/retrospective_compact.json @@ -0,0 +1,10 @@ +{ + "key": "compact", + "title": "오늘의 회고", + "intro": [], + "questions": [ + { "id": "q1_study", "order": 1, "title": "오늘 무엇을 공부했나요?" }, + { "id": "q2_hard", "order": 2, "title": "어려웠던 지점은 무엇이었고, 어떻게 해결하거나 해결하지 못했나요?" }, + { "id": "q3_tomorrow", "order": 3, "title": "내일은 무엇을 할 것인가요?" } + ] +} diff --git a/apps/reflections/serializers.py b/apps/reflections/serializers.py index 0b157d1..4e04376 100644 --- a/apps/reflections/serializers.py +++ b/apps/reflections/serializers.py @@ -65,7 +65,7 @@ def _rebuild_content_md(self, instance_or_data: dict, template_key: str, answers return build_markdown(guide, answers_json, title=title) def create(self, validated_data): - template_key = validated_data.get("template_key") or "default" + template_key = validated_data.get("template_key") or "compact" answers_json = validated_data.get("answers_json") or {} title = validated_data.get("title") @@ -76,7 +76,7 @@ def create(self, validated_data): def update(self, instance, validated_data): # 기존 값과 병합해서 md 재생성 - template_key = validated_data.get("template_key", instance.template_key or "default") + template_key = validated_data.get("template_key", instance.template_key or "compact") answers_json = validated_data.get("answers_json", instance.answers_json or {}) title = validated_data.get("title", instance.title) diff --git a/apps/reflections/views.py b/apps/reflections/views.py index d35761e..8026939 100644 --- a/apps/reflections/views.py +++ b/apps/reflections/views.py @@ -151,7 +151,7 @@ def note_create(request): :tpl: 선택할 질문 템플릿 (현재는 default 하나만) """ - tpl_key = request.GET.get("tpl") or "default" + tpl_key = request.GET.get("tpl") or "compact" guide = load_guide(tpl_key) # ✅ draft_key 발급/유지 @@ -240,7 +240,7 @@ def note_update(request, note_id): """회고 수정 - note_create와 동일하게 guide 기반으로 렌더/저장""" note = get_object_or_404(Retrospective, id=note_id, user=request.user) - tpl = note.template_key or "default" + tpl = note.template_key or "compact" guide = load_guide(tpl) # 기존 답변(answers_json)로 textarea 기본값 채우기 diff --git a/static/css/reflections.css b/static/css/reflections.css index a8be99a..a76c7ba 100644 --- a/static/css/reflections.css +++ b/static/css/reflections.css @@ -119,6 +119,7 @@ button { position: relative; display: inline-flex; align-items: center; + min-width: 0; } .ref-selectbox { @@ -130,7 +131,8 @@ button { font-size: 14px; font-weight: 600; color: var(--gray-900); - min-width: 180px; + min-width: 0px; + width: 100%; } .ref-selectchev { @@ -664,16 +666,27 @@ button { } .ref-q-num { + flex: 0 0 22px; width: 22px; height: 22px; + min-width: 22px; + min-height: 22px; + aspect-ratio: 1 / 1; + + /* 원 스타일 */ border-radius: 50%; - background: #2f2f2f; - color: #fff; + background: #2f2f2f; /* 검은 원 */ + color: #ffffff; /* 흰 숫자 */ + + /* 가운데 정렬 */ display: inline-flex; align-items: center; justify-content: center; + + /* 숫자 안정화 */ font-size: 12px; font-weight: 900; + line-height: 1; } .ref-q-actions { From eafa79c0ae7acf6210d3cee8752bd4d02bfee096 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 22:41:28 +0900 Subject: [PATCH 301/380] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=A7=84=EC=B2=99=EB=8F=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/projects/views.py | 68 ++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/apps/projects/views.py b/apps/projects/views.py index d61e677..ea1660b 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -40,34 +40,50 @@ def _get_project_context(project, user): if active_season.project_start <= project.created_at <= active_season.project_end: season = active_season - # 가이드 진척도 계산 + # 가이드 진척도 계산 (전체 미션 합산) guide_progress = None - if hasattr(project, 'current_stage') and project.current_stage: - try: - from apps.guides.models import GuideTask, GuideTaskProgress - - total_tasks = GuideTask.objects.filter( - card__stage=project.current_stage - ).count() - - completed_tasks = GuideTaskProgress.objects.filter( - task__card__stage=project.current_stage, - project=project, - user=user, - is_completed=True - ).count() - - progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) + try: + from apps.guides.models import GuideCard, GuideTaskProgress + from apps.accounts.models import Role + + # 프로젝트의 모든 팀원 역할 가져오기 + team_members_data = team.members.filter(is_active=True).values('role').distinct() + team_role_ids = [member['role'] for member in team_members_data] + team_roles = Role.objects.filter(id__in=team_role_ids) + + total_tasks = 0 + completed_tasks = 0 + + # 각 역할별 모든 미션 카드의 태스크를 합산 + for team_role in team_roles: + cards = GuideCard.objects.filter( + role=team_role, + is_active=True + ).prefetch_related('tasks') - guide_progress = { - 'stage': project.current_stage, - 'total_tasks': total_tasks, - 'completed_tasks': completed_tasks, - 'progress_percent': progress_percent, - } - except: - # GuideTask 모델이 없거나 데이터가 없으면 None으로 처리 - guide_progress = None + for card in cards: + for task in card.tasks.all(): + total_tasks += 1 + # 이 태스크가 프로젝트에서 완료되었는지 확인 + is_completed = GuideTaskProgress.objects.filter( + task=task, + project=project, + is_completed=True + ).exists() + + if is_completed: + completed_tasks += 1 + + progress_percent = int((completed_tasks / total_tasks * 100) if total_tasks > 0 else 0) + + guide_progress = { + 'total_tasks': total_tasks, + 'completed_tasks': completed_tasks, + 'progress_percent': progress_percent, + } + except: + # GuideCard 모델이 없거나 데이터가 없으면 None으로 처리 + guide_progress = None return { "project": project, From de397138634e3f66038951ec317109d796d546f6 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 10 Feb 2026 22:41:47 +0900 Subject: [PATCH 302/380] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=A7=84=EC=B2=99=EB=8F=84=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/dashboard.css | 45 +++++++++++++++++------- static/css/dashboard_update.css | 43 +++++++++++++++------- templates/projects/dashboard.html | 14 +++++--- templates/projects/dashboard_update.html | 17 ++++----- 4 files changed, 82 insertions(+), 37 deletions(-) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 05f8dab..b093f9d 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -391,53 +391,72 @@ hr { color: #4272EF; transition: 0.3s ease; } - .d_progress { margin-top: 30px; text-align: start; + margin-bottom: 40px; } .p_title { display: flex; align-items: center; position: relative; + margin-bottom: 10px; } .p_title > img { width: 50px; height: 50px; + margin-right: 10px; } -.p_title > h3 { +.p_title > div > h3 { font-weight: 600; font-size: 22px; - margin-top: 2px; + margin: 0; } -.p_content { - position: relative; - margin: 5px 0 100px 47px; +.role_progress_bar { + margin: 10px 0 15px 0; + display: flex; + align-items: center; + gap: 15px; } -.p_bar { - width: 100%; - height: 20px; - background: #ccc; - border-radius: 20px; +.role_progress_bar > h3 { + width: 40px; + font-size: 18px; + font-weight: 550; + margin: 0; +} + +.progress_bar_container { position: relative; + flex: 1; + height: 15px; + background: #DDDDDD; + border-radius: 20px; overflow: hidden; } -.p_real { +.progress_bar_fill { position: absolute; top: 0; left: 0; height: 100%; background: #5A88FF; border-radius: 20px; - transition: width 0.3s ease; + transition: width 0.5s ease; +} + +.progress_percent { + min-width: 45px; + font-size: 14px; + font-weight: 600; + color: #4272EF; } + button { width: 200px; height: 40px; diff --git a/static/css/dashboard_update.css b/static/css/dashboard_update.css index 3165b01..0e30661 100644 --- a/static/css/dashboard_update.css +++ b/static/css/dashboard_update.css @@ -419,47 +419,66 @@ hr { .d_progress { margin-top: 30px; text-align: start; + margin-bottom: 40px; } .p_title { display: flex; align-items: center; position: relative; + margin-bottom: 10px; } .p_title > img { width: 50px; height: 50px; + margin-right: 10px; } -.p_title > h3 { +.p_title > div > h3 { font-weight: 600; font-size: 22px; - margin-top: 2px; + margin: 0; } -.p_content { - position: relative; - margin: 5px 0 100px 47px; +.role_progress_bar { + margin: 10px 0 15px 0; + display: flex; + align-items: center; + gap: 15px; } -.p_bar { - width: 100%; - height: 20px; - background: #ccc; - border-radius: 20px; +.role_progress_bar > h3 { + width: 40px; + font-size: 18px; + font-weight: 550; + margin: 0; +} + +.progress_bar_container { position: relative; + flex: 1; + height: 15px; + background: #DDDDDD; + border-radius: 20px; overflow: hidden; } -.p_real { +.progress_bar_fill { position: absolute; top: 0; left: 0; height: 100%; background: #5A88FF; border-radius: 20px; - transition: width 0.3s ease; + transition: width 0.5s ease; +} + +.progress_percent { + min-width: 45px; + font-size: 14px; + font-weight: 600; + color: #4272EF; } button { diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index 0fc67d8..c874899 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -126,14 +126,20 @@

팀 규칙

{% endif %}
+
progress -

진척도

+
+

{{ project.title }}의 진척도

+
-
-
-
+
+

전체

+
+
+
+ {% if guide_progress %}{{ guide_progress.progress_percent }}{% else %}0{% endif %}%
{% if is_team_member %} diff --git a/templates/projects/dashboard_update.html b/templates/projects/dashboard_update.html index c3a4e90..ede53b9 100644 --- a/templates/projects/dashboard_update.html +++ b/templates/projects/dashboard_update.html @@ -130,15 +130,16 @@

팀 규칙

progress -

진척도

+
+

{{ project.title }}의 진척도

+
-
-
- {% if guide_progress %} -
- {% else %} -
- {% endif %} +
+

전체

+
+
+
+ {% if guide_progress %}{{ guide_progress.progress_percent }}{% else %}0{% endif %}%
From d6197ea2cf0c9ff2acaab1d2a6421e4b171af1ff Mon Sep 17 00:00:00 2001 From: knana6 Date: Tue, 10 Feb 2026 23:40:03 +0900 Subject: [PATCH 303/380] fix: mypage image default --- templates/account/mypage.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/account/mypage.html b/templates/account/mypage.html index e4f3f75..abdb237 100644 --- a/templates/account/mypage.html +++ b/templates/account/mypage.html @@ -14,7 +14,7 @@ src="{% if user_obj.profile_image %} {{ user_obj.profile_image.url }} {% else %} - {% static 'images/default_img.png' %} + {% static 'images/default_profile.png' %} {% endif %}" alt="프로필 사진" class="profile-img" From 448abb97bb1c736892557ed6d4350ee6f79e52a2 Mon Sep 17 00:00:00 2001 From: knana6 Date: Tue, 10 Feb 2026 23:58:08 +0900 Subject: [PATCH 304/380] fix: mypage image class delete --- templates/account/mypage.html | 7 ++----- templates/account/profile_edit.html | 8 ++++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/templates/account/mypage.html b/templates/account/mypage.html index abdb237..49ffaa1 100644 --- a/templates/account/mypage.html +++ b/templates/account/mypage.html @@ -10,15 +10,12 @@

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