SECRET_KEY / POSTGRES_PASSWORD 가 유출되었다고 생각해서 재발급 받을예정 env를 지우려고 좀 두두리다보니 로컬에서도 삭제됨 .env를 복구하기위해서 git checkout <커밋해시> -- .env를 사용하면.env를 추적할 수 있기 때문에 과거 기록에서 복사하여 새로 파일을 만들자
# 이전 커밋 기록 중에서 .env 파일이 포함되었던 커밋들의 해시(고유번호)를 한 줄씩 짧게 출력하여 찾음
git log --oneline -- .env
# 위 명령어 결과로 나온 최신 커밋 해시(예: abc1234)를 이용해 당시의 .env 파일 내용을 터미널에 텍스트로만 출력
git show abc1234:.env
# 저장소의 모든 과거 커밋 내역을 탐색하며 .env 파일을 삭제하는 강력한 명령어를 강제로 실행
git filter-branch --force \
# 각 커밋의 스테이징 영역(캐시)에서 .env 파일을 삭제하며, 파일이 없더라도 에러 없이 무시하고 넘어감
--index-filter "git rm --cached --ignore-unmatch .env" \
# .env 파일 삭제로 인해 내용이 전혀 없어진 빈 커밋들을 히스토리에서 깔끔하게 제거
--prune-empty \
# 태그가 있다면 태그 정보도 변경된 히스토리에 맞게 그대로 유지하며 덮어씀
--tag-name-filter cat \
# 현재 브랜치뿐만 아니라 저장소 내의 모든 브랜치와 태그(전체 히스토리)에 대해 위 작업들을 일괄 적용
-- --all
(.venv) ➜ django_blog_project git:(develop) git filter-branch --force \
> --index-filter "git rm --cached --ignore-unmatch .env" \
> --prune-empty \
> --tag-name-filter cat \
> -- --all
WARNING: git-filter-branch has a glut of gotchas generating mangled history
rewrites. Hit Ctrl-C before proceeding to abort, then use an
alternative filtering tool such as 'git filter-repo'
(https://github.com/newren/git-filter-repo/) instead. See the
filter-branch manual page for more details; to squelch this warning,
set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...
Rewrite b3db6d465cd97b3208deded33468932bd1c06777 (4/121)
(0 seconds passed, remaining 0 predicted) rm '.env'
...
Rewrite 2aa9d3e3a3c52b9037ba757d696c0b265b73d3c7 (95/121)
(4 seconds passed, remaining 1 predicted) rm '.env'
Ref 'refs/heads/develop' was rewritten
Ref 'refs/heads/feature/11-create-signup' was rewritten
...
Ref 'refs/remotes/origin/feature/59-env-error' was rewritten
Ref 'refs/remotes/origin/feature/9-social-google' was rewritten
WARNING: Ref 'refs/remotes/origin/main' is unchanged
# 프로젝트 최상단 폴더에 아무 내용이 없는 빈 .env 파일을 새롭게 생성
touch .env
# Mac/Linux 또는 Git Bash에서 32바이트 길이의 강력한 무작위 난수를 생성하여 새로운 SECRET_KEY 용도로 출력
openssl rand -base64 32
git push origin develop --force# 저장소의 전체 히스토리(--all)를 샅샅이 뒤져서, 과거에 .env 파일이 포함되었거나 수정되었던 모든 커밋 기록을 찾아 출력합니다.
git log --all -- .env
commit c38cfb42b247a6f262c098735ef16ee8bb7fe5d9
Author: kihoon <nike6736@naver.com>
Date: Sun Mar 8 17:49:44 2026 +0900
S3 연동 작업 적용 (시크릿 키 완전히 제외)
commit eeaffc2fae640050a3396bc5ab53e1bc293f7951
Author: kihoon <nike6736@naver.com>
Date: Sun Jan 25 13:20:26 2026 +0900
✨ feat(#2): 마이그레이션 진행
commit 951f32ff9ae1f6d7950a31615511447cb12c6a3c
Author: kihoon <nike6736@naver.com>
Date: Sat Jan 24 12:49:34 2026 +0900
💡 chore(#1): 프로젝트 세팅 - 2
commit b3db6d465cd97b3208deded33468932bd1c06777
Author: kihoon <nike6736@naver.com>
Date: Fri Jan 23 17:03:22 2026 +0900
✨ feat(#1): docker 세팅
--all 옵션으로 인해 출력된것git filter-branch 명령어로 히스토리를 도려내면.git/refs/original/ 경로에 몰래 백업함--all 옵션은 이 숨겨진 백업본까지 전부 뒤져서 보여줌# 전체(--all) 백업본을 제외하고, 현재 작업 중인 develop 브랜치의 히스토리에서만 .env 파일의 흔적을 찾음
git log develop -- .env

# Git이 filter-branch 작업 시 만약을 위해 복사해둔 원본 백업(refs/original) 경로를 강제로 삭제
rm -rf .git/refs/original/
# Git의 내부 창고를 청소(Garbage Collection)하여 어디에도 연결되지 않은 과거 커밋과 찌꺼기 파일들을
지금 당장(--prune=now) 영구 삭제
git gc --prune=now

git push origin develop --force.env 파일에 새로운 비밀번호를 수정한 것은 접속 에러(Authentication failed)가 발생함
# 현재 데이터베이스에 등록된 모든 유저(role)의 이름을 조회하여 표 형태로 보여줌
SELECT rolname FROM pg_roles;
# 본인의 아이디를 넣고, 새로운 비밀번호를 설정
ALTER USER 본인_진짜_아이디 WITH PASSWORD '여기에_env에_적은_새_비밀번호_입력';

[Edit Connection (연결 편집)]을 클릭'Password (비밀번호)' 입력 칸에 기존에 적혀있던 내용을 지우고 .env에 적었던 새로운 비밀번호를 붙여넣음[Test Connection (Test 연결)] 버튼을 눌러 성공(Connected) 메시지가 뜨는지 확인한 후, [확인]을 눌러 저장# 콘솔 로그
Access to fetch at 'https://hoon-blog-uploader-0112.s3.amazonaws.com/post/thumbnails/
2026/03/09/cf5bc13c8eca430db0a6c3aa3b1a0e54.png?...
Access to fetch at 'https://hoon-blog-uploader-0112.s3.amazonaws.com...'...s3.ap-northeast-2.amazonaws.com 형식으로 나와야 함 Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present...
'preflight request' 에러 메시지"*"(모두 허용)를 설정했는데 거절당했다면? "*" 같은 대충 만든 출입증은 무시하고 깐깐하게 차단하는 경우가 많음 <Error>
<Code>SignatureDoesNotMatch</Code>
...
<CanonicalRequest>GET /post/thumbnails/2026/03/09/661480bcf5ca4651bdb142f7a9ef7f93.png ...
</CanonicalRequest>
...
</Error>
SignatureDoesNotMatch 에러와의 연결s3_client = boto3.client(
"s3",
region_name=settings.AWS_S3_REGION_NAME,
# 이 줄 추가
endpoint_url=f"https://s3.{settings.AWS_S3_REGION_NAME}.amazonaws.com",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4")
)
"AllowedOrigins": ["*"] *(모든 곳 허용)를 무시하고 깐깐하게 차단하는 경우가 있음[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE",
"HEAD"
],
"AllowedOrigins": [
"http://127.0.0.1:8000",
"http://localhost:8000",
"*"
],
"ExposeHeaders": [
"ETag"
]
}
]
config.settings를 초기화하려고 할 때AWS_ACCESS_KEY_ID 환경 변수를 찾지 못해 발생한 ImproperlyConfigured 에러Traceback (most recent call last):
Error constructing plugin instance of NewSemanalDjangoPlugin
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/environ/environ.py", line 409, in get_value
value = self.ENVIRON[var_name]
~~~~~~~~~~~~^^^^^^^^^^
...
"/home/runner/work/indi_Blog_Project/indi_Blog_Project/config/settings.py", line 229, in <module>
AWS_ACCESS_KEY_ID = env(
^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/environ/environ.py", line 207, in __call__
return self.get_value(
^^^^^^^^^^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/environ/environ.py", line 413, in get_value
raise ImproperlyConfigured(error_msg) from exc
django.core.exceptions.ImproperlyConfigured: Set the AWS_ACCESS_KEY_ID environment variable
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
AWS_S3_REGION_NAME = env("AWS_REGION", default="ap-northeast-2")
——————————————————————————————————————[비교]—————————————————————————————————————————
AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default='dummy')
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", , default='dummy')
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", , default='dummy')
AWS_S3_REGION_NAME = env("AWS_REGION", default="ap-northeast-2")
- name: Run Mypy (Type Checking)
run: poetry run mypy .
——————————————————————————————————————[비교]—————————————————————————————————————————
- name: Run Mypy (Type Checking)
run: poetry run mypy .
env:
AWS_ACCESS_KEY_ID: "dummy"
AWS_SECRET_ACCESS_KEY: "dummy"
AWS_STORAGE_BUCKET_NAME: "dummy"
Traceback (most recent call last):
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/environ/environ.py", line 409, in get_value
value = self.ENVIRON[var_name]
~~~~~~~~~~~~^^^^^^^^^^
File "<frozen os>", line 714, in __getitem__
KeyError: 'AWS_ACCESS_KEY_ID'
The above exception was the direct cause of the following exception:
...
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/django/conf/__init__.py", line 62, in _setup
self._wrapped = Settings(settings_module)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/django/conf/__init__.py", line 162, in __init__
mod = importlib.import_module(self.SETTINGS_MODULE)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/importlib/__init__.py", line 90, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 999, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/runner/work/indi_Blog_Project/indi_Blog_Project/config/settings.py", line 229, in <module>
AWS_ACCESS_KEY_ID = env(
^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/environ/environ.py", line 207, in __call__
return self.get_value(
^^^^^^^^^^^^^^^
File "/home/runner/.cache/pypoetry/virtualenvs/django-blog-project-dIrepBIM-py3.12/lib/python3.12/site-packages/environ/environ.py", line 413, in get_value
raise ImproperlyConfigured(error_msg) from exc
django.core.exceptions.ImproperlyConfigured: Set the AWS_ACCESS_KEY_ID environment variable
- name: Run Django Migration
run: |
poetry run python manage.py migrate
——————————————————————————————————————[비교]—————————————————————————————————————————
- name: Run Django Migration
run: |
poetry run python manage.py migrate
env:
# DB 및 AWS 관련 필수 환경 변수들을 주입합니다.
AWS_ACCESS_KEY_ID: "dummy"
AWS_SECRET_ACCESS_KEY: "dummy"
AWS_STORAGE_BUCKET_NAME: "dummy"
on:
pull_request:
branches:
- "main"
- "develop"
jobs:
——————————————————————————————————————[비교]—————————————————————————————————————————
on:
pull_request:
branches:
- "main"
- "develop"
# ────────── [여기에 정의하면 모든 Job과 Step에서 공유됩니다] ──────────
env: # 워크플로우 전체에서 사용할 전역 환경 변수 설정입니다.
AWS_ACCESS_KEY_ID: "dummy" # 모든 스텝에서 공통으로 쓰일 더미 액세스 키입니다.
AWS_SECRET_ACCESS_KEY: "dummy" # 모든 스텝에서 공통으로 쓰일 더미 시크릿 키입니다.
AWS_STORAGE_BUCKET_NAME: "dummy" # 모든 스텝에서 공통으로 쓰일 더미 버킷 이름입니다.
DJANGO_SETTINGS_MODULE: "config.settings" # 장고 설정을 명시적으로 지정하여 에러를 방지합니다.




login.html에서 [GitHub로 로그인] 버튼을 누릅니다.?code=...)를 달아서 다시 login.html로 돌려보냅니다.login.html의 자바스크립트가 주소창의 코드를 낚아채서 백엔드에 POST로 보냅니다.localStorage에 저장하고 메인 화면으로 이동합니다!GitHub 계정의 Settings -> Developer settings -> OAuth Apps에서 새로운 앱을 만듬
Authorization callback URL에
http://localhost:3000/auth/github/callback)를 적음발급된 Client ID와 Client Secret을 Django 프로젝트의 .env 파일에 저장


[Settings](설정)를 누름[Developer settings]를 클릭[OAuth Apps]를 클릭[New OAuth App] 버튼을 클릭http://localhost:8000/login/github/callback/[Register application] 버튼을 클릭[Generate a new client secret] 버튼을 클릭GITHUB_CLIENT_ID=복사한_클라이언트_ID
GITHUB_CLIENT_SECRET=복사한_클라이언트_시크릿
# settings.py의 적당한 위치
import os
# .env 파일에서 키를 읽어와 Django 설정 변수로 만듭니다.
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
Callback URL: http://localhost:3000/auth/github/callbackCallback URL: http://localhost:8000/api/user/login/github/callback # apps/user/services/social_login_service.py
import requests # HTTP 요청을 보내기 위한 라이브러리입니다.
from django.conf import settings # Django 설정에서 Client ID 등을 가져오기 위해 임포트합니다.
from django.contrib.auth import get_user_model # 커스텀 User 모델을 안전하게 가져옵니다.
from django.db import transaction # DB 상태를 안전하게 유지하기 위해 트랜잭션을 사용합니다.
from rest_framework_simplejwt.tokens import RefreshToken # JWT 토큰 생성을 위해 임포트합니다.
from apps.user.models.social_account import SocialAccount # 만들어두신 소셜 계정 모델을 임포트합니다.
User = get_user_model() # User 모델 클래스를 변수에 할당합니다.
class GithubLoginService:
# 메서드를 인스턴스화 없이 사용할 수 있도록 정적 메서드로 선언합니다.
@staticmethod
def github_login(code: str):
# 1. 깃허브에서 Access Token을 받아오기 위한 URL입니다.
token_req_url = "https://github.com/login/oauth/access_token"
# 2. Access Token을 요청할 때 보낼 데이터(페이로드)를 구성합니다.
data = {
"client_id": settings.GITHUB_CLIENT_ID, # 환경변수에 저장된 Client ID
"client_secret": settings.GITHUB_CLIENT_SECRET, # 환경변수에 저장된 Client Secret
"code": code, # 프론트엔드로부터 전달받은 인가 코드
}
# 3. 깃허브 API가 JSON 형태로 응답하도록 헤더를 설정합니다.
headers = {"Accept": "application/json"}
# 4. 깃허브 서버로 POST 요청을 보내 토큰을 발급받습니다.
token_req = requests.post(token_req_url, data=data, headers=headers)
# 5. 응답받은 JSON 데이터에서 access_token 값만 추출합니다.
token_json = token_req.json()
error = token_json.get("error")
# 6. 토큰 발급 중 에러가 발생했다면 예외를 발생시킵니다.
if error is not None:
raise ValueError("GitHub 토큰을 받아오는데 실패했습니다.")
access_token = token_json.get("access_token")
# 7. 발급받은 토큰으로 깃허브 유저 정보를 요청할 URL입니다.
user_req_url = "https://api.github.com/user"
# 8. 토큰을 Authorization 헤더에 담아 GET 요청을 보냅니다.
user_req = requests.get(
user_req_url, headers={"Authorization": f"Bearer {access_token}"}
)
# 9. 응답받은 유저 정보를 JSON 객체로 변환합니다.
user_json = user_req.json()
# 10. 깃허브의 유저 고유 ID와 아이디(login)를 가져옵니다.
github_id = str(user_json.get("id"))
nickname = user_json.get("login")
# 11. 깃허브는 이메일을 비공개할 수 있으므로, 이메일이 없다면 추가 API 호출이 필요할 수 있습니다. (여기선 제공됐다고 가정)
email = user_json.get("email", f"{github_id}@github.dummy.com")
# 12. DB 작업 중 오류 발생 시 롤백하기 위해 트랜잭션 블록을 엽니다.
with transaction.atomic():
# 13. SocialAccount 테이블에서 깃허브 ID로 등록된 소셜 계정이 있는지 찾습니다.
social_account = SocialAccount.objects.filter(
provider="github", social_id=github_id
).first()
# 14. 소셜 계정이 이미 존재한다면 (기존 가입 유저)
if social_account:
user = social_account.user # 해당 소셜 계정과 연결된 User 객체를 가져옵니다.
# 15. 소셜 계정이 없다면 (신규 가입 유저)
else:
# 16. 혹시 같은 이메일로 가입한 기존 일반 유저가 있는지 확인합니다.
user = User.objects.filter(email=email).first()
# 17. 일반 유저도 없다면 새로운 유저를 생성합니다. (UserManager의 create_user 활용)
if not user:
user = User.objects.create_user(
email=email,
nickname=nickname,
password=None # 소셜 로그인이므로 비밀번호는 사용 불가 처리됩니다.
)
# 18. 새로 생성한(혹은 기존) 유저와 깃허브 ID를 연결하는 SocialAccount 레코드를 생성합니다.
SocialAccount.objects.create(
user=user,
provider="github",
social_id=github_id
)
# 19. 유저 인증이 완료되었으므로, 프론트엔드에 전달할 자체 JWT 토큰을 생성합니다.
refresh = RefreshToken.for_user(user)
# 20. Access Token, Refresh Token, 그리고 유저 정보를 딕셔너리 형태로 반환합니다.
return {
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
"user": user,
}
from django.shortcuts import redirect
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from apps.user.services.social_login_service import GithubLoginService
from rest_framework.response import Response
from rest_framework import status
class GithubLoginAPIView(APIView):
"""
1. 유저가 '깃허브 로그인' 버튼을 눌렀을 때 깃허브 서버로 보내주는 역할만 합니다.
"""
permission_classes = [AllowAny]
def get(self, request):
# 1. settings에 저장된 클라이언트 ID를 가져옵니다.
client_id = settings.GITHUB_CLIENT_ID
# 2. 깃허브에 등록한 콜백 주소입니다. (이리로 다시 돌려보내 달라는 뜻)
redirect_uri = "http://127.0.0.1:8000/api/v1/user/login-page/"
# 3. 깃허브의 권한 인증 페이지 URL을 만듭니다.
github_auth_url = f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}"
# 4. 유저의 브라우저를 깃허브 로그인 창으로 강제 이동(리다이렉트) 시킵니다.
return redirect(github_auth_url)
class GithubLoginCallbackAPIView(APIView):
"""프론트엔드가 넘겨준 코드를 받아 토큰을 발급하는 API (POST)"""
permission_classes = [AllowAny]
def post(self, request):
code = request.data.get("code")
if not code:
return Response({"error": "인가 코드가 필요합니다."}, status=status.HTTP_400_BAD_REQUEST)
try:
# 1. 서비스 호출
login_data = GithubLoginService.github_login(code)
# 2. JSON 형태로 토큰 응답 (프론트엔드가 받아서 localStorage에 저장할 데이터)
return Response(
{
"message": "GitHub 로그인 성공",
"token": {
"access": login_data["access_token"],
"refresh": login_data["refresh_token"],
},
"user": {
"email": login_data["user"].email,
"nickname": login_data["user"].nickname,
},
},
status=status.HTTP_200_OK
)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)


class UserManager(BaseUserManager):
def create_user(self, email, nickname, password=None, **extra_fields):
if not email:
raise ValueError("이메일은 필수 입력값입니다.")
...
user_json.get("email", "기본값")은 아예 키가 없을 때는 기본값을 주지만# 10. 깃허브의 유저 고유 ID와 아이디(login)를 가져옵니다.
github_id = str(user_json.get("id"))
nickname = user_json.get("login")
# 11. 깃허브는 이메일을 비공개할 수 있으므로, 이메일이 없다면 추가 API 호출이 필요할 수 있습니다. (여기선 제공됐다고 가정)
email = user_json.get("email", f"{github_id}@github.dummy.com")
——————————————————————————————————————[비교]—————————————————————————————————————————
# 10. 깃허브의 유저 고유 ID와 아이디(login)를 가져옵니다.
github_id = str(user_json.get("id"))
nickname = user_json.get("login")
# 11. 깃허브 이메일이 비공개(null)로 올 경우를 대비하여 확실하게 처리합니다.
email = user_json.get("email")
if not email:
email = f"{github_id}@github.dummy.com"



<script>)만이 저장 가능