Python 기반 공공임대주택 상담 AI 챗봇 시스템입니다.
- Upstage Solar LLM 기반 대화형 AI
- PostgreSQL pgvector를 활용한 RAG (Retrieval-Augmented Generation)
- Redis 기반 세션 메모리 관리
- 하이브리드 검색 (벡터 유사도 + 키워드 매칭)
- FastAPI 기반 REST API
- 대화 컨텍스트 유지 (세션별 대화 히스토리 관리)
- 실시간 스트리밍 응답 (Server-Sent Events) ✨
- 엄격한 범위 제한 (공공임대주택 전용, 범위 외 질문 자동 거부)
┌─────────────────┐
│ 사용자 요청 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ FastAPI │
│ (api/routes) │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌──────────────┐
│ QA Chain │◄─────│ Redis │
│ (chains/) │ │ (세션 메모리) │
└────────┬────────┘ └──────────────┘
│
├─────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌──────────────┐
│ Retrieval │ │ LLM Service │
│ Service │ │ (Upstage) │
│ (RAG 검색) │ └──────────────┘
└────────┬────────┘
│
├─────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌──────────────┐
│ Vector Store │ │ Embeddings │
│ (pgvector) │ │ (Upstage) │
└─────────────────┘ └──────────────┘
│
▼
┌─────────────────┐
│ PostgreSQL DB │
│ - embeddings │
└─────────────────┘
두 가지 실행 방법을 제공합니다:
- 🐳 Docker로 실행 (권장) - 빠르고 쉬운 설정
- 💻 로컬 실행 - 개발 및 디버깅용
- Docker 20.10 이상
- Docker Compose 2.0 이상
# .env 파일 생성
cp .env.example .env
# .env 파일 수정
nano .env.env 파일에서 다음 항목들을 설정하세요:
# LLM (필수!)
UPSTAGE_API_KEY=up_xxxxxxxxxxxxxxxxxxxxx
# Database
DB_HOST=postgres
DB_PORT=5432
DB_NAME=housing_db
DB_USER=postgres
DB_PASSWORD=your_strong_password
# Redis
REDIS_HOST=redis
REDIS_PORT=6379# 이미지 빌드 및 컨테이너 시작
docker-compose up -d
# 로그 확인
docker-compose logs -f
# 특정 서비스 로그만 확인
docker-compose logs -f app
docker-compose logs -f postgres
docker-compose logs -f redis# 개발 모드로 실행
docker-compose -f docker-compose.dev.yml up -d
# pgAdmin 포함 (데이터베이스 관리 도구)
# 접속: http://localhost:5050
# Email: admin@example.com
# Password: admin브라우저에서 접속:
- API 서버: http://localhost:8000
- API 문서 (Swagger): http://localhost:8000/docs
- API 문서 (ReDoc): http://localhost:8000/redoc
- 헬스 체크: http://localhost:8000/health
# 컨테이너 상태 확인
docker-compose ps
# 컨테이너 중지
docker-compose down
# 컨테이너 재시작
docker-compose restart
# 특정 서비스만 재시작
docker-compose restart app
# 로그 실시간 확인
docker-compose logs -f --tail=100
# 볼륨 포함 완전 삭제 (데이터 삭제됨!)
docker-compose down -v# PostgreSQL 컨테이너 접속
docker-compose exec postgres psql -U postgres -d housing_db
# SQL 쿼리 실행
SELECT COUNT(*) FROM announcement_program_embeddings;
# 테이블 구조 확인
\d announcement_program_embeddings
# 종료
\q# 앱 컨테이너 접속
docker-compose exec app bash
# Python 인터프리터 실행
docker-compose exec app python
# 특정 스크립트 실행
docker-compose exec app python -c "from services.embeddings import embeddings_service; print('OK')"# 포트 변경 (docker-compose.yml 수정)
ports:
- "8001:8000" # 8000 대신 8001 사용# 볼륨 삭제 후 재시작
docker-compose down -v
docker-compose up -d
# 초기화 스크립트 재실행
docker-compose exec postgres psql -U postgres -d housing_db -f /docker-entrypoint-initdb.d/init-db.sql# 캐시 없이 재빌드
docker-compose build --no-cache
docker-compose up -d- Python 3.11 이상
- PostgreSQL 14+ (pgvector 확장 포함)
- Redis 6+
- Upstage API Key
housing-chatbot/
├── config/
│ ├── settings.py # 환경 설정
│ └── prompts.py # 시스템 프롬프트
├── models/
│ ├── schemas.py # Pydantic 스키마
│ └── database.py # DB 모델
├── services/
│ ├── embeddings.py # Upstage 임베딩 서비스
│ ├── vector_store.py # pgvector 검색
│ ├── llm_service.py # LLM 인터페이스
│ ├── memory_service.py # Redis 메모리
│ └── retrieval_service.py # RAG 검색
├── chains/
│ └── qa_chain.py # QA 체인 (메인 로직)
├── api/
│ └── routes.py # FastAPI 라우트
├── main.py # 앱 진입점
├── requirements.txt # 의존성
├── .env.example # 환경 변수 예시
├── docker-compose.yml # Docker Compose 설정
└── Dockerfile # Docker 이미지 설정
# 가상환경 생성
python -m venv venv
# 가상환경 활성화
# macOS/Linux:
source venv/bin/activate
# Windows:
# venv\Scripts\activate
# 의존성 설치
pip install -r requirements.txt# PostgreSQL 설치
brew install postgresql@14
# pgvector 설치
brew install pgvector
# PostgreSQL 시작
brew services start postgresql@14
# 데이터베이스 생성
createdb housing_db# PostgreSQL 설치
sudo apt-get update
sudo apt-get install postgresql postgresql-contrib
# pgvector 설치
sudo apt-get install postgresql-14-pgvector
# PostgreSQL 서비스 시작
sudo systemctl start postgresql
sudo systemctl enable postgresqlPostgreSQL 콘솔에서:
-- PostgreSQL 접속
psql postgres
-- 데이터베이스 생성
CREATE DATABASE housing_db;
-- 데이터베이스 연결
\c housing_db
-- pgvector 확장 설치
CREATE EXTENSION IF NOT EXISTS vector;
-- 테이블 생성
CREATE TABLE announcement_program_embeddings (
announcement_id SERIAL PRIMARY KEY,
embedding_text TEXT NOT NULL,
embedding_vector vector(4096), -- Upstage embedding dimension
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 벡터 인덱스 생성 (성능 향상)
CREATE INDEX ON announcement_program_embeddings
USING ivfflat (embedding_vector vector_cosine_ops)
WITH (lists = 100);
-- 종료
\q# Redis 설치
brew install redis
# Redis 시작
brew services start redis
# 또는 포그라운드 실행
redis-server# Redis 설치
sudo apt-get install redis-server
# Redis 시작
sudo systemctl start redis
sudo systemctl enable redis
# Redis 연결 테스트
redis-cli ping
# 응답: PONG# .env 파일 생성
cp .env.example .env.env 파일을 편집하여 다음 값들을 입력:
# LLM
UPSTAGE_API_KEY=up_xxxxxxxxxxxxxxxxxxxxx # Upstage API 키
LLM_MODEL=solar-pro
TEMPERATURE=0.3
STREAMING=True
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_password # PostgreSQL 비밀번호
DB_NAME=housing_db
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
SESSION_TTL=1800
# RAG
RETRIEVER_TOP_K=10
CONTEXT_WINDOW_LENGTH=15
VECTOR_STORE_TABLE=announcement_program_embeddings
VECTOR_COLUMN=embedding_vector
CONTENT_COLUMN=embedding_text
ID_COLUMN=announcement_id
# API
API_HOST=0.0.0.0
API_PORT=8000
API_ALLOW_ORIGINS=*# 서버 시작
python main.py서버가 성공적으로 시작되면:
INFO: Started server process [xxxxx]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
.env 파일의 각 설정 항목:
# LLM 설정
UPSTAGE_API_KEY=up_xxxxx # Upstage API 키 (필수)
LLM_MODEL=solar-pro # 사용할 LLM 모델
TEMPERATURE=0.3 # 응답 창의성 (0.0~1.0)
STREAMING=True # 스트리밍 응답 사용 여부
# Database 설정
DB_HOST=localhost # PostgreSQL 호스트
DB_PORT=5432 # PostgreSQL 포트
DB_USER=postgres # DB 사용자명
DB_PASSWORD=your_password # DB 비밀번호
DB_NAME=housing_db # DB 이름
# Redis 설정
REDIS_HOST=localhost # Redis 호스트
REDIS_PORT=6379 # Redis 포트
REDIS_DB=0 # Redis DB 번호
SESSION_TTL=1800 # 세션 유지 시간 (초)
# RAG 설정
RETRIEVER_TOP_K=10 # 검색할 문서 개수
CONTEXT_WINDOW_LENGTH=15 # 컨텍스트 윈도우 길이
VECTOR_STORE_TABLE=announcement_program_embeddings # 벡터 테이블명
VECTOR_COLUMN=embedding_vector # 벡터 컬럼명
CONTENT_COLUMN=embedding_text # 텍스트 컬럼명
ID_COLUMN=announcement_id # ID 컬럼명
# API 설정
API_HOST=0.0.0.0 # API 서버 호스트
API_PORT=8000 # API 서버 포트
API_ALLOW_ORIGINS=* # CORS 허용 도메인curl http://localhost:8000/health응답:
{"status": "healthy"}curl -X POST "http://localhost:8000/chat" \
-H "Content-Type: application/json" \
-d '{
"content": "청년이 신청할 수 있는 공공임대주택 알려줘",
"session_id": "test_session_001"
}'응답 예시:
{
"response": "청년을 위한 공공임대주택으로는...",
"sources": [
{
"content": "청년 대상 공공임대주택 공고...",
"score": 0.85,
"announcement_id": 123
}
],
"session_id": "test_session_001",
"timestamp": "2024-11-10T12:34:56"
}curl -N -X POST "http://localhost:8000/chat/stream" \
-H "Content-Type: application/json" \
-d '{
"content": "청년이 신청할 수 있는 공공임대주택 알려줘",
"session_id": "test_session_001"
}'스트리밍 응답 예시 (Server-Sent Events):
data: {"type":"sources","data":[...],"session_id":"test_session_001"}
data: {"type":"content","data":"청년을","session_id":"test_session_001"}
data: {"type":"content","data":" 위한","session_id":"test_session_001"}
data: {"type":"content","data":" 공공임대주택으로는...","session_id":"test_session_001"}
data: {"type":"done","session_id":"test_session_001"}
data: [DONE]
curl -X POST "http://localhost:8000/clear-session?session_id=test_session_001"브라우저에서 접속:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
examples/streaming_client.html 파일을 브라우저에서 열면 실시간 스트리밍 채팅을 테스트할 수 있습니다.
# 서버 실행 후
open examples/streaming_client.html주요 기능:
- 실시간 텍스트 스트리밍
- 타이핑 인디케이터
- 출처 정보 표시
- 세션 유지
# 스트리밍 테스트
python examples/streaming_client.py
# 선택: 1
# 일반 채팅 테스트
python examples/streaming_client.py
# 선택: 2import httpx
import asyncio
async def chat_example():
async with httpx.AsyncClient() as client:
# 채팅 요청
response = await client.post(
"http://localhost:8000/chat",
json={
"content": "청년이 신청할 수 있는 공공임대주택 알려줘",
"session_id": "user_123"
}
)
result = response.json()
print(f"응답: {result['response']}")
print(f"출처 개수: {len(result['sources'])}")
asyncio.run(chat_example())import httpx
import json
import asyncio
async def chat_streaming():
url = "http://localhost:8000/chat/stream"
payload = {
"content": "청년이 신청할 수 있는 공공임대주택 알려줘",
"session_id": "user_123"
}
async with httpx.AsyncClient(timeout=60.0) as client:
async with client.stream("POST", url, json=payload) as response:
async for line in response.aiter_lines():
if line.startswith("data: "):
data_str = line[6:].strip()
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
if data["type"] == "sources":
print(f"📚 출처 {len(data['data'])}개 발견")
elif data["type"] == "content":
# 실시간으로 텍스트 출력
print(data["data"], end="", flush=True)
elif data["type"] == "done":
print("\n✅ 응답 완료")
except json.JSONDecodeError:
continue
asyncio.run(chat_streaming())세션 ID를 통해 대화 컨텍스트를 유지합니다:
# 같은 session_id로 연속 대화
session_id = "user_123"
# 첫 번째 질문
response1 = await client.post("/chat", json={
"content": "청년 임대주택 알려줘",
"session_id": session_id
})
# 두 번째 질문 (이전 대화 기억)
response2 = await client.post("/chat", json={
"content": "신청 자격은 어떻게 되나요?",
"session_id": session_id
})
# 세션 삭제
await client.post(f"/clear-session?session_id={session_id}")CREATE TABLE announcement_program_embeddings (
announcement_id SERIAL PRIMARY KEY,
embedding_text TEXT NOT NULL,
embedding_vector vector(4096),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스
CREATE INDEX ON announcement_program_embeddings
USING ivfflat (embedding_vector vector_cosine_ops)
WITH (lists = 100);-- 전체 데이터 개수
SELECT COUNT(*) FROM announcement_program_embeddings;
-- 최근 데이터 조회
SELECT announcement_id,
LEFT(embedding_text, 100) as preview,
created_at
FROM announcement_program_embeddings
ORDER BY created_at DESC
LIMIT 10;
-- 특정 키워드 검색
SELECT announcement_id, embedding_text
FROM announcement_program_embeddings
WHERE embedding_text ILIKE '%청년%'
LIMIT 5;공고 데이터를 벡터 데이터베이스에 저장하기 위해 임베딩을 생성해야 합니다.
n8n을 사용하여 자동화된 임베딩 파이프라인을 실행할 수 있습니다.
# npm으로 설치
npm install n8n -g
# 또는 Docker로 실행
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n- n8n 웹 인터페이스 접속: http://localhost:5678
- 좌측 메뉴에서 "Workflows" 클릭
- "Import from File" 선택
embedding_announcements/RAG ChatBot - Embedding.json파일 선택- 워크플로우가 임포트됨
워크플로우에서 PostgreSQL 노드의 Credentials 설정:
Host: localhost (또는 DB 호스트)
Database: housing_db
User: postgres
Password: your_password
Port: 5432
SSL: Disable (로컬) / Enable (프로덕션)
Embeddings 노드에서 Upstage API 키 설정:
API Key: your_upstage_api_key
Model: solar-embedding-1-large-passage
수동 실행:
- 워크플로우 화면에서 "Execute Workflow" 버튼 클릭
자동 실행 (스케줄):
- Schedule Trigger 노드가 매일 오전 10시에 자동 실행
- 스케줄 변경: Schedule Trigger 노드 설정에서 시간 조정
┌─────────────────────┐
│ Schedule Trigger │ ← 매일 오전 10시 실행
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ PostgreSQL │ ← 임베딩 안 된 프로그램 조회
│ Get Programs │ (program_embedding_source)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Loop Over Programs │ ← 각 프로그램 순회
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Upstage Embeddings │ ← 임베딩 생성
│ API Call │ (embedding_text → vector)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Code - Format Data │ ← 데이터 포맷팅
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ PostgreSQL │ ← DB에 저장 (Upsert)
│ Save Embeddings │ (announcement_program_embeddings)
└─────────────────────┘
- ✅ 자동 중복 방지: 이미 임베딩된 데이터는 자동으로 스킵
- ✅ 배치 처리: 여러 프로그램을 순차적으로 처리
- ✅ 에러 핸들링: 실패한 항목은 로그에 기록
- ✅ 스케줄링: 매일 자동으로 신규 데이터 임베딩
- ✅ 시각적 모니터링: n8n UI에서 실행 상태 확인
n8n 웹 인터페이스에서:
- "Executions" 탭에서 실행 히스토리 확인
- 각 노드의 입출력 데이터 확인
- 에러 발생 시 상세 로그 확인
n8n을 사용하지 않는 경우, Python 스크립트로도 임베딩을 생성할 수 있습니다.
# embedding_announcements/manual_embed.py
import asyncio
from services.embeddings import embeddings_service
import psycopg2
import json
async def embed_announcements():
conn = psycopg2.connect(
host="localhost",
database="housing_db",
user="postgres",
password="your_password"
)
cur = conn.cursor()
# 임베딩 안 된 프로그램 조회
cur.execute("""
SELECT
p.announcement_id,
p.subscription_type,
p.title,
p.published_date,
p.embedding_text
FROM program_embedding_source p
WHERE NOT EXISTS (
SELECT 1
FROM announcement_program_embeddings e
WHERE e.announcement_id = p.announcement_id
)
ORDER BY p.published_date DESC;
""")
programs = cur.fetchall()
print(f"임베딩할 프로그램: {len(programs)}개")
for i, (ann_id, sub_type, title, pub_date, emb_text) in enumerate(programs, 1):
print(f"[{i}/{len(programs)}] 처리 중: {title[:50]}...")
# 임베딩 생성
embedding = await embeddings_service.embed_text(emb_text)
embedding_json = json.dumps(embedding)
# DB 저장
cur.execute("""
INSERT INTO announcement_program_embeddings
(announcement_id, subscription_type, title, published_date,
embedding_text, embedding_vector)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (announcement_id) DO UPDATE
SET embedding_vector = EXCLUDED.embedding_vector,
updated_at = NOW();
""", (ann_id, sub_type, title, pub_date, emb_text, embedding_json))
conn.commit()
cur.close()
conn.close()
print(f"✅ 완료: {len(programs)}개 임베딩 생성")
if __name__ == "__main__":
asyncio.run(embed_announcements())# SQL로 확인
psql -U postgres -d housing_db -c "
SELECT
COUNT(*) as total,
COUNT(CASE WHEN embedding_vector IS NOT NULL THEN 1 END) as embedded,
COUNT(CASE WHEN embedding_vector IS NULL THEN 1 END) as not_embedded
FROM announcement_program_embeddings;
"출력 예시:
total | embedded | not_embedded
-------+----------+--------------
25 | 25 | 0
| 메서드 | 경로 | 설명 | 요청 본문 | 응답 |
|---|---|---|---|---|
| POST | /chat |
채팅 메시지 전송 (일반) | {"content": "질문", "session_id": "세션ID"} |
{"response": "답변", "sources": [...], "session_id": "세션ID"} |
| POST | /chat/stream |
채팅 메시지 전송 (스트리밍) ✨ | {"content": "질문", "session_id": "세션ID"} |
Server-Sent Events 스트림 |
| POST | /clear-session |
세션 삭제 | Query: session_id |
{"status": "ok"} |
| GET | /health |
헬스 체크 | - | {"status": "healthy"} |
| GET | /docs |
API 문서 (Swagger) | - | HTML |
| GET | /redoc |
API 문서 (ReDoc) | - | HTML |
/chat/stream 엔드포인트는 Server-Sent Events (SSE) 형식으로 응답합니다:
// 1. 출처 정보
{"type": "sources", "data": [...], "session_id": "..."}
// 2. 텍스트 청크 (실시간)
{"type": "content", "data": "텍스트 조각", "session_id": "..."}
// 3. 완료 신호
{"type": "done", "session_id": "..."}
// 4. 에러 (발생 시)
{"type": "error", "data": "에러 메시지", "session_id": "..."}챗봇은 공공임대주택 입주자 모집공고에 관한 질문만 답변합니다:
✅ 답변 가능:
- 청년/신혼부부/다자녀 등 대상별 임대주택 검색
- 특정 지역의 공공임대주택 공고 찾기
- 신청자격, 소득기준, 임대조건 확인
- 신청방법, 일정, 제출서류 안내
- 공고 비교 및 추천
❌ 답변 불가 (자동 거부):
- 음악, 영화, 음식, 날씨 등 일반 주제
- 프로그래밍, 수학, 과학 문제
- 의료, 법률, 금융 자문 (임대주택 무관)
- 기타 공공임대주택과 무관한 모든 질문
범위 외 질문 시 응답 예시:
죄송합니다. 저는 공공임대주택(LH, SH 등) 입주자 모집공고 정보 제공에 특화된 챗봇이에요. 😅
말씀하신 내용은 제 전문 분야가 아니라 답변을 드릴 수 없습니다.
공공임대주택 관련해서 궁금하신 점이 있으시면 언제든 물어봐 주세요! 🏠
# Black 포맷터 실행
black .
# 특정 파일만 포맷팅
black services/llm_service.py# 전체 테스트 실행
pytest
# 특정 테스트 파일 실행
pytest tests/test_retrieval.py
# 커버리지 포함
pytest --cov=services --cov-report=html로그는 콘솔에 출력됩니다. 파일로 저장하려면:
# 로그 파일로 저장
python main.py > app.log 2>&1
# 실시간 로그 확인
tail -f app.log# uvicorn으로 개발 모드 실행
uvicorn api.routes:app --reload --host 0.0.0.0 --port 8000Error: port is already allocated
해결방법:
# docker-compose.yml에서 포트 변경
ports:
- "8081:8000" # 8080 대신 8081 사용PermissionError: [Errno 13] Permission denied
해결방법:
# 폴더 권한 수정
sudo chown -R $(id -u):$(id -g) .해결방법:
# 캐시 없이 다시 빌드
docker-compose build --no-cache
# 이전 이미지 삭제
docker-compose down --rmi all
docker-compose up -d --build해결방법:
# 볼륨 삭제 후 재시작 (데이터 삭제됨!)
docker-compose down -v
docker-compose up -dsqlalchemy.exc.OperationalError: could not connect to server
해결방법:
# PostgreSQL 상태 확인
# macOS
brew services list
brew services restart postgresql@14
# Ubuntu
sudo systemctl status postgresql
sudo systemctl restart postgresql
# 연결 테스트
psql -U postgres -d housing_db -c "SELECT 1;"ERROR: extension "vector" is not available
해결방법:
# macOS
brew install pgvector
# Ubuntu
sudo apt-get install postgresql-14-pgvector
# PostgreSQL에서 확장 활성화
psql housing_db -c "CREATE EXTENSION IF NOT EXISTS vector;"redis.exceptions.ConnectionError: Error connecting to Redis
해결방법:
# Redis 상태 확인
redis-cli ping
# 응답: PONG
# Redis 재시작
# macOS
brew services restart redis
# Ubuntu
sudo systemctl restart redisopenai.error.AuthenticationError: Incorrect API key provided
해결방법:
.env파일의UPSTAGE_API_KEY확인- API 키가 유효한지 확인
- API 사용량 초과 여부 확인
해결방법:
# 캐시 삭제
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -name "*.pyc" -delete
# 서버 재시작
python main.pyERROR: dimension mismatch
해결방법:
-- 테이블 재생성 (데이터 삭제됨!)
DROP TABLE announcement_program_embeddings;
CREATE TABLE announcement_program_embeddings (
announcement_id SERIAL PRIMARY KEY,
embedding_text TEXT NOT NULL,
embedding_vector vector(4096), -- Upstage는 4096 차원
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);프로젝트의 상세한 가이드:
- 빠른 시작 가이드: 5분 안에 실행하기
- Docker 설정 가이드: Docker 상세 설명 및 문제 해결
- API 사용 가이드: API 상세 설명 및 예제
- 프로젝트 구조: 디렉토리 구조 및 파일 설명
.env파일은 절대 Git에 커밋하지 마세요 (.gitignore에 포함됨)- 프로덕션 환경에서는
API_ALLOW_ORIGINS를 특정 도메인으로 제한하세요 - PostgreSQL과 Redis는 방화벽으로 보호하세요
- API 키는 환경 변수로만 관리하세요
이슈 및 풀 리퀘스트는 언제나 환영합니다!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Commit Message') - Push to the Branch (`git push origin feature/)
- Open a Pull Request
문제가 있거나 질문이 있으시면 이슈를 등록해주세요.
MIT License