From 79b2c665c90a735e764b1d140ddd5a2d154cde9b Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Sat, 1 Apr 2023 23:20:58 +0900 Subject: [PATCH 01/44] connect mysql and add models --- .gitignore | 2 +- api/models.py | 54 ++++++++++++++++++++- django_rest_framework_17th/settings/base.py | 2 +- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0bc3174..0609490 100644 --- a/.gitignore +++ b/.gitignore @@ -132,7 +132,7 @@ celerybeat.pid *.sage.py # Environments -.env +venv/.env .venv env/ venv/ diff --git a/api/models.py b/api/models.py index 71a8362..53e01d7 100644 --- a/api/models.py +++ b/api/models.py @@ -1,3 +1,55 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. + +class BaseModel(models.Model): + status = models.CharField(max_length=10, default='A') + createdAt = models.DateTimeField(auto_now_add=True) + updatedAt = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + def __str__(self): + return self.field_name + + +class Profile(BaseModel): + user = models.OneToOneField(User, on_delete=models.CASCADE) + nickname = models.TextField(max_length=60) + email = models.EmailField(max_length=60) + password = models.TextField(max_length=200) + profileImgPath = models.TextField(null=True) + friends = models.ManyToManyField('self') + + +class School(BaseModel): + name = models.TextField(max_length=60) + campus = models.TextField(max_length=60, null=True) + + +class Board(BaseModel): + name = models.TextField(max_length=60) + school = models.ForeignKey("School", related_name="school", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + + +class Post(BaseModel): + board = models.ForeignKey("Board", related_name="board", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + title = models.TextField(max_length=100, null=True) + contents = models.TextField(max_length=200) + isAnonymous = models.CharField(max_length=10, default='Y') + isQuestion = models.CharField(max_length=10, default='N') + + +class Photo(BaseModel): + post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) + path = models.TextField() + + +class Comment(BaseModel): + post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + contents = models.TextField(max_length=200) \ No newline at end of file diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 2a2381c..ad77100 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -19,7 +19,7 @@ DEBUG=(bool, False) ) -environ.Env.read_env(os.path.join(BASE_DIR, '.env')) +environ.Env.read_env(os.path.join(BASE_DIR, 'venv/.env')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ From 7985c89325d52b6f80f2b866efb8b5b673765de8 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Sat, 1 Apr 2023 23:55:34 +0900 Subject: [PATCH 02/44] add other models --- api/models.py | 69 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/api/models.py b/api/models.py index 53e01d7..8afcb7e 100644 --- a/api/models.py +++ b/api/models.py @@ -16,20 +16,20 @@ def __str__(self): class Profile(BaseModel): user = models.OneToOneField(User, on_delete=models.CASCADE) - nickname = models.TextField(max_length=60) + nickname = models.CharField(max_length=60) email = models.EmailField(max_length=60) - password = models.TextField(max_length=200) + password = models.CharField(max_length=200) profileImgPath = models.TextField(null=True) friends = models.ManyToManyField('self') class School(BaseModel): - name = models.TextField(max_length=60) - campus = models.TextField(max_length=60, null=True) + name = models.CharField(max_length=60) + campus = models.CharField(max_length=60, null=True) class Board(BaseModel): - name = models.TextField(max_length=60) + name = models.CharField(max_length=60) school = models.ForeignKey("School", related_name="school", on_delete=models.CASCADE) profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) @@ -37,8 +37,8 @@ class Board(BaseModel): class Post(BaseModel): board = models.ForeignKey("Board", related_name="board", on_delete=models.CASCADE) profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) - title = models.TextField(max_length=100, null=True) - contents = models.TextField(max_length=200) + title = models.CharField(max_length=100, null=True) + contents = models.CharField(max_length=200) isAnonymous = models.CharField(max_length=10, default='Y') isQuestion = models.CharField(max_length=10, default='N') @@ -52,4 +52,57 @@ class Comment(BaseModel): post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) - contents = models.TextField(max_length=200) \ No newline at end of file + contents = models.CharField(max_length=200) + + +class PostLike(BaseModel): + post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + + +class CommentLike(BaseModel): + comment = models.ForeignKey("Comment", related_name="post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + + +class Scrap(BaseModel): + post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + + +class TimeTable(BaseModel): + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + name = models.CharField(max_length=60) + + +class LectureDomain(BaseModel): + name = models.CharField(max_length=60) + parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + + +class Lecture(BaseModel): + lectureDomain = models.ForeignKey("LectureDomain", related_name="lectureDomain", on_delete=models.CASCADE, null=True) + collegeYear = models.CharField(max_length=10, default='0') + credit = models.CharField(max_length=10) + category = models.CharField(max_length=100) + professor = models.CharField(max_length=100) + lectureCode = models.CharField(max_length=60) + classRoom = models.CharField(max_length=100) + dayAndTime = models.CharField(max_length=100) + + +class TakeLecture(BaseModel): + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + lecture = models.ForeignKey("Lecture", related_name="lecture", on_delete=models.CASCADE) + + +class LectureReview(BaseModel): + lecture = models.ForeignKey("Lecture", related_name="lecture", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + rating = models.CharField(max_length=10) + contents = models.CharField(max_length=200) + + +class ReviewLike(BaseModel): + lectureReview = models.ForeignKey("LectureReview", related_name="lectureReview", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) \ No newline at end of file From 53f7c672fa724cb8d4afaa6fcd48c70a0548debf Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Sun, 2 Apr 2023 01:33:57 +0900 Subject: [PATCH 03/44] edit __str__() methods, edit related_names, migrate --- api/migrations/0001_initial.py | 275 ++++++++++++++++++++ api/models.py | 93 +++++-- django_rest_framework_17th/settings/base.py | 5 +- 3 files changed, 347 insertions(+), 26 deletions(-) create mode 100644 api/migrations/0001_initial.py diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..dc0eaf5 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,275 @@ +# Generated by Django 3.2.16 on 2023-04-02 01:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Board', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('contents', models.CharField(max_length=200)), + ('parent', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='api.comment')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Lecture', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=150)), + ('collegeYear', models.CharField(default='0', max_length=10)), + ('credit', models.CharField(max_length=10)), + ('category', models.CharField(max_length=100)), + ('professor', models.CharField(max_length=100)), + ('lectureCode', models.CharField(max_length=60)), + ('classRoom', models.CharField(max_length=100)), + ('dayAndTime', models.CharField(max_length=100)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LectureReview', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('rating', models.CharField(max_length=10)), + ('contents', models.CharField(max_length=200)), + ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.lecture')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=100, null=True)), + ('contents', models.CharField(max_length=200)), + ('isAnonymous', models.CharField(default='Y', max_length=10)), + ('isQuestion', models.CharField(default='N', max_length=10)), + ('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.board')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('nickname', models.CharField(max_length=60)), + ('email', models.EmailField(max_length=60)), + ('password', models.CharField(max_length=200)), + ('profileImgPath', models.TextField(null=True)), + ('friends', models.ManyToManyField(blank=True, related_name='_api_profile_friends_+', to='api.Profile')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='School', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('campus', models.CharField(max_length=60, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TimeTable', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TakeLecture', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.lecture')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Scrap', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ReviewLike', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('lectureReview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.lecturereview')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to='api.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PostLike', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='post', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), + ), + migrations.CreateModel( + name='Photo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('path', models.TextField()), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='lecturereview', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), + ), + migrations.CreateModel( + name='LectureDomain', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('parent', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='api.lecturedomain')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='lecture', + name='lectureDomain', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.lecturedomain'), + ), + migrations.CreateModel( + name='CommentLike', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('createdAt', models.DateTimeField(auto_now_add=True)), + ('updatedAt', models.DateTimeField(auto_now=True)), + ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.comment')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='comment', + name='post', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post'), + ), + migrations.AddField( + model_name='comment', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), + ), + migrations.AddField( + model_name='board', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), + ), + migrations.AddField( + model_name='board', + name='school', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.school'), + ), + ] diff --git a/api/models.py b/api/models.py index 8afcb7e..b2c269f 100644 --- a/api/models.py +++ b/api/models.py @@ -10,9 +10,6 @@ class BaseModel(models.Model): class Meta: abstract = True - def __str__(self): - return self.field_name - class Profile(BaseModel): user = models.OneToOneField(User, on_delete=models.CASCADE) @@ -20,68 +17,102 @@ class Profile(BaseModel): email = models.EmailField(max_length=60) password = models.CharField(max_length=200) profileImgPath = models.TextField(null=True) - friends = models.ManyToManyField('self') + friends = models.ManyToManyField('self', blank=True) + + def __str__(self): + return self.nickname class School(BaseModel): name = models.CharField(max_length=60) campus = models.CharField(max_length=60, null=True) + def __str__(self): + return self.name + class Board(BaseModel): name = models.CharField(max_length=60) - school = models.ForeignKey("School", related_name="school", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + school = models.ForeignKey("School", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return self.name class Post(BaseModel): - board = models.ForeignKey("Board", related_name="board", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + board = models.ForeignKey("Board", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) title = models.CharField(max_length=100, null=True) contents = models.CharField(max_length=200) isAnonymous = models.CharField(max_length=10, default='Y') isQuestion = models.CharField(max_length=10, default='N') + def __str__(self): + return self.contents + class Photo(BaseModel): - post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) + post = models.ForeignKey("Post", on_delete=models.CASCADE) path = models.TextField() + def __str__(self): + return f"{self.path} included in {self.post}" + class Comment(BaseModel): - post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + post = models.ForeignKey("Post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) contents = models.CharField(max_length=200) + def __str__(self): + return self.contents + class PostLike(BaseModel): - post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + post = models.ForeignKey("Post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's like on {self.post}" class CommentLike(BaseModel): - comment = models.ForeignKey("Comment", related_name="post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + comment = models.ForeignKey("Comment", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's like on {self.comment}" class Scrap(BaseModel): - post = models.ForeignKey("Post", related_name="post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + post = models.ForeignKey("Post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's scrap of {self.post}" class TimeTable(BaseModel): - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) name = models.CharField(max_length=60) + def __str__(self): + return self.name + class LectureDomain(BaseModel): name = models.CharField(max_length=60) parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + def __str__(self): + return self.name + class Lecture(BaseModel): - lectureDomain = models.ForeignKey("LectureDomain", related_name="lectureDomain", on_delete=models.CASCADE, null=True) + name = models.CharField(max_length=150) + lectureDomain = models.ForeignKey("LectureDomain", on_delete=models.CASCADE, null=True) collegeYear = models.CharField(max_length=10, default='0') credit = models.CharField(max_length=10) category = models.CharField(max_length=100) @@ -90,19 +121,31 @@ class Lecture(BaseModel): classRoom = models.CharField(max_length=100) dayAndTime = models.CharField(max_length=100) + def __str__(self): + return self.name + class TakeLecture(BaseModel): - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) - lecture = models.ForeignKey("Lecture", related_name="lecture", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} started taking {self.lecture}" class LectureReview(BaseModel): - lecture = models.ForeignKey("Lecture", related_name="lecture", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) + lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) rating = models.CharField(max_length=10) contents = models.CharField(max_length=200) + def __str__(self): + return self.contents + class ReviewLike(BaseModel): - lectureReview = models.ForeignKey("LectureReview", related_name="lectureReview", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profile", on_delete=models.CASCADE) \ No newline at end of file + lectureReview = models.ForeignKey("LectureReview", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profiles", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's like on {self.lectureReview}" diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index ad77100..744f7c3 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -104,7 +104,7 @@ USE_L10N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) @@ -116,3 +116,6 @@ # Media files MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# AutoField setting +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' From c5b5bbc93f6356b0b59fd1eae6fbee148ff8473e Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Sun, 2 Apr 2023 09:05:57 +0900 Subject: [PATCH 04/44] edit model profile, add admin user --- api/admin.py | 18 +++++++++++++++- api/migrations/0002_auto_20230402_0802.py | 25 +++++++++++++++++++++++ api/models.py | 5 +---- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 api/migrations/0002_auto_20230402_0802.py diff --git a/api/admin.py b/api/admin.py index 8c38f3f..7397f14 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,3 +1,19 @@ from django.contrib import admin -# Register your models here. +from api.models import * + +admin.site.register(Profile) +admin.site.register(School) +admin.site.register(Board) +admin.site.register(Post) +admin.site.register(Comment) +admin.site.register(Photo) +admin.site.register(PostLike) +admin.site.register(CommentLike) +admin.site.register(Scrap) +admin.site.register(TimeTable) +admin.site.register(LectureDomain) +admin.site.register(Lecture) +admin.site.register(TakeLecture) +admin.site.register(LectureReview) +admin.site.register(ReviewLike) \ No newline at end of file diff --git a/api/migrations/0002_auto_20230402_0802.py b/api/migrations/0002_auto_20230402_0802.py new file mode 100644 index 0000000..03f97fc --- /dev/null +++ b/api/migrations/0002_auto_20230402_0802.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.16 on 2023-04-02 08:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='email', + ), + migrations.RemoveField( + model_name='profile', + name='nickname', + ), + migrations.RemoveField( + model_name='profile', + name='password', + ), + ] diff --git a/api/models.py b/api/models.py index b2c269f..63b3aaa 100644 --- a/api/models.py +++ b/api/models.py @@ -13,14 +13,11 @@ class Meta: class Profile(BaseModel): user = models.OneToOneField(User, on_delete=models.CASCADE) - nickname = models.CharField(max_length=60) - email = models.EmailField(max_length=60) - password = models.CharField(max_length=200) profileImgPath = models.TextField(null=True) friends = models.ManyToManyField('self', blank=True) def __str__(self): - return self.nickname + return self.user.username class School(BaseModel): From dbc2f4c47c3fc48194eef1e5d06254b55d703c0f Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Sun, 2 Apr 2023 11:20:30 +0900 Subject: [PATCH 05/44] edit field of TakeLecture --- api/migrations/0003_auto_20230402_1115.py | 23 +++++++++++++++++++++++ api/models.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 api/migrations/0003_auto_20230402_1115.py diff --git a/api/migrations/0003_auto_20230402_1115.py b/api/migrations/0003_auto_20230402_1115.py new file mode 100644 index 0000000..bd82eb1 --- /dev/null +++ b/api/migrations/0003_auto_20230402_1115.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2023-04-02 11:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_auto_20230402_0802'), + ] + + operations = [ + migrations.RemoveField( + model_name='takelecture', + name='profile', + ), + migrations.AddField( + model_name='takelecture', + name='timeTable', + field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to='api.timetable'), + ), + ] diff --git a/api/models.py b/api/models.py index 63b3aaa..9bb6cc8 100644 --- a/api/models.py +++ b/api/models.py @@ -123,7 +123,7 @@ def __str__(self): class TakeLecture(BaseModel): - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + timeTable = models.ForeignKey("TimeTable", on_delete=models.CASCADE, default='') lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) def __str__(self): From aa01fb8a46425a2414ffd72d26fe4e0cf3e1629a Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Sun, 2 Apr 2023 12:57:11 +0900 Subject: [PATCH 06/44] add 'through=' for manyToManyField --- api/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/models.py b/api/models.py index 9bb6cc8..5c82ae0 100644 --- a/api/models.py +++ b/api/models.py @@ -94,6 +94,7 @@ def __str__(self): class TimeTable(BaseModel): profile = models.ForeignKey("Profile", on_delete=models.CASCADE) name = models.CharField(max_length=60) + lecture = models.ManyToManyField("Lecture", through="TakeLecture") def __str__(self): return self.name From 605dbf084eae6f1335e3d0773b0e4cee7ca2f71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sun, 2 Apr 2023 13:11:55 +0900 Subject: [PATCH 07/44] Update README.md --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c0caf2..af2a938 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ -# CEOS 17기 백엔드 스터디 +# CEOS 17기 백엔드 스터디 +### ORM 이용해보기 +1. 게시글 생성 +![image](https://user-images.githubusercontent.com/90256209/229326431-a573d2b4-03b6-4c8c-a2bf-7e14ad507831.png) +2. 게시글 수정 및 쿼리셋 조회 +![image](https://user-images.githubusercontent.com/90256209/229326491-4cb57511-12ba-45a1-9274-ba3bde83da88.png) +3. filter 함수 사용 +filter의 `__contains`와 `__startswith`을 사용해봤다. +![image](https://user-images.githubusercontent.com/90256209/229326506-cf1c83a1-0b5c-43bd-bda9-5308a030df27.png) + +--- +### ERD 설계 +실무에서 ERD를 그린 후에 SQL문을 export해서 DB에 넣는 경우, ERD에 관계선으로 연관관계(Ex 1:N)를 설정해두면 테스트 데이터를 넣기가 불편해서 거의 하지 않는다고 배워서 습관적으로 관계선 설정을 안했더니 테이블간 관계가 한눈에 안들어오는것 같다 + +![CEOS everytime](https://user-images.githubusercontent.com/90256209/229327752-7a75378c-b28a-4298-96c8-baa159f407f6.png) +ERD를 설계하기전에 연관관계 매핑에 대해 생각해봤다. +- User1과 User2의 관계 : User1은 여러명의 유저와 친구가 될 수 있고, User2도 여러명의 유저와 친구가 될 수 있으므로 다대다(N:M) 관계임 -> 중간 테이블 'Friend'로 관리 +- Board(게시판), Post(게시글), Comment(댓글)의 관계 : 각 게시판에 여러개의 글이 있고, 각 글은 하나의 게시판에 소속되므로 게시판과 게시글은 1:N 관계임. 각 게시글에 여러개의 댓글이 있을 수 있고, 각 댓글은 하나의 게시글에 소속되므로 게시글과 댓글도 1:N 관계 +- User가 Post/Comment를 작성하는 관계 : 유저는 여러개의 게시글/댓글을 쓸 수 있고, 각 게시글/댓글은 한명의 유저에 의해 쓰였으로 유저가 게시글/댓글을 작성하는 관계는 1:N 관계 +- User가 Post를 Likes(공감)과 Scrap(스크랩)하는 관계 : 유저는 여러개의 게시글을 공감/스크랩할 수 있고, 각 게시글은 여러명의 유저에 의해 공감/ 스크랩될 수 있으므로 다대다(N:M) 관계임 -> 중간 테이블 'Likes', 'Scrap'으로 관리 +- Photo(사진)와 Post의 관계: 각 게시글에 여러 사진을 올릴 수 있으므로 1:N +- TimeTable(시간표), Lecture(강의)의 관계: 각 시간표에 여러 강의를 추가할 수 있고, 각 강의도 여러 시간표에 포함될 수 있으므로 N:M -> 중간 테이블 'TakeLecture(수강)'로 관리 +- LectureReview(강의평)와 Lecture의 관계: 각 강의에 여러 강의평이 달릴 수 있고, 각 강의평은 하나의 강의에 소속되므로 1:N 관계 + +그리고 고민됐던건 시간표에 강의를 추가할때 '일반교양' > '16이후, 공학' > '인문계열' 이런식으로 전공/영역을 선택하면 강의가 여러개 나오는 부분이었다. 이렇게 전공/영역이 계층구조를 가지기 때문에 LectureDomain(전공/영역) 테이블을 따로 추가했다. +예를들어, +![image](https://user-images.githubusercontent.com/90256209/229328515-3fa2a18f-efb0-4c1c-b6b3-b38727d8a8a1.png) +'한국근현대사' 강의가 lectureDomain_id로 3을 가지면, 인문계열 강의라는 뜻이고, parent_id를 통해 거슬러 올라갈 수 있다! +parent_id의 default는 0으로 최상위 영역이다. +강의 시간에 대해서도 고민이 됐는데 강의 테이블에 string으로 넣었다. + +--- +### 겪은 오류와 해결 과정 +처음에 model을 작성할때 굳이 역참조 안해도 되는 테이블들에 related_name을 다 넣었더니 Reverse accessor for ~ clashes with reverse accessor for ~ 에러가 났다. 구글링해보니 related_name이 필수인 경우는 한 테이블(클래스)에서 서로 다른 두 컬럼(필드)이 같은 테이블(클래스)을 참조하는 경우뿐이어서 나의 경우에는 해당되지 않아 삭제했다. + +오류는 안났지만 User를 OneToOne 방식으로 확장할때 User에서 email, password, username과 같은 필드를 기본 제공해준다는걸 까먹고 Profile에 중복되는 필드를 넣어서 나중에 삭제했다. + +--- +### 새롭게 배운 점 +ManyToMany(다대다)관계를 설정하는 방법에 대해 알게되었다. 다대다 관계에서 중간테이블을 만든다는건 알고있었지만 Django에서 어떻게 모델링하는지(through를 통해서)는 처음 알게됐다. through는 반드시 다대다 관계중에서 한곳에만 선언해야 한다. 다대다 관련해서는 더 공부해야겠다,, +[참고1](https://velog.io/@jiffydev/Django-9.-ManyToManyField-1) [참고2](https://hoorooroob.tistory.com/entry/%ED%95%B4%EC%84%A4%EA%B3%BC-%ED%95%A8%EA%BB%98-%EC%9D%BD%EB%8A%94-Django-%EB%AC%B8%EC%84%9C-Models-%EB%8B%A4%EB%8C%80%EB%8B%A4-%EA%B4%80%EA%B3%84%EC%97%90%EC%84%9C%EC%9D%98-%EC%B6%94%EA%B0%80-%ED%95%84%EB%93%9C) + +--- +### 느낀점 +장고를 쓰다보니 편한점들이 보이는것 같다. 일단 admin.py 기능은 최고야,, User 클래스에서 이미 많은 필드가 정의되어있는것도 신기했다. +ERD 설계는 언제해도 고민되는 부분이 많은것 같다. 하다보니까 진짜 에브리타임 DB는 어떻게 되어있을지 궁금해졌다. 테이블이 엄청엄청 많겠지? +모델을 분리했어야 하는데 이번 과제는 다른일들때문에 너무 늦게시작해서 아쉬움이 남는다 담부턴 더 미리 시작하자 From 680165c21651a779370b9042d81705b22ae463e8 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Tue, 11 Apr 2023 12:54:16 +0900 Subject: [PATCH 08/44] Add DRF settings and remove migration files --- api/migrations/0001_initial.py | 275 -------------------- api/migrations/0002_auto_20230402_0802.py | 25 -- api/migrations/0003_auto_20230402_1115.py | 23 -- django_rest_framework_17th/settings/base.py | 1 + 4 files changed, 1 insertion(+), 323 deletions(-) delete mode 100644 api/migrations/0001_initial.py delete mode 100644 api/migrations/0002_auto_20230402_0802.py delete mode 100644 api/migrations/0003_auto_20230402_1115.py diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py deleted file mode 100644 index dc0eaf5..0000000 --- a/api/migrations/0001_initial.py +++ /dev/null @@ -1,275 +0,0 @@ -# Generated by Django 3.2.16 on 2023-04-02 01:30 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Board', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=60)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('contents', models.CharField(max_length=200)), - ('parent', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='api.comment')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Lecture', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=150)), - ('collegeYear', models.CharField(default='0', max_length=10)), - ('credit', models.CharField(max_length=10)), - ('category', models.CharField(max_length=100)), - ('professor', models.CharField(max_length=100)), - ('lectureCode', models.CharField(max_length=60)), - ('classRoom', models.CharField(max_length=100)), - ('dayAndTime', models.CharField(max_length=100)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='LectureReview', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('rating', models.CharField(max_length=10)), - ('contents', models.CharField(max_length=200)), - ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.lecture')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Post', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('title', models.CharField(max_length=100, null=True)), - ('contents', models.CharField(max_length=200)), - ('isAnonymous', models.CharField(default='Y', max_length=10)), - ('isQuestion', models.CharField(default='N', max_length=10)), - ('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.board')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Profile', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('nickname', models.CharField(max_length=60)), - ('email', models.EmailField(max_length=60)), - ('password', models.CharField(max_length=200)), - ('profileImgPath', models.TextField(null=True)), - ('friends', models.ManyToManyField(blank=True, related_name='_api_profile_friends_+', to='api.Profile')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='School', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=60)), - ('campus', models.CharField(max_length=60, null=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='TimeTable', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=60)), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='TakeLecture', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.lecture')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Scrap', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='ReviewLike', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('lectureReview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.lecturereview')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to='api.profile')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PostLike', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='post', - name='profile', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), - ), - migrations.CreateModel( - name='Photo', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('path', models.TextField()), - ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='lecturereview', - name='profile', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), - ), - migrations.CreateModel( - name='LectureDomain', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=60)), - ('parent', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='api.lecturedomain')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='lecture', - name='lectureDomain', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.lecturedomain'), - ), - migrations.CreateModel( - name='CommentLike', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(default='A', max_length=10)), - ('createdAt', models.DateTimeField(auto_now_add=True)), - ('updatedAt', models.DateTimeField(auto_now=True)), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.comment')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='comment', - name='post', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.post'), - ), - migrations.AddField( - model_name='comment', - name='profile', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), - ), - migrations.AddField( - model_name='board', - name='profile', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.profile'), - ), - migrations.AddField( - model_name='board', - name='school', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.school'), - ), - ] diff --git a/api/migrations/0002_auto_20230402_0802.py b/api/migrations/0002_auto_20230402_0802.py deleted file mode 100644 index 03f97fc..0000000 --- a/api/migrations/0002_auto_20230402_0802.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.2.16 on 2023-04-02 08:02 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='profile', - name='email', - ), - migrations.RemoveField( - model_name='profile', - name='nickname', - ), - migrations.RemoveField( - model_name='profile', - name='password', - ), - ] diff --git a/api/migrations/0003_auto_20230402_1115.py b/api/migrations/0003_auto_20230402_1115.py deleted file mode 100644 index bd82eb1..0000000 --- a/api/migrations/0003_auto_20230402_1115.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.16 on 2023-04-02 11:15 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_auto_20230402_0802'), - ] - - operations = [ - migrations.RemoveField( - model_name='takelecture', - name='profile', - ), - migrations.AddField( - model_name='takelecture', - name='timeTable', - field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to='api.timetable'), - ), - ] diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 744f7c3..aa7e901 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -41,6 +41,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'api', + 'rest_framework' ] MIDDLEWARE = [ From 143033de40997c89f864cbda8179c20f06fcab61 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Tue, 11 Apr 2023 14:15:18 +0900 Subject: [PATCH 09/44] Refactor packages --- {api => account}/__init__.py | 0 account/admin.py | 6 + account/apps.py | 6 + {api => account}/migrations/__init__.py | 0 account/models.py | 20 ++++ {api => account}/tests.py | 0 {api => account}/views.py | 0 api/admin.py | 19 --- api/apps.py | 5 - api/models.py | 149 ------------------------ community/__init__.py | 0 community/admin.py | 11 ++ community/apps.py | 6 + community/migrations/__init__.py | 0 community/models.py | 65 +++++++++++ community/tests.py | 3 + community/views.py | 3 + timetable/__init__.py | 0 timetable/admin.py | 10 ++ timetable/apps.py | 6 + timetable/migrations/__init__.py | 0 timetable/models.py | 60 ++++++++++ timetable/tests.py | 3 + timetable/views.py | 3 + utils/__init__.py | 0 utils/admin.py | 3 + utils/apps.py | 6 + utils/migrations/__init__.py | 0 utils/models.py | 10 ++ utils/tests.py | 3 + utils/views.py | 3 + 31 files changed, 227 insertions(+), 173 deletions(-) rename {api => account}/__init__.py (100%) create mode 100644 account/admin.py create mode 100644 account/apps.py rename {api => account}/migrations/__init__.py (100%) create mode 100644 account/models.py rename {api => account}/tests.py (100%) rename {api => account}/views.py (100%) delete mode 100644 api/admin.py delete mode 100644 api/apps.py delete mode 100644 api/models.py create mode 100644 community/__init__.py create mode 100644 community/admin.py create mode 100644 community/apps.py create mode 100644 community/migrations/__init__.py create mode 100644 community/models.py create mode 100644 community/tests.py create mode 100644 community/views.py create mode 100644 timetable/__init__.py create mode 100644 timetable/admin.py create mode 100644 timetable/apps.py create mode 100644 timetable/migrations/__init__.py create mode 100644 timetable/models.py create mode 100644 timetable/tests.py create mode 100644 timetable/views.py create mode 100644 utils/__init__.py create mode 100644 utils/admin.py create mode 100644 utils/apps.py create mode 100644 utils/migrations/__init__.py create mode 100644 utils/models.py create mode 100644 utils/tests.py create mode 100644 utils/views.py diff --git a/api/__init__.py b/account/__init__.py similarity index 100% rename from api/__init__.py rename to account/__init__.py diff --git a/account/admin.py b/account/admin.py new file mode 100644 index 0000000..1e541bd --- /dev/null +++ b/account/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from account.models import * + +admin.site.register(Profile) +admin.site.register(School) diff --git a/account/apps.py b/account/apps.py new file mode 100644 index 0000000..50ab6e3 --- /dev/null +++ b/account/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'account' diff --git a/api/migrations/__init__.py b/account/migrations/__init__.py similarity index 100% rename from api/migrations/__init__.py rename to account/migrations/__init__.py diff --git a/account/models.py b/account/models.py new file mode 100644 index 0000000..629d059 --- /dev/null +++ b/account/models.py @@ -0,0 +1,20 @@ +from django.db import models +from django.contrib.auth.models import User +from utils.models import BaseModel + + +class Profile(BaseModel): + user = models.OneToOneField(User, on_delete=models.CASCADE) + profile_img_path = models.URLField(blank=True) + friends = models.ManyToManyField('self', blank=True) + + def __str__(self): + return self.user.username + + +class School(BaseModel): + name = models.CharField(max_length=60) + campus = models.CharField(max_length=60, blank=True) + + def __str__(self): + return self.name diff --git a/api/tests.py b/account/tests.py similarity index 100% rename from api/tests.py rename to account/tests.py diff --git a/api/views.py b/account/views.py similarity index 100% rename from api/views.py rename to account/views.py diff --git a/api/admin.py b/api/admin.py deleted file mode 100644 index 7397f14..0000000 --- a/api/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin - -from api.models import * - -admin.site.register(Profile) -admin.site.register(School) -admin.site.register(Board) -admin.site.register(Post) -admin.site.register(Comment) -admin.site.register(Photo) -admin.site.register(PostLike) -admin.site.register(CommentLike) -admin.site.register(Scrap) -admin.site.register(TimeTable) -admin.site.register(LectureDomain) -admin.site.register(Lecture) -admin.site.register(TakeLecture) -admin.site.register(LectureReview) -admin.site.register(ReviewLike) \ No newline at end of file diff --git a/api/apps.py b/api/apps.py deleted file mode 100644 index d87006d..0000000 --- a/api/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - name = 'api' diff --git a/api/models.py b/api/models.py deleted file mode 100644 index 5c82ae0..0000000 --- a/api/models.py +++ /dev/null @@ -1,149 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User - - -class BaseModel(models.Model): - status = models.CharField(max_length=10, default='A') - createdAt = models.DateTimeField(auto_now_add=True) - updatedAt = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True - - -class Profile(BaseModel): - user = models.OneToOneField(User, on_delete=models.CASCADE) - profileImgPath = models.TextField(null=True) - friends = models.ManyToManyField('self', blank=True) - - def __str__(self): - return self.user.username - - -class School(BaseModel): - name = models.CharField(max_length=60) - campus = models.CharField(max_length=60, null=True) - - def __str__(self): - return self.name - - -class Board(BaseModel): - name = models.CharField(max_length=60) - school = models.ForeignKey("School", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - - def __str__(self): - return self.name - - -class Post(BaseModel): - board = models.ForeignKey("Board", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - title = models.CharField(max_length=100, null=True) - contents = models.CharField(max_length=200) - isAnonymous = models.CharField(max_length=10, default='Y') - isQuestion = models.CharField(max_length=10, default='N') - - def __str__(self): - return self.contents - - -class Photo(BaseModel): - post = models.ForeignKey("Post", on_delete=models.CASCADE) - path = models.TextField() - - def __str__(self): - return f"{self.path} included in {self.post}" - - -class Comment(BaseModel): - post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) - contents = models.CharField(max_length=200) - - def __str__(self): - return self.contents - - -class PostLike(BaseModel): - post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - - def __str__(self): - return f"{self.profile} 's like on {self.post}" - - -class CommentLike(BaseModel): - comment = models.ForeignKey("Comment", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - - def __str__(self): - return f"{self.profile} 's like on {self.comment}" - - -class Scrap(BaseModel): - post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - - def __str__(self): - return f"{self.profile} 's scrap of {self.post}" - - -class TimeTable(BaseModel): - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - name = models.CharField(max_length=60) - lecture = models.ManyToManyField("Lecture", through="TakeLecture") - - def __str__(self): - return self.name - - -class LectureDomain(BaseModel): - name = models.CharField(max_length=60) - parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) - - def __str__(self): - return self.name - - -class Lecture(BaseModel): - name = models.CharField(max_length=150) - lectureDomain = models.ForeignKey("LectureDomain", on_delete=models.CASCADE, null=True) - collegeYear = models.CharField(max_length=10, default='0') - credit = models.CharField(max_length=10) - category = models.CharField(max_length=100) - professor = models.CharField(max_length=100) - lectureCode = models.CharField(max_length=60) - classRoom = models.CharField(max_length=100) - dayAndTime = models.CharField(max_length=100) - - def __str__(self): - return self.name - - -class TakeLecture(BaseModel): - timeTable = models.ForeignKey("TimeTable", on_delete=models.CASCADE, default='') - lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) - - def __str__(self): - return f"{self.profile} started taking {self.lecture}" - - -class LectureReview(BaseModel): - lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) - rating = models.CharField(max_length=10) - contents = models.CharField(max_length=200) - - def __str__(self): - return self.contents - - -class ReviewLike(BaseModel): - lectureReview = models.ForeignKey("LectureReview", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profiles", on_delete=models.CASCADE) - - def __str__(self): - return f"{self.profile} 's like on {self.lectureReview}" diff --git a/community/__init__.py b/community/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/community/admin.py b/community/admin.py new file mode 100644 index 0000000..8e44f32 --- /dev/null +++ b/community/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from community.models import * + +admin.site.register(Board) +admin.site.register(Post) +admin.site.register(Comment) +admin.site.register(Photo) +admin.site.register(PostLike) +admin.site.register(CommentLike) +admin.site.register(Scrap) diff --git a/community/apps.py b/community/apps.py new file mode 100644 index 0000000..4f52712 --- /dev/null +++ b/community/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommunityConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'community' diff --git a/community/migrations/__init__.py b/community/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/community/models.py b/community/models.py new file mode 100644 index 0000000..4c7ea25 --- /dev/null +++ b/community/models.py @@ -0,0 +1,65 @@ +from django.db import models +from utils.models import BaseModel + + +class Board(BaseModel): + name = models.CharField(max_length=60) + school = models.ForeignKey("School", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return self.name + + +class Post(BaseModel): + board = models.ForeignKey("Board", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + title = models.CharField(max_length=100, blank=True) + contents = models.CharField(max_length=200) + is_anonymous = models.CharField(max_length=10, default='Y') + is_question = models.CharField(max_length=10, default='N') + + def __str__(self): + return self.contents + + +class Photo(BaseModel): + post = models.ForeignKey("Post", on_delete=models.CASCADE) + path = models.TextField() + + def __str__(self): + return f"{self.path} included in {self.post}" + + +class Comment(BaseModel): + post = models.ForeignKey("Post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + contents = models.CharField(max_length=200) + + def __str__(self): + return self.contents + + +class PostLike(BaseModel): + post = models.ForeignKey("Post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's like on {self.post}" + + +class CommentLike(BaseModel): + comment = models.ForeignKey("Comment", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's like on {self.comment}" + + +class Scrap(BaseModel): + post = models.ForeignKey("Post", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's scrap of {self.post}" diff --git a/community/tests.py b/community/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/community/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/community/views.py b/community/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/community/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/timetable/__init__.py b/timetable/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/timetable/admin.py b/timetable/admin.py new file mode 100644 index 0000000..8b227c9 --- /dev/null +++ b/timetable/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from timetable.models import * + +admin.site.register(TimeTable) +admin.site.register(LectureDomain) +admin.site.register(Lecture) +admin.site.register(TakeLecture) +admin.site.register(LectureReview) +admin.site.register(ReviewLike) diff --git a/timetable/apps.py b/timetable/apps.py new file mode 100644 index 0000000..f3abe50 --- /dev/null +++ b/timetable/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TimetableConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'timetable' diff --git a/timetable/migrations/__init__.py b/timetable/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/timetable/models.py b/timetable/models.py new file mode 100644 index 0000000..af4f897 --- /dev/null +++ b/timetable/models.py @@ -0,0 +1,60 @@ +from django.db import models +from utils.models import BaseModel + + +class TimeTable(BaseModel): + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + name = models.CharField(max_length=60) + lecture = models.ManyToManyField("Lecture", through="TakeLecture") + + def __str__(self): + return self.name + + +class LectureDomain(BaseModel): + name = models.CharField(max_length=60) + parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + + def __str__(self): + return self.name + + +class Lecture(BaseModel): + name = models.CharField(max_length=150) + lectureDomain = models.ForeignKey("LectureDomain", on_delete=models.CASCADE, blank=True) + collegeYear = models.CharField(max_length=10, default='0') + credit = models.CharField(max_length=10) + category = models.CharField(max_length=100) + professor = models.CharField(max_length=100) + lectureCode = models.CharField(max_length=60) + classRoom = models.CharField(max_length=100) + dayAndTime = models.CharField(max_length=100) + + def __str__(self): + return self.name + + +class TakeLecture(BaseModel): + timeTable = models.ForeignKey("TimeTable", on_delete=models.CASCADE, default='') + lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} started taking {self.lecture}" + + +class LectureReview(BaseModel): + lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + rating = models.CharField(max_length=10) + contents = models.CharField(max_length=200) + + def __str__(self): + return self.contents + + +class ReviewLike(BaseModel): + lectureReview = models.ForeignKey("LectureReview", on_delete=models.CASCADE) + profile = models.ForeignKey("Profile", related_name="profiles", on_delete=models.CASCADE) + + def __str__(self): + return f"{self.profile} 's like on {self.lectureReview}" diff --git a/timetable/tests.py b/timetable/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/timetable/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/timetable/views.py b/timetable/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/timetable/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/admin.py b/utils/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/utils/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/utils/apps.py b/utils/apps.py new file mode 100644 index 0000000..83e83de --- /dev/null +++ b/utils/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UtilsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'utils' diff --git a/utils/migrations/__init__.py b/utils/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/models.py b/utils/models.py new file mode 100644 index 0000000..3f15f2b --- /dev/null +++ b/utils/models.py @@ -0,0 +1,10 @@ +from django.db import models + + +class BaseModel(models.Model): + status = models.CharField(max_length=10, default='A') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/utils/tests.py b/utils/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/utils/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/utils/views.py b/utils/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/utils/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 3401ba7a5963b80ecdcc83e5817271fb212f8619 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Tue, 11 Apr 2023 14:42:42 +0900 Subject: [PATCH 10/44] Add new apps in INSTALLED_APPS --- django_rest_framework_17th/settings/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index aa7e901..5dac2e4 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -40,8 +40,11 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'api', - 'rest_framework' + 'rest_framework', + 'account', + 'community', + 'timetable', + 'utils' ] MIDDLEWARE = [ From 8b53001bdc6c40103687bda5a288410884d1d3e3 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Wed, 12 Apr 2023 17:57:03 +0900 Subject: [PATCH 11/44] Add serializers --- account/migrations/0001_initial.py | 46 +++++++ account/migrations/0002_profile_school.py | 20 +++ account/models.py | 1 + account/serializers.py | 16 +++ account/urls.py | 0 community/migrations/0001_initial.py | 129 ++++++++++++++++++++ community/models.py | 15 +-- community/serializers.py | 77 ++++++++++++ community/urls.py | 0 django_rest_framework_17th/settings/base.py | 2 +- django_rest_framework_17th/urls.py | 3 +- requirements.txt | 3 + timetable/migrations/0001_initial.py | 118 ++++++++++++++++++ timetable/models.py | 7 +- timetable/serializers.py | 0 timetable/urls.py | 0 16 files changed, 425 insertions(+), 12 deletions(-) create mode 100644 account/migrations/0001_initial.py create mode 100644 account/migrations/0002_profile_school.py create mode 100644 account/serializers.py create mode 100644 account/urls.py create mode 100644 community/migrations/0001_initial.py create mode 100644 community/serializers.py create mode 100644 community/urls.py create mode 100644 timetable/migrations/0001_initial.py create mode 100644 timetable/serializers.py create mode 100644 timetable/urls.py diff --git a/account/migrations/0001_initial.py b/account/migrations/0001_initial.py new file mode 100644 index 0000000..bdd029e --- /dev/null +++ b/account/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.16 on 2023-04-11 14:56 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='School', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('campus', models.CharField(blank=True, max_length=60)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('profile_img_path', models.URLField(blank=True)), + ('friends', models.ManyToManyField(blank=True, related_name='_account_profile_friends_+', to='account.Profile')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/account/migrations/0002_profile_school.py b/account/migrations/0002_profile_school.py new file mode 100644 index 0000000..2c73ff4 --- /dev/null +++ b/account/migrations/0002_profile_school.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.16 on 2023-04-11 15:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='school', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='account.school'), + preserve_default=False, + ), + ] diff --git a/account/models.py b/account/models.py index 629d059..995c48e 100644 --- a/account/models.py +++ b/account/models.py @@ -7,6 +7,7 @@ class Profile(BaseModel): user = models.OneToOneField(User, on_delete=models.CASCADE) profile_img_path = models.URLField(blank=True) friends = models.ManyToManyField('self', blank=True) + school = models.ForeignKey("School", on_delete=models.CASCADE) def __str__(self): return self.user.username diff --git a/account/serializers.py b/account/serializers.py new file mode 100644 index 0000000..54d9b56 --- /dev/null +++ b/account/serializers.py @@ -0,0 +1,16 @@ +from rest_framework import serializers +from account.models import * + + +class SchoolSerializer(serializers.ModelSerializer): + class Meta: + model = School + fields = '__all__' + + +class ProfileSerializer(serializers.ModelSerializer): + school = SchoolSerializer(read_only=True) + + class Meta: + model = Profile + fields = '__all__' diff --git a/account/urls.py b/account/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/community/migrations/0001_initial.py b/community/migrations/0001_initial.py new file mode 100644 index 0000000..98a375f --- /dev/null +++ b/community/migrations/0001_initial.py @@ -0,0 +1,129 @@ +# Generated by Django 3.2.16 on 2023-04-11 14:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Board', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.school')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('contents', models.CharField(max_length=200)), + ('parent', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='community.comment')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('title', models.CharField(blank=True, max_length=100)), + ('contents', models.CharField(max_length=200)), + ('is_anonymous', models.CharField(default='Y', max_length=10)), + ('is_question', models.CharField(default='N', max_length=10)), + ('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.board')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Scrap', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.post')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PostLike', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.post')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Photo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('path', models.TextField()), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.post')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CommentLike', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.comment')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='comment', + name='post', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='community.post'), + ), + migrations.AddField( + model_name='comment', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile'), + ), + ] diff --git a/community/models.py b/community/models.py index 4c7ea25..c9a2b6f 100644 --- a/community/models.py +++ b/community/models.py @@ -1,11 +1,12 @@ from django.db import models from utils.models import BaseModel +from account.models import * class Board(BaseModel): name = models.CharField(max_length=60) - school = models.ForeignKey("School", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + school = models.ForeignKey("account.School", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) def __str__(self): return self.name @@ -13,7 +14,7 @@ def __str__(self): class Post(BaseModel): board = models.ForeignKey("Board", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) title = models.CharField(max_length=100, blank=True) contents = models.CharField(max_length=200) is_anonymous = models.CharField(max_length=10, default='Y') @@ -33,7 +34,7 @@ def __str__(self): class Comment(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) contents = models.CharField(max_length=200) @@ -43,7 +44,7 @@ def __str__(self): class PostLike(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) def __str__(self): return f"{self.profile} 's like on {self.post}" @@ -51,7 +52,7 @@ def __str__(self): class CommentLike(BaseModel): comment = models.ForeignKey("Comment", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) def __str__(self): return f"{self.profile} 's like on {self.comment}" @@ -59,7 +60,7 @@ def __str__(self): class Scrap(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) def __str__(self): return f"{self.profile} 's scrap of {self.post}" diff --git a/community/serializers.py b/community/serializers.py new file mode 100644 index 0000000..ca5120f --- /dev/null +++ b/community/serializers.py @@ -0,0 +1,77 @@ +from rest_framework import serializers +from community.models import * + + +class BoardSerializer(serializers.ModelSerializer): + school = serializers.StringRelatedField() + + class Meta: + model = Board + fields = ['name', 'school'] + + +class CommentSerializer(serializers.ModelSerializer): + profile_username = serializers.SerializerMethodField() + profile_profile_img_path = serializers.SerializerMethodField() + + class Meta: + model = Comment + fields = ['id', 'post', 'profile', 'profile_username', 'profile_profile_img_path', 'parent', 'contents', + 'created_at'] + + def get_profile_username(self, obj): + return obj.profile.user.username + + def get_profile_profile_img_path(self, obj): + return obj.profile.profile_img_path + + +class PostSerializer(serializers.ModelSerializer): + profile_username = serializers.SerializerMethodField() + profile_profile_img_path = serializers.SerializerMethodField() + comment_set = CommentSerializer(many=True, read_only=True) + + class Meta: + model = Post + fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'title', 'contents', 'is_anonymous', + 'is_question', 'created_at', 'updated_at', 'comment_set'] + + def get_profile_username(self, obj): + return obj.profile.user.username + + def get_profile_profile_img_path(self, obj): + return obj.profile.profile_img_path + + +class PostListSerializer(serializers.ModelSerializer): + profile_username = serializers.SerializerMethodField() + profile_profile_img_path = serializers.SerializerMethodField() + + class Meta: + model = Post + fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'title', 'contents', 'is_anonymous', + 'is_question', 'created_at', 'updated_at'] + + +class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + fields = '__all__' + + +class PostLikeSerializer(serializers.ModelSerializer): + class Meta: + model = PostLike + fields = '__all__' + + +class CommentLikeSerializer(serializers.ModelSerializer): + class Meta: + model = CommentLike + fields = '__all__' + + +class ScrapSerializer(serializers.ModelSerializer): + class Meta: + model = Scrap + fields = '__all__' diff --git a/community/urls.py b/community/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 5dac2e4..eca1df8 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -44,7 +44,7 @@ 'account', 'community', 'timetable', - 'utils' + 'utils', ] MIDDLEWARE = [ diff --git a/django_rest_framework_17th/urls.py b/django_rest_framework_17th/urls.py index cf9bedd..85bb00d 100644 --- a/django_rest_framework_17th/urls.py +++ b/django_rest_framework_17th/urls.py @@ -14,8 +14,9 @@ 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), + path('community/', include('community.urls')), ] diff --git a/requirements.txt b/requirements.txt index 31cf4e5..f9287ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,6 @@ django-environ==0.4.5 gunicorn==20.0.4 pytz==2020.1 sqlparse==0.3.1 + +djangorestframework~=3.14.0 +environ~=1.0 \ No newline at end of file diff --git a/timetable/migrations/0001_initial.py b/timetable/migrations/0001_initial.py new file mode 100644 index 0000000..79e1c95 --- /dev/null +++ b/timetable/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# Generated by Django 3.2.16 on 2023-04-11 14:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Lecture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=150)), + ('collegeYear', models.CharField(default='0', max_length=10)), + ('credit', models.CharField(max_length=10)), + ('category', models.CharField(max_length=100)), + ('professor', models.CharField(max_length=100)), + ('lectureCode', models.CharField(max_length=60)), + ('classRoom', models.CharField(max_length=100)), + ('dayAndTime', models.CharField(max_length=100)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LectureReview', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('rating', models.CharField(max_length=10)), + ('contents', models.CharField(max_length=200)), + ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='timetable.lecture')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TakeLecture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='timetable.lecture')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TimeTable', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('lecture', models.ManyToManyField(through='timetable.TakeLecture', to='timetable.Lecture')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='takelecture', + name='timeTable', + field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to='timetable.timetable'), + ), + migrations.CreateModel( + name='ReviewLike', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lectureReview', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='timetable.lecturereview')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to='account.profile')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LectureDomain', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(default='A', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=60)), + ('parent', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='timetable.lecturedomain')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='lecture', + name='lectureDomain', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='timetable.lecturedomain'), + ), + ] diff --git a/timetable/models.py b/timetable/models.py index af4f897..bf4d34b 100644 --- a/timetable/models.py +++ b/timetable/models.py @@ -1,9 +1,10 @@ from django.db import models from utils.models import BaseModel +from account.models import * class TimeTable(BaseModel): - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) name = models.CharField(max_length=60) lecture = models.ManyToManyField("Lecture", through="TakeLecture") @@ -44,7 +45,7 @@ def __str__(self): class LectureReview(BaseModel): lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) rating = models.CharField(max_length=10) contents = models.CharField(max_length=200) @@ -54,7 +55,7 @@ def __str__(self): class ReviewLike(BaseModel): lectureReview = models.ForeignKey("LectureReview", on_delete=models.CASCADE) - profile = models.ForeignKey("Profile", related_name="profiles", on_delete=models.CASCADE) + profile = models.ForeignKey("account.Profile", related_name="profiles", on_delete=models.CASCADE) def __str__(self): return f"{self.profile} 's like on {self.lectureReview}" diff --git a/timetable/serializers.py b/timetable/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/timetable/urls.py b/timetable/urls.py new file mode 100644 index 0000000..e69de29 From 9ecc9d8b3be388a0a0b5013c2bb148e4cc1e9503 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Thu, 13 Apr 2023 19:55:50 +0900 Subject: [PATCH 12/44] Add CBV API in 'community' --- .../migrations/0003_alter_profile_friends.py | 18 ++++++++ .../migrations/0004_alter_profile_friends.py | 18 ++++++++ .../migrations/0002_alter_comment_parent.py | 19 ++++++++ community/models.py | 2 +- community/serializers.py | 16 ++++--- community/urls.py | 8 ++++ community/views.py | 46 ++++++++++++++++++- .../0002_alter_lecturedomain_parent.py | 19 ++++++++ timetable/models.py | 2 +- 9 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 account/migrations/0003_alter_profile_friends.py create mode 100644 account/migrations/0004_alter_profile_friends.py create mode 100644 community/migrations/0002_alter_comment_parent.py create mode 100644 timetable/migrations/0002_alter_lecturedomain_parent.py diff --git a/account/migrations/0003_alter_profile_friends.py b/account/migrations/0003_alter_profile_friends.py new file mode 100644 index 0000000..476d1d2 --- /dev/null +++ b/account/migrations/0003_alter_profile_friends.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-04-13 09:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0002_profile_school'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='friends', + field=models.ManyToManyField(blank=True, null=True, related_name='_account_profile_friends_+', to='account.Profile'), + ), + ] diff --git a/account/migrations/0004_alter_profile_friends.py b/account/migrations/0004_alter_profile_friends.py new file mode 100644 index 0000000..2a47655 --- /dev/null +++ b/account/migrations/0004_alter_profile_friends.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-04-13 09:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0003_alter_profile_friends'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='friends', + field=models.ManyToManyField(blank=True, related_name='_account_profile_friends_+', to='account.Profile'), + ), + ] diff --git a/community/migrations/0002_alter_comment_parent.py b/community/migrations/0002_alter_comment_parent.py new file mode 100644 index 0000000..cc0c13f --- /dev/null +++ b/community/migrations/0002_alter_comment_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.16 on 2023-04-13 09:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='parent', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='community.comment'), + ), + ] diff --git a/community/models.py b/community/models.py index c9a2b6f..e875bf5 100644 --- a/community/models.py +++ b/community/models.py @@ -35,7 +35,7 @@ def __str__(self): class Comment(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) - parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True) contents = models.CharField(max_length=200) def __str__(self): diff --git a/community/serializers.py b/community/serializers.py index ca5120f..c4bf993 100644 --- a/community/serializers.py +++ b/community/serializers.py @@ -3,11 +3,9 @@ class BoardSerializer(serializers.ModelSerializer): - school = serializers.StringRelatedField() - class Meta: model = Board - fields = ['name', 'school'] + fields = ['id', 'name'] class CommentSerializer(serializers.ModelSerializer): @@ -16,7 +14,7 @@ class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment - fields = ['id', 'post', 'profile', 'profile_username', 'profile_profile_img_path', 'parent', 'contents', + fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'parent', 'contents', 'status', 'created_at'] def get_profile_username(self, obj): @@ -34,7 +32,7 @@ class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'title', 'contents', 'is_anonymous', - 'is_question', 'created_at', 'updated_at', 'comment_set'] + 'is_question', 'status', 'created_at', 'updated_at', 'comment_set'] def get_profile_username(self, obj): return obj.profile.user.username @@ -50,7 +48,13 @@ class PostListSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'title', 'contents', 'is_anonymous', - 'is_question', 'created_at', 'updated_at'] + 'is_question', 'status', 'created_at', 'updated_at'] + + def get_profile_username(self, obj): + return obj.profile.user.username + + def get_profile_profile_img_path(self, obj): + return obj.profile.profile_img_path class PhotoSerializer(serializers.ModelSerializer): diff --git a/community/urls.py b/community/urls.py index e69de29..fff4714 100644 --- a/community/urls.py +++ b/community/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from community import views + +urlpatterns = [ + path('boards/', views.BoardList.as_view()), + path('boards//', views.PostList.as_view()), + path('posts//', views.PostDetail.as_view()), +] \ No newline at end of file diff --git a/community/views.py b/community/views.py index 91ea44a..cce70da 100644 --- a/community/views.py +++ b/community/views.py @@ -1,3 +1,47 @@ from django.shortcuts import render +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from .serializers import * +from .models import * +from rest_framework.generics import get_object_or_404 -# Create your views here. + +class BoardList(APIView): + def get(self, request, format=None): + boards = Board.objects.filter(status='A') + serializer = BoardSerializer(boards, many=True) + return Response(serializer.data) + + +class PostList(APIView): + def post(self, request, board_id, format=None): + serializer = PostSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(board_id=board_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get(self, request, board_id, format=None): + posts = Post.objects.filter(board_id=board_id, status='A').order_by('-created_at') + serializer = PostListSerializer(posts, many=True) + return Response(serializer.data) + + +class PostDetail(APIView): + def get_object(self, post_id): + post = get_object_or_404(Post, pk=post_id) + return post + + def get(self, request, post_id, format=None): + post = self.get_object(post_id) + serializer = PostSerializer(post) + return Response(serializer.data) + + def put(self, request, post_id, format=None): + post = self.get_object(post_id) + serializer = PostSerializer(post, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/timetable/migrations/0002_alter_lecturedomain_parent.py b/timetable/migrations/0002_alter_lecturedomain_parent.py new file mode 100644 index 0000000..cb3c166 --- /dev/null +++ b/timetable/migrations/0002_alter_lecturedomain_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.16 on 2023-04-13 09:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('timetable', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='lecturedomain', + name='parent', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='timetable.lecturedomain'), + ), + ] diff --git a/timetable/models.py b/timetable/models.py index bf4d34b..8e46d35 100644 --- a/timetable/models.py +++ b/timetable/models.py @@ -14,7 +14,7 @@ def __str__(self): class LectureDomain(BaseModel): name = models.CharField(max_length=60) - parent = models.ForeignKey('self', on_delete=models.CASCADE, default=0) + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True) def __str__(self): return self.name From f592f3bbee0481e3c9252e4a456d222792ca4668 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Thu, 13 Apr 2023 22:44:30 +0900 Subject: [PATCH 13/44] Refactor to ViewSet --- community/filters.py | 0 community/urls.py | 21 +++++++--- community/views.py | 91 +++++++++++++++++++++++++------------------- 3 files changed, 67 insertions(+), 45 deletions(-) create mode 100644 community/filters.py diff --git a/community/filters.py b/community/filters.py new file mode 100644 index 0000000..e69de29 diff --git a/community/urls.py b/community/urls.py index fff4714..ef6e07c 100644 --- a/community/urls.py +++ b/community/urls.py @@ -1,8 +1,19 @@ -from django.urls import path +from django.urls import path, include from community import views +from rest_framework.routers import DefaultRouter +from .views import * + +# urlpatterns = [ +# path('boards/', views.BoardList.as_view()), +# path('boards//', views.PostList.as_view()), +# path('posts//', views.PostDetail.as_view()), +# ] + + +router = DefaultRouter() +router.register('posts', PostViewSet) +router.register('boards', BoardViewSet) urlpatterns = [ - path('boards/', views.BoardList.as_view()), - path('boards//', views.PostList.as_view()), - path('posts//', views.PostDetail.as_view()), -] \ No newline at end of file + path('', include(router.urls)) +] diff --git a/community/views.py b/community/views.py index cce70da..ebc24f8 100644 --- a/community/views.py +++ b/community/views.py @@ -5,43 +5,54 @@ from .serializers import * from .models import * from rest_framework.generics import get_object_or_404 - - -class BoardList(APIView): - def get(self, request, format=None): - boards = Board.objects.filter(status='A') - serializer = BoardSerializer(boards, many=True) - return Response(serializer.data) - - -class PostList(APIView): - def post(self, request, board_id, format=None): - serializer = PostSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(board_id=board_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get(self, request, board_id, format=None): - posts = Post.objects.filter(board_id=board_id, status='A').order_by('-created_at') - serializer = PostListSerializer(posts, many=True) - return Response(serializer.data) - - -class PostDetail(APIView): - def get_object(self, post_id): - post = get_object_or_404(Post, pk=post_id) - return post - - def get(self, request, post_id, format=None): - post = self.get_object(post_id) - serializer = PostSerializer(post) - return Response(serializer.data) - - def put(self, request, post_id, format=None): - post = self.get_object(post_id) - serializer = PostSerializer(post, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +from rest_framework import viewsets + + +# class BoardList(APIView): +# def get(self, request, format=None): +# boards = Board.objects.filter(status='A') +# serializer = BoardSerializer(boards, many=True) +# return Response(serializer.data) +# +# +# class PostList(APIView): +# def post(self, request, board_id, format=None): +# serializer = PostSerializer(data=request.data) +# if serializer.is_valid(): +# serializer.save(board_id=board_id) +# return Response(serializer.data, status=status.HTTP_201_CREATED) +# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +# +# def get(self, request, board_id, format=None): +# posts = Post.objects.filter(board_id=board_id, status='A').order_by('-created_at') +# serializer = PostListSerializer(posts, many=True) +# return Response(serializer.data) +# +# +# class PostDetail(APIView): +# def get_object(self, post_id): +# post = get_object_or_404(Post, pk=post_id) +# return post +# +# def get(self, request, post_id, format=None): +# post = self.get_object(post_id) +# serializer = PostSerializer(post) +# return Response(serializer.data) +# +# def put(self, request, post_id, format=None): +# post = self.get_object(post_id) +# serializer = PostSerializer(post, data=request.data, partial=True) +# if serializer.is_valid(): +# serializer.save() +# return Response(serializer.data) +# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class BoardViewSet(viewsets.ModelViewSet): + serializer_class = BoardSerializer + queryset = Board.objects.all() + + +class PostViewSet(viewsets.ModelViewSet): + serializer_class = PostSerializer + queryset = Post.objects.all() From 930e2fa5114c5cbf42c0e9fcf49918ef4a2e2634 Mon Sep 17 00:00:00 2001 From: hyejunseo Date: Fri, 14 Apr 2023 01:18:32 +0900 Subject: [PATCH 14/44] Add filter class for Post --- community/filters.py | 19 +++++++++++++++++++ community/views.py | 4 ++++ django_rest_framework_17th/settings/base.py | 1 + requirements.txt | 4 +++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/community/filters.py b/community/filters.py index e69de29..0a9f649 100644 --- a/community/filters.py +++ b/community/filters.py @@ -0,0 +1,19 @@ +from django_filters.rest_framework import FilterSet, filters +from .models import * + + +class PostFilter(FilterSet): + # board = filters.NumberFilter(field_name='board_id') + board = filters.NumberFilter(method='filter_board') + profile = filters.NumberFilter(field_name='profile_id') + title = filters.CharFilter(field_name='title', lookup_expr='icontains') + contents = filters.CharFilter(field_name='contents', lookup_expr='icontains') + + class Meta: + model = Post + fields = ['board', 'profile', 'title', 'contents'] + + def filter_board(self, queryset, board_id, value): + return queryset.filter(**{ + board_id: value, + }) diff --git a/community/views.py b/community/views.py index ebc24f8..699087e 100644 --- a/community/views.py +++ b/community/views.py @@ -6,6 +6,8 @@ from .models import * from rest_framework.generics import get_object_or_404 from rest_framework import viewsets +from django_filters.rest_framework import DjangoFilterBackend +from .filters import * # class BoardList(APIView): @@ -56,3 +58,5 @@ class BoardViewSet(viewsets.ModelViewSet): class PostViewSet(viewsets.ModelViewSet): serializer_class = PostSerializer queryset = Post.objects.all() + filter_backends = [DjangoFilterBackend] + filterset_class = PostFilter diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index eca1df8..191c5d5 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -45,6 +45,7 @@ 'community', 'timetable', 'utils', + 'django_filters', ] MIDDLEWARE = [ diff --git a/requirements.txt b/requirements.txt index f9287ef..0bcd50b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,6 @@ pytz==2020.1 sqlparse==0.3.1 djangorestframework~=3.14.0 -environ~=1.0 \ No newline at end of file +environ~=1.0 +serializers~=0.2.4 +django-filter~=23.1 \ No newline at end of file From 7d4fba20bcad7a27afc0b48fc5682c6217f03611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:12:53 +0900 Subject: [PATCH 15/44] Update README.md --- README.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/README.md b/README.md index af2a938..0ca6275 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # CEOS 17기 백엔드 스터디 +## [2주차] + ### ORM 이용해보기 1. 게시글 생성 ![image](https://user-images.githubusercontent.com/90256209/229326431-a573d2b4-03b6-4c8c-a2bf-7e14ad507831.png) @@ -46,3 +48,102 @@ ManyToMany(다대다)관계를 설정하는 방법에 대해 알게되었다. 장고를 쓰다보니 편한점들이 보이는것 같다. 일단 admin.py 기능은 최고야,, User 클래스에서 이미 많은 필드가 정의되어있는것도 신기했다. ERD 설계는 언제해도 고민되는 부분이 많은것 같다. 하다보니까 진짜 에브리타임 DB는 어떻게 되어있을지 궁금해졌다. 테이블이 엄청엄청 많겠지? 모델을 분리했어야 하는데 이번 과제는 다른일들때문에 너무 늦게시작해서 아쉬움이 남는다 담부턴 더 미리 시작하자 + +--- +## [3주차] + +### API 명세 +우선적으로 어떤 CBV API를 만들지 에브리타임을 구경하면서 리스트업해봤다 +![image](https://user-images.githubusercontent.com/90256209/231825029-5d62ead3-c302-45c6-82ed-8cea6ec22733.png) +게시글 삭제 및 수정 API에 대해 말을 하자면, 원래 DELETE 는 사용을 지양해야 한다고 들은지라… (사실 손사래 치시면서 절대 쓰지 말라고 하셨따 하하) 항상 PATCH 로 모델의 status 를 ‘D’로 바꾸는 식으로 개발했었는데 장고에서는 아무리 구글링해봐도 views.py 에서 수정을 이런식으로 구현한게 없더라 그래서 partial 을 활용해서 PUT 으로 status 필드만 수정할 수 있도록 구현했다(그냥 PUT 은 모든 필드를 다 채워서 요청해야해서 불편하니까) 찾아보니까 장고에서는 소프트 딜리트를 하기 위해서 SoftDelete 모델을 구현하는 것 같더라. 어쩐지! 나중에 시간 나면 해봐야겠다 + +--- +### 과제 진행 과정 +1. 패키지 수정 + + 이전에 api 앱 속 [models.py](http://models.py) 안에 모든 모델이 있던 구조를 여러개의 앱으로 분리했다. + +2. migration 초기화 + + 모델 구조를 바꿔서 혹시나 하고 migration 파일들을 [init.py](http://init.py) 빼고 다 삭제했더니 아예 데이터베이스 자체가 삭제되서 에러가 나길래 `create database ceosDB;` 로 다시 만들어줬다. + +3. account / community / timetable 3개의 앱으로 분리하고, BaseModel 정의를 위한 utils 앱도 만들었다. +4. API 명세서 작성 - 이번에는 게시글 관련 API들만 만들어봤다. +5. [serializers.py](http://serializers.py) 작성 + + 게시글(Post)을 가져올때 글쓴이(Profile)의 __PK__, __이름__, __프로필사진URL__ 만 가져오도록 했다. (항상 API의 응답으로 PK는 주는게 좋다, 잊지말자) +6. [views.py](http://views.py) 및 [urls.py](http://urls.py) 작성 +7. 리팩토링 - 피드백 반영 및 Comment 테이블과 LectureDomain 테이블의 parent 컬럼 null 허용 +8. CBV API 작성 +9. ViewSet으로 리팩토링 (짱신기) +10. Filter 기능 구현 + +--- +### 웹 브라우저와 Postman을 통한 API 테스트 + +1. 게시글 생성 API (Method: `POST`, URL: `/community/boards/1/`) +게시판을 지정해서 게시글을 작성해보자 +![image](https://user-images.githubusercontent.com/90256209/231829358-b2af2e20-e2d9-4516-98c6-d8ad87a7ad62.png) + +2. 게시글 수정 및 삭제 API (Method: `PUT`, URL: `/community/posts/1/`) +partial update로 원래는 `'A'`였던 `status`를 `'D'`로 바꿔줌으로써 게시글을 삭제해보자 +물론 당연히 `title`이나 `contents` 수정도 가능하다 +![image](https://user-images.githubusercontent.com/90256209/231832804-54d1b27c-28b4-4ffe-9acd-1d7c2a573b36.png) + +3. 특정 게시글 조회 API (Method: `GET`, URL: `/community/posts/1/`) +특정 게시글을 조회해보자 +방금 삭제했기 때문에 `status`가 `'D'`로 바뀐걸 볼수있다 +![image](https://user-images.githubusercontent.com/90256209/231834896-9d1bd72b-97c9-4c21-915c-f6a42942c3de.png) + +4. 전체 게시판 조회 API (Method: `GET`, URL: `/community/boards/`) +일단은 PK랑 이름만 나오게 했다 +![image](https://user-images.githubusercontent.com/90256209/231837072-5a93ce5f-d33d-44c4-8ec4-2ce1705bb2ad.png) + +5. 특정 게시판 전체 게시글 조회 API (Method: `GET`, URL: `/community/boards/1/`) +전체 게시글이 조회될때 삭제된 게시글은 보이면 안되니까 `status`가 `'A'`인것만 보이도록 _filter_ 해줬다 +원래는 있었는데요 +![image](https://user-images.githubusercontent.com/90256209/231837919-4b764e4a-5e49-4d6c-84ec-b2025317696f.png) +삭제하고나면 없어요 +![image](https://user-images.githubusercontent.com/90256209/231838078-43ede8d3-be4b-440f-861f-acc41be05194.png) + +--- +### ViewSet으로 리팩토링하기 +요건.. 너무 신기했다 이게 될까? 했는데 진짜 되더랑 +몇줄 안되는 코드로 이렇게 특정 게시글 디테일 보기 및 수정/삭제가 된다 +![image](https://user-images.githubusercontent.com/90256209/231839566-90d6656f-6908-4f44-80c5-c779ddba986e.png) + +--- +### Filter 기능 구현하기 +Post가 외래키로 가지는 Board랑 Profile을 서로 다른 방식으로 filter해봤는데 둘다 잘된다. +Board는 +``` +board = filters.NumberFilter(method='filter_board') +# ... +def filter_board(self, queryset, board_id, value): + return queryset.filter(**{ + board_id: value, + }) +``` +이렇게 메소드로 필터링해봤다. +Profile은 `profile = filters.NumberFilter(field_name='profile_id')` 이렇게 NumberFilter로 필터링해줬다. +Title은 `title = filters.CharFilter(field_name='title', lookup_expr='icontains')` 이렇게 'icontains'를 사용해서 검색할 수 있더라 +1. 게시판으로 필터링 +![image](https://user-images.githubusercontent.com/90256209/231843912-bf6032f7-a2bb-4e2a-996d-fd2558233c40.png) +2. 제목으로 필터링 +![image](https://user-images.githubusercontent.com/90256209/231844302-22024d96-df75-4ac3-bb01-c5ee0eec933f.png) +3. 글쓴이로 필터링 +![image](https://user-images.githubusercontent.com/90256209/231844565-8c26fa84-7f3f-4c2f-baa4-d96baf1aa42c.png) + +--- +### 겪은 오류와 해결 과정 +- 앱을 분리하면서 `Field defines a relation with model 'Profile', which is either not installed, or is abstract` 에러가 나서, `models.ForeignKey("account.Profile", on_delete=models.CASCADE)` 이렇게 외래키에 app이름을 명시함으로써 해결했다. +- API 명세를 고민하다가 Profile 클래스에 `school_id` 를 외래키로 안 넣은걸 발견해서 수정했다. NOT NULL로 설정되어 있다보니 migration할 때 에러가 났는데, 이번만 default를 설정하는 옵션이 있어서 그렇게 해결했다. +- 이후의 오류와 해결 과정은.. 너무 많은데 머리 싸매느라 못적었다 + +--- +### 느낀점 +- 장고에서는 어노테이션을 ‘데코레이터’라고 하던데 이름이 뭔가 귀엽다ㅎㅎ +- 장고는 기본 제공해주는 기능들도 많지만 그만큼 custom할 수 있는 요소도 꽤 많은 것 같아서 생각보다 좋당 근데 구글링을 열심히 해도 자료가 좀 부족한 느낌이 있다ㅠㅠ 장고도 글 많이 써주세요 +- 핫게시판의 기준을 오로지 댓글수+공감수로 한다면 filter 로 구현할 수 있을 것 같다 +- 어짜피 실제로 사용할 만한 세분화된 기능들을 개발하려면 ViewSet안에서도 따로 정의해야할게 많은 것 같은데 이럼 CBV보다 더 좋은건지는 아직 잘 모르겠따 +- 다들 중간고사 잘 마무리하고 만나요👻 From 622a467566ee3e435f2abe3d5afc3be432019cc4 Mon Sep 17 00:00:00 2001 From: hyejun Date: Thu, 4 May 2023 19:08:53 +0900 Subject: [PATCH 16/44] [Refact] Add soft-delete, edit custom filtering methods --- account/migrations/0005_auto_20230504_1050.py | 23 +++++++++ community/filters.py | 27 ++++++++--- .../migrations/0003_auto_20230504_1050.py | 48 +++++++++++++++++++ .../migrations/0003_auto_20230504_1050.py | 43 +++++++++++++++++ utils/models.py | 7 +++ 5 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 account/migrations/0005_auto_20230504_1050.py create mode 100644 community/migrations/0003_auto_20230504_1050.py create mode 100644 timetable/migrations/0003_auto_20230504_1050.py diff --git a/account/migrations/0005_auto_20230504_1050.py b/account/migrations/0005_auto_20230504_1050.py new file mode 100644 index 0000000..7ad7f34 --- /dev/null +++ b/account/migrations/0005_auto_20230504_1050.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2023-05-04 10:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0004_alter_profile_friends'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='school', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/community/filters.py b/community/filters.py index 0a9f649..6e52a9a 100644 --- a/community/filters.py +++ b/community/filters.py @@ -5,15 +5,30 @@ class PostFilter(FilterSet): # board = filters.NumberFilter(field_name='board_id') board = filters.NumberFilter(method='filter_board') - profile = filters.NumberFilter(field_name='profile_id') - title = filters.CharFilter(field_name='title', lookup_expr='icontains') - contents = filters.CharFilter(field_name='contents', lookup_expr='icontains') + # profile = filters.NumberFilter(field_name='profile_id') + profile = filters.NumberFilter(method='filter_profile') + # title = filters.CharFilter(field_name='title', lookup_expr='icontains') + title = filters.CharFilter(method='filter_title') + # contents = filters.CharFilter(field_name='contents', lookup_expr='icontains') + contents = filters.CharFilter(method='filter_contents') class Meta: model = Post fields = ['board', 'profile', 'title', 'contents'] + # def filter_board(self, queryset, board_id, value): + # return queryset.filter(**{ + # board_id: value, + # }) + def filter_board(self, queryset, board_id, value): - return queryset.filter(**{ - board_id: value, - }) + return queryset.filter(board_id=value, deleted_at__isnull=True) + + def filter_profile(self, queryset, profile_id, value): + return queryset.filter(profile_id=value, deleted_at__isnull=True) + + def filter_title(self, queryset, title, value): + return queryset.filter(title__icontains=value, deleted_at__isnull=True) + + def filter_contents(self, queryset, contents, value): + return queryset.filter(contents__icontains=value, deleted_at__isnull=True) diff --git a/community/migrations/0003_auto_20230504_1050.py b/community/migrations/0003_auto_20230504_1050.py new file mode 100644 index 0000000..0affe5a --- /dev/null +++ b/community/migrations/0003_auto_20230504_1050.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.16 on 2023-05-04 10:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0002_alter_comment_parent'), + ] + + operations = [ + migrations.AddField( + model_name='board', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='comment', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='commentlike', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='photo', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='post', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='postlike', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='scrap', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/timetable/migrations/0003_auto_20230504_1050.py b/timetable/migrations/0003_auto_20230504_1050.py new file mode 100644 index 0000000..c44564e --- /dev/null +++ b/timetable/migrations/0003_auto_20230504_1050.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.16 on 2023-05-04 10:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('timetable', '0002_alter_lecturedomain_parent'), + ] + + operations = [ + migrations.AddField( + model_name='lecture', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='lecturedomain', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='lecturereview', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='reviewlike', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='takelecture', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='timetable', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/utils/models.py b/utils/models.py index 3f15f2b..c2da14f 100644 --- a/utils/models.py +++ b/utils/models.py @@ -1,10 +1,17 @@ from django.db import models +from datetime import datetime class BaseModel(models.Model): status = models.CharField(max_length=10, default='A') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(null=True, blank=True) class Meta: abstract = True + + def delete(self, using=None, keep_parents=False): + print('test1') + self.deleted_at = datetime.now() + self.save() \ No newline at end of file From a1d4eead14c44222c4617a95938c656cca371d65 Mon Sep 17 00:00:00 2001 From: hyejun Date: Fri, 5 May 2023 14:38:52 +0900 Subject: [PATCH 17/44] [Add] Add simple-jwt settings --- account/models.py | 2 + django_rest_framework_17th/settings/base.py | 44 ++++++++++++++++++++ requirements.txt | Bin 179 -> 544 bytes 3 files changed, 46 insertions(+) diff --git a/account/models.py b/account/models.py index 995c48e..1970a66 100644 --- a/account/models.py +++ b/account/models.py @@ -19,3 +19,5 @@ class School(BaseModel): def __str__(self): return self.name + + diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 191c5d5..52531a5 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -12,6 +12,7 @@ import os import environ +from datetime import timedelta # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -46,8 +47,51 @@ 'timetable', 'utils', 'django_filters', + 'rest_framework_simplejwt', ] +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": False, + "UPDATE_LAST_LOGIN": False, + + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + "VERIFYING_KEY": "", + "AUDIENCE": None, + "ISSUER": None, + "JSON_ENCODER": None, + "JWK_URL": None, + "LEEWAY": 0, + + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", + "USER_ID_FIELD": "id", + "USER_ID_CLAIM": "user_id", + "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", + + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "TOKEN_TYPE_CLAIM": "token_type", + "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", + + "JTI_CLAIM": "jti", + + "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", + "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), + "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/requirements.txt b/requirements.txt index 0bcd50b1cafa9fe3f472e8e77825ab4c71a4d1e3..4cc5256322223499b2740dfa108a86589a7244c4 100644 GIT binary patch literal 544 zcmZ{iI}d_D5QL{T@uz4!z(-?eWo2PvO%Mz|Ko6D3k5^}RBbul&XCN~>_w9toJw}BM za(JZ3Fy${qA0wUty69krEn*b(l&u<2b@Yh7{?L2OC~(!Zl24L0pf}?s4Xn zS8t$qyDz7(RrF=_#W?V-CXL6gC31R)5q}c>pX`ylW~MMY-ADc{;d_g>mf^ywi}v-} z&tbyZP`NkM3htFCp>+^w3)X~mcEFAMoVa_!lWS{6V$Djs;#<%+#|q1qa;&7H9jTqs ns3Q4xhs{;mXwI%Az0tQyox&h@M4E35NBTT@($*ngfF(TvI6zXS literal 179 zcmXZWK@I{T3{k~MO8(5Lae>peGv dHKVzsQwVVmC2z#iHSD!{JP>nPi933Y{Qz2KI9UJy From e27395e4aafae246dd7d8a2a159f17dc60661c8c Mon Sep 17 00:00:00 2001 From: hyejun Date: Fri, 5 May 2023 17:04:44 +0900 Subject: [PATCH 18/44] [Add] Add custom user model, manager --- account/models.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/account/models.py b/account/models.py index 1970a66..7daca05 100644 --- a/account/models.py +++ b/account/models.py @@ -1,6 +1,53 @@ from django.db import models from django.contrib.auth.models import User from utils.models import BaseModel +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser + + +class MyUserManager(BaseUserManager): + def create_user(self, email, nickname, school_id, password=None, **extra_fields): + if not email: + raise ValueError("Users must have an email address") + + user = self.model( + email=self.normalize_email(email), + nickname=nickname, + school_id=school_id, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, nickname, password=None, **extra_fields): + superuser = self.create_user( + email, + nickname=nickname, + ) + superuser.is_admin = True + superuser.save(using=self._db) + return superuser + + +class MyUser(AbstractBaseUser): + email = models.EmailField(max_length=255, unique=True) + nickname = models.CharField(max_length=100, unique=True) + # password, last_login 은 기본 제공 + profile_img_path = models.URLField(blank=True, null=True) + friends = models.ManyToManyField('self', blank=True, null=True) + school = models.ForeignKey("School", on_delete=models.CASCADE) + is_admin = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + objects = MyUserManager() + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["nickname", "school_id"] + + def __str__(self): + return self.email class Profile(BaseModel): From 1219594705e5d6d9e39d66a2f6adf30a87457aa0 Mon Sep 17 00:00:00 2001 From: hyejun Date: Fri, 5 May 2023 18:37:12 +0900 Subject: [PATCH 19/44] [Refact] Edit user model, manager --- account/models.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/account/models.py b/account/models.py index 7daca05..e9b15df 100644 --- a/account/models.py +++ b/account/models.py @@ -5,24 +5,25 @@ class MyUserManager(BaseUserManager): - def create_user(self, email, nickname, school_id, password=None, **extra_fields): + def create_user(self, email, nickname, school_id=None, password=None, **extra_fields): if not email: raise ValueError("Users must have an email address") user = self.model( - email=self.normalize_email(email), + email=email, nickname=nickname, school_id=school_id, ) - user.set_password(password) user.save(using=self._db) return user - def create_superuser(self, email, nickname, password=None, **extra_fields): + def create_superuser(self, email, nickname, school_id=None, password=None, **extra_fields): superuser = self.create_user( - email, + email=email, nickname=nickname, + school_id=school_id, + password=password, ) superuser.is_admin = True superuser.save(using=self._db) @@ -44,7 +45,7 @@ class MyUser(AbstractBaseUser): objects = MyUserManager() USERNAME_FIELD = "email" - REQUIRED_FIELDS = ["nickname", "school_id"] + REQUIRED_FIELDS = ["nickname"] def __str__(self): return self.email From 4727078c7bf5ca7b2a9444c7349bcbe0620d1e1a Mon Sep 17 00:00:00 2001 From: hyejun Date: Fri, 5 May 2023 18:52:00 +0900 Subject: [PATCH 20/44] [Add] Create myUser serializer --- account/admin.py | 1 + account/serializers.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/account/admin.py b/account/admin.py index 1e541bd..753a214 100644 --- a/account/admin.py +++ b/account/admin.py @@ -4,3 +4,4 @@ admin.site.register(Profile) admin.site.register(School) +admin.site.register(MyUser) \ No newline at end of file diff --git a/account/serializers.py b/account/serializers.py index 54d9b56..10b1085 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -2,6 +2,26 @@ from account.models import * +class MyUserSerializer(serializers.ModelSerializer): + class Meta: + model = MyUser + fields = '__all__' + + def create(self, validated_data): + email = validated_data.get('email') + nickname = validated_data.get('nickname') + school_id = validated_data.get('school_id') + password = validated_data.get('password') + user = MyUser( + email=email, + nickname=nickname, + school_id=school_id + ) + user.set_password(password) + user.save() + return user + + class SchoolSerializer(serializers.ModelSerializer): class Meta: model = School From ae17fb44f2e4f73480acd1aae88d304828706e78 Mon Sep 17 00:00:00 2001 From: hyejun Date: Fri, 5 May 2023 20:37:31 +0900 Subject: [PATCH 21/44] [Feat] Register, login, logout --- account/urls.py | 9 +++++ account/views.py | 59 +++++++++++++++++++++++++++++- django_rest_framework_17th/urls.py | 1 + 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/account/urls.py b/account/urls.py index e69de29..52edbc4 100644 --- a/account/urls.py +++ b/account/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, include +from account import views + + +urlpatterns = [ + path("register/", views.RegisterAPIView.as_view()), + path("login/", views.LoginAPIView.as_view()), + path("logout/", views.LogoutAPIView.as_view()), +] diff --git a/account/views.py b/account/views.py index 91ea44a..cdc3d72 100644 --- a/account/views.py +++ b/account/views.py @@ -1,3 +1,60 @@ from django.shortcuts import render +from .serializers import * +from rest_framework.views import APIView +from rest_framework import status +from rest_framework.response import Response +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer +from django.contrib.auth import authenticate -# Create your views here. + +# 회원 가입 +class RegisterAPIView(APIView): + def post(self, request): + serializer = MyUserSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +# 로그인 +class LoginAPIView(APIView): + def post(self, request): + # 유저 인증 + user = authenticate( + email=request.data.get("email"), password=request.data.get("password") + ) + # 해당 이메일, 비밀 번호로 가입한 유저가 있는 경우 + if user is not None: + serializer = MyUserSerializer(user) + # jwt, refresh token 발급 + token = TokenObtainPairSerializer.get_token(user) + refresh_token = str(token) + access_token = str(token.access_token) + res = Response( + { + "user": serializer.data, + "message": "Login success", + "token": { + "access": access_token, + "refresh": refresh_token, + }, + }, + status=status.HTTP_200_OK, + ) + # refresh token 은 쿠키에 저장 + res.set_cookie("refresh", refresh_token, httponly=True) + return res + else: + return Response(status=status.HTTP_400_BAD_REQUEST) + + +# 로그 아웃 +class LogoutAPIView(APIView): + def post(self, request): + # 클라이언트의 쿠키에 저장된 refresh 토큰을 삭제함으로써 로그 아웃 처리 + response = Response({ + "message": "Logout success" + }, status=status.HTTP_202_ACCEPTED) + response.delete_cookie('refresh') + return response diff --git a/django_rest_framework_17th/urls.py b/django_rest_framework_17th/urls.py index 85bb00d..084d4ab 100644 --- a/django_rest_framework_17th/urls.py +++ b/django_rest_framework_17th/urls.py @@ -19,4 +19,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('community/', include('community.urls')), + path('account/', include('account.urls')), ] From e414a64d595103da1dcbdd1f68a214c284980ac2 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 6 May 2023 09:49:29 +0900 Subject: [PATCH 22/44] [Refact] Edit community models' user(fk) --- account/admin.py | 2 +- account/migrations/0006_myuser.py | 34 +++++++++ account/migrations/0007_auto_20230505_2341.py | 24 +++++++ account/models.py | 44 ++++++------ account/serializers.py | 30 ++++---- account/views.py | 14 ++-- community/filters.py | 8 +-- .../migrations/0004_auto_20230505_2341.py | 70 +++++++++++++++++++ community/models.py | 24 ++++--- community/serializers.py | 42 +++++------ django_rest_framework_17th/settings/base.py | 4 +- .../migrations/0004_auto_20230505_2341.py | 43 ++++++++++++ timetable/models.py | 10 +-- 13 files changed, 266 insertions(+), 83 deletions(-) create mode 100644 account/migrations/0006_myuser.py create mode 100644 account/migrations/0007_auto_20230505_2341.py create mode 100644 community/migrations/0004_auto_20230505_2341.py create mode 100644 timetable/migrations/0004_auto_20230505_2341.py diff --git a/account/admin.py b/account/admin.py index 753a214..fc2b686 100644 --- a/account/admin.py +++ b/account/admin.py @@ -2,6 +2,6 @@ from account.models import * -admin.site.register(Profile) +# admin.site.register(Profile) admin.site.register(School) admin.site.register(MyUser) \ No newline at end of file diff --git a/account/migrations/0006_myuser.py b/account/migrations/0006_myuser.py new file mode 100644 index 0000000..3436e11 --- /dev/null +++ b/account/migrations/0006_myuser.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.16 on 2023-05-05 20:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_auto_20230504_1050'), + ] + + operations = [ + migrations.CreateModel( + name='MyUser', + 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')), + ('email', models.EmailField(max_length=255, unique=True)), + ('nickname', models.CharField(max_length=100, unique=True)), + ('profile_img_path', models.URLField(blank=True, null=True)), + ('is_admin', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('friends', models.ManyToManyField(blank=True, null=True, related_name='_account_myuser_friends_+', to='account.MyUser')), + ('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.school')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/account/migrations/0007_auto_20230505_2341.py b/account/migrations/0007_auto_20230505_2341.py new file mode 100644 index 0000000..50bc41b --- /dev/null +++ b/account/migrations/0007_auto_20230505_2341.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.16 on 2023-05-05 23:41 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('timetable', '0004_auto_20230505_2341'), + ('community', '0004_auto_20230505_2341'), + ('account', '0006_myuser'), + ] + + operations = [ + migrations.AlterField( + model_name='myuser', + name='friends', + field=models.ManyToManyField(blank=True, related_name='_account_myuser_friends_+', to=settings.AUTH_USER_MODEL), + ), + migrations.DeleteModel( + name='Profile', + ), + ] diff --git a/account/models.py b/account/models.py index e9b15df..48008e5 100644 --- a/account/models.py +++ b/account/models.py @@ -4,25 +4,33 @@ from django.contrib.auth.models import BaseUserManager, AbstractBaseUser +class School(BaseModel): + name = models.CharField(max_length=60) + campus = models.CharField(max_length=60, blank=True) + + def __str__(self): + return self.name + + class MyUserManager(BaseUserManager): - def create_user(self, email, nickname, school_id=None, password=None, **extra_fields): + def create_user(self, email, nickname, school=None, password=None, **extra_fields): if not email: raise ValueError("Users must have an email address") user = self.model( email=email, nickname=nickname, - school_id=school_id, + school=school, ) user.set_password(password) user.save(using=self._db) return user - def create_superuser(self, email, nickname, school_id=None, password=None, **extra_fields): + def create_superuser(self, email, nickname, school=None, password=None, **extra_fields): superuser = self.create_user( email=email, nickname=nickname, - school_id=school_id, + school=school, password=password, ) superuser.is_admin = True @@ -35,7 +43,7 @@ class MyUser(AbstractBaseUser): nickname = models.CharField(max_length=100, unique=True) # password, last_login 은 기본 제공 profile_img_path = models.URLField(blank=True, null=True) - friends = models.ManyToManyField('self', blank=True, null=True) + friends = models.ManyToManyField('self', blank=True) school = models.ForeignKey("School", on_delete=models.CASCADE) is_admin = models.BooleanField(default=False) is_active = models.BooleanField(default=True) @@ -51,21 +59,11 @@ def __str__(self): return self.email -class Profile(BaseModel): - user = models.OneToOneField(User, on_delete=models.CASCADE) - profile_img_path = models.URLField(blank=True) - friends = models.ManyToManyField('self', blank=True) - school = models.ForeignKey("School", on_delete=models.CASCADE) - - def __str__(self): - return self.user.username - - -class School(BaseModel): - name = models.CharField(max_length=60) - campus = models.CharField(max_length=60, blank=True) - - def __str__(self): - return self.name - - +# class Profile(BaseModel): +# user = models.OneToOneField(User, on_delete=models.CASCADE) +# profile_img_path = models.URLField(blank=True) +# friends = models.ManyToManyField('self', blank=True) +# school = models.ForeignKey("School", on_delete=models.CASCADE) +# +# def __str__(self): +# return self.user.username diff --git a/account/serializers.py b/account/serializers.py index 10b1085..6747f56 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -2,7 +2,15 @@ from account.models import * +class SchoolSerializer(serializers.ModelSerializer): + class Meta: + model = School + fields = '__all__' + + class MyUserSerializer(serializers.ModelSerializer): + school = SchoolSerializer + class Meta: model = MyUser fields = '__all__' @@ -10,27 +18,21 @@ class Meta: def create(self, validated_data): email = validated_data.get('email') nickname = validated_data.get('nickname') - school_id = validated_data.get('school_id') + school = validated_data.get('school') password = validated_data.get('password') user = MyUser( email=email, nickname=nickname, - school_id=school_id + school=school ) user.set_password(password) user.save() return user -class SchoolSerializer(serializers.ModelSerializer): - class Meta: - model = School - fields = '__all__' - - -class ProfileSerializer(serializers.ModelSerializer): - school = SchoolSerializer(read_only=True) - - class Meta: - model = Profile - fields = '__all__' +# class ProfileSerializer(serializers.ModelSerializer): +# school = SchoolSerializer(read_only=True) +# +# class Meta: +# model = Profile +# fields = '__all__' diff --git a/account/views.py b/account/views.py index cdc3d72..87760a6 100644 --- a/account/views.py +++ b/account/views.py @@ -21,14 +21,14 @@ def post(self, request): class LoginAPIView(APIView): def post(self, request): # 유저 인증 - user = authenticate( + myUser = authenticate( email=request.data.get("email"), password=request.data.get("password") ) # 해당 이메일, 비밀 번호로 가입한 유저가 있는 경우 - if user is not None: - serializer = MyUserSerializer(user) + if myUser is not None: + serializer = MyUserSerializer(myUser) # jwt, refresh token 발급 - token = TokenObtainPairSerializer.get_token(user) + token = TokenObtainPairSerializer.get_token(myUser) refresh_token = str(token) access_token = str(token.access_token) res = Response( @@ -46,7 +46,11 @@ def post(self, request): res.set_cookie("refresh", refresh_token, httponly=True) return res else: - return Response(status=status.HTTP_400_BAD_REQUEST) + print("Login error") + return Response( + {"message": "Failed to login"}, + status=status.HTTP_400_BAD_REQUEST + ) # 로그 아웃 diff --git a/community/filters.py b/community/filters.py index 6e52a9a..ae937b0 100644 --- a/community/filters.py +++ b/community/filters.py @@ -6,7 +6,7 @@ class PostFilter(FilterSet): # board = filters.NumberFilter(field_name='board_id') board = filters.NumberFilter(method='filter_board') # profile = filters.NumberFilter(field_name='profile_id') - profile = filters.NumberFilter(method='filter_profile') + myUser = filters.NumberFilter(method='filter_myUser') # title = filters.CharFilter(field_name='title', lookup_expr='icontains') title = filters.CharFilter(method='filter_title') # contents = filters.CharFilter(field_name='contents', lookup_expr='icontains') @@ -14,7 +14,7 @@ class PostFilter(FilterSet): class Meta: model = Post - fields = ['board', 'profile', 'title', 'contents'] + fields = ['board', 'myUser', 'title', 'contents'] # def filter_board(self, queryset, board_id, value): # return queryset.filter(**{ @@ -24,8 +24,8 @@ class Meta: def filter_board(self, queryset, board_id, value): return queryset.filter(board_id=value, deleted_at__isnull=True) - def filter_profile(self, queryset, profile_id, value): - return queryset.filter(profile_id=value, deleted_at__isnull=True) + def filter_myUser(self, queryset, myUser_id, value): + return queryset.filter(myUser_id=value, deleted_at__isnull=True) def filter_title(self, queryset, title, value): return queryset.filter(title__icontains=value, deleted_at__isnull=True) diff --git a/community/migrations/0004_auto_20230505_2341.py b/community/migrations/0004_auto_20230505_2341.py new file mode 100644 index 0000000..5685bd3 --- /dev/null +++ b/community/migrations/0004_auto_20230505_2341.py @@ -0,0 +1,70 @@ +# Generated by Django 3.2.16 on 2023-05-05 23:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('community', '0003_auto_20230504_1050'), + ] + + operations = [ + migrations.RemoveField( + model_name='board', + name='profile', + ), + migrations.RemoveField( + model_name='comment', + name='profile', + ), + migrations.RemoveField( + model_name='commentlike', + name='profile', + ), + migrations.RemoveField( + model_name='post', + name='profile', + ), + migrations.RemoveField( + model_name='postlike', + name='profile', + ), + migrations.RemoveField( + model_name='scrap', + name='profile', + ), + migrations.AddField( + model_name='board', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='comment', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='commentlike', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='post', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='postlike', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='scrap', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/community/models.py b/community/models.py index e875bf5..8be4c60 100644 --- a/community/models.py +++ b/community/models.py @@ -6,7 +6,8 @@ class Board(BaseModel): name = models.CharField(max_length=60) school = models.ForeignKey("account.School", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + # profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) def __str__(self): return self.name @@ -14,7 +15,8 @@ def __str__(self): class Post(BaseModel): board = models.ForeignKey("Board", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + # profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) title = models.CharField(max_length=100, blank=True) contents = models.CharField(max_length=200) is_anonymous = models.CharField(max_length=10, default='Y') @@ -34,7 +36,8 @@ def __str__(self): class Comment(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + # profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True) contents = models.CharField(max_length=200) @@ -44,23 +47,26 @@ def __str__(self): class PostLike(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + # profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) def __str__(self): - return f"{self.profile} 's like on {self.post}" + return f"{self.myUser} 's like on {self.post}" class CommentLike(BaseModel): comment = models.ForeignKey("Comment", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + # profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) def __str__(self): - return f"{self.profile} 's like on {self.comment}" + return f"{self.myUser} 's like on {self.comment}" class Scrap(BaseModel): post = models.ForeignKey("Post", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + # profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) def __str__(self): - return f"{self.profile} 's scrap of {self.post}" + return f"{self.myUser} 's scrap of {self.post}" diff --git a/community/serializers.py b/community/serializers.py index c4bf993..74b7328 100644 --- a/community/serializers.py +++ b/community/serializers.py @@ -9,52 +9,52 @@ class Meta: class CommentSerializer(serializers.ModelSerializer): - profile_username = serializers.SerializerMethodField() - profile_profile_img_path = serializers.SerializerMethodField() + myUser_nickname = serializers.SerializerMethodField() + myUser_profile_img_path = serializers.SerializerMethodField() class Meta: model = Comment - fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'parent', 'contents', 'status', + fields = ['id', 'myUser', 'myUser_nickname', 'myUser_profile_img_path', 'parent', 'contents', 'status', 'created_at'] - def get_profile_username(self, obj): - return obj.profile.user.username + def get_myUser_nickname(self, obj): + return obj.myUser.nickname - def get_profile_profile_img_path(self, obj): - return obj.profile.profile_img_path + def get_myUser_profile_img_path(self, obj): + return obj.myUser.profile_img_path class PostSerializer(serializers.ModelSerializer): - profile_username = serializers.SerializerMethodField() - profile_profile_img_path = serializers.SerializerMethodField() + myUser_nickname = serializers.SerializerMethodField() + myUser_profile_img_path = serializers.SerializerMethodField() comment_set = CommentSerializer(many=True, read_only=True) class Meta: model = Post - fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'title', 'contents', 'is_anonymous', + fields = ['id', 'myUser', 'myUser_nickname', 'myUser_profile_img_path', 'title', 'contents', 'is_anonymous', 'is_question', 'status', 'created_at', 'updated_at', 'comment_set'] - def get_profile_username(self, obj): - return obj.profile.user.username + def get_myUser_nickname(self, obj): + return obj.myUser.nickname - def get_profile_profile_img_path(self, obj): - return obj.profile.profile_img_path + def get_myUser_profile_img_path(self, obj): + return obj.myUser.profile_img_path class PostListSerializer(serializers.ModelSerializer): - profile_username = serializers.SerializerMethodField() - profile_profile_img_path = serializers.SerializerMethodField() + myUser_nickname = serializers.SerializerMethodField() + myUser_profile_img_path = serializers.SerializerMethodField() class Meta: model = Post - fields = ['id', 'profile', 'profile_username', 'profile_profile_img_path', 'title', 'contents', 'is_anonymous', + fields = ['id', 'myUser', 'myUser_nickname', 'myUser_profile_img_path', 'title', 'contents', 'is_anonymous', 'is_question', 'status', 'created_at', 'updated_at'] - def get_profile_username(self, obj): - return obj.profile.user.username + def get_myUser_nickname(self, obj): + return obj.myUser.nickname - def get_profile_profile_img_path(self, obj): - return obj.profile.profile_img_path + def get_myUser_profile_img_path(self, obj): + return obj.myUser.profile_img_path class PhotoSerializer(serializers.ModelSerializer): diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 52531a5..71a6987 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -50,9 +50,11 @@ 'rest_framework_simplejwt', ] +AUTH_USER_MODEL = 'account.MyUser' + REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.AllowAny', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', diff --git a/timetable/migrations/0004_auto_20230505_2341.py b/timetable/migrations/0004_auto_20230505_2341.py new file mode 100644 index 0000000..500bce3 --- /dev/null +++ b/timetable/migrations/0004_auto_20230505_2341.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.16 on 2023-05-05 23:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('timetable', '0003_auto_20230504_1050'), + ] + + operations = [ + migrations.RemoveField( + model_name='lecturereview', + name='profile', + ), + migrations.RemoveField( + model_name='reviewlike', + name='profile', + ), + migrations.RemoveField( + model_name='timetable', + name='profile', + ), + migrations.AddField( + model_name='lecturereview', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='reviewlike', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='myUsers', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='timetable', + name='myUser', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/timetable/models.py b/timetable/models.py index 8e46d35..37ffbef 100644 --- a/timetable/models.py +++ b/timetable/models.py @@ -4,7 +4,7 @@ class TimeTable(BaseModel): - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) name = models.CharField(max_length=60) lecture = models.ManyToManyField("Lecture", through="TakeLecture") @@ -40,12 +40,12 @@ class TakeLecture(BaseModel): lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) def __str__(self): - return f"{self.profile} started taking {self.lecture}" + return f"{self.timeTable.myUser} started taking {self.lecture}" class LectureReview(BaseModel): lecture = models.ForeignKey("Lecture", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1) rating = models.CharField(max_length=10) contents = models.CharField(max_length=200) @@ -55,7 +55,7 @@ def __str__(self): class ReviewLike(BaseModel): lectureReview = models.ForeignKey("LectureReview", on_delete=models.CASCADE) - profile = models.ForeignKey("account.Profile", related_name="profiles", on_delete=models.CASCADE) + myUser = models.ForeignKey("account.MyUser", related_name="myUsers", on_delete=models.CASCADE, default=1) def __str__(self): - return f"{self.profile} 's like on {self.lectureReview}" + return f"{self.myUser} 's like on {self.lectureReview}" From af0d80b4077fc01b6827e01ed66c7527360d8fe4 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 6 May 2023 10:58:02 +0900 Subject: [PATCH 23/44] [Feat] Refresh access token --- account/urls.py | 1 + account/views.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/account/urls.py b/account/urls.py index 52edbc4..78cbb56 100644 --- a/account/urls.py +++ b/account/urls.py @@ -6,4 +6,5 @@ path("register/", views.RegisterAPIView.as_view()), path("login/", views.LoginAPIView.as_view()), path("logout/", views.LogoutAPIView.as_view()), + path("token-refresh/", views.RefreshAccessToken.as_view()), ] diff --git a/account/views.py b/account/views.py index 87760a6..ea3d7d2 100644 --- a/account/views.py +++ b/account/views.py @@ -1,3 +1,4 @@ +import jwt from django.shortcuts import render from .serializers import * from rest_framework.views import APIView @@ -5,6 +6,7 @@ from rest_framework.response import Response from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer from django.contrib.auth import authenticate +from django_rest_framework_17th.settings.base import SECRET_KEY # 회원 가입 @@ -53,6 +55,52 @@ def post(self, request): ) +class RefreshAccessToken(APIView): + def post(self, request): + # 쿠키에 저장된 refresh 토큰 확인 + refresh_token = request.COOKIES.get('refresh') + + if refresh_token is None: + return Response({ + "message": "Refresh token does not exist" + }, status=status.HTTP_403_FORBIDDEN) + + # refresh 토큰 디코딩 진행 + try: + payload = jwt.decode( + refresh_token, SECRET_KEY, algorithms=['HS256'] + ) + except: + # refresh 토큰도 만료된 경우 에러 처리 + return Response({ + "message": "Expired refresh token, please login again" + }, status=status.HTTP_403_FORBIDDEN) + + # 해당 refresh 토큰을 가진 유저 정보 불러 오기 + user = MyUser.objects.get(id=payload['user_id']) + + if user is None: + return Response({ + "message": "User not found" + }, status=status.HTTP_400_BAD_REQUEST) + if not user.is_active: + return Response({ + "message": "User is inactive" + }, status=status.HTTP_400_BAD_REQUEST) + + # access 토큰 재발급 (유효한 refresh 토큰을 가진 경우에만) + token = TokenObtainPairSerializer.get_token(user) + access_token = str(token.access_token) + + return Response( + { + "message": "New access token", + "access_token": access_token + }, + status=status.HTTP_200_OK + ) + + # 로그 아웃 class LogoutAPIView(APIView): def post(self, request): From eda8c120b40a1572ef0c41e165810493681051b4 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 6 May 2023 16:46:37 +0900 Subject: [PATCH 24/44] [Feat] Set permission --- account/permissions.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 account/permissions.py diff --git a/account/permissions.py b/account/permissions.py new file mode 100644 index 0000000..e69de29 From bd7044d6e29dc65b2a0babc4b97131b31a74a5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sat, 6 May 2023 18:32:52 +0900 Subject: [PATCH 25/44] Create README.md --- README.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/README.md b/README.md index 0ca6275..62e658a 100644 --- a/README.md +++ b/README.md @@ -147,3 +147,117 @@ Title은 `title = filters.CharFilter(field_name='title', lookup_expr='icontains' - 핫게시판의 기준을 오로지 댓글수+공감수로 한다면 filter 로 구현할 수 있을 것 같다 - 어짜피 실제로 사용할 만한 세분화된 기능들을 개발하려면 ViewSet안에서도 따로 정의해야할게 많은 것 같은데 이럼 CBV보다 더 좋은건지는 아직 잘 모르겠따 - 다들 중간고사 잘 마무리하고 만나요👻 + +--- +## [3주차] + +## 👀 로그인 인증은 어떻게 하나요? JWT 는 무엇인가요? +### 1. Cookie & Session 기반 인증 +- Cookie: 클라이언트가 어떠한 웹사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해 `클라이언트의 브라우저`에 설치되는 작은 기록 정보 파일 +- Session: 세션은 비밀번호 등 클라이언트의 인증 정보를 쿠키가 아닌 `서버 측에 저장하고 관리`, 브라우저 종료할 때까지 인증상태가 유지됨 +- 동작 방식: + 1️⃣ 서버는 클라이언트의 로그인 요청에 대한 응답을 작성할 때, 인증 정보는 서버에 저장하고 클라이언트 식별자인 SESSION ID를 쿠키에 담음 + 2️⃣ 이후 클라이언트는 요청을 보낼 때마다, SESSION ID 쿠키를 함께 보냄 + 3️⃣ 서버는 SESSION ID 유효성을 판별해 클라이언트를 식별함 +- 장점: 각 사용자마다 고유한 세션 ID가 발급되기 때문에, 요청이 들어올 때마다 회원정보를 확인할 필요가 없음 +- 단점: 쿠키를 해커가 중간에 탈취하여 클라이언트인척 위장할 수 있는 위험성 존재, 서버에서 세션 저장소를 사용하므로 요청이 많아지면 서버에 부하가 심해짐 + +### 2. JWT 기반 인증 +- JWT(JSON Web Token): 인증에 필요한 정보들을 암호화시킨 토큰 +- JWT 구조: `Header` , `Payload` , `Signature` 로 이루어짐. Header는 정보를 암호화할 해싱 알고리즘 및 토큰의 타입을 지정, Payload는 실제 정보(클라이언트의 고유 ID 값 및 유효 기간 등)를 지님, Signature는 인코딩된 Header와 Payload를 더한 뒤 비밀키로 해싱하여 생성 → 토큰의 위변조 여부를 확인하는데 사용됨 +- 동작 방식: + 1️⃣ 클라이언트 로그인 요청이 들어오면, 서버는 검증 후 클라이언트 고유 ID 등의 정보를 Payload에 담음 + 2️⃣ 암호화할 비밀키를 사용해 Access Token(JWT)을 발급함 + 3️⃣ 클라이언트는 전달받은 토큰을 저장해두고, 서버에 요청할 때 마다 토큰을 요청 헤더 Authorization에 포함시켜 함께 전달함 + 4️⃣ 서버는 토큰의 Signature를 비밀키로 복호화한 다음, 위변조 여부 및 유효 기간 등을 확인함 + 5️⃣ 유효한 토큰이라면 요청에 응답함 + - 장점: 인증 정보에 대한 별도의 저장소가 필요없음, 확장성이 우수함 + - 단점: 토큰의 길이가 길어, 인증 요청이 많아질수록 네트워크 부하가 심해짐 + +### 3. OAuth 2.0을 이용한 인증 +- OAuth: 구글, 페이스북, 트위터와 같은 다양한 플랫폼의 특정한 사용자 데이터에 접근하기 위해 클라이언트(우리의 서비스)가 사용자의 접근 권한을 위임(Delegated Authorization)받을 수 있는 표준 프로토콜. +쉽게 말하자면, 우리의 서비스가 우리 서비스를 이용하는 유저의 타사 플랫폼 정보에 접근하기 위해서 권한을 타사 플랫폼으로부터 위임 받는 것 +- 나의 앱은 클라이언트👧 / 사용자는 리소스 오너🙋‍♂️ / 구글, 카카오 같은 큰 서비스는 리소스 서버🧝 (사실 데이터 처리를 담당하는 Resource 서버와 인증을 담당하는 Authorization Server로 구성됨) +- 동작 방식: +![image](https://user-images.githubusercontent.com/90256209/236613747-47da422f-971f-4d1b-a4c2-2ed0f5dbd972.png) + +--- +## 🗣️ 피드백 반영 및 수정사항 +1. 찬혁오빠가 구현한 ***safe delete*** 참고해서 base model에 delete 메소드 추가함 + 데이터 삭제시 deleted_at 컬럼에 삭제시간을 저장하는 방식 = deleted_at이 null이면 정상(=삭제 안된) 게시물 +2. "해당 글의 status는 D인데 조회했을때 보이면 안될 것 같아요🥹🥹" + → Filter 클래스에 ***deleted_at이 null인 것만*** 조건을 추가한 ***filter 메소드***를 구현함 + +#### (수정한 filter 메소드) +![image](https://user-images.githubusercontent.com/90256209/236614033-d4ef3388-8987-4dfb-bce0-fcd28f63b5a2.png) +#### (결과 확인) +⬇️ 이렇게 DB에서 `deleted_at` 컬럼에 삭제시간이 들어가 있는 경우, +![image](https://user-images.githubusercontent.com/90256209/236614073-0a8baead-feec-4088-906c-55b99d86fed4.png) +➡️ postman으로 조회했을때 안보이는걸 확인할 수 있다!! profile_id가 2인 글은 삭제되었으므로 필터링해도 안보임! +![image](https://user-images.githubusercontent.com/90256209/236614174-b11ebd10-677d-424d-b7d0-5d542ce06e4d.png) + +--- +## ⭐ JWT 로그인 구현하기 + +### 📌 커스텀 User 모델 사용하기 +`AbstractBaseUser` 를 상속한 커스텀 User 모델을 만들었다. (기존에는 기본 User 모델을 OneToOne 필드로 사용한 Profile 모델을 사용했었음) +AbstractUser와 AbstractBaseUser의 차이는 기본 제공하는 필드들이 다르다! (AbstractUser가 더 많이 제공함ㅎㅎ) ++유저 모델을 커스텀할 때 +- `USERNAME_FIELD` 은 유저를 고유하게 식별할때 쓰는 필드고, +- `REQUIRED_FIELDS` 는 반드시 필요한 필드다. +나는 유저 식별을 `email`로 하게끔 만들었다. +``` +class MyUser(AbstractBaseUser): + email = models.EmailField(max_length=255, unique=True) + nickname = models.CharField(max_length=100, unique=True) + # password, last_login 은 기본 제공 + profile_img_path = models.URLField(blank=True, null=True) + friends = models.ManyToManyField('self', blank=True) + school = models.ForeignKey("School", on_delete=models.CASCADE) + is_admin = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + objects = MyUserManager() + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["nickname"] + + def __str__(self): + return self.email +``` +UserManager 클래스도 `BaseUserManager`를 상속받아서 커스텀해주었다. + +``` +class MyUserManager(BaseUserManager): + def create_user(self, email, nickname, school=None, password=None, **extra_fields): + if not email: + raise ValueError("Users must have an email address") + + user = self.model( + email=email, + nickname=nickname, + school=school, + ) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, nickname, school=None, password=None, **extra_fields): +``` +`create_superuser()`는 `create_user`와 거의 비슷하지만, `superuser.is_admin = True`를 자동 설정한다는 점이 다르다. + +### 📌 회원가입 구현하기 +Postman으로 확인해보니 잘된다ㅎㅎ +![image](https://user-images.githubusercontent.com/90256209/236615823-7dde4a0b-a8f7-4824-aa42-f054c8362583.png) +번외) 원래 회원가입할땐 자동로그인이 아니면 토큰 발급을 안한다. 근데 사진에서 쿠키에 뭔가가 있는건 직전에 테스트하던 회원 로그아웃을 안해서 아직 쿠키가 남아있음...ㅎ ~~(NG)~~ + +### 📌 JWT Login 구현하기 (Access 토큰, Refresh 토큰 발급) + + +### 📌 Refresh 토큰을 통한 Access 토큰 재발급 + +### 📌 JWT Logout 구현하기 + +### 📌 Permission 설정하기 From 75807998dae788a549455c3fd4d71901a9c37a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sat, 6 May 2023 19:31:21 +0900 Subject: [PATCH 26/44] Update README.md --- README.md | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62e658a..7ec7918 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Title은 `title = filters.CharFilter(field_name='title', lookup_expr='icontains' ![image](https://user-images.githubusercontent.com/90256209/236614174-b11ebd10-677d-424d-b7d0-5d542ce06e4d.png) --- -## ⭐ JWT 로그인 구현하기 +## 👩‍💻 JWT 로그인 구현하기 ### 📌 커스텀 User 모델 사용하기 `AbstractBaseUser` 를 상속한 커스텀 User 모델을 만들었다. (기존에는 기본 User 모델을 OneToOne 필드로 사용한 Profile 모델을 사용했었음) @@ -248,16 +248,140 @@ class MyUserManager(BaseUserManager): ``` `create_superuser()`는 `create_user`와 거의 비슷하지만, `superuser.is_admin = True`를 자동 설정한다는 점이 다르다. + ### 📌 회원가입 구현하기 Postman으로 확인해보니 잘된다ㅎㅎ ![image](https://user-images.githubusercontent.com/90256209/236615823-7dde4a0b-a8f7-4824-aa42-f054c8362583.png) 번외) 원래 회원가입할땐 자동로그인이 아니면 토큰 발급을 안한다. 근데 사진에서 쿠키에 뭔가가 있는건 직전에 테스트하던 회원 로그아웃을 안해서 아직 쿠키가 남아있음...ㅎ ~~(NG)~~ - + + ### 📌 JWT Login 구현하기 (Access 토큰, Refresh 토큰 발급) +로그인 구현할 때 **Access 토큰**은 **HTTP Response**로 프론트한테 주는게 맞는거 같은데, **Refresh 토큰**도 이렇게 줄지 고민이 됐다. +여러 블로그들을 봤는데, 어떤 사람은 그냥 둘다 Response(JSON 형태)로 주고... 또 어떤 사람은 둘다 쿠키에 넣고... 어떤 사람은 Access 토큰은 Response에, Refresh 토큰은 쿠키에 넣더라ㅎㅎ +대체 뭐가 더 좋은 방법일까?? 궁금해졌다. 그래서 바아로 구글링했다. + + +결론은.. JWT로 보안성이 높은 로그인을 구현하려면, +⭐**백엔드에서 프론트엔드로 Access Token은 JSON 형태로 넘겨주고, Refresh Token은 Cookie에 넣어주어야 한다**⭐ +아래 링크에 자세한 이유가 나와있다! +https://medium.com/@uk960214/refresh-token-%EB%8F%84%EC%9E%85%EA%B8%B0-f12-dd79de9fb0f0 + + +그래서 나도 리프레시 토큰을 쿠키에 넣어주는 코드를 구현했다! (근데 이건 과제니까.. 리프레시 토큰도 JSON 응답에서 한눈에 보고 싶어서 JSON 응답에도 넣어줬다) + + +이제, Postman으로 확인해보자! +- 로그인 성공시 JSON 응답으로 access 토큰, refresh 토큰 둘다 잘 오는걸 확인 가능하다 +![image](https://user-images.githubusercontent.com/90256209/236616656-8ce25ea2-a412-4868-8b48-a951d15c52f2.png) +- 쿠키에도 refresh 토큰이 잘 들어가 있다 +![image](https://user-images.githubusercontent.com/90256209/236616689-3158903d-eda2-4f05-a272-8c033323d83a.png) + + +➡️ 발급 받은 토큰을 디코딩해보면, 유저의 id(pk)와 토큰 발급시간(iat), 토큰 만료시간(exp)을 볼 수 있다. +내가 `2023-05-06 08:04:08' 에 로그인 했고, +``` +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), +``` +⬇️ 이렇게 토큰 유효기간을 30분으로 설정했기 때문에, 토큰 만료시간은 아래 사진처럼 `2023-05-06 08:34:08` 로 나오는게 맞다!! +![image](https://user-images.githubusercontent.com/90256209/236616826-4b4bd354-0988-4f1a-be66-ce5841e31122.png) ### 📌 Refresh 토큰을 통한 Access 토큰 재발급 +로그인할때 access 토큰, refresh 토큰을 발급해주는 걸로 끝내는게 아니라, **실제로 토큰이 만료되었을때 refresh 토큰으로 토큰을 재발급받는 기능**을 구현하고 싶어서 해봤다. +대략적인 흐름은 `refresh 토큰이 유효한지 확인` → `refresh 토큰에 담긴 유저 id 로 유저 불러오기` → `그 유저로 다시 access 토큰 발급` 이렇다ㅎㅎ +코드 설명은 주석으로 자세하게 해놓았다..! +``` +class RefreshAccessToken(APIView): + def post(self, request): + # 쿠키에 저장된 refresh 토큰 확인 + refresh_token = request.COOKIES.get('refresh') + + if refresh_token is None: + return Response({ + "message": "Refresh token does not exist" + }, status=status.HTTP_403_FORBIDDEN) + + # refresh 토큰 디코딩 진행 + try: + payload = jwt.decode( + refresh_token, SECRET_KEY, algorithms=['HS256'] + ) + except: + # refresh 토큰도 만료된 경우 에러 처리 + return Response({ + "message": "Expired refresh token, please login again" + }, status=status.HTTP_403_FORBIDDEN) + + # 해당 refresh 토큰을 가진 유저 정보 불러 오기 + user = MyUser.objects.get(id=payload['user_id']) + + if user is None: + return Response({ + "message": "User not found" + }, status=status.HTTP_400_BAD_REQUEST) + if not user.is_active: + return Response({ + "message": "User is inactive" + }, status=status.HTTP_400_BAD_REQUEST) + + # access 토큰 재발급 (유효한 refresh 토큰을 가진 경우에만) + token = TokenObtainPairSerializer.get_token(user) + access_token = str(token.access_token) + + return Response( + { + "message": "New access token", + "access_token": access_token + }, + status=status.HTTP_200_OK + ) +``` + + +➡️포스트맨으로 테스트 해봤더니 새로운 토큰이 잘 발급된다..! 이제 프론트에서는 이 새로운 토큰을 헤더에 넣어서 요청을 보내면 된다. +![image](https://user-images.githubusercontent.com/90256209/236617483-aabc803a-4800-4e6b-9683-dbd3d9db60eb.png) + ### 📌 JWT Logout 구현하기 +로그아웃 로직은 이렇다. +1️⃣ 프론트에서 LogoutApi를 호출한다. +2️⃣ 호출과 동시에 프론트는 가지고 있던 Access token을 삭제한다. +3️⃣ 백엔드에서는 cookie에 존재하는 Refresh token을 삭제한다. +그래서 나는 쿠키의 Refresh 토큰을 삭제해주도록 구현했다. Postman으로 확인해보자ㅎㅎ +![image](https://user-images.githubusercontent.com/90256209/236617700-df7ef90c-afe3-4c40-9541-757e38c4900d.png) +➡️ 로그아웃이 잘되서 쿠키에 있던 refresh 토큰이 사라진다..! + ### 📌 Permission 설정하기 +`permissions.py` 파일을 새로 만들어서 permission을 커스텀해주고, `community` 에 있는 게시판, 게시글 API에 적용해줬다. +``` +class IsOwnerOrReadonly(permissions.BasePermission): + def has_permission(self, request, view): + # 로그인한 사용자인 경우 API 사용 가능 + return request.user and request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + # GET, OPTION, HEAD 요청일 때는 그냥 허용 + if request.method in permissions.SAFE_METHODS: + return True + # DELETE, PATCH 일 때는 현재 사용자와 객체가 참조 중인 사용자가 일치할 때만 허용 + return obj.myUser == request.user +``` +➡️ Postman으로 확인해보자. 게시판 조회 API에 JWT가 잘 적용되었는지 볼 것이다 +- 유효기간이 만료된 경우: 이렇게 친절하게 알려준다ㅎㅎ +![image](https://user-images.githubusercontent.com/90256209/236618467-ceff90f0-a51a-4207-8db5-be76f02758a2.png) +- 유효한 토큰으로 다시 요청을 보내면, 다시 잘 보인다! +![image](https://user-images.githubusercontent.com/90256209/236618489-518bad5d-c67d-4a3f-8552-7d6a0c3c9f7b.png) + +--- +## 🍀 느낀점 +***장고는 편리하다...*** 놀랐던게 장고에서는 클라이언트가 넘겨준 JWT로 유저를 불러오는걸 무려 **함수 하나**로 제공한다.. JWT를 추출해서, 파싱하고, 디코딩하고, 유저ID를 추출해서, 그 유저ID로 DB에서 유저 정보를 불러오는 로직을 내가 직접 클래스에 작성할 필요 없이 `authenticate()` 함수 하나로 그냥 끝나버리는 것... (약간 허무한거같기두 ㅎ) + +공식 문서에는 이렇게 나와있다. + + +![image](https://user-images.githubusercontent.com/90256209/236618752-bd3c3149-b8b0-4272-a12a-02a942f8fe54.png) + + +이번 기회로 로그인 및 사용자 인증에 대해 다시 자세히 복습해 볼 수 있어서 재밌었당! From 750c9a07310a303e3d6abfa195f7bb3316c07552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sat, 6 May 2023 19:41:11 +0900 Subject: [PATCH 27/44] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ec7918..9e356f6 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Title은 `title = filters.CharFilter(field_name='title', lookup_expr='icontains' - 다들 중간고사 잘 마무리하고 만나요👻 --- -## [3주차] +## [5주차 - Simple JWT] ## 👀 로그인 인증은 어떻게 하나요? JWT 는 무엇인가요? ### 1. Cookie & Session 기반 인증 From 66b9fcfc47a5a17e2a38fd5bb9f5373301cffbdf Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 13 May 2023 16:45:48 +0900 Subject: [PATCH 28/44] [Feat] Deploy through github actions --- .github/workflows/deploy.yml | 4 +-- .gitignore | 2 ++ Dockerfile | 2 +- account/permissions.py | 14 +++++++++++ community/views.py | 6 +++++ django_rest_framework_17th/settings/base.py | 2 +- docker-compose.prod.yml | 25 +++++++++++++------ docker-compose.yml | 26 ++++++++++++++++++-- requirements.txt | Bin 544 -> 554 bytes 9 files changed, 68 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 044cdd1..53a9bcf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,8 +16,8 @@ jobs: - name: create env file run: | - touch .env - echo "${{ secrets.ENV_VARS }}" >> .env + touch .env.prod + echo "${{ secrets.ENV_VARS }}" >> .env.prod - name: create remote directory uses: appleboy/ssh-action@master diff --git a/.gitignore b/.gitignore index 0609490..0eb9829 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,8 @@ celerybeat.pid # Environments venv/.env .venv +.env +.env.prod env/ venv/ ENV/ diff --git a/Dockerfile b/Dockerfile index b2d0d07..e4a7bd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ COPY requirements.txt /app/requirements.txt RUN pip install -r requirements.txt # Now copy in our code, and run it -COPY . /app/ \ No newline at end of file +COPY . /app/ diff --git a/account/permissions.py b/account/permissions.py index e69de29..96bfdaa 100644 --- a/account/permissions.py +++ b/account/permissions.py @@ -0,0 +1,14 @@ +from rest_framework import permissions + + +class IsOwnerOrReadonly(permissions.BasePermission): + def has_permission(self, request, view): + # 로그인한 사용자인 경우 API 사용 가능 + return request.user and request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + # GET, OPTION, HEAD 요청일 때는 그냥 허용 + if request.method in permissions.SAFE_METHODS: + return True + # DELETE, PATCH 일 때는 현재 사용자와 객체가 참조 중인 사용자가 일치할 때만 허용 + return obj.myUser == request.user diff --git a/community/views.py b/community/views.py index 699087e..31a15c8 100644 --- a/community/views.py +++ b/community/views.py @@ -8,6 +8,8 @@ from rest_framework import viewsets from django_filters.rest_framework import DjangoFilterBackend from .filters import * +from rest_framework import permissions +from account.permissions import IsOwnerOrReadonly # class BoardList(APIView): @@ -51,11 +53,15 @@ class BoardViewSet(viewsets.ModelViewSet): + # permission 추가 + permission_classes = [IsOwnerOrReadonly] serializer_class = BoardSerializer queryset = Board.objects.all() class PostViewSet(viewsets.ModelViewSet): + # permission 추가 + permission_classes = [IsOwnerOrReadonly] serializer_class = PostSerializer queryset = Post.objects.all() filter_backends = [DjangoFilterBackend] diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 71a6987..421ece3 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -35,6 +35,7 @@ # Application definition INSTALLED_APPS = [ + 'account', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -42,7 +43,6 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', - 'account', 'community', 'timetable', 'utils', diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index cf94cd6..8308bf2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,23 +1,34 @@ -version: '3' -services: +version: '3' # 버전 명시 필수 +services: # container 이름들 작성 web: container_name: web - #작성 - volumes: + build: # 빌드할 Dockerfile + context: ./ + dockerfile: Dockerfile.prod + command: gunicorn django_rest_framework_17th.wsgi:application --bind 0.0.0.0:8000 # container 가 실행될 때 수행할 명령어 + environment: # 환경 설정 + DJANGO_SETTINGS_MODULE: django_rest_framework_17th.settings.prod + env_file: # 환경 변수 파일 설정 + - .env + expose: # container 포트 번호 + - 8000 + volumes: # 데이터 볼륨 매핑 - static:/home/app/web/static - media:/home/app/web/media - entrypoint: + entrypoint: # container 가 실행될 때 '반드시' 실행 되는 명령어 - sh - config/docker/entrypoint.prod.sh nginx: container_name: nginx - #작성 + build: ./config/nginx # 여기에 nginx 에 대한 Dockerfile 이 존재, nginx 에 대한 상위 설정 파일인 nginx.conf 도 있음 volumes: - static:/home/app/web/static - media:/home/app/web/media - depends_on: + ports: + - "80:80" # 포트포워딩 (Host 포트 번호 : Container 포트 번호) + depends_on: # container 생성 순서 규정 (먼저 생성되어야 하는 container 명시) - web volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 44008b5..6a2e87d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,13 +3,35 @@ services: db: container_name: db - #작성 + image: mysql:5.7 # window + restart: always + environment: + MYSQL_ROOT_HOST: '%' + MYSQL_ROOT_PASSWORD: mysql + expose: + - 3306 + ports: + - "3307:3306" + env_file: + - .env volumes: - dbdata:/var/lib/mysql web: container_name: web - #작성 + build: . + command: sh -c "python manage.py runserver 0.0.0.0:8000" + environment: + MYSQL_ROOT_PASSWORD: mysql + DATABASE_NAME: mysql + DATABASE_USER: 'root' + DATABASE_PASSWORD: mysql + DATABASE_PORT: 3306 + DATABASE_HOST: db + DJANGO_SETTINGS_MODULE: django_rest_framework_17th.settings.dev + restart: always + ports: + - "8000:8000" volumes: - .:/app depends_on: diff --git a/requirements.txt b/requirements.txt index 4cc5256322223499b2740dfa108a86589a7244c4..9e0e47e0f83f127d4b2feb307910ead44f6f1c04 100644 GIT binary patch delta 43 ucmZ3$vWjKGANgX2Oom*B0)`xhRE8{ua)uHHTOc%L&|@$H;mv}K{EPtkCkY_{ delta 33 jcmZ3*vVdj6AJHs^a)uHHTOc%K&;w!+X|P$Ek)II&k<$jj From d1035b0f18a03bb103c9e0823471856fccaa1c91 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 13 May 2023 16:55:19 +0900 Subject: [PATCH 29/44] [Add] Add dummy code for deploy test --- community/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/community/views.py b/community/views.py index 31a15c8..1b5fbee 100644 --- a/community/views.py +++ b/community/views.py @@ -66,3 +66,5 @@ class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() filter_backends = [DjangoFilterBackend] filterset_class = PostFilter + +# deploy test \ No newline at end of file From 73521a42bdc53d22786b0cc3dd313aa85a1f6568 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 13 May 2023 17:06:55 +0900 Subject: [PATCH 30/44] [Refact] Fix deploy.yml branches: --- .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 53a9bcf..6cef339 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy to EC2 on: push: branches: - - dev + - master jobs: From 832c466f0da8595080dafff6b6f7e2fc0d74feed Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 13 May 2023 17:15:22 +0900 Subject: [PATCH 31/44] [Fix] Edit env_file in docker-compose.prod.yml --- docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8308bf2..b7cacdb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -10,7 +10,7 @@ services: # container 이름들 작성 environment: # 환경 설정 DJANGO_SETTINGS_MODULE: django_rest_framework_17th.settings.prod env_file: # 환경 변수 파일 설정 - - .env + - .env.prod expose: # container 포트 번호 - 8000 volumes: # 데이터 볼륨 매핑 From 02489efc4cb0c37eb2dd1e94e7a0ab7257318a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sat, 13 May 2023 23:45:48 +0900 Subject: [PATCH 32/44] Update README.md --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/README.md b/README.md index 9e356f6..28e4af1 100644 --- a/README.md +++ b/README.md @@ -385,3 +385,89 @@ class IsOwnerOrReadonly(permissions.BasePermission): 이번 기회로 로그인 및 사용자 인증에 대해 다시 자세히 복습해 볼 수 있어서 재밌었당! + +--- +## [6주차 - AWS EC2, RDS & Docker & Github Actions] + +## ✨Docker Compose✨ +Docker Compose란? +내가 이해한 Docker Compose는 여러 이미지들에 대한 복잡한 run 명령어들을 docker-compose.yml에 작성해서 여러 컨테이너들을 한번에 실행시킬 수 있게 해주는 도구다. + +참고로 `docker-compose.yml` 에서 `expose:` 는 컨테이너의 포트번호를 알려주는 용도이다. 그럼 이게 `ports:` 랑 뭐가 다르지? 궁금해지는 게 당연하다ㅎㅎ +둘의 차이점은 `ports:` 는 실제로 **외부**에서 접속할 때 **Host의 포트**와 **컨테이너의 포트**를 매칭시켜주지만, `expose:` 는 **내부**에서 사용되도록 **컨테이너의 포트**만 노출시키는 것이다. + + --- + ## ✨Github Actions의 Secrets✨ +`.gitignore` 에 `.env.prod` 를 추가하면, 배포할 때는 이 파일이 없어서 환경변수를 사용할 수 없다. +그래서 깃허브는 Actions Secrets을 통해 환경 변수를 암호화해서 저장할 수 있는 기능을 제공한다. +프로젝트 레포지토리의 [Settings] > [Secrets and variables] > [Actions] 에 들어가서 [New repository secret] 버튼을 눌러 배포할 때 필요한 환경변수들을 추가해주면, Actions가 실행될 때 환경변수 설정 파일이 자동으로 생성되는 것이다. + +--- +## ✨로컬에서 Docker 컨테이너 실행하기✨ +여러 에러들을 겪었다ㅎㅎ.. + + +에러 1) 로컬에서 `docker-compose -f docker-compose.yml up --build` 로 웹 컨테이너랑 db 컨테이너 실행하려니까 +`django no module named 'rest_framework_simplejwt` 에러가 났다. Simple JWT 설치는 `requirements.txt` 에 있어서 당연히 자동으로 될줄 알았는데 왜 안되지? 하고 `requirements.txt` 를 다시 봤다. +그랬더니,,ㅎ `djangorestframework-jwt==1.11.0` 이게 들어가 있더라ㅎㅎ 그래서 후딱 `djangorestframework-simplejwt==5.2.2` 로 수정해줬다!😎 (처음엔 `Dockerfile` 에 `RUN pip3 install djangorestframework-simplejwt` 를 추가해줬는데 이건 에러가 나더라..) + +에러 2) `ValueError: Related model 'account.myuser' cannot be resolved` ➡️ 구글링해보니 `AUTH_USER_MODEL = 'account.MyUser'` 로 설정한 모델이 가장 먼저 migrate 되어야 하는데 그냥 `python manage.py migrate` 를 하면, 다른 모델이 먼저 migrate 되서 발생하는 에러인것 같았다... +그래서 `docker-compose.yml` 안에서 `python manage.py migrate account && [나머지 앱들] ...` 이렇게 바꿨더니 또 이미 만들어진 모델이라는 에러가 나서 이번에는 migrate 하는 코드를 다 지우고 `python manage.py runserver 0.0.0.0:8000` 만 남겼더니 드디어 성공했다..🫠🫠 + + +루트 URL에 아무것도 안만들어놔서 404 에러 페이지가 뜨지만 그래도 접속에 성공했다! +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/f830ae2b-ab6c-40a2-acec-8016552ffe57) + +--- +## ✨실제 배포하기(with AWS, Github Actions, Docker)✨ +### AWS EC2, RDS 구축 +EC2는 스토리지 크기를 최대인 30Gb로 설정해주고, 탄력적 IP를 할당해줬다. +RDS는 MySQL 버전 5.7.41 로 설정해줬다. + +⬇️ EC2에서 RDS에 접속하려면, EC2의 보안그룹ID를 RDS의 보안그룹 인바운드 규칙에 추가해줘야 한다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/4e5c61da-f144-47f0-a114-521e39108b21) + + +타임존, 인코딩 설정을 위해서 새로운 파라미터 그룹도 생성해줬다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/09db23da-7608-4bf7-af5c-699cf030eeec) + + +`docker-compose.prod.yml` 에 포트포워딩이 80:80 으로 되어있길래 EC2 인바운드 규칙에 80번(HTTP) 포트도 추가해줬다. (추가로 HTTPS인 443번도 열어줬다.) + +### Github Actions 를 통한 배포 +에러1) 프로젝트에 `.env.prod` 파일을 만들어주고, Github에 가서 Actions Secrets 설정도 스터디 노션에 있는대로 쭉 진행해줬다. 이후 master 브랜치에 `git push origin shj718:master` 로 push 해줬는데도 workflow가 실행이 안됐다. +이유는 `deploy.yml` 에 **dev 브랜치**에 push할 때 자동으로 배포되게 설정되어 있어서였다ㅎㅎ 다시 **master** 브랜치로 수정했다. + +에러2) `docker-compose.prod.yml` 에 `env_file` 설정이 `.env.prod` 가 아닌 `.env` 로 되어있어서 에러가 났다. 고쳐줬다. + + +다 고쳐주니 잘 빌드 됐다! +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/d7e35fe7-fd50-402c-8448-1e916085f2ea) + +브라우저로 EC2에 접속해보면 서버가 떠있는걸 확인 가능🤗 +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/813337f1-ff19-4c41-8e64-eda78c3d24c8) + +### API 요청 보내기 +JWT 없이 보냈으니 자격 인증 데이터가 없다는 응답이 오는게 당연하다. +image + +근데 로그인 요청을 보냈더니 500 에러가 났다.. +image + + +그동안 Spring으로 개발할때 500 Internal Server 에러가 나는건 경험상 8-90% 내 프로젝트 Service단에서 로직이 잘못된 경우였다. 근데 여기선 왜 나는지 모르겠어서 `docker logs --details 050f4c270e9f` 로 로그를 확인해봤다. +image +근데 뜬금없이 +``` + /usr/local/lib/python3.8/site-packages/environ/environ.py:637: UserWarning: Error reading /home/app/web/venv/.env - if you're not configuring your environme + warnings.warn( +``` +요런 에러 메세지가 나왔다.. 구글링해봤을때 `docker-compose.yml` 과 동일한 위치에 `.env` 파일을 두면 저절로 `docker-compose` 할 때 웹 애플리케이션에 `.env` 파일이 포함되지 않는다고 하던데.. 무슨 일인지 잘 모르겠다. +에러 메세지를 복붙해서 구글링해도 똑같은 에러를 겪은 사람이 없어보인다. 그래서 `docker exec` 로 컨테이너 안에 들어가서 이것저것 많이 해봤는데 뭐가 문제인지 아직 모르겠다 하하... 혹시 이 에러의 이유를 아신다면 알려주세요🥲🥲 + +--- +## ✨회고✨ +확실히 도커는 이론으로 공부할때보다 실제 프로젝트에 적용하는게 어렵다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/ee5954b1-db9b-4c8a-9080-35adde77457e) +[출처](https://velog.io/@kdh92417/Nginx%EC%99%80-Django-EC2%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-feat.-RDS-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%97%B0%EA%B2%B0) +그래도 이번 기회를 통해 배포 아키텍쳐와 처음 써보는 Github Actions에 대해 공부해봐서 매우매우 유익했다.. From 41956824797a9d353070ba8aab344fa01c500b60 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 16:59:48 +0900 Subject: [PATCH 33/44] [Test] Health check test --- account/urls.py | 1 + account/views.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/account/urls.py b/account/urls.py index 78cbb56..b1e1b55 100644 --- a/account/urls.py +++ b/account/urls.py @@ -7,4 +7,5 @@ path("login/", views.LoginAPIView.as_view()), path("logout/", views.LogoutAPIView.as_view()), path("token-refresh/", views.RefreshAccessToken.as_view()), + path("health-check/", views.HealthCheck.as_view()), ] diff --git a/account/views.py b/account/views.py index ea3d7d2..f367208 100644 --- a/account/views.py +++ b/account/views.py @@ -110,3 +110,11 @@ def post(self, request): }, status=status.HTTP_202_ACCEPTED) response.delete_cookie('refresh') return response + + +class HealthCheck(APIView): + def get(self, request, format=None): + response = Response({ + "message": "Instance is healthy!" + }, status=status.HTTP_200_OK) + return response From 9f696d08acf69b2023ede3f13dd433a0d5ab25b2 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 18:30:23 +0900 Subject: [PATCH 34/44] [Add] Add prod settings --- Dockerfile.prod | 2 +- config/docker/entrypoint.prod.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.prod b/Dockerfile.prod index b13e601..6a19d91 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -45,7 +45,7 @@ WORKDIR $APP_HOME RUN apk update && apk add libpq RUN apk update \ && apk add --virtual build-deps gcc python3-dev musl-dev \ - && apk add --no-cache mariadb-dev + && apk add --no-cache jpeg-dev zlib-dev mariadb-dev COPY --from=builder /usr/src/app/wheels /wheels COPY --from=builder /usr/src/app/requirements.txt . RUN pip install mysqlclient diff --git a/config/docker/entrypoint.prod.sh b/config/docker/entrypoint.prod.sh index 09f6570..0cb4cda 100644 --- a/config/docker/entrypoint.prod.sh +++ b/config/docker/entrypoint.prod.sh @@ -1,5 +1,7 @@ #!/bin/sh python manage.py collectstatic --no-input +echo "Apply database migrations" +python manage.py migrate exec "$@" \ No newline at end of file From f9e30c5e61dd3a69a8648e749796a15240e3e337 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 19:31:23 +0900 Subject: [PATCH 35/44] [Add] Add nginx redirection settings --- config/nginx/nginx.conf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index fb084b1..251b9d1 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -21,3 +21,9 @@ server { alias /home/app/web/media/; } } + +server { + if ($http_x_forwarded_proto != 'https') { + return 301 https://$host$request_uri; + } +} \ No newline at end of file From 10711968fcef987d963d0538b8ec7ca98b398fc0 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 20:29:29 +0900 Subject: [PATCH 36/44] [Fix] Edit nginx redirection settings --- config/nginx/nginx.conf | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index 251b9d1..1ddc346 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -6,6 +6,10 @@ server { listen 80; + if ($http_x_forwarded_proto != 'https') { + return 301 https://$host$request_uri; + } + location / { proxy_pass http://django_rest_framework_17th; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -21,9 +25,3 @@ server { alias /home/app/web/media/; } } - -server { - if ($http_x_forwarded_proto != 'https') { - return 301 https://$host$request_uri; - } -} \ No newline at end of file From d4750d3fd93c35b36ff590a71160ae1b8c5c6e7a Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 21:43:40 +0900 Subject: [PATCH 37/44] [Fix] Edit settings --- django_rest_framework_17th/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 421ece3..3c54243 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -29,7 +29,7 @@ SECRET_KEY = env('DJANGO_SECRET_KEY') DEBUG = env('DEBUG') -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['0.0.0.0', '.hyejun.store'] # Application definition From 946d767949c91e45f7d799a4f07cc5927822eb2d Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 22:20:09 +0900 Subject: [PATCH 38/44] [Fix] Edit env... --- django_rest_framework_17th/settings/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 3c54243..61e5edc 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -20,7 +20,7 @@ DEBUG=(bool, False) ) -environ.Env.read_env(os.path.join(BASE_DIR, 'venv/.env')) +environ.Env.read_env(os.path.join(BASE_DIR, '.env')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ @@ -29,7 +29,7 @@ SECRET_KEY = env('DJANGO_SECRET_KEY') DEBUG = env('DEBUG') -ALLOWED_HOSTS = ['0.0.0.0', '.hyejun.store'] +ALLOWED_HOSTS = ['0.0.0.0', '.hyejun.store', '58.233.200.22'] # Application definition From 7e39d56916e854a15998ad30517cdda0f4318350 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 22:50:26 +0900 Subject: [PATCH 39/44] =?UTF-8?q?[Fix]=20=EC=A0=9C=EB=B0=9C...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- django_rest_framework_17th/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py index 61e5edc..d2144e4 100644 --- a/django_rest_framework_17th/settings/base.py +++ b/django_rest_framework_17th/settings/base.py @@ -20,7 +20,7 @@ DEBUG=(bool, False) ) -environ.Env.read_env(os.path.join(BASE_DIR, '.env')) +environ.Env.read_env(os.path.join(BASE_DIR, '.env.prod')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ From e92994cd4a6b4199b712423b6a2a4f3d2bf453e7 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 23:04:31 +0900 Subject: [PATCH 40/44] =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=EC=A4=91..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/nginx/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index 1ddc346..ee89473 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -6,7 +6,7 @@ server { listen 80; - if ($http_x_forwarded_proto != 'https') { + if ($http_x_forwarded_proto != 'https') { # redirection return 301 https://$host$request_uri; } From 16043745d700d1a015dddf61fb123f2a67632a47 Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 23:20:51 +0900 Subject: [PATCH 41/44] =?UTF-8?q?=EC=A7=84=EC=A7=9C=20=EB=AF=B8=EC=B3=A4?= =?UTF-8?q?=EB=8B=A4=E3=85=A0=E3=85=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/docker/entrypoint.prod.sh | 1 + docker-compose.prod.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/docker/entrypoint.prod.sh b/config/docker/entrypoint.prod.sh index 0cb4cda..112ad16 100644 --- a/config/docker/entrypoint.prod.sh +++ b/config/docker/entrypoint.prod.sh @@ -2,6 +2,7 @@ python manage.py collectstatic --no-input echo "Apply database migrations" +python manage.py makemigrations python manage.py migrate exec "$@" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b7cacdb..725d2a1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -18,7 +18,7 @@ services: # container 이름들 작성 - media:/home/app/web/media entrypoint: # container 가 실행될 때 '반드시' 실행 되는 명령어 - sh - - config/docker/entrypoint.prod.sh + - config/docker/entrypoint.prod.sh # 여기에 migration 명령어 추가 nginx: container_name: nginx From 3ba1f031ab31c23513b3955c70c731fb4e82a8ff Mon Sep 17 00:00:00 2001 From: hyejun Date: Sat, 20 May 2023 23:38:58 +0900 Subject: [PATCH 42/44] [Feat] Show school list --- account/urls.py | 1 + account/views.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/account/urls.py b/account/urls.py index b1e1b55..a4f48a1 100644 --- a/account/urls.py +++ b/account/urls.py @@ -8,4 +8,5 @@ path("logout/", views.LogoutAPIView.as_view()), path("token-refresh/", views.RefreshAccessToken.as_view()), path("health-check/", views.HealthCheck.as_view()), + path("schools/", views.SchoolListAPIView.as_view()), ] diff --git a/account/views.py b/account/views.py index f367208..e63faf2 100644 --- a/account/views.py +++ b/account/views.py @@ -118,3 +118,10 @@ def get(self, request, format=None): "message": "Instance is healthy!" }, status=status.HTTP_200_OK) return response + + +class SchoolListAPIView(APIView): + def get(self, request, format=None): + schools = School.objects.filter(status='A') + serializer = SchoolSerializer(schools, many=True) + return Response(serializer.data) From 84f3dac83b11cb93923bae3625f0d142604de29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sun, 21 May 2023 01:54:58 +0900 Subject: [PATCH 43/44] Update README.md --- README.md | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/README.md b/README.md index 28e4af1..dfd0a55 100644 --- a/README.md +++ b/README.md @@ -471,3 +471,169 @@ JWT 없이 보냈으니 자격 인증 데이터가 없다는 응답이 오는게 ![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/ee5954b1-db9b-4c8a-9080-35adde77457e) [출처](https://velog.io/@kdh92417/Nginx%EC%99%80-Django-EC2%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-feat.-RDS-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%97%B0%EA%B2%B0) 그래도 이번 기회를 통해 배포 아키텍쳐와 처음 써보는 Github Actions에 대해 공부해봐서 매우매우 유익했다.. + +--- +## [7주차 - AWS : https 인증] +저 혼자 주차를 앞서나가네요.. (~~과제가 없던 중간고사 기간 주차를 빼먹었나봐요~~) 혼란을 드린다면 죄송합니다..ㅎㅎ + + +## 💙개념부터 알아보자💙 +### HTTPS와 SSL +`HTTPS`는 보안이 강화된 HTTP다. HTTP는 암호화되지 않은 방법으로 데이터를 전송하기 때문에 서버와 클라이언트가 주고 받는 메시지(Ex. 로그인할 때 비밀번호)를 감청하는 것이 매우 쉽다. +`SSL 인증서`는 클라이언트와 서버간의 통신을 제3자가 보증해주는 전자화된 문서다. 이를 통해 통신 내용이 공격자에게 노출되는 것을 막을 수 있고, 클라이언트가 접속하려는 서버가 신뢰 할 수 있는 서버인지를 판단할 수 있다. +따라서 정상적인 서비스라면 HTTPS와 SSL은 선택이 아닌 **필수**다. + + +보통 서비스가 소규모라면, 1대의 서버에 Nginx 를 설치하고 Let's Encrypt 를 설치해서 SSL을 등록한다. +다만 이럴 경우 트래픽이 늘어 로드밸런서 + 여러 서버 구성으로 확장하기가 쉽지 않다. +그래서 우리는 ACM(AWS Certificate Manager) + Route 53 + ALB(Application Load Balancer) 를 사용할 것이다. + +### ALB(Application Load Balancer) +ALB를 이용한 SSL 동작 과정은 이렇다. + +1) 서버로 request 가 들어오면 load balancer는 요청이 https(port 443) 요청인지 확인한다. +2) 만약 http(port 80) 요청이면 load balancer 가 이 요청을 https 로 redirection 한다. +https 요청이면 load balancer 가 SSL session 의 종단점 역할을 대신해 요청을 decryption 해 target group 의 80 번 포트로 요청을 forwarding 한다. + + +이렇게 구성 하면 ec2 인스턴스에서 실행 중인 server가 ssl decryption 을 수행 하지 않아도 되니 조금더 가벼워 질수 있다. (내 서버의 짐을 줄여 준다.) +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/e4c94d49-c9ec-4034-a0d1-a6dda8a9718a) + +### Amazon Certificate Manager +AWS에서 제공하는 SSL/TLS 인증서 관리 시스템 + +### Route 53 +AWS에서 제공하는 DNS임. 도메인 네임 시스템(DNS)은 사람이 읽을 수 있는 도메인 이름(예: www.amazon.com)을 머신이 읽을 수 있는 IP 주소(예: 192.0.2.44)로 변환해주는 는 시스템 + +### ELB(ALB)의 구성 요소 +ELB는 외부의 요청을 받아들이는 리스너(Listener)와 요청을 분산/전달할 리소스의 집합인 대상 그룹(Target Group)으로 구성된다. ELB는 다수의 리스너와 대상 그룹을 거느릴 수 있다. + + +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/07253625-8cce-45db-ab56-ccf1a90fa881) + +### 배포 점검 사항 +`DEBUG` +운영서버에서는 절대로 디버깅 모드를 사용하지 않는다. +``` +DEBUG = False +``` + + +`ALLOWED_HOSTS` +디버깅 모드에서 ALLOWED_HOSTS 변수가 빈 리스트일 경우 ['localhost', '127.0.0.1', '[::1]'] 의미가 된다. 즉, 로컬 호스트에서만 접속이 가능하다. +디버깅 모드를 끄면 일체 접속이 허용되지 않고 아래와 같이 명시적으로 지정한 호스트에만 접속할 수 있다. +``` +ALLOWED_HOSTS = ['example.com', 'www.example.com', 'localhost', ] +``` + +--- +## 💙AWS를 이용한 HTTPS 적용💙 +### 1️⃣ SSL 인증 +1) 가비아에서 hyejun.store 도메인을 샀다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/2b25c87e-f962-46ca-8ad8-b25df06e1790) + + +2) ACM에서 내 도메인에 대한 SSL 인증서를 받았다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/577d7e29-aff1-4a05-845f-73c58d567176) + + +3) Route 53에 hyejun.store 에 대한 호스팅 영역 생성을 해주고, 다시 ACM으로 돌아가서 'Route 53 에서 레코드 생성' 눌러주면 된다. + + +4) 가비아에 들어가서 네임서버를 AWS로 이관해줘야 한다. 빨간 영역의 네임서버 4개를 모두 추가해준다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/94748433-1315-470f-aff5-ffebeef5a86f) +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/da175281-32ee-491c-993b-9691b3a3d097) + + +5) 인증서 발급 완료! +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/63d15f7a-4c44-404e-b34a-eb6840d2ceb4) + + +### 2️⃣ 로드밸런서(ALB) 설정 +1) 대상 그룹을 만든다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/daef24d5-9a04-4600-ac47-8f27e7f3d108) + + +2) 로드밸런서를 생성하고, 방금 만든 대상 그룹과 그에 대한 리스너 2개 (HTTP:80, HTTPS:443)를 추가해준다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/2c356a90-814c-408f-bee2-c64d5bf8a13a) + + +3) 아까 발급한 SSL 인증서도 추가해준다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/ba35009c-3c12-48ba-bfd7-e7b0950149f4) + + +4) HTTP → HTTPS 리다이렉션 설정 추가하기 +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/040b946f-188c-4c6b-a299-754aa74dc853) + + +### 3️⃣ 로드밸런서를 Route 53의 도메인의 레코드에 등록 +루트, www 에 대한 A레코드를 각각 생성했다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/f6995231-e52c-4168-a1ff-87f01bff0ac3) + + +### 4️⃣ ALB 타겟 그룹 Health Check 수정 +나는 루트 URL에 대해 아무것도 만들어놓지 않아서, 접속시 404 에러가 뜨기 때문에 Health Check Path가 루트(/)로 되어있고 Success Code가 200으로 되어있다면 Unhealthy로 뜰 수 밖에 없다. + +그래서 타겟 그룹으로 들어가서 Health Check Path를 바꿔주고 해당 URL로 접속하면 무조건 HTTP 200 코드를 주는 테스트용 API를 만들어놨다. +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/7dc7db46-8717-4172-b321-e57c28cfaad3) +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/cdd3d81f-09d6-4620-bc90-ec1d7830d89e) + + + + + + +### 5️⃣ 결과 확인 +브라우저에 `http://hyejun.store`로 접속해도 `https://hyejun.store`로 잘 리다이렉션 된다🤗 +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/d38a68df-a172-4167-923c-dfcd9d98fff9) + + +Postman도 잘된다👍 +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/47f1fcb4-40e4-4ab8-ae85-78472e54c2bf) + + +--- +## 🥲에러 해결 과정🥲 +에러 해결하는데 좀 많은 시간을 쓴거 같다ㅎㅎ + + +HTTPS 적용이 안되는건 `ALLOWED_HOSTS=*`로 해결했고, +저번에 까먹은 migrate을 하려는데 `env` 파일 관련 에러도 나고.. `Unknown MySQL server` 에러도 났다. + + +나는 분명 `env.prod`에 RDS 설정을 다 제대로 해줬고.. +`entrypoint.prod.sh`에 이것도 추가해보고.. +``` +echo "Apply database migrations" +python manage.py makemigrations +python manage.py migrate +``` + + +`Dockerfile.prod`에 `jpeg-dev zlib-dev` 도 추가해보고.. + +RDS 마스터 비밀번호를 아예 바꿔서 다시 설정해줘도.. + + +도저히 RDS에 migrate가 안되더라🥲 그래도 혹시나 하는 마음에 MySQL Workbench로 RDS에 접속해봤는데 테이블이 하나도 안만들어졌다. + + +근데... 내가 `env.prod`에서 DB 이름, 마스터 유저 이름, 비밀번호, 포트, ALLOWED_HOSTS 다 계속 확인했는데 하나 확인 안한게 있더라...ㅎ +알고보니까 맨 윗줄에 떡하니 있는 RDS 엔드포인트에 **다른 RDS**가 들어가 있었다...ㅎ + + +바꿔주니까 바로 되더라ㅎㅎㅎㅎㅎ + +물론 아직 일부 테이블만 migrate되서 해결해야하는 상황이긴 한데 정말 너무너무 어이없었다ㅎ 앞으로는 에러가 나면 너무 당연하다고 생각되는 것부터 확인하자.. + + +--- +## 💙회고💙 +이전에는 Nginx에서 직접 SSL인증서를 발급받는 것밖에 안해봐서 AWS가 SSL인증을 이렇게 지원하는지 전혀 몰랐다.. 앞으로 잘 써먹어야겠다 +서브도메인에 대해서도 SSL인증을 하느라 골머리를 앓은적이 있는데 역시 아마존 최고당 +그래서 이번 과제도 넘넘 유익했다 +다음부턴 RDS 엔트포인트 두번 세번 확인할거다ㅎㅎ +벌써 기말고사라니 😵😵 그래두 다들 화이팅! + + +![image](https://github.com/shj718/django_rest_framework_17th/assets/90256209/2d4f7f8a-da80-4647-a4b4-e2e605bf69eb) From 5c2c7afdad4a4dea9be6042c79e18cd837ff3e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=ED=98=9C=EC=A4=80=20=28Riel=29?= <90256209+shj718@users.noreply.github.com> Date: Sun, 21 May 2023 02:22:50 +0900 Subject: [PATCH 44/44] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfd0a55..c8b5ae5 100644 --- a/README.md +++ b/README.md @@ -503,7 +503,7 @@ https 요청이면 load balancer 가 SSL session 의 종단점 역할을 대신 AWS에서 제공하는 SSL/TLS 인증서 관리 시스템 ### Route 53 -AWS에서 제공하는 DNS임. 도메인 네임 시스템(DNS)은 사람이 읽을 수 있는 도메인 이름(예: www.amazon.com)을 머신이 읽을 수 있는 IP 주소(예: 192.0.2.44)로 변환해주는 는 시스템 +AWS에서 제공하는 DNS임. 도메인 네임 시스템(DNS)은 사람이 읽을 수 있는 도메인 이름(예: www.amazon.com)을 머신이 읽을 수 있는 IP 주소(예: 192.0.2.44)로 변환해주는 시스템 ### ELB(ALB)의 구성 요소 ELB는 외부의 요청을 받아들이는 리스너(Listener)와 요청을 분산/전달할 리소스의 집합인 대상 그룹(Target Group)으로 구성된다. ELB는 다수의 리스너와 대상 그룹을 거느릴 수 있다.