본문 바로가기
프로젝트

[팀프로젝트] 머신러닝 project - 띵곡맛집 S.A

by 코드뭉치 2023. 5. 31.

 

Team Rule

이건 지켜주세요!★★★

 

┌팀프로젝트에 연관된 것 팀원에게 질문

└기본 개념or방법 관련 튜터님에게 질문

 

12:00~13:00 점심

17:00~18:00 현황공유 - issue있을 경우!

18:00~19:00 저녁

 

정기 회의

  1. 시작회의 - AM 09:00 - 1시간 이내로 | 타임오버X - 하루계획, 현황공유, 역할분담, 건의사항, 개발issue공유
  1. 현황공유 - PM 17:00 → 다음날 or 저녁시간에 튜터님 질문
  1. 마감회의 - PM 20:30 - 1시간 이내로 | 타임오버X - 내일계획, 일정 및 현황공유 + 내일계획(간략히만)

소통법 Slack

  1. Team slack 창을 통해 일정 및 정보 공유!
  1. ★★★잠수금지!!★★★
  1. 진행 현황은 솔직하게!
  1. 식사 시간은 정하되, 팀프로젝트 전에는 자유롭게!
  1. 소극적이지 마라! 너는 이미! 할 수 있다!
  1. ★한숨 금지!
  1. 연락두절 금지! slack 수시 확인!
  1. ★고민공유! 제발! 속에 담지 마요! 슬렉도 있구요, 따라나와도 있어요.

 


코드 컨벤션
  1. 표기법
    • 변수/함수 : Snake 표기법 ex) 'python_is_very_good'
    • Class : Pascal 표기법 ex) 'PythonIsVeryGood'
  1. html 클래스명
    • Bootstrap 사용 시 오버라이딩 금지! 반드시 추가로 class를 지정해주거나, 태그 인라인으로 기입!
    • class명 지정 시 이니셜로 구분하기! ex) box_myo, box_ljm, box_yhm, box_nmh

깃 커밋 컨벤션
깃 커밋 컨벤션  
###########################################################################################

#<타입>: <제목> 의 형식으로 제목을 아래 공백줄에 작성  
#제목은 50자 이내 / 변경사항이 "무엇인지 명확히 작성/ 끝에 마침표 금지 # 예) :sparkles:Feat: 로그인 기능 추가,

# 바로 아래 공백은 지우지 마세요 (제목과 본문의 분리를 위함)

#본문(구체적인 내용)을 아랫줄에 작성  
#여러 줄의 메시지를 작성할 땐 "-"로 구분 (한 줄은 72자 이내)

###########################################################################################

#꼬릿말(footer)을 아랫줄에 작성 (현재 커밋과 관련된 이슈 번호 추가 등) #예) Close #7, related\_to: #7

###########################################################################################  
\[스파클 이모티콘\]Feat:

# ✨Feat: 새로운 기능 추가

# 🐛Fix: 버그 수정

# 🚑!HOTFIX: 급한 오류 수정

# 📚Docs: 문서 수정

# ☔Test: 테스트 코드 추가

# 🔨Refact: 코드 리팩토링

# 🎨Style: 코드 의미에 영향을 주지 않는 변경사항

# 💄Design: CSS 등 사용자 UI디자인 변경

# 💬Comment: 필요한 주석 추가 및 변경

# 🏷️Rename: 파일 혹은 폴더명을 수정하거나 옮기는 작업만의 경우

# ❌Remove: 파일을 삭제하는 작업만 수행한 경우

# 📦Package: 빌드 부분 혹은 패키지 매니저 수정사항

# :sparkles:Feat: 새로운 기능 추가

# :bug:Fix: 버그 수정

# :ambulance:HOTFIX: 급한 오류 수정

# :books:Docs: 문서 수정

# :umbrella:Test: 테스트 코드 추가

# :hammer:Refact: 코드 리팩토링

# :art:Style: 코드 의미에 영향을 주지 않는 변경사항

# :lipstick:Design: CSS 등 사용자 UI디자인 변경

# :speech\_balloon:Comment: 필요한 주석 추가 및 변경

# :label:Rename: 파일 혹은 폴더명을 수정하거나 옮기는 작업만의 경우

# :remove:Remove: 파일을 삭제하는 작업만 수행한 경우

# :package:Package: 빌드 부분 혹은 패키지 매니저 수정사항

fetch = 로컬 저장소에서 하는 Synk fork  
pull = fetch + merge  
origin/develop = 원격저장소의develop

\[PR후 다른 브랜치 생성해서 작업하는 경우\]  
fork한 내 repo Synk fork -> git fetch ->  
git checkout origin/develop로 fetch한 원격repo 로컬repo 연결 확인  
git checkout develop-> git merge origin/develop으로 머지 -> 기존 branch -D 삭제  
\-> develop에서 feature/기능으로 git log 반영한 branch생성 -> 작업 시작

\[원본 develop에 바뀐 코드가 필요한 경우 & rejected 발생시\]  
기존 작업 중인 내용 git stash로 저장 -> fork한 내 repo Synk fork ->  
git fetch -> git checkout origin/develop로 fetch한 원격repo 로컬repo 연결 확인  
git checkout develop-> git merge origin/develop으로 머지 -> 기존 branch -D 삭제  
\-> develop에서 feature/기능으로 git log 반영한 branch생성 ->  
git stash apply stash@{0}로 stash 적용  
\-> conflict발생시 해결 -> 작업 시작

\[push 실수 했을 때 조치 방법\]  
git log로 commit 내역 확인 -> git reset HEAD^로 최근 commit 취소 -> git push -f로 강제 push  
or  
git log --oneline으로 commit 내역 확인 -> 되돌릴 지점 해시 확인 -> git reset <해시>  
\-> git push -f로 강제 push

\[branch명 잘못 만든 경우\]  
이름 바꿀 브랜치에서 git branch -m \[새 브랜치 명\]  
\-> 이미 push --set-upstream한 경우 git push --set-upstream origin \[새 브랜치명\]  
\-> 잘못 push한 branch 원격 repo에서 삭제

\[가장 최근 commit 메시지 수정\]  
git commit --amend -> git log로 수정 확인

\[이전 commit 메시지 수정\]  
git rebase -i HEAD~\[head에서 떨어진 수\] -> 수정할 commit의 pick을 reword로 변경 후 저장  
\-> 순서대로 commit 변경 메시지 작성 -> git log로 수정 확인

# squash 사용시 commit 합치기 ,reword


-   GIT COMMIT MESSAGE 컨벤션 - 풀리퀘스트 시 확인하기!

git commit -m ‘내용~~~ #1’

제목의 맨 뒤에 #을 붙인다.

git commit > i 누르면 작성 시작 > 작성 > esc > :wq

커밋 메시지 예시  
✨Feat: 게시글 상세 페이지 추가 #이슈 번호
 

주석 및 독스트링

  1. 조건문 반복문 옵션 등 해석이 필요한 부분은 주석으로 남겨두기
  1. 함수 def 아래 줄에 함수 설명을 독스트링으로 추가하기

Github 브랜치 전략
  1. 팀장이 레파지토리 형성
  1. 각 팀원들이 main을 syncfork
  1. 각자 브랜치 만들어 작업
  1. 각 작업물을 dev 에 Pull Request
  1. dev local에서 검증 후 dev를 main에 merge
  1. GIT - 코드 컨벤션 black (autopep8미사용)pull request : 기술 개발 및 결합 과정의 방법 논의
    1. issue 만들어서 commit -m에 해당 번호 추가하기
    1. 브랜치 병합 : 기능구현 완료 되었을 때
    1. 브랜치는 어떻게 나눌 것인지
    • 브랜치는 대체로 아래처럼 나눕니다.
      • 메인 브랜치(main) - 배포용
      • 디벨롭 브랜치(dev)
      • 기능 단위 별 브랜치(feature/기능명)
    • 병합 순서는 대체로 이렇게 진행해요.
      • 기능 단위 별 브랜치 — 병합 → 디벨롭 브랜치 —최종 병합→ 메인 브랜치
      깃허브로 협업할때 풀리퀘스트 하기
      1. 작업하기 전에 메인 브랜치에서 pull하기!!!(관리자도 해야함),(안하면 나중에 커밋 엇갈려서 오류생김 >> 많이 힘들어집니다)
      1. 메인 브랜치가 수정되면 팀원들에게 꼭 말해서 pull시키기!!(이유는 1번과 같음)
      1. 결론 : fetch, merge orgin/main == pull-push-pull request 순서대로 하기

branch 이름 목록

띵곡-BE

  • main
  • dev
  • feature/user | 김광운
    • 회원가입
    • 로그인(+소셜로그인)
  • feature/user_page | 문영오
    • 프로필 수정(닉네임, 선호장르)
    • 댓글 좋아요 리스트 조회
    • 게시글 북마크 리스트 조회
  • feature/article_crud | 김혁준
    • 게시글 작성, 조회, 수정, 삭제
  • feature/comment_crud | 이기웅
    • 댓글 작성, 조회, 수정, 삭제
  • feature/openai | 이기웅
    • chat-gpt 연동
  • feature/follow | 문영오
    • 유저 간 팔로우, 언팔로우 기능
  • feature/like | 구민정
    • 댓글 좋아요 기능
  • feature/bookmark | 구민정
    • 게시글 북마크 기능
  • feature/testcode | 구민정

띵곡-FE

  • main
  • dev
  • feature/main | 김광운
    • Header, Footer
  • feature/user | 구민정
    • 회원가입
    • 로그인
    • 로그아웃
  • feature/user_page | 문영오
    • 프로필 페이지

깃허브 주소

백엔드

프론트엔드


컨셉

음악 커뮤니티 - 띵곡🎵

원하는 음악을 추천하고 추천 받는 커뮤니티 사이트


기능

핵심 목표

  • 머신러닝 라이브러리 활용
  • 프론트엔드와 백엔드의 분리
  • aws를 이용한 서비스의 배포

하고 싶은 기능

민정 : 테스트코드, 페이지네이션

광운 : 소셜 로그인, 프로젝트 완성하기

영오 : 비밀번호 리셋하기, 회원 탈퇴하기, 회원정보수정

혁준 : 백오피스, 클라우드플레어

기웅 : 머신러닝, 하고 싶었지만 하지 못했던 기능. 로봇계정을 하나 더 만들어서 댓글에 로봇계정으로 음악을 추천해주었으면 더 좋았을 것 같다.

구현한 기능

백엔드

  1. 로그인
    • 회원가입
    • 로그인(+소셜 로그인 - 카카오)
  1. 유저
    • 프로필 조회
    • 프로필 이미지 업로드
    • 프로필 이미지 삭제
    • 프로필 수정
    • 팔로잉
  1. 게시글
    • 작성
    • 조회
    • 수정
    • 삭제
    • 이미지 업로드
    • 이미지 삭제
    • 북마크
  1. 댓글
    • 작성
    • 조회
    • 수정
    • 삭제
    • 좋아요
  1. AI 노래 추천
    • 게시글 작성 시 게시글의 내용을 읽고 챗-GPT가 유튜브에서 노래를 추천해서 댓글로 자동 작성됨

프론트엔드

  1. 헤더, 푸터
    • 토글 기능
      • 로그인 하지 않은 유저는 글쓰기, 마이페이지 버튼이 없음
    • 로그인한 사용자의 프로필 사진 보임
    • 닉네임 부분 클릭하면 마이페이지로 이동함
  1. 로그인
    • 회원가입
    • 로그인(카카오, 네이버, 구글 로그인)
    • 로그아웃
  1. 메인 페이지
    • 검색창
    • 좋아요 개수 순으로 댓글 9개 정렬
    • 최신 순으로 게시글 9개 정렬
  1. 유저 페이지
    • 프로필 수정(프로필 사진, 비밀번호, 닉네임, 성별, 나이 변경 가능)
      • 소셜로그인 유저의 경우 비밀번호 박스 막아놓음(어차피 변경해도 변경 안 됨)
    • 내가 쓴 글 최신순으로 정렬
    • 내가 좋아요 누른 댓글 최신순으로 정렬
    • 내가 북마크한 게시글 최신순으로 정렬
    • 다른 유저의 페이지 접속해서 팔로잉 클릭 가능
    • 존재하지 않는 유저 id로 접속 시 404 페이지 보여줌
  1. 게시글
    • 글 목록 조회
      • 페이지네이션: 한 페이지에 게시글 10개씩, 페이지 수는 5개씩 보임. 5페이지 넘어가면 다음 페이지로 넘기는 아이콘 생김
    • 작성 페이지
      • 제목, 내용, 이미지 업로드 가능
      • 이미지는 선택사항
    • 수정 페이지
      • 기존의 제목, 내용이 보임
      • 사진은 보이지 않지만 새로 업로드 하지 않을 경우 이미지가 그대로 유지됨
      • 이미지를 삭제할 수 있는 삭제 버튼이 있음
    • 상세 페이지
      • 존재하지 않는 게시글 id로 접속 시 404 페이지 보여줌
      • 로그인한 유저와 게시글 작성자가 일치하면 수정, 삭제 버튼 보임
      • 로그인한 유저와 게시글 작성자가 일치하지 않으면 북마크하기 버튼 보임
      • 작성자의 닉네임을 누르면 해당 유저의 유저페이지로 이동함
  1. 댓글
    • 게시글 작성 시 작성자의 닉네임으로 AI가 노래를 추천해주는 댓글이 달림
    • 자신이 작성한 댓글에는 수정, 삭제 버튼 보임
    • 좋아요 버튼 구현
      • 자신이 작성한 글 첫 번째 댓글(AI가 노래 추천하는 댓글)은 좋아요 버튼과 삭제 버튼이 보임
      • 타인이 작성한 댓글은 좋아요 버튼만 있음
      • 좋아요 개수는 일부러 보이지 않게 함(자신의 취향에 따라 소신껏 좋아요를 누르기 위해)
      • 근데 인기댓글은 메인페이지에서 확인할 수 있기는 함
    • 댓글 작성자의 닉네임 누르면 해당 유저의 유저페이지로 이동함

기술 스택

  • Frontend
    • HTML
    • JavaScript
    • CSS
  • Backend
    • Python 3.11.3
    • Django 4.2.1
    • DRF 3.14.0
    • DRF-simplejwt 5.2.2

와이어프레임

DB설계

ERD(23-05-22)

0522

 

0528

API 설계(23-05-22)

띵곡맛집

기능분류 기능 Method URL Request Response
1.로그인 회원가입 POST users/signup/ { ”email” : “email@email.email”, ”nickname” : “nickname”, ”password” : “password”, ”password2” : “password”, ”gender” : “M or W”, ”age” : number, } { ”HTTP Response” : “201 Created” }
  로그인 GET users/login/ { ”email” : “email@email.email”, ”password” : “password” } { ”refresh” : “refresh token”, ”access” : “access Token” }
  카카오 api key GET users/kakao/ - { ”api_key”: “api_key” }
  구글 api key GET users/google/ - { ”api_key”: “api_key” }
  네이버 api key GET users/naver/ - { ”api_key”: “api_key” }
  카카오 소셜 로그인 POST users/kakao/ { ”code” : “code” } { ”refresh” : “refresh token”, ”access” : “access token” }
  구글 소셜 로그인 POST users/google/ { ”access_token” : “access_token” } { ”refresh” : “refresh token”, ”access” : “access token” }
  네이버 소셜 로그인 POST users/naver/ { ”code” : “code”, ”state” : “state” } { ”refresh” : “refresh token”, ”access” : “access token” }
  로그아웃   프론트엔드에서 진행 - -
2. 프로필페이지 마이페이지 GET users/profile/<int:user_id>/ - { ”HTTP Response” : “200 OK”, { “user_id” : 1, “nickname” : “nickname”, “profile_img” : “image URL”, “gerne” : “prefer gerne” } }
  프로필 수정 PUT users/profile/<int:user_id>/ { ”nickname” : “modify nickname”, ”profile_img” : “image URL”, ”gerne” : “prefer genre” } { ”HTTP Response” : “200 OK” }
7. 팔로잉 팔로잉 생성/삭제 POST users/follow/<int:user_id>/ - { ”HTTP Response” : “200 OK” }, { ”HTTP Response” : “204 NO_CONTENT” }
  내가 쓴 글 모두 보기 GET users/profile/<int:user_id>/myarticles/ - { ”HTTP Response” : “200 OK”, { “user_id” : 1, “title” : “title”, “content” : “content”, “image” : “image URL” }, … }
  좋아요 목록 보기 GET users/profile/<int:user_id>/likes/ - { ”HTTP Response” : “200 OK”, { “comment_id” : 1, “comment” : “comment”, “url” : “url” }, … }
  북마크 게시글 보기 GET users/profile/<int:user_id>/bookmarks/ - { ”HTTP Response” : “200 OK”, { “user_id” : 1, “title” : “title”, “content” : “content”, “image” : “image URL” }, … }
3.게시글 게시글 전체보기 GET articles/ - { ”HTTP Response” : “200 OK”, { “user_id” : 1, “title” : “title”, “content” : “content”, “image” : “image URL” }, … }
  게시글 생성 POST articles/ { ”title” : “title”, ”content” : “content”, “image” : “image URL” } { ”HTTP Response” : “201 Created” }
  게시글 상세보기 GET articles/<int:article_id>/ - { ”HTTP Response” : “200 OK”, { “user_id” : 1, “title” : “title”, “content” : “content”, “image” : “image URL”, { “comment_id” : 1, “comment” : “comment”, “url” : “url” }, … }, … }
  게시글 수정하기 PUT articles/<int:article_id>/ { ”title” : “title”, ”content” : “content”, “photo” : “photo URL” } { ”HTTP Response” : “200 OK” }
  게시글 삭제하기 DELETE articles/<int:article_id>/ - { ”HTTP Response” : “204 NO_CONTENT” }
  게시글 사진올리기 POST articles/<int:article_id>/photos/ {”file” : “article photo url”} { ”HTTP Response” : “201 Created” }
4. 댓글 댓글 생성 POST articles/<int:article_id>/comment/ { ”comment” : “comment”, ”url” : “url”, } { ”HTTP Response” : “201 Created” }
  댓글 조회 GET articles/<int:article_id>/comment/ - { ”HTTP Response” : “200 OK”, { “comment_id” : 1, “comment” : “comment”, “url” : “url” “like_count” : n }, … }
  댓글 수정하기 PUT comment/<int:comment_id>/ { ”comment” : “comment”, ”url” : “url”, } { ”HTTP Response” : “200 OK” }
  댓글 삭제하기 DELETE comment/<int:comment_id>/ - { ”HTTP Response” : “204 NO_CONTENT” }
5. 좋아요 댓글 좋아요 생성/삭제 POST articles/like/<int:comment_id>/ - { ”HTTP Response” : “201 Created” }, { ”HTTP Response” : “204 NO_CONTENT” }
  좋아요순 댓글 모음 페이지 GET articles/likes/ - { ”HTTP Response” : “200 OK”, { “comment_id” : 1, “comment” : “comment”, “url” : “url” “like_count” : n }, … }
6. 북마크 게시글 북마크 생성/삭제 POST articles/bookmark/<int:post_id> - { ”HTTP Response” : “201 Created” }, { ”HTTP Response” : “204 NO_CONTENT” }
  사진 삭제 DELETE medias/photos/    
8. 이미지 사진 조회 GET medias/photos/get-url/    
  유저 프로필 사진 올리기 POST users/<int:user_id>/photos/ { ”file” : “profile photo url” } { ”HTTP Response” : “201 Created” }
9. 검색 검색 GET search/<str:query>/   { ”HTTP Response” : “200 OK”, { “user_id” : 1, “title” : “title”, “owner” : “owner”, “created_at” : “2023-05-24 18:30” }, … }

 

이름 태그 날짜
S.A.작성 개발일정
Merge 개발일정
MVP완성 개발일정
front 완성  
기능추가 개발일정
베타 테스트  
배포 개발일정
발표준비 개발일정
발표  

트러블 슈팅

팀원별 마음에 드는 코드

구민정

선정코드 | 이유 : 프론트에서 문자를 백엔드로 넘겨줄 때 URL escape code로 인코딩 된다 그래서 받은 문자열을 그대로 백엔드의 DB와 비교하면 일치하는 값이 없는데 그걸 urllib 라이브러리의 parse_unquote 메서드로 URL escape code를 한글로 바꿔서 DB와 비교하게끔 하는 걸 새로 배웠기 때문에 마음에 든다

  • articles/views.py - SearchView 클래스
    class SearchView(APIView):
        def get(self, request, query):
            """검색하기
    
            제목이나 내용에 입력한 검색어가 포함되어 있는 게시글들을 가져옴"""
            decoded_query = urllib.parse.unquote(query)
            articles = Article.objects.filter(
                Q(title__contains=decoded_query) | Q(content__contains=decoded_query)
            )
            serializer = ArticleListSerializer(articles, many=True)
            if articles:
                return Response(serializer.data, status=status.HTTP_200_OK)
            else:
                return Response("검색 결과가 없습니다", status=status.HTTP_204_NO_CONTENT)

     

김광운

선정코드 | 이유 : 소셜 로그인 과정에서 중복되는 코드를 따로 함수로 빼와서 가독성이 올라간 것 같아 기부니가 좋았습니다..

  • users/views.py - SocialLogin()
    def SocialLogin(**kwargs):
        """소셜 로그인, 회원가입"""
        # 각각 소셜 로그인에서 email, nickname, login_type등을 받아옴!!
        data = {k: v for k, v in kwargs.items() if v is not None}
        # none인 값들은 빼줌
        email = data.get("email")
        login_type = data.get("login_type")
        # 그 중 email이 없으면 회원가입이 불가능하므로
        # 프론트에서 메시지를 띄워주고, 다시 로그인 페이지로 이동시키기
        if not email:
            return Response(
                {"error": "해당 계정에 email정보가 없습니다."}, status=status.HTTP_400_BAD_REQUEST
            )
        try:
            user = User.objects.get(email=email)
            # 로그인 타입까지 같으면, 토큰 발행해서 프론트로 보내주기
            if login_type == user.login_type:
                refresh = RefreshToken.for_user(user)
                access_token = CustomTokenObtainPairSerializer.get_token(user)
                return Response(
                    {"refresh": str(refresh), "access": str(access_token.access_token)},
                    status=status.HTTP_200_OK,
                )
            # 유저의 다른 소셜계정으로 로그인한 유저라면, 해당 로그인 타입을 보내줌.
            # (프론트에서 "{login_type}으로 로그인한 계정이 있습니다!" alert 띄워주기)
            else:
                return Response(
                    {"error": f"{user.login_type}으로 이미 가입된 계정이 있습니다!"},
                    status=status.HTTP_400_BAD_REQUEST,
                )
        # 유저가 존재하지 않는다면 회원가입시키기
        except User.DoesNotExist:
            new_user = User.objects.create(**data)
            # pw는 사용불가로 지정
            new_user.set_unusable_password()
            new_user.save()
            # 이후 토큰 발급해서 프론트로
            refresh = RefreshToken.for_user(new_user)
            access_token = CustomTokenObtainPairSerializer.get_token(new_user)
            return Response(
                {"refresh": str(refresh), "access": str(access_token.access_token)},
                status=status.HTTP_200_OK,
            )

 

문영오

선정코드 | 이유 : 특정유저의 정보를 불러오는 과정에서 게시글의 총합 등 연산정보를 프론트가 아닌 백엔드에서 제공함으로 프론트 작업이 편해짐을 경험했습니다.

  • user/serializers.py - UserProfileSerializer
    class UserProfileSerializer(serializers.ModelSerializer):
        like_comments = serializers.SerializerMethodField()
        bookmarks = serializers.SerializerMethodField()
        followings = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
        followers = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
        articles = serializers.SerializerMethodField()
        
        def get_articles(self, obj):
            return obj.articles.count()
        
        def get_like_comments(self, obj):
            return list(obj.like_comments.values())
        
        def get_bookmarks(self, obj):
            return list(obj.bookmarks.values())
    
        class Meta:
            model = User
            exclude = (
                "user_permissions",
                "is_superuser",
                "last_login",
                "is_active",
                "is_admin",
                "password",
                "groups",
            )
            extra_kwargs = {
                "password": {
                    "write_only": True,  # 작성만 가능하도록 제한! 비밀번호 조회 불가
                },
            }

 

김혁준

선정코드 | 이유 : 어드민 페이지를 자유롭게 커스터마이징 할 수 있어서 신기하고 유용하겠다는 생각이 들었습니다.(특히 comments_url_list 함수)

  • backend - articles/models.py - Article
    class Article(models.Model):
        title = models.CharField(max_length=50, default="title")
        content = models.TextField()
        owner = models.ForeignKey(
            "users.User",
            on_delete=models.CASCADE,
            related_name="articles",
        )
        bookmark = models.ManyToManyField(
            User, blank=True, verbose_name="북마크", related_name="bookmarks"
        )
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
    
        def __str__(self):
            return str(self.title)
    
        def total_comments(self):
            return self.comments.count()
    
        def total_bookmarks(self):
            return self.bookmark.count()
    
        def comments_url_list(self):
            comments = self.comments.all()
            comment_url_content = ""
            url_regex = r"(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)"
            reg = re.compile(url_regex)
    
            for comment in comments:
                res = reg.search(comment.comment)
                if res:
                    indexes = res.span()
                    comment_url_txt = comment.comment[indexes[0] : indexes[1]]
                    article_id = comment.article.pk
                    comment_id = comment.id
                    comment_user = comment.user
                    is_AI = False
                    if comment == comments[0]:
                        is_AI = True
                    comment_url_content += f"게시글 id : {article_id} / 댓글 id : {comment_id} / 댓글 유저 이름 : {comment_user} / 댓글 유저는 ai인가? : {is_AI} / 댓글 내용 URL : {comment_url_txt}\n\n"
            if comment_url_content == "":
                return "Url이 포함된 댓글이 없습니다!"
            else:
                return comment_url_content

     

 

이기웅

선정코드 | 이유 : 사용자가 게시글을 작성할때 게시글 내용을 GPT에게 전달하여 내용을 분석하고, 분석한 내용에 어울릴만한 음악을 추천받는 코드. 직접 구현한거라 뿌듯한 마음이 있어서 선정한 것 같습니다. 아쉬운점이 있다면 GPT의 힘을 받지않고 입력되는 내용들을 계속 학습하면서 조금 더 정교한 추천을 하는 머신너링 모델을 만들었으면 좋았을 텐데, 배움이 깊지못해서 직접 구현하지못하고 GPT의 힘을 빌렸습니다. 모델링도 해볼수있는 지식을 가져보도록 노력하겠습니다!

  • 코드명 openai_utility.py
    # GPT API 키 설정
    openai.api_key = OPENAI_API_KEY
    
    import requests
    from bs4 import BeautifulSoup
    
    
    def is_artist_first(text):
        if re.match(r"^[a-zA-Z가-힣\s]+[-]\s*", text):
            return True
        else:
            return False
    
    
    def extract_song_title(text):
        if is_artist_first(text):
            pattern = r"^[a-zA-Z가-힣\s]+[-]\s*"
        else:
            pattern = r"[-]\s*[a-zA-Z가-힣\s]+"
    
        without_artist = re.sub(pattern, "", text)
        return without_artist.strip()
    
    
    def gpt_music_recommendation(content):
        gpt_input = (
            f"이 내용 분위기에 어울리는 완벽한 팝송 추천 한곡만 부탁드립니다: {content}. 제목만 알려주세요. 가수 이름과 링크는 필요없어요. "
        )
    
        response = openai.Completion.create(
            engine="text-davinci-003",
            prompt=gpt_input,
            max_tokens=100,
            n=1,
            stop=None,
            temperature=0.3,
        )
    
        recommendation = response.choices[0].text.strip()
        song_title = extract_song_title(recommendation)
        return song_title
    
    
    def get_youtube_music_link(song):
        youtube = googleapiclient.discovery.build(
            "youtube", "v3", developerKey=YOUTUBE_API_KEY
        )
    
        request = youtube.search().list(
            part="id,snippet",
            type="video",
            videoCategoryId="10",  # 추가 - 음악 카테고리만 해당
            q=song,
            videoDefinition="high",
            maxResults=1,
            fields="items(id(videoId),snippet(publishedAt,channelId,channelTitle,title,description))",
        )
    
        response = request.execute()
    
        if response["items"]:
            video_url = (
                f"https://www.youtube.com/watch?v={response['items'][0]['id']['videoId']}"
            )
    
            html_doc = requests.get(video_url)
            soup = BeautifulSoup(html_doc.text, "html.parser")
            meta_tags = soup.find_all("meta")
    
            og_title = None
            og_url = None
            for tag in meta_tags:
                if "property" not in tag.attrs:
                    continue
                if tag.attrs["property"] == "og:title":
                    og_title = tag.attrs["content"]
                if tag.attrs["property"] == "og:url":
                    og_url = tag.attrs["content"]
                if og_title is not None and og_url is not None:
                    break
    
            return og_title, og_url
        else:
            return "찾을 수 없습니다."
    
    
    def recommend_music_and_link(content):
        recommendation = gpt_music_recommendation(content)
        youtube_link = get_youtube_music_link(recommendation)
        return recommendation, youtube_link

     

 


 


Uploaded by

N2T

댓글