Custom-TAλ κ°μ μλ£λ₯Ό κΈ°λ°μΌλ‘ νμ μ§λ¬Έμ λ΅λ³νκ³ , νμ΅ λ°μ΄ν°λ₯Ό λΆμν΄ κ΅κ°μ¬μκ² κ°μ μ μμ μ 곡νλ RAG κΈ°λ° AI μ‘°κ΅ λ°±μλμ λλ€. FastAPI, SQLAlchemy, TiDB, AWS S3, Gemini APIλ₯Ό μ¬μ©ν΄ ꡬννμ΅λλ€.
- κ΅κ°μ¬κ° μ λ‘λν PDF κ°μ μλ£λ₯Ό λΆμν΄ νμ μ§λ¬Έμ κ·Όκ±° μλ λ΅λ³ μ 곡
- λ΅λ³μ μ¬μ©λ κ°μ μλ£μ νμ΄μ§ μΆμ²λ₯Ό ν¨κ» μ μ₯
- νμμ μ§λ¬Έ ν€μλ, νμ€νΈ μ€λ΅, μ·¨μ½ κ°λ μ λμ λΆμ
- κ΅κ°μ¬ λμ보λμ 보좩 νμ€νΈ, λκΈ°λΆμ¬ λ©μμ§, μΆκ° μλ£ μ λ‘λ μ μ μ 곡
- μΆν λͺ¨λΈκ³Ό μ μ₯μλ₯Ό κ΅μ²΄νκΈ° μ½λλ‘ AI, Storage, DB κ³μΈ΅μ λΆλ¦¬
λ³Έ νλ‘μ νΈλ AIμμ κΈ°ν, μ€κ³, ꡬν νμ κ³Όμ μ λ¬Έμλ‘ λ¨κ²Όμ΅λλ€.
docs/AI_COLLABORATION_GUIDE.md: AI κ°λ° μμ΄μ νΈ νμ μ§μΉ¨μdocs/PRODUCT_REQUIREMENTS.md: μ ν κΈ°ν λ° κΈ°λ₯ μ μμdocs/AI_DEVELOPMENT_LOG.md: AI νμ κ°λ° λ‘κ·Έ
| μμ | μ¬μ© κΈ°μ |
|---|---|
| Backend | FastAPI, Python 3.10+ |
| ORM | SQLAlchemy 2.0 Async |
| Database | TiDB, MySQL νΈν |
| LLM | Gemini 2.5 Flash-Lite |
| Embedding | Gemini Embedding 001 |
| File Storage | AWS S3, Presigned URL |
| RAG μ μ₯ | TiDB document_chunks.embedding_json |
| Auth | JWT Bearer Token |
νμ¬ RAGλ Pinecone μμ΄ S3, TiDB, Geminiλ§μΌλ‘ λμν©λλ€.
PDF μ
λ‘λ
β AWS S3μ μλ³Έ PDF μ μ₯
β S3μμ PDF μ½κΈ°
β νμ΄μ§ ν
μ€νΈ μΆμΆ
β ν
μ€νΈ chunk μμ±
β Gemini EmbeddingμΌλ‘ chunk λ²‘ν° μμ±
β TiDB document_chunks.embedding_jsonμ μ μ₯
νμ μ§λ¬Έ
β μ§λ¬Έμ Gemini EmbeddingμΌλ‘ 벑ν°ν
β TiDBμμ ν΄λΉ κ°μ chunk embedding μ‘°ν
β Python cosine similarityλ‘ Top-K κ²μ
β κ²μλ κ°μ μλ£λ₯Ό Gemini 2.5 Flash-Liteμ μ λ¬
β λ΅λ³κ³Ό μΆμ²λ₯Ό chat_messages, chat_message_sourcesμ μ μ₯
Back/
|-- webapp/ # FastAPI νν κ³μΈ΅
| |-- main.py # μ± μ΄κΈ°ν, CORS, μμΈ μ²λ¦¬
| `-- routers/ # API λΌμ°ν°
| |-- auth.py
| |-- courses.py
| |-- documents.py
| |-- chat.py
| |-- quests.py
| |-- dashboard.py
| |-- interventions.py
| |-- course_messages.py
| `-- router.py
|-- src/ # λλ©μΈ λ° λΉμ¦λμ€ λ‘μ§
| |-- ai/ # Gemini νΈμΆ, embedding, λ΅λ³ μμ±
| |-- auth/ # μ¬μ©μ λͺ¨λΈ
| |-- courses/ # κ°μ λͺ¨λΈ
| |-- enrollments/ # μκ° λ±λ‘, μ·¨μ½ κ°λ
| |-- documents/ # S3 μ μ₯, PDF μ²λ¦¬, RAG μμΈ
| |-- chat/ # μ±ν
λͺ¨λΈ
| |-- quests/ # νμ€νΈ, λ¬Έν, μ±μ
| |-- interventions/ # AI μλ κ°μ
μ μ
| |-- analytics/ # ν€μλ/μ·¨μ½ κ°λ
λΆμ
| `-- models/ # κ³΅ν΅ enum export
|-- dependencies/ # μΈμ¦/μμ‘΄μ± μ£Όμ
|-- database/ # Async DB engine/session
|-- core/ # νκ²½ μ€μ , 보μ, μ΄λ²€νΈ 루ν
|-- requirements.txt
|-- run_server.py
|-- .env.example
`-- README.md
- νμκ°μ , λ‘κ·ΈμΈ, λ‘κ·Έμμ
- JWT κΈ°λ° μΈμ¦
- νμκ³Ό κ΅κ°μ¬ μν λΆλ¦¬
- κ΅κ°μ¬ κ°μ μμ±
- νμ μ μ₯ μ½λ κΈ°λ° κ°μ μ°Έμ¬
- κ΅κ°μ¬/νμλ³ κ°μ λͺ©λ‘ μ‘°ν
- PDF μ λ‘λ
- AWS S3 μ μ₯
- Presigned URL κΈ°λ° λ€μ΄λ‘λ
- μ£Όμ°¨/μ£Όμ λ©νλ°μ΄ν° μ€μ
- 곡κ°/λΉκ³΅κ° μ ν
- νμμ 곡κ°λ μλ£λ§ μ‘°ν κ°λ₯
- νμ μ§λ¬Έ μ μ₯
- κ°μλ³ μλ£ chunk κ²μ
- Gemini λ΅λ³ μμ±
- λ΅λ³ μΆμ² μ μ₯
- μ§λ¬Έ ν€μλ ν΅κ³ λμ
- νμλ³ μ·¨μ½ κ°λ κ°±μ
- κ΅κ°μ¬ μ§μ νμ€νΈ μμ±
- κ°κ΄μ λ¬Έν/보기/μ λ΅ μ μ₯
- μμ μ μ₯κ³Ό λ°μ‘ λΆλ¦¬
- νμ μ μΆ λ° μλ μ±μ
- XP μ§κΈ λ° λ±κΈ κ°±μ
- AI νμ€νΈ μ΄μ μμ±
- μλ£ νμ€νΈ
completedμν λ°ν λ° μ€λ³΅ μ μΆ μ°¨λ¨
- μ κ· μκ°μμ Cλ±κΈ, 0 XPμμ μμ
- Cλ±κΈ: 0~599 λμ XP
- Bλ±κΈ: 600~1199 λμ XP
- Aλ±κΈ: 1200 XP μ΄μμ΄λ©° μ΅κ³ λ±κΈ
GET /courses/{course_id}/me/statsλ λ±κΈ λ΄ μ§ν XPμ λμ XPλ₯Ό ν¨κ» λ°ν- νμ€νΈ μ€λ΅κ³Ό μ±ν κΈ°λ° μ·¨μ½ κ°λ μ AI μ€λ΅λ ΈνΈλ‘ μ‘°ν
- μ€λ΅λ ΈνΈλ λ¬Έν, λ΄ λ΅, μ λ΅, ν΄μ€, κ΄λ ¨ νμ€νΈ λλ μλ£ μΆμ²λ₯Ό ν¬ν¨
κ΅κ°μ¬μ© λμ보λμμ λ€μ μΈ κ°μ§ μ νμ κ°μ μ μμ μ 곡ν©λλ€.
- μ·¨μ½ κ°λ 보좩 νμ€νΈ λ°μ‘
- Cλ±κΈ λλ μ μ°Έμ¬ νμ λμ λκΈ°λΆμ¬ λ©μμ§
- λ°λ³΅ μ§λ¬Έ ν€μλ κΈ°λ° μΆκ° μλ£ μ λ‘λ κΆκ³
κ° μ μμ μ±ν μ§λ¬Έ ν€μλ, νμ€νΈ μ°Έμ¬μ¨, μ€λ΅λ₯ , μ·¨μ½ κ°λ , μλ£ μ€λΉ μνλ₯Ό μ’ ν©ν΄ νλ¨ν©λλ€.
νλ‘ νΈμλ κ³μ½μ λ§μΆ° /api/v1 prefix μμ΄ μ 곡ν©λλ€.
POST /auth/signupPOST /auth/loginPOST /auth/logoutGET /auth/me
GET /courses/meGET /courses/{course_id}POST /coursesPOST /courses/joinDELETE /courses/{course_id}
GET /courses/{course_id}/filesPOST /courses/{course_id}/filesPATCH /courses/{course_id}/files/{file_id}PATCH /courses/{course_id}/files/{file_id}/publishDELETE /courses/{course_id}/files/{file_id}
GET /courses/{course_id}/chatPOST /courses/{course_id}/chatPOST /courses/{course_id}/chat/stream
GET /courses/{course_id}/questsGET /courses/{course_id}/quests/{quest_id}/contentPOST /courses/{course_id}/questsPUT /courses/{course_id}/quests/{quest_id}POST /courses/{course_id}/quests/{quest_id}/sendDELETE /courses/{course_id}/quests/{quest_id}POST /courses/{course_id}/quests/{quest_id}/submitPOST /courses/{course_id}/quests/ai-draft
GET /courses/{course_id}/analyticsGET /courses/{course_id}/analytics/keywordsGET /courses/{course_id}/analytics/studentsGET /courses/{course_id}/ai-proposalsGET /courses/{course_id}/ai-configPUT /courses/{course_id}/ai-configGET /courses/{course_id}/me/statsGET /courses/{course_id}/me/weak-pointsGET /courses/{course_id}/notificationsPATCH /courses/{course_id}/notifications/{notification_id}/readPATCH /courses/{course_id}/notifications/read-allPOST /courses/{course_id}/quiz/submit
μ€μ κ°μ .envμ μμ±ν©λλ€. .envλ Gitμ μ¬λ¦¬μ§ μμ΅λλ€.
DATABASE_URL=mysql+aiomysql://<DB_USER>:<DB_PASSWORD>@<DB_HOST>:4000/<DB_NAME>
DATABASE_SSL=true
JWT_SECRET_KEY=<JWT_SECRET>
STORAGE_PROVIDER=s3
AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY_ID>
AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY>
AWS_REGION=ap-northeast-2
S3_BUCKET_NAME=<S3_BUCKET_NAME>
S3_PREFIX=uploads
S3_PRESIGNED_URL_EXPIRES=3600
AI_PROVIDER=gemini
AI_MAX_OUTPUT_TOKENS=1024
GEMINI_API_KEY=<GEMINI_API_KEY>
GEMINI_MODEL=gemini-2.5-flash-lite
EMBEDDING_PROVIDER=gemini
GEMINI_EMBEDDING_MODEL=gemini-embedding-001
GEMINI_EMBEDDING_OUTPUT_DIMENSIONALITY=768
LOCAL_RAG_ENABLED=true
RAG_CHUNK_SIZE=1200
RAG_CHUNK_OVERLAP=200
RAG_TOP_K=5Windows νκ²½μμλ uvicornμ μ§μ μ€ννμ§ μκ³ run_server.pyλ₯Ό μ¬μ©ν©λλ€. TiDB μ°κ²° μ Windows Proactor event loop λ¬Έμ λ₯Ό νΌνκΈ° μν΄ Selector loopλ₯Ό μ¬μ©ν©λλ€.
cd Back
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
python run_server.pyμλ² μ€ν ν νμΈ:
http://127.0.0.1:8000/docs
http://127.0.0.1:8000/health
http://127.0.0.1:8000/health/db
κΈ°λ³Έ 17κ° ν μ΄λΈ μΈμ νμ¬ λ°±μλ κΈ°λ₯μ μν΄ λ€μ 컬λΌκ³Ό 보쑰 ν μ΄λΈμ΄ νμν©λλ€.
ALTER TABLE enrollments
MODIFY current_rank ENUM('A', 'B', 'C') DEFAULT 'C';
ALTER TABLE course_documents
ADD COLUMN topic VARCHAR(255) NULL,
ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE document_chunks
ADD COLUMN embedding_json LONGTEXT NULL,
ADD COLUMN embedding_model VARCHAR(100) NULL,
ADD COLUMN embedding_dim INT NULL;
CREATE INDEX idx_course_documents_course_id_deleted
ON course_documents(course_id, deleted_at);
CREATE TABLE IF NOT EXISTS notification_reads (
notification_read_id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
notification_key VARCHAR(64) NOT NULL,
read_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_notification_read_user_course_key (
user_id,
course_id,
notification_key
),
KEY idx_notification_reads_user_course (user_id, course_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
CREATE TABLE IF NOT EXISTS quiz_attempts (
quiz_attempt_id BIGINT AUTO_INCREMENT PRIMARY KEY,
course_id BIGINT NOT NULL,
enrollment_id BIGINT NOT NULL,
message_key VARCHAR(64) NOT NULL,
selected_index INT NOT NULL,
is_correct BOOLEAN NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_quiz_attempt_enrollment_course_message (
enrollment_id,
course_id,
message_key
),
KEY idx_quiz_attempts_course_enrollment (course_id, enrollment_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id),
FOREIGN KEY (enrollment_id) REFERENCES enrollments(enrollment_id)
);notification_readsμ quiz_attemptsλ μ± μμ μ μμΌλ©΄ μλ μμ±λ©λλ€.
.envλ Gitμ ν¬ν¨νμ§ μμ΅λλ€.- AWS/Gemini/DB ν€λ μλ² νκ²½ λ³μλ‘λ§ κ΄λ¦¬ν©λλ€.
- S3 λ²ν·μ Public Access Blockμ μ μ§ν©λλ€.
- νμΌ λ€μ΄λ‘λλ S3 presigned URLλ‘λ§ μ 곡ν©λλ€.
- Presigned URL κΈ°λ³Έ λ§λ£ μκ°μ 1μκ°μ λλ€.
- νμμ λ³ΈμΈμ΄ μκ° μ€μΈ κ°μμ κ³΅κ° μλ£μ λ°μ‘λ νμ€νΈλ§ μ κ·Όν μ μμ΅λλ€.
- Gemini ν μ€νΈ μμ±
- Gemini embedding 768μ°¨μ μμ±
- S3 μ λ‘λ, presigned λ€μ΄λ‘λ, μμ
- PDF νκΈ νμΌλͺ μ λ‘λ
- S3 PDF κΈ°λ° RAG μμΈ
- TiDB
document_chunks.embedding_jsonμ μ₯ - RAG μ±ν λ΅λ³ λ° μΆμ² λ°ν
- νμ€νΈ μμ±, μμ , λ°μ‘, μ μΆ, μ±μ
- μ κ· μκ°μ Cλ±κΈ μμ λ° XP κΈ°λ° λ±κΈ κ°±μ
- A/B/C λ±κΈ κΈ°μ€ λ° λ±κΈ λ΄ XP μ§νλ λ°ν
- νμ€νΈ μλ£ μν λ°ν λ° μ€λ³΅ μ μΆ λ°©μ§
- μλ¦Ό μ½μ μν DB μ μ₯
- AI μ€λ΅λ ΈνΈ λ¬Έν/λ΄ λ΅/μ λ΅/ν΄μ€ λ°ν
- μ±ν ν΄μ¦ κ²°κ³Ό μ μ₯ λ° ν΅κ³ λ°μ
- AI νμ€νΈ μ΄μ μμ± μ μ£Όμ°¨λ³ μλ£ νν°λ§
- Cloudflare Tunnelμ ν΅ν HTTPS νλ‘ νΈ μ°λ
νμ¬ λ°±μλλ AWS EC2 μΈμ€ν΄μ€μμ FastAPI μ ν리μΌμ΄μ μΌλ‘ μ€νλκ³ μμ΅λλ€. νλ‘ νΈμλλ Vercelμ λ°°ν¬λμ΄ μμΌλ©°, λΈλΌμ°μ μ HTTPS μμ²μ μ²λ¦¬νκΈ° μν΄ Cloudflare Tunnelμ ν΅ν΄ λ°±μλ HTTPS μλν¬μΈνΈλ₯Ό μ 곡ν©λλ€.
Vercel Frontend
https://custom-ta.vercel.app
|
| HTTPS API request
v
Cloudflare Tunnel
https://scheduled-cars-nest-warranty.trycloudflare.com
|
| forwards to
v
AWS EC2 Ubuntu Server
FastAPI / Uvicorn / systemd
http://127.0.0.1:8000
|
+-- TiDB Cloud
+-- AWS S3
+-- Gemini API
FastAPI μλ²λ EC2 λ΄λΆμμ systemd μλΉμ€λ‘ μ€νν©λλ€.
sudo systemctl status custom-ta
sudo systemctl restart custom-ta
journalctl -u custom-ta -fμλΉμ€ μ€ν λͺ λ Ήμ λ€μκ³Ό κ°μ΅λλ€.
/home/ubuntu/Back/.venv/bin/uvicorn webapp.main:app --host 0.0.0.0 --port 8000Cloudflare Tunnelμ EC2 λ΄λΆμ 127.0.0.1:8000μΌλ‘ λ€μ΄μ€λ FastAPI μλ²λ₯Ό μΈλΆ HTTPS μ£Όμλ‘ λ
ΈμΆν©λλ€.
cloudflared tunnel --url http://127.0.0.1:8000ν°λμ tmux μΈμ
μμ μ μ§ν©λλ€.
tmux ls
tmux attach -t tunnelν°λ νλ©΄μμ λΉ μ Έλμ¬ λλ μλ²λ₯Ό λμ§ μκ³ detachν©λλ€.
Ctrl + B
D
Vercel νλ‘ νΈμλλ μλ νκ²½λ³μλ‘ λ°±μλ API μ£Όμλ₯Ό λ°λΌλ΄ λλ€.
VITE_API_BASE_URL=https://scheduled-cars-nest-warranty.trycloudflare.comGitHub main λΈλμΉμ λ³κ²½μ¬νμ pushν λ€, EC2μμ λ€μ λͺ
λ ΉμΌλ‘ μ΅μ μ½λλ₯Ό λ°μν©λλ€.
cd ~/Back
git pull origin main
sudo systemctl restart custom-ta
sudo systemctl status custom-taμ μ λμ νμΈ:
curl https://scheduled-cars-nest-warranty.trycloudflare.com/health- Google Cloud λλ AI Studio μμ° μλ¦Ό μ€μ
- AWS Budget μλ¦Ό μ€μ
- S3 IAM κΆνμ νΉμ λ²ν·μ
uploads/*λ‘ μ ν - Cloudflare quick tunnel λμ named tunnel λλ κ³ μ λλ©μΈ μ μ©
WEEKLY_INTERVENTION_INTERVAL_SECONDS=604800APP_DEBUG=false- μΆ©λΆν κΈ΄
JWT_SECRET_KEYμ¬μ©