Skip to content

Instantly share code, notes, and snippets.

@hikaMaeng
Created July 27, 2025 13:00
Show Gist options
  • Save hikaMaeng/28c10c6a22460da59b4be6f4d565ab3f to your computer and use it in GitHub Desktop.
Save hikaMaeng/28c10c6a22460da59b4be6f4d565ab3f to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic API Manager</title>
<style>
body { font-family: Arial, sans-serif; display: flex; height: 100vh; margin: 0; }
.sidebar { width: 250px; background: #f4f4f4; border-right: 1px solid #ccc; overflow-y: auto; padding: 10px; }
.content { flex: 1; padding: 20px; }
.route-item { padding: 10px; cursor: pointer; border-bottom: 1px solid #ccc; }
.route-item:hover { background: #ddd; }
.form-group { margin-bottom: 10px; }
textarea { width: 100%; height: 400px; font-family: monospace; }
input[type="text"] { width: 100%; padding: 6px; }
button { margin: 5px; padding: 8px 14px; }
.btn-danger { background: #e74c3c; color: white; }
</style>
</head>
<body>
<div class="sidebar">
<h3>Endpoints</h3>
<button onclick="newRoute()" style="width:100%;margin-bottom:10px;">+ Add New</button>
<div id="routes"></div>
</div>
<div class="content">
<h2 id="formTitle">Details</h2>
<div class="form-group">
<label>File Name:</label>
<input type="text" id="fileName">
</div>
<div class="form-group">
<label>Endpoint:</label>
<input type="text" id="endpoint">
</div>
<div class="form-group">
<label>Function Name:</label>
<input type="text" id="funcName">
</div>
<div class="form-group">
<label>File Content:</label>
<textarea id="fileContent"></textarea>
</div>
<div>
<button id="createBtn" onclick="createRoute()" style="display:none;">Create</button>
<button id="updateBtn" onclick="updateRoute()">Update</button>
<button onclick="deleteRoute()" class="btn-danger">Delete</button>
</div>
</div>
<script>
let currentEndpoint = null;
async function loadRoutes() {
const res = await fetch('/routes');
const data = await res.json();
let html = '';
data.forEach(r => {
html += `<div class="route-item" onclick="viewRoute('${r.endpoint}')">${r.endpoint}</div>`;
});
document.getElementById('routes').innerHTML = html;
}
async function viewRoute(endpoint) {
const res = await fetch('/view-route?endpoint=' + encodeURIComponent(endpoint));
const data = await res.json();
document.getElementById('fileName').value = data.file_name;
document.getElementById('endpoint').value = data.endpoint;
document.getElementById('funcName').value = data.function_name;
document.getElementById('fileContent').value = data.file_content;
currentEndpoint = endpoint;
document.getElementById('formTitle').innerText = 'Details';
document.getElementById('createBtn').style.display = 'none';
document.getElementById('updateBtn').style.display = 'inline-block';
}
function newRoute() {
currentEndpoint = null;
document.getElementById('fileName').value = '';
document.getElementById('endpoint').value = '';
document.getElementById('funcName').value = '';
document.getElementById('fileContent').value = '';
document.getElementById('formTitle').innerText = 'Create New Endpoint';
document.getElementById('createBtn').style.display = 'inline-block';
document.getElementById('updateBtn').style.display = 'none';
}
async function createRoute() {
const fileName = document.getElementById('fileName').value;
const endpoint = document.getElementById('endpoint').value;
const funcName = document.getElementById('funcName').value;
const content = document.getElementById('fileContent').value;
if (!fileName || !endpoint || !funcName || !content) {
return alert('All fields are required');
}
const file = new Blob([content], { type: 'text/plain' });
const formData = new FormData();
formData.append('file', file, fileName);
formData.append('name', funcName);
formData.append('endpoint', endpoint);
formData.append('method', 'POST');
const res = await fetch('/upload-endpoint', { method: 'POST', body: formData });
const result = await res.json();
if (result.status === 'added') {
alert('Created successfully');
loadRoutes();
viewRoute(endpoint);
} else {
alert('Failed: ' + (result.error || 'Unknown error'));
}
}
async function updateRoute() {
if (!currentEndpoint) return alert('Select a route first');
const newEndpoint = document.getElementById('endpoint').value;
const newName = document.getElementById('funcName').value;
const content = document.getElementById('fileContent').value;
const file = new Blob([content], { type: 'text/plain' });
const formData = new FormData();
formData.append('endpoint', currentEndpoint);
formData.append('new_endpoint', newEndpoint);
formData.append('new_name', newName);
formData.append('file', file, document.getElementById('fileName').value);
const res = await fetch('/update-endpoint', { method: 'POST', body: formData });
const result = await res.json();
alert('Update: ' + result.status);
loadRoutes();
currentEndpoint = newEndpoint;
}
async function deleteRoute() {
if (!currentEndpoint) return alert('Select a route first');
if (!confirm('Delete this endpoint?')) return;
const res = await fetch('/delete-endpoint?endpoint=' + encodeURIComponent(currentEndpoint), { method: 'DELETE' });
const result = await res.json();
alert('Delete: ' + result.status);
loadRoutes();
newRoute();
}
loadRoutes();
</script>
</body>
</html>
import uvicorn
from fastapi import FastAPI, UploadFile, Form, HTTPException, Query
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi import Request
import shutil
import json
import importlib.util
import os
import subprocess
import sys
app = FastAPI()
templates = Jinja2Templates(directory="templates")
ENDPOINT_DIR = "endpoints"
ROUTES_FILE = "routes.json"
# 라우트 메타데이터 로드
if os.path.exists(ROUTES_FILE):
with open(ROUTES_FILE, "r") as f:
routes = json.load(f)
else:
routes = []
def parse_requirements(file_path: str):
requirements = []
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
in_requirements = False
for line in lines:
stripped = line.strip()
if stripped.startswith("# requirements:"):
in_requirements = True
continue
if in_requirements:
if stripped.startswith("#") and len(stripped) > 1:
pkg = stripped.lstrip("#").strip()
if pkg:
requirements.append(pkg)
else:
break
return requirements
def install_requirements(requirements: list):
if not requirements:
return True, "No requirements"
try:
subprocess.run([sys.executable, "-m", "pip", "install", *requirements], check=True)
return True, "Installed successfully"
except subprocess.CalledProcessError as e:
return False, str(e)
def load_handler(file_path: str, func_name: str):
spec = importlib.util.spec_from_file_location("dynamic_module", file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, func_name):
raise AttributeError(f"Function '{func_name}' not found in {file_path}")
return getattr(module, func_name)
def register_route(route_meta: dict):
handler = load_handler(route_meta["file"], route_meta["name"])
app.add_api_route(route_meta["endpoint"], handler, methods=[route_meta["method"]])
def unregister_route(endpoint: str):
for route in list(app.router.routes):
if getattr(route, "path", None) == endpoint:
app.router.routes.remove(route)
return True
return False
# 서버 시작 시 기존 라우트 복원
for route in routes:
register_route(route)
@app.post("/upload-endpoint")
async def upload_endpoint(file: UploadFile, name: str = Form(...), endpoint: str = Form(...), method: str = Form(...)):
os.makedirs(ENDPOINT_DIR, exist_ok=True)
file_path = os.path.join(ENDPOINT_DIR, file.filename)
# 파일 저장
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# requirements 처리
requirements = parse_requirements(file_path)
success, message = install_requirements(requirements)
if not success:
return {"status": "install_failed", "error": message}
# 라우트 등록
route_meta = {
"name": name,
"endpoint": endpoint,
"method": method.upper(),
"file": file_path
}
register_route(route_meta)
# 메타데이터 저장
routes.append(route_meta)
with open(ROUTES_FILE, "w") as f:
json.dump(routes, f)
return {"status": "added", "route": endpoint}
@app.get("/routes")
async def list_routes():
return routes
@app.get("/view-route")
async def view_route(endpoint: str = Query(...)):
route = next((r for r in routes if r["endpoint"] == endpoint), None)
if not route:
raise HTTPException(status_code=404, detail="Route not found")
file_path = route["file"]
file_name = os.path.basename(file_path)
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
return {
"file_name": file_name,
"file_path": file_path,
"endpoint": route["endpoint"], # ✅ 추가
"function_name": route["name"],
"file_content": content
}
@app.post("/update-endpoint")
async def update_endpoint(
endpoint: str = Form(...), # 기존 엔드포인트
new_endpoint: str = Form(...),
new_name: str = Form(...),
file: UploadFile = None
):
global routes
route = next((r for r in routes if r["endpoint"] == endpoint), None)
if not route:
raise HTTPException(status_code=404, detail="Route not found")
# 기존 라우트 제거
unregister_route(endpoint)
routes = [r for r in routes if r["endpoint"] != endpoint]
# 파일 업데이트
file_path = route["file"]
if file:
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# ✅ requirements 재파싱 및 재설치
requirements = parse_requirements(file_path)
success, message = install_requirements(requirements)
if not success:
return {
"status": "install_failed",
"error": message,
"hint": "Dependencies installation failed. Original route removed."
}
# 새 라우트 등록
new_meta = {
"name": new_name,
"endpoint": new_endpoint,
"method": route["method"], # 기존 method 유지
"file": file_path
}
register_route(new_meta)
# 메타데이터 저장
routes.append(new_meta)
with open(ROUTES_FILE, "w") as f:
json.dump(routes, f)
return {"status": "updated", "endpoint": new_endpoint}
@app.get("/admin", response_class=HTMLResponse)
async def admin_page(request: Request):
return templates.TemplateResponse("admin.html", {"request": request})
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8001)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment