์ง๋๋ฒ๊น์ง ๋ฐ๋ชจ๋ฅผ ๋ง์น๊ณ , ์ด์ ์ ๋๋ก RESUMAI ์๋น์ค๋ฅผ ๊ฐ๋ฐํ๊ธฐ ์์ํ๋ค. ํต์ฌ ์์ด๋์ด๋ ์ด์ ์ ๋ฐ๋ชจ์์ ๋ณด์ฌ์ค ์์ด๋์ด๋ก, ๋ฐฑ์๋ ๊ฐ๋ฐ์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํ๊ฒ ๋์๋ค.
๋ณธ ํ๋ก์ ํธ๋ Django Rest Framework (DRF)๋ก ๊ฐ๋ฐํ์์ผ๋ฉฐ, ํ๋ก ํธ์๋๋ React๋ฅผ ์ฌ์ฉํ๋ค. (๋๋ ๋ฐฑ์๋ ๊ฐ๋ฐ๋ง ๋งก์๋ค.)
๋ณธ ๊ธ์ DRF๋ก ์นด์นด์ค ์์ ๋ก๊ทธ์ธ์ ๊ตฌํํ๋ฉฐ ๊ฒช์๋ ์ด๋ ค์์ ๋ชจ๋ ๋ด์๋ค.
์๋ง DRF๋ก ์ฒ์ ์์ ๋ก๊ทธ์ธ์ ๊ฐ๋ฐํด๋ณด๋ ค๋ ์ฌ๋๋ค๋ ๋ณธ ๊ธ๋ง ๋ณด๋ฉด ์ํ๋ ๊ธฐ๋ฅ์ ๋ชจ๋ ๊ตฌํํ ์ ์์ ๊ฒ์ด๋ค !!
1. Models
์นด์นด์ค ๋ก๊ทธ์ธ์ ๊ตฌํํ๊ธฐ์ ์์ ๋จผ์ user model์ ์ ์ํด์ผ ํ๋ค. ์ฌ์ค ์ด model์ ํ๋ก์ ํธ๋ง๋ค ๋ค๋ฅด๊ธฐ ๋๋ฌธ์, ๊ธฐ๋ณธ์ ์ธ ๊ฒ๋ค์ ์ ์ธํ๋ฉด ๊ฐ๋ฐ์๊ฐ ๋ฐ๋ก customize ํด์ผ ํ๋ค.
๋ฒ ์ด์ค๊ฐ ๋๋ ์ ์ ๋ชจ๋ธ์ ๋ค์ ๊ธ์ ์ฐธ๊ณ ํด๋ณด๋ฉด ์ข๋ค.
1–1. ์นด์นด์ค ๋น์ฆ์ฑ ๋ฑ๋ก
์ฒ์์ ๋๋ ์นด์นด์ค ๋ก๊ทธ์ธ ์, ๋น์ฆ๋์ค ๊ณ์ ์ผ๋ก ๋ฑ๋ก์ด ์๋์ด ์์ผ๋ฉด ์ด๋ฉ์ผ์ ๋ฐํ๋ฐ์ง ๋ชปํ๋ค๋ ์ ๋ณด๋ฅผ ๋ฃ๊ณ , ์ ๋ง ๋ง์ ๊ณ ๋ฏผ์ ํ์๋ค. ๊ทธ๋์ ์ฒ์์๋ oid(์นด์นด์ค์์ ๋ฐํํ๋ ์ ์ id)๋ฅผ ๊ณ ์ ํ๋๋ก ์ผ์์ผ ํ๋? ๋ผ๋ ๊ณ ๋ฏผ์ ํ๊ณ , ์ค์ ๋ก ๊ทธ๋ ๊ฒ ๊ตฌํ๋ ํ์๋ค.
๊ทธ๋ฌ๋, ์์นญ ๋์ ๊ฐ์ธ๊ฐ๋ฐ์ ๋น์ฆ ์ฑ ์ ํ์ ํตํด ์ด๋ฉ์ผ์ ๋ฐํ๋ฐ์ ์ ์๋ค๋ ์ ๋ณด๋ฅผ ์๊ฒ ๋์ด, ๋ฐ๋ก ๋น์ฆ์ฑ ์ ํ์ ์ ์ฒญํ๋ค. (๋น์ฆ์ฑ ์ ํ์ ๊ทธ๋ฅ ์นด์นด์ค ๋๋ฒจ๋กํผ์ค ํํ์ด์ง์์ ์ ์ฒญํ๋ฉด ๋๋ค.)
1–2. model ์ ์
๋จผ์ , ๋ด ํ๋ก์ ํธ์ user ERD๋ ๋ค์๊ณผ ๊ฐ์๋ค.
์ ์ ์ ๋ณด๋ฅผ ํฌ๊ฒ ์ ์ฅํ ๊ฒ์ด ์์ด์..์๊ฐ๋ณด๋ค ๋ณ๋ก ๋ฃ์ ๊ฒ ์์๋ค..ใ ใ
๊ทธ๋์ ๋จผ์ python manage.py startapp accounts ๋ช ๋ น์ด๋ฅผ ํตํด accounts ์ฑ์ ๋ง๋ค๊ณ , ๊ทธ ์์ models.py์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ํ๋ค.
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
username = models.CharField(max_length=100, unique=False) # username ํ๋
kakao_oid = models.BigIntegerField(
null=True, unique=True, blank=False
) # ์นด์นด์ค user_id
position = models.CharField(max_length=100, null=True, blank=True)
profile_image = models.URLField(max_length=200, blank=True)
is_staff = models.BooleanField(default=False) # ์ํผ์ ์ ๊ถํ
is_superuser = models.BooleanField(default=False)
is_active = models.BooleanField(default=True) # ๊ณ์ ํ์ฑํ ์ํ
created_at = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
def __str__(self):
return self.email
์ค๊ฐ์ CutomUserManager ๋ผ๋ manager๊ฐ ์๋๋ฐ, ์ด๋ managers.py์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ค.
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("์ด๋ฉ์ผ ํ๋๊ฐ ์์ฑ๋์ด์ผ ํฉ๋๋ค.")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return self.create_user(email, password, **extra_fields)
์ด manager์ ์ญํ ์ ์ ์ ๋ฐ ์ํผ์ ์ ๋ฅผ ๋ง๋ค ๋ ๊ถํ์ ๋ถ์ฌํ๊ณ , ๋ฐ๋์ ํ์ํ ํ๋๊ฐ ์๋ค๋ฉด ํด๋น ํ๋๋ฅผ ์์ฑํด์ผ ํ๋ค๊ณ ๊ฒฝ๊ณ ํ๋ ์ญํ ์ด๋ค.
์ด๋ ๊ฒ ์์ฑํ์ผ๋ฉด, python manage.py makemigrations && migrate ๋ฅผ ํตํด ์ ์ ๋ชจ๋ธ์ ๋ง๋ค ์ ์๋ค.
2. ์ธ์ฆ ๋ก์ง
์ด๋ ๊ฒ user model์ ์ ์ํ ํ์๋, ์ธ์ฆ ๋ก์ง์ ๋ํด ์์์ผ ๋ค์ ๋จ๊ณ๋ก ์งํํ ์ ์๋ค.
๋จผ์ , ์์ฃผ ์ ์๋ ค์ง ์นด์นด์ค ์ธ์ฆ ๋ก๊ทธ์ธ ๋ก์ง ๊ทธ๋ฆผ์ ํ๋ฒ ๋ณด๋ฉด,
๊ฐ๋ตํ๊ฒ ๋งํด ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ด ์์ฝํ ์ ์๋ค.
- ํด๋ผ์ด์ธํธ์์ ์นด์นด์ค ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ์์ฑํ์ฌ ์ธ๊ฐ ์ฝ๋๋ฅผ ๋ฐ์ ์๋ฒ์ ์ ๋ฌํ๋ค.
- ์๋ฒ๋ ์ธ๊ฐ ์ฝ๋๋ฅผ ์ ๋ฌ๋ฐ์ ์นด์นด์ค์์์ ์ฌ์ฉ์์ access, refresh token์ ์ ๋ฌ ๋ฐ๋๋ค. ์ด๋, access token๊ณผ ์ธ๊ฐ ์ฝ๋๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ acess token๊ณผ code๋ฅผ ์๋ฒ์๊ฒ ๋ณด๋ด๋ฉด, ๋ก๊ทธ์ธ/ํ์๊ฐ์ ์ด ์๋ฃ๋๊ณ , ๋ด๋ถ์์ access, refresh token, code๋ฅผ ๋ฐํํ๋ค.
์ด์ ์ด ๋ก์ง์ ๊ทธ๋๋ก ๊ตฌํํด๋ณด์. ๋ณธ ํ๋ก์ ํธ์์๋ ๊ตฌํ์ ๊ฐ๋จํ๊ฒ ํ๊ธฐ ์ํด allauth์ dj_rest_auth๋ฅผ์ฌ์ฉํ๋ค.
3. Views
์์ ์ฒจ๋ถํ ์ธ์ฆ ๋ก์ง์ ๋ณด๋ฉด,
- ์นด์นด์ค ์ธ๊ฐ ์ฝ๋๋ฅผ ๋ฐ๊ธํ๋ view
- ์นด์นด์ค์์ ๋ฐํํ access_token๊ณผ code๋ฅผ ๋ฐํํ์ฌ ์นด์นด์ค ํ๋กํ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ view
์์ ๊ฐ์ 2๊ฐ์ view๊ฐ ํ์ํ ๊ฒ์ ์ ์ ์๋ค. ํ์ง๋ง ์ฌ์ค ์นด์นด์ค ์ธ๊ฐ ์ฝ๋ ๋ฐ๊ธํ๋ view๋ ์นด์นด์ค ๋๋ฒจ๋กํผ์ค ํํ์ด์ง์ ๋ค์ด๊ฐ redirect uri๋ง ์ ์ค์ ํด ์ฃผ๊ณ , ์๋ url๋ก ์ ์ํ๋ฉด ๋๋ค.
"https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={KAKAO_CALLBACK_URI}&response_type=code"
๊ทธ๋ฌ๋ฏ๋ก ๋๋จธ์ง 1๊ฐ์ view์ ๋ํด์ ์์๋ณด๋๋ก ํ์.
๋จผ์ ์ ์ฒด ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
# accouts/views.py
User = get_user_model()
class KakaoLoginView(APIView):
def post(self, request, *args, **kwargs):
code = request.data.get("code")
if not code:
return Response(
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
)
# ์นด์นด์ค ์ธ๊ฐ์ฝ๋๋ฅผ ์ฌ์ฉํด access_token ํ๋
token_res = requests.get(
f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={REST_API_KEY}&client_secret={CLIENT_SECRET}&redirect_uri={KAKAO_CALLBACK_URI}&code={code}"
)
logger.fatal(token_res)
if token_res.status_code != 200:
logger.fatal(token_res.json())
return Response(
{"error": "Failed to obtain access token"},
status=status.HTTP_400_BAD_REQUEST,
)
token_json = token_res.json()
access_token = token_json.get("access_token")
# ์นด์นด์ค access_token์ผ๋ก๋ถํฐ ์ฌ์ฉ์ ์ ๋ณด ํ๋
headers = {"Authorization": f"Bearer {access_token}"}
profile_res = requests.get("https://kapi.kakao.com/v2/user/me", headers=headers)
if profile_res.status_code != 200:
return Response(
{"error": "Failed to obtain user information"},
status=status.HTTP_400_BAD_REQUEST,
)
profile_json = profile_res.json()
kakao_oid = profile_json.get("id")
nickname = profile_json.get("properties")["nickname"]
profile_image = profile_json.get("properties")["profile_image"]
email = profile_json.get("kakao_account")["email"]
user, created = User.objects.get_or_create(
email=email,
defaults={
"username": f"{nickname}",
"kakao_oid": kakao_oid,
"profile_image": f"{profile_image}",
},
)
# ์ฌ์ฉ์์ ๋ํ ํ ํฐ ์์ฑ
refresh = RefreshToken.for_user(user)
data = {
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
"user_info": {
"id": user.id,
"email": user.email,
"username": user.username,
"profile_image": user.profile_image,
"is_created": created,
},
}
return Response(data, status=status.HTTP_200_OK)
์ด ์ฝ๋ ์กฐ๊ฐ์ ํ๋์ฉ ์ดํด๋ณด์.
3-1. ์ธ๊ฐ ์ฝ๋ ๋ฐ์์ค๊ธฐ
User = get_user_model()
def kakao_login(request):
return redirect(
f"https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={KAKAO_CALLBACK_URI}&response_type=code"
)
์ด ๋ถ๋ถ์ ํด๋ผ์ด์ธํธ ๋ถ๋ถ๊ณผ์ ์ฑํฌ๋ฅผ ๋ง์ถ๊ธฐ ์ํด์ ์์ฑํ๋๋ฐ, ์์ ์์ฑํ url๋ก ์ ์ํ๋ฉด code? ๋ผ๋ query๋ก ์นด์นด์ค์์ ์ธ์ ์ฝ๋๊ฐ ๋ฐํ๋๋ค.
์ด๋ ๊ฒ ๋ค์ code๋ฅผ ํด๋ผ์ด์ธํธ ์ธก์์ ํ์ฑํ์ฌ ์๋ฒ๋ก ๋ณด๋ด๋ฉด, ์นด์นด์ค์ AT (Access Token)์ ๋ฐ์ ์ ์ ์ ๋ณด๋ฅผ ๋ฐํ๋ฐ๊ณ , ์์ฒด AT, RT๋ฅผ ๋ง๋ค์ด ๋ฐํํ๋ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ๋ค.
3-2. ์์ฒด AT, RT ๋ฐํ
1. ์ธ๊ฐ์ฝ๋๋ก ์นด์นด์ค access_token ํ๋
๋จผ์ , ์ฌ์ฉ์๊ฐ ์ธ๊ฐ ์ฝ๋๋ฅผ postํ๊ฒ ํ์ฌ, kauth๋ก๋ถํฐ ์นด์นด์ค AT๋ฅผ ๋ฐํ๋ฐ๋๋ค.
์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
class KakaoLoginView(APIView):
def post(self, request, *args, **kwargs):
code = request.data.get("code")
if not code:
return Response(
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
)
# ์นด์นด์ค ์ธ๊ฐ์ฝ๋๋ฅผ ์ฌ์ฉํด access_token ํ๋
token_res = requests.get(
f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={REST_API_KEY}&client_secret={CLIENT_SECRET}&redirect_uri={KAKAO_CALLBACK_URI}&code={code}"
)
if token_res.status_code != 200:
return Response(
{"error": "Failed to obtain access token"},
status=status.HTTP_400_BAD_REQUEST,
)
token_json = token_res.json()
access_token = token_json.get("access_token")
2. ์ธ๊ฐ์ฝ๋๋ก ์นด์นด์ค access_token ํ๋
๊ทธ๋ ๊ฒ ๋ฐ์ ์นด์นด์ค AT๋ฅผ Bearer์ ํจ๊ป header๋ก ์ ์ ํ๋กํ์ ๋ฐ์์ค๋ request๋ฅผ ๋ณด๋ธ๋ค. ๊ทธ๋ ๊ฒ ๋๋ฉด, ์ ์ ์ ์นด์นด์ค oid,๋๋ค์, ํ๋กํ ์ด๋ฏธ์ง, email์ ๋ฐ์์ฌ ์ ์๋ค.
์ดํ, ‘user, created = User.objects.get_or_create’ ํจ์๋ฅผ ์ฌ์ฉํด์ ‘user’๋ก ์ ์ ์ ๋ณด๋ฅผ, ‘created’๋ก ์ ์ ๊ฐ ์๋ก ์์ฑ๋๋์ง ์ฌ๋ถ๋ฅผ ๋ฐํ๋ฐ์ ์ ์๋ค.
# ์นด์นด์ค access_token์ผ๋ก๋ถํฐ ์ฌ์ฉ์ ์ ๋ณด ํ๋
headers = {"Authorization": f"Bearer {access_token}"}
profile_res = requests.get("https://kapi.kakao.com/v2/user/me", headers=headers)
if profile_res.status_code != 200:
return Response(
{"error": "Failed to obtain user information"},
status=status.HTTP_400_BAD_REQUEST,
)
profile_json = profile_res.json()
kakao_oid = profile_json.get("id")
nickname = profile_json.get("properties")["nickname"]
profile_image = profile_json.get("properties")["profile_image"]
email = profile_json.get("kakao_account")["email"]
user, created = User.objects.get_or_create(
email=email,
defaults={
"username": f"{nickname}",
"kakao_oid": kakao_oid,
"profile_image": f"{profile_image}",
},
)
3. ์์ฒด AT, RT ์์ฑ
๋ง์ง๋ง์ผ๋ก, rest_framework ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ RefreshToken์ ์ฌ์ฉํด์ ์์ฒด RT, AT๋ฅผ ๋ฐ๊ธ๋ฐ์ ์ ์๋ค. ๋ณธ์ธ์ user_info๋ ํจ๊ป ๋ฐํํ๋ค.
from rest_framework_simplejwt.tokens import RefreshToken
# ์ฌ์ฉ์์ ๋ํ ํ ํฐ ์์ฑ
refresh = RefreshToken.for_user(user)
data = {
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
"user_info": {
"id": user.id,
"email": user.email,
"username": user.username,
"profile_image": user.profile_image,
"is_created": created,
},
}
return Response(data, status=status.HTTP_200_OK)
4. ๊ฒฐ๊ณผ
Swagger๋ก ๋ณด์ด๋ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ๋ค.
์ค์ ์ธ๊ฐ ์ฝ๋๋ฅผ ์ฒจ๋ถํ์ฌ ์คํํด๋ณด๋ฉด ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ๋ค.
๊ฒฐ๊ตญ์ ๊ทธ๋ฅ 1๊ฐ์ api๋ง ์์ผ๋ฉด ๋๋๋ค. (url๋ก ์ ์ํด์ ์ธ๊ฐ ์ฝ๋๋ฅผ ๋ฐ์์ค๋ ์์ ์ ์์ ๋งํ๋ฏ์ด ๊ทธ๋ฅ ํด๋ผ์ด์ธํธ ์ธก์์ ํ๋ฉด ๋๊ธฐ ๋๋ฌธ์..)
์ด๋ ๊ฒ ๊ฐ๋จํ๊ฒ DRF๋ก ์นด์นด์ค ์์ ๋ก๊ทธ์ธ์ ๋๋๋ค. ์๊ฐ๋ณด๋ค ํฌ๊ฒ ํ ๊ฒ ๋ณ๊ฑฐ ์์๋๋ฐ ๊ฐ๊ณ ์ํ๋ ๊ธฐ์ต์ด ์๋ค. ใ ใ ใ ใ
๋ค์ ๊ธ์์๋ DRF+Docker+ELB ๋ฐฐํฌ ๋ฐฉ๋ฒ์ ๋ํด ๋ค๋ค๋ณผ ์์ ์ด๋ค.
'๐ป ํ๋ก์ ํธ > ๐ RESUMAI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[RESUMAI] 5. ์๊ธฐ์๊ฐ์ ์์ฑ ๋ฐ๋ชจ ์๋น์ค (1) | 2024.01.03 |
---|---|
[RESUMAI] 4. ๋ฐ์ดํฐ ํฌ๋กค๋ง๊ณผ vectorDB ์ ์ฅ (1) | 2024.01.02 |
[RESUMAI] 3. RAG์ ๋์ ๊ณผ ์๊ธฐ์๊ฐ์ ํฌ๋กค๋ง (1) | 2023.12.21 |
[RESUMAI] 2. ํ๋กฌํํธ ์คํ (2) | 2023.11.12 |
[RESUMAI] 1. ์ฐ๋ก ๋ง๋๋ ์์์์ ์์ / ๊ธฐํ (0) | 2023.11.03 |