Created
July 27, 2025 13:00
-
-
Save hikaMaeng/28c10c6a22460da59b4be6f4d565ab3f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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