From 6767be54a8c82ec7cb504f094558b27ece388e0f Mon Sep 17 00:00:00 2001 From: sm4640 Date: Mon, 19 May 2025 23:58:07 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9E=95=20Dependency:=20[#58]=20boto3,dja?= =?UTF-8?q?ngo-storages=20=EC=84=A4=EC=B9=98(s3=EC=97=B0=EB=8F=99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | Bin 1578 -> 1858 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index d9f0322b7bfe24eddd65cc73dc5f661fabaae99c..5d5421685265c22dc6fbfdf786213ac16dcfaf16 100644 GIT binary patch delta 259 zcmZ3*bBJ%k496sfe1;MrHfFE|LPG{UAj<+s8UjgP1}+Aed@@i?5ko4bs*RJ^Gb-vb z6oXAEVn}322dXNDt21OU0h(sE*^tSbQ8tSq7c5x-R93=}0as)IvT$=bvoWK33Ro(Y zp%kbflOYGJ#0V&D3B({}1weNI?V3E5CD+;*s0L(X9#A9=XmAl&HP|o)kRAiD4~l{2 SRKTq=WH1NGZ2rxn&jc<)WW~5yj75)easz9`W+^reMgX%23Hty5 From 333f19bea64de4f0e95d7af9310f5a201fe93c09 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Mon, 19 May 2025 23:59:40 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#58]=20aws=20s3=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20storage?= =?UTF-8?q?=20=EC=95=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=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/settings.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/config/settings.py b/config/settings.py index 5eb300d..cb7351a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'rest_framework_simplejwt', 'rest_framework_simplejwt.token_blacklist', 'corsheaders', + 'storages', 'users', 'portfolios', 'projects', @@ -112,6 +113,25 @@ DATABASES = { } } +# aws s3 +AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') +AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') +AWS_STORAGE_BUCKET_NAME = 'colio-service' +AWS_S3_REGION_NAME = 'ap-northeast-2' + +AWS_S3_FILE_OVERWRITE = True # 같은 이름 파일 덮어쓰기 +AWS_DEFAULT_ACL = None # 권한 제어 (None이면 기본 권한) +AWS_QUERYSTRING_AUTH = False # 서명 없는 URL 사용 + +# DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + +STORAGES = { + "default": { + "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", + }, + "staticfiles": "storages.backends.s3boto3.S3Boto3Storage", +} + # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators From 54e04e1c940d2374e471424112ec7b9dd30aa4bd Mon Sep 17 00:00:00 2001 From: sm4640 Date: Tue, 20 May 2025 00:01:30 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix:=20[#58]=20media?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/urls.py b/config/urls.py index 9a6a6f4..0e2116a 100644 --- a/config/urls.py +++ b/config/urls.py @@ -13,5 +13,5 @@ urlpatterns = [ path('api/notification/', include('notifications.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +# if settings.DEBUG: +# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From d4926be0d53f47b75cd35b82dbb248e58b44ccee Mon Sep 17 00:00:00 2001 From: sm4640 Date: Tue, 20 May 2025 00:02:19 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#58]=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=95=A8=EC=88=98=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/utils/fileManager.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 common/utils/fileManager.py diff --git a/common/utils/fileManager.py b/common/utils/fileManager.py new file mode 100644 index 0000000..6a19d18 --- /dev/null +++ b/common/utils/fileManager.py @@ -0,0 +1,20 @@ +import os + +def dynamic_upload_to(prefix, field_name_func): + def wrapper(instance, filename): + ext = filename.split('.')[-1] + field_name = field_name_func(instance) + + if prefix == 'user': + filename = f'{instance.nickname}-{field_name}.{ext}' + else: + filename = f'{instance.id}-{field_name}.{ext}' + + return os.path.join(prefix, filename) + + return wrapper + +def file_delete(obj, field): + getattr(obj, field).delete(save=False) + setattr(obj, field, None) + obj.save(update_fields=[field]) From b6a45434cacb77c191ee9f431ec6c50efbdec4c3 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Tue, 20 May 2025 00:06:10 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#58]=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=95=84=EB=93=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=8F=99=EC=A0=81=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EA=B0=9D=EC=B2=B4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- portfolios/models.py | 11 +++++++++-- projects/models.py | 9 ++++++++- users/models.py | 12 +++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/portfolios/models.py b/portfolios/models.py index ca8601e..f66ec11 100644 --- a/portfolios/models.py +++ b/portfolios/models.py @@ -2,6 +2,8 @@ from django.db import models from common.models.baseModels import BaseModel +from common.utils.fileManager import dynamic_upload_to + from django.contrib.postgres.fields import ArrayField from django.conf import settings @@ -16,8 +18,13 @@ class Portfolio(BaseModel): like_count = models.IntegerField(default=0) scrap_count = models.IntegerField(default=0) is_represent = models.BooleanField(default=False) - thumbnail = models.ImageField(upload_to='', blank=True) + thumbnail = models.ImageField(upload_to=dynamic_upload_to('portfolio', lambda instance: 'thumbnail'), blank=True) code_id = models.CharField(max_length=26, blank=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='owned_portfolios', to_field="id") likers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_portfolios', blank=True) - scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_portfolios', blank=True) \ No newline at end of file + scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_portfolios', blank=True) + + def delete(self, *args, **kwargs): + if self.thumbnail: + self.thumbnail.delete(save=False) + super().delete(*args, **kwargs) \ No newline at end of file diff --git a/projects/models.py b/projects/models.py index d034525..d03f375 100644 --- a/projects/models.py +++ b/projects/models.py @@ -3,6 +3,8 @@ from django.db import models from common.models.baseModels import BaseModel from common.models.choiceModels import InvitationStatus +from common.utils.fileManager import dynamic_upload_to + from django.contrib.postgres.fields import ArrayField from django.conf import settings @@ -20,12 +22,17 @@ class Project(BaseModel): like_count = models.IntegerField(default=0) scrap_count = models.IntegerField(default=0) is_represent = models.BooleanField(default=False) - thumbnail = models.ImageField(upload_to='', blank=True) + thumbnail = models.ImageField(upload_to=dynamic_upload_to('project', lambda instance: 'thumbnail'), blank=True) code_id = models.CharField(max_length=26, blank=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='owned_projects', to_field="id") likers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_projects', blank=True) scrappers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='scrapped_projects', blank=True) + def delete(self, *args, **kwargs): + if self.thumbnail: + self.thumbnail.delete(save=False) + super().delete(*args, **kwargs) + class ProjectTeamList(BaseModel): project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='team_project_member_list', to_field='id') user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='team_project_list',to_field='id') diff --git a/users/models.py b/users/models.py index ed643fd..3a24fad 100644 --- a/users/models.py +++ b/users/models.py @@ -2,6 +2,7 @@ from django.db import models from common.models.baseModels import BaseModel from common.models.choiceModels import GenderChoices, CertificateCodeUseType from common.utils.codeManger import set_expire +from common.utils.fileManager import dynamic_upload_to from django.contrib.postgres.fields import ArrayField from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin @@ -51,8 +52,8 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin): skills = ArrayField(models.CharField(max_length=20), default=list, blank=True) external_links = ArrayField(models.TextField(), default=list, blank=True) short_bio = models.CharField(max_length=100, blank=True) - profile_image = models.ImageField(upload_to='', blank=True) - banner_image = models.ImageField(upload_to='', blank=True) + profile_image = models.ImageField(upload_to=dynamic_upload_to('user', lambda instance: 'profile'), blank=True) + banner_image = models.ImageField(upload_to=dynamic_upload_to('user', lambda instance: 'banner'), blank=True) is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=True) @@ -65,4 +66,9 @@ class User(BaseModel, AbstractBaseUser, PermissionsMixin): def __str__(self): return self.nickname - + def delete(self, *args, **kwargs): + if self.profile_image: + self.profile_image.delete(save=False) + if self.banner_image: + self.banner_image.delete(save=False) + super().delete(*args, **kwargs) From a64c9fcd011b22b04b4a350adec14375bc8c2c8a Mon Sep 17 00:00:00 2001 From: sm4640 Date: Tue, 20 May 2025 00:08:29 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#58]=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=95=84=EB=93=9C=EC=97=90=20=EA=B0=92=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=EC=A7=80=20=EC=B2=B4=ED=81=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- users/services.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/users/services.py b/users/services.py index ea673f4..af36696 100644 --- a/users/services.py +++ b/users/services.py @@ -24,6 +24,15 @@ class CheckUserFieldDuplicateService: return User.objects.filter(**filter_dict).exists() +class CheckUserFieldValueExistService: + @staticmethod + def check_exist(user: User, field) -> bool: + if not field: + return False + if getattr(user, field) == None: + return False + else: + return True class UserToNotificationService: @staticmethod From e46a8638a174bc7db50c349c913259cd3e62827c Mon Sep 17 00:00:00 2001 From: sm4640 Date: Tue, 20 May 2025 00:09:50 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix:=20[#58]=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A1=B4=EC=9E=AC=EC=97=AC=EB=B6=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81?= =?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 --- users/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/users/views.py b/users/views.py index b0a18e9..381eaf0 100644 --- a/users/views.py +++ b/users/views.py @@ -19,6 +19,8 @@ from .models import * from .serializers import * from .services import * +from common.utils.fileManager import file_delete + from projects.serializers import * from portfolios.serializers import * @@ -236,6 +238,10 @@ class MyPageProfileAPIView(APIView): if user == target_user: serializer = UserProfileSerializer(user, request.data, partial=True) if serializer.is_valid(): + if serializer.validated_data.get('profile_image') and CheckUserFieldValueExistService.check_exist(user, 'profile_image'): + file_delete(user, 'profile_image') + if serializer.validated_data.get('banner_image') and CheckUserFieldValueExistService.check_exist(user, 'banner_image'): + file_delete(user, 'banner_image') serializer.save() data = serializer.data data['represent_portfolio_id'] = UserToPortfolioService.get_represent_portfolio(target_user)