diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 044cdd1..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:
@@ -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 0bc3174..0eb9829 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,8 +132,10 @@ celerybeat.pid
*.sage.py
# Environments
-.env
+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/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/README.md b/README.md
index 8c0caf2..c8b5ae5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,639 @@
-# CEOS 17기 백엔드 스터디
+# CEOS 17기 백엔드 스터디
+## [2주차]
+
+### ORM 이용해보기
+1. 게시글 생성
+
+2. 게시글 수정 및 쿼리셋 조회
+
+3. filter 함수 사용
+filter의 `__contains`와 `__startswith`을 사용해봤다.
+
+
+---
+### ERD 설계
+실무에서 ERD를 그린 후에 SQL문을 export해서 DB에 넣는 경우, ERD에 관계선으로 연관관계(Ex 1:N)를 설정해두면 테스트 데이터를 넣기가 불편해서 거의 하지 않는다고 배워서 습관적으로 관계선 설정을 안했더니 테이블간 관계가 한눈에 안들어오는것 같다
+
+
+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(전공/영역) 테이블을 따로 추가했다.
+예를들어,
+
+'한국근현대사' 강의가 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는 어떻게 되어있을지 궁금해졌다. 테이블이 엄청엄청 많겠지?
+모델을 분리했어야 하는데 이번 과제는 다른일들때문에 너무 늦게시작해서 아쉬움이 남는다 담부턴 더 미리 시작하자
+
+---
+## [3주차]
+
+### API 명세
+우선적으로 어떤 CBV API를 만들지 에브리타임을 구경하면서 리스트업해봤다
+
+게시글 삭제 및 수정 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/`)
+게시판을 지정해서 게시글을 작성해보자
+
+
+2. 게시글 수정 및 삭제 API (Method: `PUT`, URL: `/community/posts/1/`)
+partial update로 원래는 `'A'`였던 `status`를 `'D'`로 바꿔줌으로써 게시글을 삭제해보자
+물론 당연히 `title`이나 `contents` 수정도 가능하다
+
+
+3. 특정 게시글 조회 API (Method: `GET`, URL: `/community/posts/1/`)
+특정 게시글을 조회해보자
+방금 삭제했기 때문에 `status`가 `'D'`로 바뀐걸 볼수있다
+
+
+4. 전체 게시판 조회 API (Method: `GET`, URL: `/community/boards/`)
+일단은 PK랑 이름만 나오게 했다
+
+
+5. 특정 게시판 전체 게시글 조회 API (Method: `GET`, URL: `/community/boards/1/`)
+전체 게시글이 조회될때 삭제된 게시글은 보이면 안되니까 `status`가 `'A'`인것만 보이도록 _filter_ 해줬다
+원래는 있었는데요
+
+삭제하고나면 없어요
+
+
+---
+### ViewSet으로 리팩토링하기
+요건.. 너무 신기했다 이게 될까? 했는데 진짜 되더랑
+몇줄 안되는 코드로 이렇게 특정 게시글 디테일 보기 및 수정/삭제가 된다
+
+
+---
+### 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. 게시판으로 필터링
+
+2. 제목으로 필터링
+
+3. 글쓴이로 필터링
+
+
+---
+### 겪은 오류와 해결 과정
+- 앱을 분리하면서 `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보다 더 좋은건지는 아직 잘 모르겠따
+- 다들 중간고사 잘 마무리하고 만나요👻
+
+---
+## [5주차 - Simple JWT]
+
+## 👀 로그인 인증은 어떻게 하나요? 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로 구성됨)
+- 동작 방식:
+
+
+---
+## 🗣️ 피드백 반영 및 수정사항
+1. 찬혁오빠가 구현한 ***safe delete*** 참고해서 base model에 delete 메소드 추가함
+ 데이터 삭제시 deleted_at 컬럼에 삭제시간을 저장하는 방식 = deleted_at이 null이면 정상(=삭제 안된) 게시물
+2. "해당 글의 status는 D인데 조회했을때 보이면 안될 것 같아요🥹🥹"
+ → Filter 클래스에 ***deleted_at이 null인 것만*** 조건을 추가한 ***filter 메소드***를 구현함
+
+#### (수정한 filter 메소드)
+
+#### (결과 확인)
+⬇️ 이렇게 DB에서 `deleted_at` 컬럼에 삭제시간이 들어가 있는 경우,
+
+➡️ postman으로 조회했을때 안보이는걸 확인할 수 있다!! profile_id가 2인 글은 삭제되었으므로 필터링해도 안보임!
+
+
+---
+## 👩💻 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으로 확인해보니 잘된다ㅎㅎ
+
+번외) 원래 회원가입할땐 자동로그인이 아니면 토큰 발급을 안한다. 근데 사진에서 쿠키에 뭔가가 있는건 직전에 테스트하던 회원 로그아웃을 안해서 아직 쿠키가 남아있음...ㅎ ~~(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 토큰 둘다 잘 오는걸 확인 가능하다
+
+- 쿠키에도 refresh 토큰이 잘 들어가 있다
+
+
+
+➡️ 발급 받은 토큰을 디코딩해보면, 유저의 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` 로 나오는게 맞다!!
+
+
+
+### 📌 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
+ )
+```
+
+
+➡️포스트맨으로 테스트 해봤더니 새로운 토큰이 잘 발급된다..! 이제 프론트에서는 이 새로운 토큰을 헤더에 넣어서 요청을 보내면 된다.
+
+
+
+### 📌 JWT Logout 구현하기
+로그아웃 로직은 이렇다.
+1️⃣ 프론트에서 LogoutApi를 호출한다.
+2️⃣ 호출과 동시에 프론트는 가지고 있던 Access token을 삭제한다.
+3️⃣ 백엔드에서는 cookie에 존재하는 Refresh token을 삭제한다.
+그래서 나는 쿠키의 Refresh 토큰을 삭제해주도록 구현했다. Postman으로 확인해보자ㅎㅎ
+
+➡️ 로그아웃이 잘되서 쿠키에 있던 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가 잘 적용되었는지 볼 것이다
+- 유효기간이 만료된 경우: 이렇게 친절하게 알려준다ㅎㅎ
+
+- 유효한 토큰으로 다시 요청을 보내면, 다시 잘 보인다!
+
+
+---
+## 🍀 느낀점
+***장고는 편리하다...*** 놀랐던게 장고에서는 클라이언트가 넘겨준 JWT로 유저를 불러오는걸 무려 **함수 하나**로 제공한다.. JWT를 추출해서, 파싱하고, 디코딩하고, 유저ID를 추출해서, 그 유저ID로 DB에서 유저 정보를 불러오는 로직을 내가 직접 클래스에 작성할 필요 없이 `authenticate()` 함수 하나로 그냥 끝나버리는 것... (약간 허무한거같기두 ㅎ)
+
+공식 문서에는 이렇게 나와있다.
+
+
+
+
+
+이번 기회로 로그인 및 사용자 인증에 대해 다시 자세히 복습해 볼 수 있어서 재밌었당!
+
+---
+## [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 에러 페이지가 뜨지만 그래도 접속에 성공했다!
+
+
+---
+## ✨실제 배포하기(with AWS, Github Actions, Docker)✨
+### AWS EC2, RDS 구축
+EC2는 스토리지 크기를 최대인 30Gb로 설정해주고, 탄력적 IP를 할당해줬다.
+RDS는 MySQL 버전 5.7.41 로 설정해줬다.
+
+⬇️ EC2에서 RDS에 접속하려면, EC2의 보안그룹ID를 RDS의 보안그룹 인바운드 규칙에 추가해줘야 한다.
+
+
+
+타임존, 인코딩 설정을 위해서 새로운 파라미터 그룹도 생성해줬다.
+
+
+
+`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` 로 되어있어서 에러가 났다. 고쳐줬다.
+
+
+다 고쳐주니 잘 빌드 됐다!
+
+
+브라우저로 EC2에 접속해보면 서버가 떠있는걸 확인 가능🤗
+
+
+### API 요청 보내기
+JWT 없이 보냈으니 자격 인증 데이터가 없다는 응답이 오는게 당연하다.
+
+
+근데 로그인 요청을 보냈더니 500 에러가 났다..
+
+
+
+그동안 Spring으로 개발할때 500 Internal Server 에러가 나는건 경험상 8-90% 내 프로젝트 Service단에서 로직이 잘못된 경우였다. 근데 여기선 왜 나는지 모르겠어서 `docker logs --details 050f4c270e9f` 로 로그를 확인해봤다.
+
+근데 뜬금없이
+```
+ /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` 로 컨테이너 안에 들어가서 이것저것 많이 해봤는데 뭐가 문제인지 아직 모르겠다 하하... 혹시 이 에러의 이유를 아신다면 알려주세요🥲🥲
+
+---
+## ✨회고✨
+확실히 도커는 이론으로 공부할때보다 실제 프로젝트에 적용하는게 어렵다.
+
+[출처](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 을 수행 하지 않아도 되니 조금더 가벼워 질수 있다. (내 서버의 짐을 줄여 준다.)
+
+
+### 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는 다수의 리스너와 대상 그룹을 거느릴 수 있다.
+
+
+
+
+### 배포 점검 사항
+`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 도메인을 샀다.
+
+
+
+2) ACM에서 내 도메인에 대한 SSL 인증서를 받았다.
+
+
+
+3) Route 53에 hyejun.store 에 대한 호스팅 영역 생성을 해주고, 다시 ACM으로 돌아가서 'Route 53 에서 레코드 생성' 눌러주면 된다.
+
+
+4) 가비아에 들어가서 네임서버를 AWS로 이관해줘야 한다. 빨간 영역의 네임서버 4개를 모두 추가해준다.
+
+
+
+
+5) 인증서 발급 완료!
+
+
+
+### 2️⃣ 로드밸런서(ALB) 설정
+1) 대상 그룹을 만든다.
+
+
+
+2) 로드밸런서를 생성하고, 방금 만든 대상 그룹과 그에 대한 리스너 2개 (HTTP:80, HTTPS:443)를 추가해준다.
+
+
+
+3) 아까 발급한 SSL 인증서도 추가해준다.
+
+
+
+4) HTTP → HTTPS 리다이렉션 설정 추가하기
+
+
+
+### 3️⃣ 로드밸런서를 Route 53의 도메인의 레코드에 등록
+루트, www 에 대한 A레코드를 각각 생성했다.
+
+
+
+### 4️⃣ ALB 타겟 그룹 Health Check 수정
+나는 루트 URL에 대해 아무것도 만들어놓지 않아서, 접속시 404 에러가 뜨기 때문에 Health Check Path가 루트(/)로 되어있고 Success Code가 200으로 되어있다면 Unhealthy로 뜰 수 밖에 없다.
+
+그래서 타겟 그룹으로 들어가서 Health Check Path를 바꿔주고 해당 URL로 접속하면 무조건 HTTP 200 코드를 주는 테스트용 API를 만들어놨다.
+
+
+
+
+
+
+
+
+### 5️⃣ 결과 확인
+브라우저에 `http://hyejun.store`로 접속해도 `https://hyejun.store`로 잘 리다이렉션 된다🤗
+
+
+
+Postman도 잘된다👍
+
+
+
+---
+## 🥲에러 해결 과정🥲
+에러 해결하는데 좀 많은 시간을 쓴거 같다ㅎㅎ
+
+
+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 엔트포인트 두번 세번 확인할거다ㅎㅎ
+벌써 기말고사라니 😵😵 그래두 다들 화이팅!
+
+
+
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..fc2b686
--- /dev/null
+++ b/account/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+
+from account.models import *
+
+# admin.site.register(Profile)
+admin.site.register(School)
+admin.site.register(MyUser)
\ No newline at end of file
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/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/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/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/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/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..48008e5
--- /dev/null
+++ b/account/models.py
@@ -0,0 +1,69 @@
+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 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=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):
+ superuser = self.create_user(
+ email=email,
+ nickname=nickname,
+ school=school,
+ password=password,
+ )
+ 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)
+ 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
+
+
+# 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/permissions.py b/account/permissions.py
new file mode 100644
index 0000000..96bfdaa
--- /dev/null
+++ 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/account/serializers.py b/account/serializers.py
new file mode 100644
index 0000000..6747f56
--- /dev/null
+++ b/account/serializers.py
@@ -0,0 +1,38 @@
+from rest_framework import serializers
+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__'
+
+ def create(self, validated_data):
+ email = validated_data.get('email')
+ nickname = validated_data.get('nickname')
+ school = validated_data.get('school')
+ password = validated_data.get('password')
+ user = MyUser(
+ email=email,
+ nickname=nickname,
+ school=school
+ )
+ user.set_password(password)
+ user.save()
+ return user
+
+
+# class ProfileSerializer(serializers.ModelSerializer):
+# school = SchoolSerializer(read_only=True)
+#
+# class Meta:
+# model = Profile
+# fields = '__all__'
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/account/urls.py b/account/urls.py
new file mode 100644
index 0000000..a4f48a1
--- /dev/null
+++ b/account/urls.py
@@ -0,0 +1,12 @@
+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()),
+ 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
new file mode 100644
index 0000000..e63faf2
--- /dev/null
+++ b/account/views.py
@@ -0,0 +1,127 @@
+import jwt
+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
+from django_rest_framework_17th.settings.base import SECRET_KEY
+
+
+# 회원 가입
+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):
+ # 유저 인증
+ myUser = authenticate(
+ email=request.data.get("email"), password=request.data.get("password")
+ )
+ # 해당 이메일, 비밀 번호로 가입한 유저가 있는 경우
+ if myUser is not None:
+ serializer = MyUserSerializer(myUser)
+ # jwt, refresh token 발급
+ token = TokenObtainPairSerializer.get_token(myUser)
+ 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:
+ print("Login error")
+ return Response(
+ {"message": "Failed to login"},
+ status=status.HTTP_400_BAD_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):
+ # 클라이언트의 쿠키에 저장된 refresh 토큰을 삭제함으로써 로그 아웃 처리
+ response = Response({
+ "message": "Logout success"
+ }, 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
+
+
+class SchoolListAPIView(APIView):
+ def get(self, request, format=None):
+ schools = School.objects.filter(status='A')
+ serializer = SchoolSerializer(schools, many=True)
+ return Response(serializer.data)
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 71a8362..0000000
--- a/api/models.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.db import models
-
-# Create your models here.
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/filters.py b/community/filters.py
new file mode 100644
index 0000000..ae937b0
--- /dev/null
+++ b/community/filters.py
@@ -0,0 +1,34 @@
+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')
+ 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')
+ contents = filters.CharFilter(method='filter_contents')
+
+ class Meta:
+ model = Post
+ fields = ['board', 'myUser', '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, 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)
+
+ def filter_contents(self, queryset, contents, value):
+ return queryset.filter(contents__icontains=value, deleted_at__isnull=True)
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/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/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/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/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..8be4c60
--- /dev/null
+++ b/community/models.py
@@ -0,0 +1,72 @@
+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("account.School", 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
+
+
+class Post(BaseModel):
+ board = models.ForeignKey("Board", 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')
+ 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("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)
+
+ def __str__(self):
+ return self.contents
+
+
+class PostLike(BaseModel):
+ post = models.ForeignKey("Post", 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.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)
+ myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1)
+
+ def __str__(self):
+ 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)
+ myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1)
+
+ def __str__(self):
+ return f"{self.myUser} 's scrap of {self.post}"
diff --git a/community/serializers.py b/community/serializers.py
new file mode 100644
index 0000000..74b7328
--- /dev/null
+++ b/community/serializers.py
@@ -0,0 +1,81 @@
+from rest_framework import serializers
+from community.models import *
+
+
+class BoardSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Board
+ fields = ['id', 'name']
+
+
+class CommentSerializer(serializers.ModelSerializer):
+ myUser_nickname = serializers.SerializerMethodField()
+ myUser_profile_img_path = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Comment
+ fields = ['id', 'myUser', 'myUser_nickname', 'myUser_profile_img_path', 'parent', 'contents', 'status',
+ 'created_at']
+
+ def get_myUser_nickname(self, obj):
+ return obj.myUser.nickname
+
+ def get_myUser_profile_img_path(self, obj):
+ return obj.myUser.profile_img_path
+
+
+class PostSerializer(serializers.ModelSerializer):
+ myUser_nickname = serializers.SerializerMethodField()
+ myUser_profile_img_path = serializers.SerializerMethodField()
+ comment_set = CommentSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = Post
+ fields = ['id', 'myUser', 'myUser_nickname', 'myUser_profile_img_path', 'title', 'contents', 'is_anonymous',
+ 'is_question', 'status', 'created_at', 'updated_at', 'comment_set']
+
+ def get_myUser_nickname(self, obj):
+ return obj.myUser.nickname
+
+ def get_myUser_profile_img_path(self, obj):
+ return obj.myUser.profile_img_path
+
+
+class PostListSerializer(serializers.ModelSerializer):
+ myUser_nickname = serializers.SerializerMethodField()
+ myUser_profile_img_path = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Post
+ fields = ['id', 'myUser', 'myUser_nickname', 'myUser_profile_img_path', 'title', 'contents', 'is_anonymous',
+ 'is_question', 'status', 'created_at', 'updated_at']
+
+ def get_myUser_nickname(self, obj):
+ return obj.myUser.nickname
+
+ def get_myUser_profile_img_path(self, obj):
+ return obj.myUser.profile_img_path
+
+
+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/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/urls.py b/community/urls.py
new file mode 100644
index 0000000..ef6e07c
--- /dev/null
+++ b/community/urls.py
@@ -0,0 +1,19 @@
+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('', include(router.urls))
+]
diff --git a/community/views.py b/community/views.py
new file mode 100644
index 0000000..1b5fbee
--- /dev/null
+++ b/community/views.py
@@ -0,0 +1,70 @@
+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
+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):
+# 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):
+ # 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]
+ filterset_class = PostFilter
+
+# deploy test
\ No newline at end of file
diff --git a/config/docker/entrypoint.prod.sh b/config/docker/entrypoint.prod.sh
index 09f6570..112ad16 100644
--- a/config/docker/entrypoint.prod.sh
+++ b/config/docker/entrypoint.prod.sh
@@ -1,5 +1,8 @@
#!/bin/sh
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/config/nginx/nginx.conf b/config/nginx/nginx.conf
index fb084b1..ee89473 100644
--- a/config/nginx/nginx.conf
+++ b/config/nginx/nginx.conf
@@ -6,6 +6,10 @@ server {
listen 80;
+ if ($http_x_forwarded_proto != 'https') { # redirection
+ 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;
diff --git a/django_rest_framework_17th/settings/base.py b/django_rest_framework_17th/settings/base.py
index 2a2381c..d2144e4 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__))))
@@ -19,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/
@@ -28,21 +29,71 @@
SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = env('DEBUG')
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['0.0.0.0', '.hyejun.store', '58.233.200.22']
# Application definition
INSTALLED_APPS = [
+ 'account',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
- 'api',
+ 'rest_framework',
+ 'community',
+ 'timetable',
+ 'utils',
+ 'django_filters',
+ 'rest_framework_simplejwt',
]
+AUTH_USER_MODEL = 'account.MyUser'
+
+REST_FRAMEWORK = {
+ 'DEFAULT_PERMISSION_CLASSES': (
+ 'rest_framework.permissions.AllowAny',
+ ),
+ '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',
@@ -104,7 +155,7 @@
USE_L10N = True
-USE_TZ = True
+USE_TZ = False
# Static files (CSS, JavaScript, Images)
@@ -116,3 +167,6 @@
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+
+# AutoField setting
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
diff --git a/django_rest_framework_17th/urls.py b/django_rest_framework_17th/urls.py
index cf9bedd..084d4ab 100644
--- a/django_rest_framework_17th/urls.py
+++ b/django_rest_framework_17th/urls.py
@@ -14,8 +14,10 @@
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')),
+ path('account/', include('account.urls')),
]
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index cf94cd6..725d2a1 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.prod
+ expose: # container 포트 번호
+ - 8000
+ volumes: # 데이터 볼륨 매핑
- static:/home/app/web/static
- media:/home/app/web/media
- entrypoint:
+ entrypoint: # container 가 실행될 때 '반드시' 실행 되는 명령어
- sh
- - config/docker/entrypoint.prod.sh
+ - config/docker/entrypoint.prod.sh # 여기에 migration 명령어 추가
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 31cf4e5..9e0e47e 100644
Binary files a/requirements.txt and b/requirements.txt differ
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/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/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/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/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/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..37ffbef
--- /dev/null
+++ b/timetable/models.py
@@ -0,0 +1,61 @@
+from django.db import models
+from utils.models import BaseModel
+from account.models import *
+
+
+class TimeTable(BaseModel):
+ myUser = models.ForeignKey("account.MyUser", on_delete=models.CASCADE, default=1)
+ 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, null=True)
+
+ 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.timeTable.myUser} started taking {self.lecture}"
+
+
+class LectureReview(BaseModel):
+ lecture = models.ForeignKey("Lecture", 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)
+
+ def __str__(self):
+ return self.contents
+
+
+class ReviewLike(BaseModel):
+ lectureReview = models.ForeignKey("LectureReview", on_delete=models.CASCADE)
+ myUser = models.ForeignKey("account.MyUser", related_name="myUsers", on_delete=models.CASCADE, default=1)
+
+ def __str__(self):
+ return f"{self.myUser} '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/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/urls.py b/timetable/urls.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/views.py b/timetable/views.py
similarity index 100%
rename from api/views.py
rename to timetable/views.py
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/admin.py b/utils/admin.py
similarity index 100%
rename from api/admin.py
rename to utils/admin.py
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..c2da14f
--- /dev/null
+++ b/utils/models.py
@@ -0,0 +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
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.