📝 팔로워 팔로잉
- followers, following 누르면 개별 창이 떠서 목록 뜨게
- html 코드에서 follower, following 이 동작하도록 코드 수정 bootstarp modal 사용 -> 5.3version 으로 접속 -> Live demo 부분 modal 부분 복사 , 붙여넣기 , 버튼부분 data 부터해서 수정
<div class="row">
<div class="col-4 text-center">{{ object.post_set.count | intcomma }} posts</div>
<div class="col-4 text-center">
<button class="border-0 bg-transparent" data-bs-toggle="modal" data-bs-target="#followers-modal">
{{ object.followers.count | intcomma }} followers
</button>
</div>
<div class="col-4 text-center">
<button class="border-0 bg-transparent" data-bs-toggle="modal" data-bs-target="#following-modal">
{{ object.following.count | intcomma }} following
</button>
</div>
</div>
<div class="modal fade" id="followers-modal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Followers</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<ul>
{% for follower in object.followers.all %}
<li>{{ follower.nickname }}</li>
{% endfor %}
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="following-modal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Following</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<ul>
{% for follower in object.following.all %}
<li>{{ follower.nickname }}</li>
{% endfor %}
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
📝 유저 검색 기능
- 네비게이션 검색 버튼, 검색페이지, 유저를, 본문을, 무엇을 검색할지 정해서 검색하도록 만들어 보기
- font-awesome 사용 -> search 검색
<div>
<a href="">
<i class="fa-solid fa-magnifying-glass text-white"></i>
</a>
</div>
- 검색페이지 디자인 따로 앱 빼도 되지만, post/views.py 로 진행, 기본 뼈대 작성
def search(request):
search_type = request.GET.get('type')
if search_type in ['user', 'tag'] :
...
return render(request, template_name='search.html')
path('search/', post_views.search, name='search'),
<div>
<a href="{% url 'search' %}">
<i class="fa-solid fa-magnifying-glass text-white"></i>
</a>
</div>
- 화면단 보면서 html 완성
{% extends 'base.html' %}
{% block content %}
<form action="" method="get">
<div class="row">
<div class="col-3">
<select name="type" id="" class="form-control">
<option value="user">User</option>
<option value="tag">Tag</option>
</select>
</div>
<div class="col-7">
<input type="text" name="q" placeholder="검색어" class="form-control">
</div>
<div class="col-2">
<button class="btn btn-primary">
검색
</button>
</div>
</div>
</form>
{% endblock %}
- def search 수정
def search(request):
search_type = request.GET.get('type')
q=request.GET.get('q')
if search_type in ['user', 'tag'] and q:
return render(request, f'search/search_{search_type}.html')
return render(request, template_name='search/search.html')
- 다양한 search html 생성 공통부분 include 에 넣어서 생성하기
<div>
<form action="" method="get">
<div class="row">
<div class="col-3">
<select name="type" id="" class="form-control">
<option value="user" {% if request.GET.type == "user" %} selected {% endif %}>User</option>
<option value="tag" {% if request.GET.type == "tag" %} selected {% endif %}>Tag</option>
</select>
</div>
<div class="col-7">
<input type="text" name="q" placeholder="검색어" class="form-control" value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}">
</div>
<div class="col-2">
<button class="btn btn-primary">
검색
</button>
</div>
</div>
</form>
</div>
{% extends 'base.html' %}
{% block content %}
<h1>Tag</h1>
{% include 'include/search_form.html' %}
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<h1>User</h1>
{% include 'include/search_form.html' %}
{% endblock %}
{% extends 'base.html' %}
{% block content %}
{% include 'include/search_form.html' %}
{% endblock %}
- views.py에서 해당검색어에 맞는 것만 출력되게 마지막으로 수정
User = get_user_model()
def search(request):
search_type = request.GET.get('type')
q=request.GET.get('q')
if search_type in ['user', 'tag'] and q:
if search_type == 'user':
object_list = User.objects.filter(nickname__icontains=q)
else:
object_list = Post.objects.filter(tags__tag=q)
context = {
'object_list' : object_list,
}
return render(request, f'search/search_{search_type}.html', context)
return render(request, template_name='search/search.html')
{% extends 'base.html' %}
{% block content %}
<h1>Tag</h1>
{% include 'include/search_form.html' %}
<div>
<ul>
{% for post in object_list %}
{{ post.id }}
{% endfor %}
</ul>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<h1>User</h1>
{% include 'include/search_form.html' %}
<div>
<ul>
{% for user in object_list %}
<li>{{ user.nickname }}</li>
{% endfor %}
</ul>
</div>
{% endblock %}
- html 업그레이드
{% extends 'base.html' %}
{% block content %}
<h1>User</h1>
{% include 'include/search_form.html' %}
<div>
{% for user in object_list %}
<div class="my-4">
<a href="{% url 'profile:detail' user.nickname %}" class="text-decoration-none text-black">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 3px;"></i>
</span>
{{ user.nickname }}
</a>
</div>
{% endfor %}
</div>
{% endblock %}
{% extends 'base.html' %}
{% block style %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
{% endblock %}
{% block content %}
<h1>Tag</h1>
{% include 'include/search_form.html' %}
<div class="row mt-2">
{% for post in object_list %}
<div class="col-4 my-2">
<div class="swiper">
<div class="border-1 swiper-wrapper">
{% for image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
const swiper = new Swiper('.swiper', {
direction: 'horizontal',
loop: false,
pagination: {
el: '.swiper-pagination',
},
});
</script>
{% endblock %}
📝 OAuth2
- obtaining Authorization
- OAuth2는 사용자의 자격 증명을 직접 노출하지 않고, 서드파티 애플리케이션이 사용자 리소스에 접근할 수 있도록 하는 인증 프로토콜이다.
- 동작과정
- 유저는 OAuth Server에서 로그인 하게됨 (facebook,naver,..)
- 설정해 놓은 Redirect Url로 OAuth 서버에서 장고 서버로 던져준다. token과 함께(get 요청)
- 다시한번 OAuth server에 token 을 보내며 확인 요청을 한다.
- 장고서버에서 다시 받아온 응답 확인후 장고서버에서 OAuth 에서 받아온 정보로 로그인을 시킴
- 유저 페이지를 처리해줌
📌 네이버를 통해 Open API 신청, 장고 설정
- 네이버 개발자 센터
- 네이버 로그인
- 오픈 API 이용 신청 - 설정
- 이름, 사용 API 네이버 로그인 선택
- 연락처 email이 필요하기에 이메일 체크
- 환경 PC 웹
- 서비스 url :http://localhost:8000
- Callback URL : http://localhost:8000/naver/callback/
- 나오는 정보 .config_secret 에 보관
# secret.json
"naver": {
"client_id": "클라이언트 id",
"secret": "클라이언트 시크릿"
}
}
- settings.py에 설정
NAVER_CLIENT_ID = SECRET['naver']['client_id']
NAVER_SECRET = SECRET['naver']['secret']
- 요청을 하고 다시 받아와야 해서 request 패키지 필요 , poetry add requests
- member/oauth.views.py 생성
- 네이버 개발자 센터에서 Documents에 로그인의 API 명세를 보면 방법이 나와있음, 필수항목은 꼭 넣어야 하는것들임, 콜백 되는거 한번 해보자
https://nid.naver.com/oauth2.0/authorize?response_type=cdoe&client_id=ygTj6IyW3ZFhzQ5OeDHq&redirect_url=http://localhost:8000/naver/callback/&state=abc
- 이 페이지로 가는 동작 제작, url 연결
class NaverLoginRedirectView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')
callback_url = domain + NAVER_CALLBACK_URL
state = signing.dumps(NAVER_STATE)
params = {
'response_type': 'code',
'client_id': settings.NAVER_CLIENT_ID,
'redirect_uri': callback_url,
'state' : state,
}
return f'{NAVER_LOGIN_URL}?{urlencode(params)}'
path('oauth/', include('member.oauth_urls')),
- auth/login.html 수정
{% extends 'base.html' %}
{% block content %}
<h1 class="title">로그인</h1>
<form method="post">
{% csrf_token %}
{% include 'include/form.html' %}
<button class="btn btn-primary">로그인</button>
</form>
<a href="{% url 'oauth:naver_login' %}" class="btn btn-info">
네이버 로그인
</a>
{% endblock %}
- callback url 잘확인하기
- callback view 제작
def naver_callback(request):
code = request.GET.get('code')
state = request.GET.get('state')
if NAVER_STATE != signing.loads(state):
raise Http404
params = {
'grant_type': 'authorization_code',
'client_id': settings.NAVER_CLIENT_ID,
'client_secret': settings.NAVER_SECRET,
'code': code,
'state': state,
}
response = requests.get(NAVER_TOKEN_URL, params=params)
result = response.json()
access_token = result.get('access_token')
print('token request', result)
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(NAVER_PROFILE_URL, headers=headers)
print('profile response', response.json())
if response.status_code != 200:
raise Http404
result = response.json()
print('profile response', result)
email = result.get('response').get('email')
print(email)
user = User.objects.filter(email=email).first()
if user:
if not user.is_active:
user.is_active = True
user.save()
login(request, user)
return redirect(reverse('main'))
- nickname 생성을위한 폼제작
class NicknameForm(BootstrapModelForm):
class Meta:
model = User
fields = ('nickname',)
- 동작과정
- user가 해당서버에서 로그인
- redirect url 로 장고 서버로 보내줌
- 유저가 직접 보내는것을 방지하기 위해, 네이버 서버로 요청해서 응답을 받아옴
- 토큰을 보고 확인가능
- 네이버에서 access_token 을 통해 프로필을 받아와서 eamil을 받아옴
- 닉네임이 없어서 넣으라고 유저에게 페이지를 보여주고
- 회원가입 완료 후 메인페이지로 돌려줌
🧑💻 네이버 로그인
class NaverLoginRedirectView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')
callback_url = domain + NAVER_CALLBACK_URL
state = signing.dumps(NAVER_STATE)
params = {
'response_type': 'code',
'client_id': settings.NAVER_CLIENT_ID,
'redirect_uri': callback_url,
'state' : state,
}
return f'{NAVER_LOGIN_URL}?{urlencode(params)}'
def naver_callback(request):
code = request.GET.get('code')
state = request.GET.get('state')
if NAVER_STATE != signing.loads(state):
raise Http404
params = {
'grant_type': 'authorization_code',
'client_id': settings.NAVER_CLIENT_ID,
'client_secret': settings.NAVER_SECRET,
'code': code,
'state': state,
}
response = requests.get(NAVER_TOKEN_URL, params=params)
result = response.json()
access_token = result.get('access_token')
print('token request', result)
profile_response = get_naver_profile(access_token)
print('profile response', result)
email = profile_response.get('email')
print(email)
user = User.objects.filter(email=email).first()
if user:
if not user.is_active:
user.is_active = True
user.save()
login(request, user)
return redirect('main')
return redirect(
reverse('oauth:nickname') + f'?access_token={access_token}'
)
def oauth_nickname(request):
access_token = request.GET.get('access_token')
if not access_token:
return redirect('login')
form = NicknameForm(request.POST or None)
if form.is_valid():
user = form.save(commit=False)
profile = get_naver_profile(access_token)
email = profile.get('email')
if User.objects.filter(email=email).exists():
raise Http404
user.email = profile.get('email')
user.ia_active = True
random_password = get_random_string(12)
user.set_password(random_password)
user.save()
login(request, user)
return redirect('main')
return render(request, 'auth/nickname.html', {'form': form})
def get_naver_profile(access_token):
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(NAVER_PROFILE_URL, headers=headers)
print('profile response', response.json())
if response.status_code != 200:
raise Http404
result = response.json()
return result.get('response')
✅ 깃허브 로그인 구현
📌 1. 깃허브 OAuth2 로그인 동작 과정
- 사용자가 Django 애플리케이션에서 "깃허브 로그인" 버튼을 클릭
- 깃허브 OAuth 서버로 리디렉트되어 로그인 진행
- 로그인 성공 후 설정한 Redirect URL로 인증 코드 전송 (
code
, state
포함)
- Django 서버가 깃허브 OAuth 서버에
access_token
요청
- 받은
access_token
을 이용하여 사용자 정보를 가져옴
- 이메일을 기반으로 로그인 처리 또는 회원가입 후 로그인
📌 2. Django 설정
1️⃣ 깃허브 개발자 설정 페이지에서 OAuth App 등록
2️⃣ secret.json
설정
{
"github": {
"client_id": "깃허브 클라이언트 ID",
"secret": "깃허브 클라이언트 Secret"
}
}
3️⃣ settings.py
등록
GITHUB_CLIENT_ID = SECRET['github']['client_id']
GITHUB_SECRET = SECRET['github']['secret']
GITHUB_CALLBACK_URL = "http://localhost:8000/oauth/github/callback/"
📌 3. 깃허브 로그인 뷰 (views.py
)
1️⃣ 깃허브 로그인 페이지로 리디렉트
class GithubLoginRedirectView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')
callback_url = domain + GITHUB_CALLBACK_URL
state = signing.dumps("github_login")
params = {
'client_id': settings.GITHUB_CLIENT_ID,
'redirect_uri': callback_url,
'state': state,
'scope': 'user:email',
}
return f"https://github.com/login/oauth/authorize?{urlencode(params)}"
2️⃣ 깃허브 콜백 처리 (views.py
)
def github_callback(request):
code = request.GET.get('code')
state = request.GET.get('state')
if signing.loads(state) != "github_login":
raise Http404
params = {
'client_id': settings.GITHUB_CLIENT_ID,
'client_secret': settings.GITHUB_SECRET,
'code': code,
'state': state,
}
headers = {'Accept': 'application/json'}
response = requests.post("https://github.com/login/oauth/access_token", data=params, headers=headers)
access_token = response.json().get('access_token')
profile_response = get_github_profile(access_token)
email = profile_response.get('email')
user = User.objects.filter(email=email).first()
if user:
login(request, user)
return redirect('main')
return redirect(f"{reverse('oauth:nickname')}?access_token={access_token}")
3️⃣ 깃허브 프로필 조회 함수
def get_github_profile(access_token):
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get("https://api.github.com/user", headers=headers)
if response.status_code != 200:
raise Http404
return response.json()
4️⃣ URL 패턴 (urls.py
)
urlpatterns = [
path('github/login/', GithubLoginRedirectView.as_view(), name='github_login'),
path('github/callback/', github_callback, name='github_callback'),
]
📌 Tip
- option + enter : naver같은 이문자는 맞춤법이 틀린걸 아니다라고 딕셔너리에 저장
- @receiver
- @receiver 데코레이터는 Django의 시그널 프레임워크와 관련된 기능으로, 시그널을 수신하여 특정 작업을 수행하는 함수(시그널 핸들러)를 정의하는 데 사용됩니다. 시그널은 Django에서 특정 이벤트가 발생했을 때 자동으로 호출될 수 있는 기능을 제공합니다.