GitHub으로 협업 프로젝트하기 - GitHub을 모르는 동료와 PR 반복훈련

  • 대상
    • 팀 프로젝트 투입 전 팀장, 팀원
    • 기본적인 Git 명렁어는 알고 있는 사람 대상의 강의입니다.
  • 내용
    • PR을 통한 협업
    • 필수적인 것만 반복 설명
    • 필수적인 것을 제외한 것, 템플릿 설정 등은 설명하지 않습니다.
  • (팀장 + 팀원) 팀 편성 후 프로세스
      1. 커뮤니케이션 도구 선택(디스코드, 슬랙, 지라, 노션, 피그마, 피그잼 등) 및 룰 만들기
      1. 오거나이제이션 만들기(수업은 개인 repo에서 진행)
      1. GitHub 위키(코드 컨벤션 세팅)
      1. GitHub 프로젝트(GitHub Project 생성 및 공개 설정)
      1. GitHub 이슈 생성
  • (팀원) 이슈 생성(업무 할당) 후 프로세스
      1. 이슈 생성
      1. 브랜치 생성
      1. 작업 진행(commmit은 여러번 할 수 있습니다.)
      1. PR 생성
      1. Merge
      1. 브랜치 삭제
      1. 반복
  • 마지막에는 Django 프로젝트를 만들어서 진행합니다. 꼭 Django를 모르셔도 눈으로 따라오시면 됩니다.
############################ # GitHub repo 2개 생성합니다. # 2개를 생성하는 이유는 일반적으로 규모있는 회사 프로젝트는 여러 repo로 구성되기 때문입니다. # 협업에 있어서도 분야가 다른 소스코드를 1개의 레포에서 통합관리하지 않도록 해주세요. fe-weniv_blog be-weniv_blog ############################# # git clone 해서 로컬에 저장소를 가져옵니다. # 최상위 폴더는 project입니다. fe-weniv_blog > git clone <https://github.com/paullabkorea/fe-weniv_blog.git> . be-weniv_blog > git clone <https://github.com/paullabkorea/be-weniv_blog.git> . ############################# GitHub Wiki > create the first page 클릭 # 프롬프트 ''' GitHub Wiki에 Django mini 프로젝트 컨벤션 가이드를 줄려고 해. 아래와 같이 간단하게 작성해줘. 1. VSC 익스텐션 가이드 2. Python 코딩 스타일 가이드 3. 깃 관리, 깃모지 및 커밋 가이드 ''' # 답변 ''' # Django Mini Project Convention Guide 이 가이드는 Django mini 프로젝트를 진행할 때 팀원들이 지켜야 할 컨벤션에 대해 설명합니다. ## 1. VSC 익스텐션 가이드 프로젝트를 진행할 때 다음과 같은 VSC 익스텐션을 사용하는 것을 권장합니다. - Python - Django - SQLite - Git History - GitLens - Prettier - Auto Rename Tag - Live Server ## 2. Python 코딩 스타일 가이드 Python 코드를 작성할 때는 PEP 8 스타일 가이드를 따릅니다. - 들여쓰기는 스페이스 4칸을 사용합니다. - 최대 줄 길이는 79자입니다. - 함수와 클래스 사이에는 빈 줄을 두 줄 추가합니다. - 모듈 임포트는 알파벳 순서로 정렬합니다. - 변수와 함수 이름은 snake_case를 사용합니다. - 클래스 이름은 PascalCase를 사용합니다. ## 3. 깃 관리, 깃모지 및 커밋 가이드 깃 관리 시 다음 규칙을 따릅니다. - 브랜치 이름은 `feature/기능명` 형식을 사용합니다. - 커밋 메시지는 영어로 작성하며, 제목은 50자 이내로 제한합니다. - 커밋 메시지 제목 끝에 마침표를 사용하지 않습니다. - 커밋 메시지 본문은 한 줄당 72자 이내로 제한합니다. 깃모지는 다음과 같이 사용합니다. - ✨ (sparkles): 새로운 기능 추가 - 🐛 (bug): 버그 수정 - 📝 (memo): 문서 수정 - 🎨 (art): 코드 스타일 개선 - ⚡️ (zap): 성능 개선 - 🔧 (wrench): 설정 파일 수정 - 🚚 (truck): 파일 혹은 경로 이동/이름 변경 - 🔥 (fire): 코드 혹은 파일 제거 - ✅ (white_check_mark): 테스트 추가/수정 ''' ############################# GitHub BE-Team repo에서 Project 클릭 > New Project > ProjectName Project 생성 Project settings(오른쪽 상단 ...)에서 Manage access 클릭 > private 변경 > Public으로 변경 ############################# # 1cicle 시작(다만 dev 브랜치는 단 1번만 만들면 됩니다.) ############################# GitHub BE-Team repo에서 Issues 클릭 > New Issue > IssueName Issue 생성 * Add a title: 기능 이름 생성 * Add a description: 기능 내용 생성 * Assignees: 팀원 선택 * Labels: bug, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix * Projects: Project 선택 submit new issue 클릭 Project에서 Todo, In Progress, Done 생성 Development에서 Create a branch 클릭 > BranchName 생성 > Create branch 클릭 ############################# 복사하라고 뜹니다. 복사해주세요. 아직 커멘드 라인에 붙여넣진 않겠습니다. git fetch origin git checkout 1-one ############################# 전략은 GitHub Flow + develop branch 전략을 사용합니다. 전략상 필요한 dev branch를 만들어줍니다. git branch git branch dev git push --set-upstream origin dev settings > General > Default branch > dev 선택 // 이제부터 merge는 develop branch로 합니다. ############################# git branch # 현재 브랜치 확인 git fetch origin # 원격 저장소의 브랜치 정보를 가져옴 git checkout 1-one # 브랜치 생성 및 이동 # 작업 진행 git add . # 변경사항을 스테이징 git commit -m "feat: test one" # 커밋 git push # 원격 저장소에 푸시 ############################# GitHub BE-Team repo에서 Compare&Pull requests 클릭 또는 Pull requests 클릭 > New pull request 클릭 add a title: feat: test one add a description: feat: test one(내용은 좀 더 상세하게 작성해주세요. table과 같은 것을 넣어주셔도 좋습니다.) Create pull request 클릭 ############################# Merge pull request 클릭 Confirm merge 클릭 Delete branch 클릭 ############################# // local에서 해야 하는 것 git checkout dev git pull git branch -d 1-one # VSC에 Source control에 가서 view git graph(git log)를 클릭 # 원하는 브랜치에 네모 박스에서 마우스 오른쪽 클릭하고 delete branch 클릭해서 # GUI로도 브랜치 삭제가 가능합니다. ############################# # 1cicle 끝, 이렇게 5번 반복합니다. ############################# New issue 생성 add a title: init django settings add a description: 장고 초기 세팅입니다. 아래와 같은 내용을 포함합니다. - requirements.txt - .gitignore - settings.py - urls.py - README.md ############################# Project에서 Todo, In Progress, Done 생성 Development에서 Create a branch 클릭 > BranchName 생성 > Create branch 클릭 ############################# 복사하라고 뜹니다. 복사해주세요. git fetch origin git checkout 3-init-django-settings git branch ############################# powershell로 작업 ################################### 파이썬 설치 VSC를 이 폴더 기준으로 열었습니다. # 3.12버전으로 최신버전 확인해야 합니다. python --version mkdir viewset cd viewset python -m venv venv .\\venv\\Scripts\\activate # source ./venv/bin/activate pip install django pip install pillow pip install djangorestframework pip install django-cors-headers django-admin startproject config . # freeze로 requirements.txt 생성 pip freeze > requirements.txt # .gitignore 생성(powershell이어서 echo 명령어 사용) # <https://www.toptal.com/developers/gitignore> echo "venv/`n__pycache__/`n*.pyc`ndb.sqlite3`n.DS_Store`n*/migrations/" > .gitignore ################################### git add . # 변경사항을 스테이징 git commit -m "feat: test one" # 커밋 git push # 원격 저장소에 푸시 ############################# BE-Team repo에서 Compare&Pull requests 클릭 또는 Pull requests 클릭 > New pull request 클릭 Merge까지 진행 ############################# // local에서 해야 하는 것 git checkout dev git pull git branch -d 3-init-django-settings ############################# New issue 생성 add a title: init django app add a description: 장고 앱을 생성합니다. 아래와 같은 내용을 포함합니다. - app 생성 - model 생성 - serializer 생성 - viewset 생성 - url 연결 ############################# Project에서 Todo, In Progress, Done 생성 Development에서 Create a branch 클릭 > BranchName 생성 > Create branch 클릭 ############################# 복사하라고 뜹니다. 복사해주세요. git fetch origin git checkout 4-init-django-app git branch ############################# python manage.py startapp posts # => 혹시 setuptools관련 error 발생하면 입력하세요. # pip install setuptools ############################# git add . git commit -m 'posts 앱 생성' ################################### ALLOWED_HOSTS = ['*'] INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # custom app 'posts', # install app 'rest_framework', 'corsheaders', ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "corsheaders.middleware.CorsMiddleware", # 추가 ] ## LANGUAGE_CODE = 'ko-kr' TIME_ZONE = 'Asia/Seoul' ## MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' ################################### git add . git commit -m 'settings.py 설정 수정' ################################### # config > urls.py from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path("posts/", include("posts.urls")), path("admin/", admin.site.urls), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ################################### git add . git commit -m 'urls.py 설정 수정' ################################### posts app 작성(models.py, urls.py, views.py, serializers.py, settings.py) ################################### # posts > models.py from django.db import models from django.contrib.auth.models import User class Post(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts") caption = models.TextField() image = models.ImageField(upload_to="post_images/", blank=True) # 다른 점: blank=True created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.author} - {self.caption[:10]}" class Comment(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments") author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments") text = models.TextField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.author.username} - {self.text[:10]}" class Like(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="likes") user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="likes") created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("post", "user") def __str__(self): return f"{self.user.username} likes {self.post.caption}" ################################### git add . git commit -m 'models.py 작성' ################################### # posts > urls.py (생성 후 저장) from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import PostViewSet, CommentViewSet, LikeView router = DefaultRouter() router.register("posts", PostViewSet) router.register("posts/(?P<post_pk>[^/.]+)/comments", CommentViewSet) urlpatterns = [ path("", include(router.urls)), path("posts/<int:post_id>/like/", LikeView.as_view(), name="post-like"), ] ################################### git add . git commit -m 'posts > urls.py 작성' ################################### # posts > views.py from rest_framework import viewsets from .serializers import PostSerializer, CommentSerializer from .models import Comment, Post, Like from rest_framework import permissions, views, response, status from django.shortcuts import get_object_or_404 class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticated] def perform_create(self, serializer): serializer.save(author=self.request.user) class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = [permissions.IsAuthenticated] def perform_create(self, serializer): serializer.save(author=self.request.user) def get_queryset(self): return self.queryset.filter(post_id=self.kwargs["post_pk"]) class LikeView(views.APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request, post_id): post = get_object_or_404(Post, id=post_id) like, created = Like.objects.get_or_create(post=post, user=request.user) if not created: return response.Response(status=status.HTTP_409_CONFLICT) return response.Response(status=status.HTTP_201_CREATED) def delete(self, request, post_id): post = get_object_or_404(Post, id=post_id) like = get_object_or_404(Like, post=post, user=request.user) like.delete() return response.Response(status=status.HTTP_204_NO_CONTENT) ################################### git add . git commit -m 'posts > views.py 작성' ################################### # posts > serializers.py from rest_framework import serializers from .models import Comment, Post, Like class CommentSerializer(serializers.ModelSerializer): author_username = serializers.ReadOnlyField(source="author.username") class Meta: model = Comment fields = ["id", "post", "author", "author_username", "text", "created_at"] read_only_fields = ["author", "author_username"] class PostSerializer(serializers.ModelSerializer): author_username = serializers.ReadOnlyField(source="author.username") comments = CommentSerializer(many=True, read_only=True) likes_count = serializers.IntegerField(source="likes.count", read_only=True) is_liked = serializers.SerializerMethodField() class Meta: model = Post fields = [ "id", "author", "author_username", "caption", "image", "created_at", "comments", "likes_count", "is_liked", ] read_only_fields = ["author", "author_username", "likes_count", "is_liked"] def get_is_liked(self, obj): user = self.context["request"].user if user.is_authenticated: return Like.objects.filter(post=obj, user=user).exists() return False ################################### git add . git commit -m 'posts > serializers.py 작성 및 posts 앱 설정 끝' git push ################################### BE-Team repo에서 Compare&Pull requests 클릭 또는 Pull requests 클릭 > New pull request 클릭 Merge까지 진행 ################################### # local에서 해야 하는 것 git checkout dev git pull git branch -d 4-init-django-app ################################### # Test를 진행하기 위해 별도 브랜치 생성 git branch test git checkout test ################################### python manage.py makemigrations python manage.py migrate python manage.py runserver python manage.py createsuperuser leehojun dlghwns1234! ################################### 요즘은 api가 되는지 안되는지 ChatGPT나 Caulde3에서 테스트도 가능합니다. ################################### API Test - VSC Extension - Thunder Client ################################### # URL <http://127.0.0.1:8000/posts/posts/> # Method POST # Auth leehojun dlghwns1234! # Body { "caption":"hello" } ################################### # URL <http://127.0.0.1:8000/posts/posts/> # Method GET ################################### # URL <http://127.0.0.1:8000/posts/posts/1/> # Method PATCH # Body { "caption":"hello patch test" } ################################### # 테스트가 끝났으니 다시 dev 브랜치로 돌아갑니다. # test 브랜치는 삭제합니다. git add . git commit -m 'test end' git checkout dev git branch -d test ################################### # Router에 URL 추가 from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import PostViewSet, CommentViewSet, LikeView router = DefaultRouter() router.register("posts", PostViewSet) router.register("posts/(?P<post_pk>[^/.]+)/comments", CommentViewSet) urlpatterns = [ path("", include(router.urls)), path("posts/<int:post_id>/like/", LikeView.as_view(), name="post-like"), ] ################################### # views.py from rest_framework import viewsets from rest_framework.decorators import action # 생략 class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticated] @action(detail=True, methods=["get"]) def sampleone(self, request, pk=None): """ detail=True: 특정 게시물에 대한 작업 detail=False: 모든 게시물에 대한 작업 /posts/{pk}/sampleone/: 개별 게시물의 제목과 내용을 반환하는 엔드포인트 (detail=True) """ data = {"title": "hello", "content": "world"} return response.Response(data) @action(detail=False, methods=["get"]) def sampletwo(self, request): """ /posts/sampletwo/: 모든 게시물의 제목과 작성자 이름을 반환하는 엔드포인트 (detail=False) """ data = [{"title": "hello 2", "author": "world 2"}] return response.Response(data) ################################### # URL <http://127.0.0.1:8000/posts/posts/1/sampleone/> # Method GET ################################### # URL <http://127.0.0.1:8000/posts/posts/sampletwo/> # Method GET ################################### # views.py # 아래와 같이 재정의 할 수 있습니다. class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticated] def list(self, request, *args, **kwargs): return response.Response("Hello, World!") ################################### # 매서드별 권한 재정의 class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer def list(self, request, *args, **kwargs): # self.permission_classes = [permissions.AllowAny] # self.permission_classes = [permissions.IsAuthenticated] return super().list(request, *args, **kwargs) ################################### # 다른 repo에 와서 똑같은 작업을 반복합니다.(repo가 여러개여도 마찬가지입니다.) # 이슈 등록 # Project 등록(기존에 등록했던 것으로 관리가 가능합니다.) # Branch 생성 # 작업 진행 # PR 생성 # Merge