SOY NV AI
commited on
Commit
Β·
ae31891
1
Parent(s):
59f2d9e
Add Hugging Face Spaces deployment support and file public/private feature
Browse files- Add Hugging Face Spaces deployment files (app.py, Dockerfile, README_HF.md)
- Add GitHub Actions workflows for auto-deployment
- Add file public/private feature (is_public field in database)
- Improve Gemini API error handling (429 quota exceeded)
- Update admin pages with responsive design
- Add file upload timeout unlimited support
- .github/workflows/README.md +83 -0
- .github/workflows/deploy-to-hf.yml +71 -0
- .github/workflows/sync_to_hub.yml +13 -0
- .github/workflows/test.yml +59 -0
- Dockerfile +30 -0
- EXAONE_μ€μΉ_κ°μ΄λ.md +1 -0
- EXAONE_μΆκ°_μλ΄.md +1 -0
- HF_UPLOAD_GUIDE.md +144 -0
- HUGGINGFACE_DEPLOY.md +146 -0
- README_HF.md +56 -0
- add_exaone_model.py +1 -0
- app.py +72 -0
- app/database.py +2 -0
- app/gemini_client.py +49 -5
- app/routes.py +64 -12
- download_exaone_model.py +1 -0
- install_exaone_direct.py +1 -0
- install_exaone_simple.py +1 -0
- migrate_add_is_public.py +61 -0
- templates/admin.html +159 -0
- templates/admin_files.html +160 -0
- templates/admin_messages.html +161 -0
- templates/admin_prompts.html +161 -0
- templates/admin_settings.html +159 -0
- templates/admin_webnovels.html +383 -37
- templates/index.html +215 -4
- templates/webnovels.html +161 -1
- upload_to_hf.ps1 +114 -0
.github/workflows/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GitHub Actions μν¬νλ‘μ°
|
| 2 |
+
|
| 3 |
+
μ΄ λλ ν 리μλ GitHub Actions μν¬νλ‘μ° νμΌμ΄ ν¬ν¨λμ΄ μμ΅λλ€.
|
| 4 |
+
|
| 5 |
+
## μν¬νλ‘μ° λͺ©λ‘
|
| 6 |
+
|
| 7 |
+
### deploy-to-hf.yml
|
| 8 |
+
|
| 9 |
+
Hugging Face Spacesμ μλ λ°°ν¬νλ μν¬νλ‘μ°μ
λλ€.
|
| 10 |
+
|
| 11 |
+
#### νΈλ¦¬κ±° 쑰건
|
| 12 |
+
- `main` λλ `master` λΈλμΉμ νΈμλ λ
|
| 13 |
+
- λ€μ νμΌ/λλ ν λ¦¬κ° λ³κ²½λ λ:
|
| 14 |
+
- `app/**`
|
| 15 |
+
- `templates/**`
|
| 16 |
+
- `static/**`
|
| 17 |
+
- `app.py`
|
| 18 |
+
- `Dockerfile`
|
| 19 |
+
- `requirements.txt`
|
| 20 |
+
- `README_HF.md`
|
| 21 |
+
- μλ μ€ν (workflow_dispatch)
|
| 22 |
+
|
| 23 |
+
#### νμν Secrets μ€μ
|
| 24 |
+
|
| 25 |
+
GitHub μ μ₯μμ Settings > Secrets and variables > Actionsμμ λ€μ secretsλ₯Ό μ€μ ν΄μΌ ν©λλ€:
|
| 26 |
+
|
| 27 |
+
1. **HF_USERNAME**: Hugging Face μ¬μ©μλͺ
|
| 28 |
+
2. **HF_SPACE_NAME**: Hugging Face Space μ΄λ¦
|
| 29 |
+
3. **HF_TOKEN**: Hugging Face Access Token
|
| 30 |
+
- [Hugging Face Settings > Access Tokens](https://huggingface.co/settings/tokens)μμ μμ±
|
| 31 |
+
- `write` κΆνμ΄ νμν©λλ€
|
| 32 |
+
|
| 33 |
+
#### μ¬μ© λ°©λ²
|
| 34 |
+
|
| 35 |
+
1. GitHub μ μ₯μμ secrets μ€μ
|
| 36 |
+
2. μ½λλ₯Ό `main` λλ `master` λΈλμΉμ νΈμ
|
| 37 |
+
3. μλμΌλ‘ Hugging Face Spacesμ λ°°ν¬λ¨
|
| 38 |
+
|
| 39 |
+
λλ GitHub Actions νμμ μλμΌλ‘ μ€νν μ μμ΅λλ€.
|
| 40 |
+
|
| 41 |
+
## Secrets μ€μ λ°©λ²
|
| 42 |
+
|
| 43 |
+
### 1. Hugging Face Access Token μμ±
|
| 44 |
+
|
| 45 |
+
1. [Hugging Face Settings](https://huggingface.co/settings/tokens) μ μ
|
| 46 |
+
2. "New token" ν΄λ¦
|
| 47 |
+
3. Token μ΄λ¦ μ
λ ₯ (μ: `github-actions-deploy`)
|
| 48 |
+
4. Token type: **Write** μ ν
|
| 49 |
+
5. "Generate a token" ν΄λ¦
|
| 50 |
+
6. μμ±λ ν ν°μ λ³΅μ¬ (ν λ²λ§ νμλ¨)
|
| 51 |
+
|
| 52 |
+
### 2. GitHub Secrets μ€μ
|
| 53 |
+
|
| 54 |
+
1. GitHub μ μ₯μμ Settings > Secrets and variables > Actions μ μ
|
| 55 |
+
2. "New repository secret" ν΄λ¦
|
| 56 |
+
3. λ€μ secrets μΆκ°:
|
| 57 |
+
|
| 58 |
+
- **Name**: `HF_USERNAME`
|
| 59 |
+
**Value**: Hugging Face μ¬μ©μλͺ
(μ: `your-username`)
|
| 60 |
+
|
| 61 |
+
- **Name**: `HF_SPACE_NAME`
|
| 62 |
+
**Value**: Space μ΄λ¦ (μ: `soy-nv-ai`)
|
| 63 |
+
|
| 64 |
+
- **Name**: `HF_TOKEN`
|
| 65 |
+
**Value**: μμμ μμ±ν Hugging Face Access Token
|
| 66 |
+
|
| 67 |
+
### 3. μν¬νλ‘μ° ν
μ€νΈ
|
| 68 |
+
|
| 69 |
+
1. μ½λ λ³κ²½ ν `main` λΈλμΉμ νΈμ
|
| 70 |
+
2. GitHub Actions νμμ μν¬νλ‘μ° μ€ν νμΈ
|
| 71 |
+
3. Hugging Face Spacesμμ λ°°ν¬ νμΈ
|
| 72 |
+
|
| 73 |
+
## λ¬Έμ ν΄κ²°
|
| 74 |
+
|
| 75 |
+
### λ°°ν¬ μ€ν¨
|
| 76 |
+
- Secretsκ° μ¬λ°λ₯΄κ² μ€μ λμλμ§ νμΈ
|
| 77 |
+
- Hugging Face Tokenμ `write` κΆνμ΄ μλμ§ νμΈ
|
| 78 |
+
- Space μ΄λ¦κ³Ό μ¬μ©μλͺ
μ΄ μ ννμ§ νμΈ
|
| 79 |
+
|
| 80 |
+
### νμΌμ΄ μ
λ‘λλμ§ μμ
|
| 81 |
+
- `.gitignore`μμ ν΄λΉ νμΌμ΄ μ μΈλμ§ μμλμ§ νμΈ
|
| 82 |
+
- μν¬νλ‘μ°μ `paths` νν°μ ν΄λΉ νμΌμ΄ ν¬ν¨λμ΄ μλμ§ νμΈ
|
| 83 |
+
|
.github/workflows/deploy-to-hf.yml
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
- master
|
| 8 |
+
paths:
|
| 9 |
+
- 'app/**'
|
| 10 |
+
- 'templates/**'
|
| 11 |
+
- 'static/**'
|
| 12 |
+
- 'app.py'
|
| 13 |
+
- 'Dockerfile'
|
| 14 |
+
- 'requirements.txt'
|
| 15 |
+
- 'README_HF.md'
|
| 16 |
+
workflow_dispatch: # μλ μ€ν κ°λ₯
|
| 17 |
+
|
| 18 |
+
jobs:
|
| 19 |
+
deploy:
|
| 20 |
+
runs-on: ubuntu-latest
|
| 21 |
+
|
| 22 |
+
steps:
|
| 23 |
+
- name: Checkout repository
|
| 24 |
+
uses: actions/checkout@v3
|
| 25 |
+
with:
|
| 26 |
+
fetch-depth: 0
|
| 27 |
+
|
| 28 |
+
- name: Setup Git
|
| 29 |
+
run: |
|
| 30 |
+
git config --global user.name "GitHub Actions"
|
| 31 |
+
git config --global user.email "actions@github.com"
|
| 32 |
+
|
| 33 |
+
- name: Checkout Hugging Face Space
|
| 34 |
+
uses: actions/checkout@v3
|
| 35 |
+
with:
|
| 36 |
+
repository: ${{ secrets.HF_USERNAME }}/${{ secrets.HF_SPACE_NAME }}
|
| 37 |
+
path: hf-space
|
| 38 |
+
token: ${{ secrets.HF_TOKEN }}
|
| 39 |
+
|
| 40 |
+
- name: Copy files to Hugging Face Space
|
| 41 |
+
run: |
|
| 42 |
+
# νμ νμΌ λ³΅μ¬
|
| 43 |
+
cp app.py hf-space/
|
| 44 |
+
cp Dockerfile hf-space/
|
| 45 |
+
cp requirements.txt hf-space/
|
| 46 |
+
cp README_HF.md hf-space/README.md
|
| 47 |
+
|
| 48 |
+
# λλ ν 리 볡μ¬
|
| 49 |
+
cp -r app hf-space/
|
| 50 |
+
cp -r templates hf-space/
|
| 51 |
+
cp -r static hf-space/
|
| 52 |
+
|
| 53 |
+
# .gitkeep νμΌμ΄ νμν λλ ν 리 νμΈ
|
| 54 |
+
mkdir -p hf-space/instance
|
| 55 |
+
mkdir -p hf-space/uploads
|
| 56 |
+
mkdir -p hf-space/vector_db
|
| 57 |
+
mkdir -p hf-space/knowledge_graphs
|
| 58 |
+
mkdir -p hf-space/logs
|
| 59 |
+
|
| 60 |
+
- name: Commit and push to Hugging Face
|
| 61 |
+
working-directory: hf-space
|
| 62 |
+
run: |
|
| 63 |
+
git add .
|
| 64 |
+
git diff --staged --quiet || git commit -m "Auto-deploy from GitHub Actions - $(date +'%Y-%m-%d %H:%M:%S')"
|
| 65 |
+
git push
|
| 66 |
+
|
| 67 |
+
- name: Deployment Status
|
| 68 |
+
run: |
|
| 69 |
+
echo "β
Deployment completed successfully!"
|
| 70 |
+
echo "Space URL: https://huggingface.co/spaces/${{ secrets.HF_USERNAME }}/${{ secrets.HF_SPACE_NAME }}"
|
| 71 |
+
|
.github/workflows/sync_to_hub.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
jobs:
|
| 2 |
+
sync-to-hub:
|
| 3 |
+
runs-on: ubuntu-latest
|
| 4 |
+
steps:
|
| 5 |
+
- uses: actions/checkout@v3
|
| 6 |
+
with:
|
| 7 |
+
fetch-depth: 0
|
| 8 |
+
lfs: true
|
| 9 |
+
- name: Push to hub
|
| 10 |
+
env:
|
| 11 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 12 |
+
# μλ μ€μμ [μμ΄λ/μ€νμ΄μ€μ΄λ¦] λΆλΆμ μ΄μΈμΈλ μ λ³΄λ‘ κΌ μμ ν΄μ£ΌμΈμ!
|
| 13 |
+
run: git push https://wiizm:$HF_TOKEN@huggingface.co/spaces/wiizm/
|
.github/workflows/test.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Test Application
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
- master
|
| 8 |
+
push:
|
| 9 |
+
branches:
|
| 10 |
+
- main
|
| 11 |
+
- master
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
test:
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
|
| 17 |
+
steps:
|
| 18 |
+
- name: Checkout repository
|
| 19 |
+
uses: actions/checkout@v3
|
| 20 |
+
|
| 21 |
+
- name: Set up Python
|
| 22 |
+
uses: actions/setup-python@v4
|
| 23 |
+
with:
|
| 24 |
+
python-version: '3.11'
|
| 25 |
+
|
| 26 |
+
- name: Install dependencies
|
| 27 |
+
run: |
|
| 28 |
+
python -m pip install --upgrade pip
|
| 29 |
+
pip install -r requirements.txt
|
| 30 |
+
pip install pytest pytest-cov flake8
|
| 31 |
+
|
| 32 |
+
- name: Lint with flake8
|
| 33 |
+
run: |
|
| 34 |
+
# flake8 μ€μΉ λ° μ€ν (μ νμ¬ν)
|
| 35 |
+
# flake8 app --count --select=E9,F63,F7,F82 --show-source --statistics
|
| 36 |
+
# flake8 app --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
| 37 |
+
echo "Linting skipped (flake8 not configured)"
|
| 38 |
+
|
| 39 |
+
- name: Check file structure
|
| 40 |
+
run: |
|
| 41 |
+
echo "Checking required files..."
|
| 42 |
+
test -f app.py && echo "β app.py exists" || echo "β app.py missing"
|
| 43 |
+
test -f Dockerfile && echo "β Dockerfile exists" || echo "β Dockerfile missing"
|
| 44 |
+
test -f requirements.txt && echo "β requirements.txt exists" || echo "β requirements.txt missing"
|
| 45 |
+
test -d app && echo "β app/ directory exists" || echo "β app/ directory missing"
|
| 46 |
+
test -d templates && echo "β templates/ directory exists" || echo "β templates/ directory missing"
|
| 47 |
+
test -d static && echo "β static/ directory exists" || echo "β static/ directory missing"
|
| 48 |
+
|
| 49 |
+
- name: Validate Python syntax
|
| 50 |
+
run: |
|
| 51 |
+
echo "Validating Python syntax..."
|
| 52 |
+
python -m py_compile app.py
|
| 53 |
+
find app -name "*.py" -exec python -m py_compile {} \;
|
| 54 |
+
echo "β All Python files are syntactically valid"
|
| 55 |
+
|
| 56 |
+
- name: Test complete
|
| 57 |
+
run: |
|
| 58 |
+
echo "β
All tests passed!"
|
| 59 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# μμ€ν
ν¨ν€μ§ μ€μΉ
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
git \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Python μμ‘΄μ± μ€μΉ
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# μ ν리μΌμ΄μ
μ½λ 볡μ¬
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
# νμν λλ ν 리 μμ±
|
| 19 |
+
RUN mkdir -p instance uploads vector_db knowledge_graphs logs static templates
|
| 20 |
+
|
| 21 |
+
# ν¬νΈ λ
ΈμΆ (Hugging Face Spacesλ 7860 ν¬νΈ μ¬μ©)
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
|
| 24 |
+
# νκ²½ λ³μ μ€μ
|
| 25 |
+
ENV PORT=7860
|
| 26 |
+
ENV HOST=0.0.0.0
|
| 27 |
+
|
| 28 |
+
# μ ν리μΌμ΄μ
μ€ν
|
| 29 |
+
CMD ["python", "app.py"]
|
| 30 |
+
|
EXAONE_μ€μΉ_κ°μ΄λ.md
CHANGED
|
@@ -167,5 +167,6 @@ tokenizer = AutoTokenizer.from_pretrained("LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct"
|
|
| 167 |
|
| 168 |
|
| 169 |
|
|
|
|
| 170 |
|
| 171 |
|
|
|
|
| 167 |
|
| 168 |
|
| 169 |
|
| 170 |
+
|
| 171 |
|
| 172 |
|
EXAONE_μΆκ°_μλ΄.md
CHANGED
|
@@ -84,5 +84,6 @@ Ollamaλ₯Ό κ±°μΉμ§ μκ³ Pythonμμ μ§μ Hugging Face λͺ¨λΈμ μ¬μ©ν
|
|
| 84 |
|
| 85 |
|
| 86 |
|
|
|
|
| 87 |
|
| 88 |
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
|
| 87 |
+
|
| 88 |
|
| 89 |
|
HF_UPLOAD_GUIDE.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces μ
λ‘λ κ°μ΄λ
|
| 2 |
+
|
| 3 |
+
μ΄ κ°μ΄λλ Hugging Face Spacesμ νμΌμ μ
λ‘λνλ λ°©λ²μ μ€λͺ
ν©λλ€.
|
| 4 |
+
|
| 5 |
+
## λ°©λ² 1: Gitμ μ¬μ©ν μ
λ‘λ (κΆμ₯)
|
| 6 |
+
|
| 7 |
+
### 1. Hugging Face Spaces μμ±
|
| 8 |
+
|
| 9 |
+
1. [Hugging Face Spaces](https://huggingface.co/spaces) μ μ
|
| 10 |
+
2. "Create new Space" ν΄λ¦
|
| 11 |
+
3. μ€μ :
|
| 12 |
+
- **Space name**: μνλ μ΄λ¦
|
| 13 |
+
- **SDK**: Docker
|
| 14 |
+
- **Docker template**: Blank
|
| 15 |
+
- **Hardware**: CPU Basic (λλ νμμ λ°λΌ)
|
| 16 |
+
- **Visibility**: Public λλ Private
|
| 17 |
+
|
| 18 |
+
### 2. Git μ μ₯μ ν΄λ‘
|
| 19 |
+
|
| 20 |
+
Spaces μμ± ν, Hugging Faceμμ μ 곡νλ Git URLμ μ¬μ©νμ¬ ν΄λ‘ :
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
# Hugging Faceμμ μ 곡νλ Git URL μ¬μ©
|
| 24 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 25 |
+
cd YOUR_SPACE_NAME
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### 3. νμν νμΌ λ³΅μ¬
|
| 29 |
+
|
| 30 |
+
λ‘컬 νλ‘μ νΈμμ νμν νμΌλ€μ 볡μ¬:
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
# Windows PowerShellμμ μ€ν
|
| 34 |
+
# νλ‘μ νΈ λ£¨νΈ λλ ν 리μμ μ€ν
|
| 35 |
+
|
| 36 |
+
# νμ νμΌ λ³΅μ¬
|
| 37 |
+
Copy-Item ..\app.py .
|
| 38 |
+
Copy-Item ..\Dockerfile .
|
| 39 |
+
Copy-Item ..\requirements.txt .
|
| 40 |
+
Copy-Item ..\README_HF.md README.md
|
| 41 |
+
|
| 42 |
+
# λλ ν 리 볡μ¬
|
| 43 |
+
Copy-Item -Recurse ..\app .
|
| 44 |
+
Copy-Item -Recurse ..\templates .
|
| 45 |
+
Copy-Item -Recurse ..\static .
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 4. Git μ»€λ° λ° νΈμ
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
git add .
|
| 52 |
+
git commit -m "Initial deployment"
|
| 53 |
+
git push
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## λ°©λ² 2: μΉ μΈν°νμ΄μ€λ₯Ό μ¬μ©ν μ
λ‘λ
|
| 57 |
+
|
| 58 |
+
1. Hugging Face Spaces νμ΄μ§ μ μ
|
| 59 |
+
2. "Files and versions" ν ν΄λ¦
|
| 60 |
+
3. "Add file" > "Upload files" ν΄λ¦
|
| 61 |
+
4. λ€μ νμΌλ€μ λλκ·Έ μ€ λλ‘μΌλ‘ μ
λ‘λ:
|
| 62 |
+
|
| 63 |
+
### νμ νμΌ λͺ©λ‘
|
| 64 |
+
|
| 65 |
+
#### λ£¨νΈ λλ ν 리 νμΌ
|
| 66 |
+
- `app.py`
|
| 67 |
+
- `Dockerfile`
|
| 68 |
+
- `requirements.txt`
|
| 69 |
+
- `README.md` (README_HF.mdμ λ΄μ© μ¬μ©)
|
| 70 |
+
|
| 71 |
+
#### λλ ν 리 (ν΄λ μ 체 μ
λ‘λ)
|
| 72 |
+
- `app/` (μ 체 λλ ν 리)
|
| 73 |
+
- `templates/` (μ 체 λλ ν 리)
|
| 74 |
+
- `static/` (μ 체 λλ ν 리)
|
| 75 |
+
|
| 76 |
+
## μ
λ‘λνμ§ λ§μμΌ ν νμΌ/ν΄λ
|
| 77 |
+
|
| 78 |
+
λ€μ νλͺ©μ `.gitignore`μ ν¬ν¨λμ΄ μμΌλ―λ‘ μ
λ‘λνμ§ λ§μΈμ:
|
| 79 |
+
|
| 80 |
+
- `venv/` - κ°μ νκ²½
|
| 81 |
+
- `instance/` - λ°μ΄ν°λ² μ΄μ€ νμΌ
|
| 82 |
+
- `uploads/` - μ
λ‘λλ νμΌ
|
| 83 |
+
- `vector_db/` - λ²‘ν° λ°μ΄ν°λ² μ΄μ€
|
| 84 |
+
- `logs/` - λ‘κ·Έ νμΌ
|
| 85 |
+
- `*.pyc`, `__pycache__/` - Python μΊμ
|
| 86 |
+
- `.env` - νκ²½ λ³μ νμΌ (λ―Όκ° μ 보)
|
| 87 |
+
- `.git/` - Git μ μ₯μ
|
| 88 |
+
|
| 89 |
+
## νκ²½ λ³μ μ€μ
|
| 90 |
+
|
| 91 |
+
νμΌ μ
λ‘λ ν, Settings > Repository secretsμμ νκ²½ λ³μ μ€μ :
|
| 92 |
+
|
| 93 |
+
### νμ
|
| 94 |
+
```
|
| 95 |
+
SECRET_KEY=your-random-secret-key-here
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### μ νμ¬ν
|
| 99 |
+
```
|
| 100 |
+
GEMINI_API_KEY=your-gemini-api-key
|
| 101 |
+
DATABASE_URL=sqlite:///instance/finance_analysis.db
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## λ°°ν¬ νμΈ
|
| 105 |
+
|
| 106 |
+
1. Spaces νμ΄μ§μμ "Logs" ν νμΈ
|
| 107 |
+
2. λΉλκ° μλ£λλ©΄ μ ν리μΌμ΄μ
μ μ ν
μ€νΈ
|
| 108 |
+
3. μ€λ₯κ° μμΌλ©΄ λ‘κ·Έ νμΈ
|
| 109 |
+
|
| 110 |
+
## λΉ λ₯Έ μ
λ‘λ μ€ν¬λ¦½νΈ (Windows PowerShell)
|
| 111 |
+
|
| 112 |
+
νλ‘μ νΈ λ£¨νΈ λλ ν 리μμ μ€ν:
|
| 113 |
+
|
| 114 |
+
```powershell
|
| 115 |
+
# Hugging Face Spaces λλ ν 리 κ²½λ‘ μ€μ
|
| 116 |
+
$HF_SPACE_DIR = "C:\path\to\huggingface\space"
|
| 117 |
+
|
| 118 |
+
# νμ νμΌ λ³΅μ¬
|
| 119 |
+
Copy-Item app.py $HF_SPACE_DIR\
|
| 120 |
+
Copy-Item Dockerfile $HF_SPACE_DIR\
|
| 121 |
+
Copy-Item requirements.txt $HF_SPACE_DIR\
|
| 122 |
+
Copy-Item README_HF.md "$HF_SPACE_DIR\README.md"
|
| 123 |
+
|
| 124 |
+
# λλ ν 리 볡μ¬
|
| 125 |
+
Copy-Item -Recurse app "$HF_SPACE_DIR\app"
|
| 126 |
+
Copy-Item -Recurse templates "$HF_SPACE_DIR\templates"
|
| 127 |
+
Copy-Item -Recurse static "$HF_SPACE_DIR\static"
|
| 128 |
+
|
| 129 |
+
Write-Host "νμΌ λ³΅μ¬ μλ£!"
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## λ¬Έμ ν΄κ²°
|
| 133 |
+
|
| 134 |
+
### λΉλ μ€ν¨
|
| 135 |
+
- `requirements.txt`μ ν¨ν€μ§ λ²μ νμΈ
|
| 136 |
+
- λ‘κ·Έμμ μ€λ₯ λ©μμ§ νμΈ
|
| 137 |
+
|
| 138 |
+
### λ°νμ μ€λ₯
|
| 139 |
+
- νκ²½ λ³μκ° μ¬λ°λ₯΄κ² μ€μ λμλμ§ νμΈ
|
| 140 |
+
- `app.py`κ° μ¬λ°λ₯Έ ν¬νΈλ₯Ό μ¬μ©νλμ§ νμΈ (7860)
|
| 141 |
+
|
| 142 |
+
### νμΌ λλ½
|
| 143 |
+
- λͺ¨λ νμ λλ ν 리(`app/`, `templates/`, `static/`)κ° μ
λ‘λλμλμ§ νμΈ
|
| 144 |
+
|
HUGGINGFACE_DEPLOY.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces λ°°ν¬ κ°μ΄λ
|
| 2 |
+
|
| 3 |
+
μ΄ κ°μ΄λλ SOY NV AI μ ν리μΌμ΄μ
μ Hugging Face Spacesμ λ°°ν¬νλ λ°©λ²μ μ€λͺ
ν©λλ€.
|
| 4 |
+
|
| 5 |
+
## μ¬μ μ€λΉ
|
| 6 |
+
|
| 7 |
+
1. Hugging Face κ³μ μμ± λ° λ‘κ·ΈμΈ
|
| 8 |
+
2. Hugging Face Spaces μμ±
|
| 9 |
+
|
| 10 |
+
## λ°°ν¬ λ¨κ³
|
| 11 |
+
|
| 12 |
+
### 1. Hugging Face Spaces μμ±
|
| 13 |
+
|
| 14 |
+
1. [Hugging Face Spaces](https://huggingface.co/spaces)μ μ μ
|
| 15 |
+
2. "Create new Space" ν΄λ¦
|
| 16 |
+
3. λ€μ μ 보 μ
λ ₯:
|
| 17 |
+
- **Space name**: μνλ μ΄λ¦ (μ: `soy-nv-ai`)
|
| 18 |
+
- **SDK**: `Docker` μ ν
|
| 19 |
+
- **Docker template**: **Blank** μ ν (λλ λΉμλκΈ°) β
|
| 20 |
+
- μ΄λ―Έ `Dockerfile`μ μμ±νμΌλ―λ‘ blank ν
νλ¦Ώμ μ ννμΈμ
|
| 21 |
+
- **Hardware**: νμμ λ°λΌ μ ν (CPU κΈ°λ³Έ, GPU νμ μ μ ν)
|
| 22 |
+
- **Visibility**: Public λλ Private μ ν
|
| 23 |
+
|
| 24 |
+
### 2. νμΌ μ
λ‘λ
|
| 25 |
+
|
| 26 |
+
λ€μ νμΌλ€μ Hugging Face Spacesμ μ
λ‘λν΄μΌ ν©λλ€:
|
| 27 |
+
|
| 28 |
+
#### νμ νμΌ
|
| 29 |
+
- `app.py` - Hugging Face Spaces μ§μ
μ
|
| 30 |
+
- `requirements.txt` - Python ν¨ν€μ§ μμ‘΄μ±
|
| 31 |
+
- `README.md` - νλ‘μ νΈ μ€λͺ
|
| 32 |
+
- `app/` - μ ν리μΌμ΄μ
μ½λ λλ ν 리
|
| 33 |
+
- `templates/` - HTML ν
νλ¦Ώ λλ ν 리
|
| 34 |
+
- `static/` - μ μ νμΌ λλ ν 리
|
| 35 |
+
|
| 36 |
+
#### μ νμ νμΌ
|
| 37 |
+
- `.gitignore` - Git 무μ νμΌ
|
| 38 |
+
- `.env.example` - νκ²½ λ³μ μμ (μ€μ `.env`λ μ
λ‘λνμ§ μμ)
|
| 39 |
+
|
| 40 |
+
### 3. νκ²½ λ³μ μ€μ
|
| 41 |
+
|
| 42 |
+
Hugging Face Spacesμ Settings > Repository secretsμμ λ€μ νκ²½ λ³μλ₯Ό μ€μ νμΈμ:
|
| 43 |
+
|
| 44 |
+
#### νμ νκ²½ λ³μ
|
| 45 |
+
- `SECRET_KEY`: Flask μν¬λ¦Ώ ν€ (λλ€ λ¬Έμμ΄ μμ±)
|
| 46 |
+
- `GEMINI_API_KEY`: Google Gemini API ν€ (Gemini μ¬μ© μ)
|
| 47 |
+
|
| 48 |
+
#### μ νμ νκ²½ λ³μ
|
| 49 |
+
- `DATABASE_URL`: λ°μ΄ν°λ² μ΄μ€ URL (κΈ°λ³Έκ°: SQLite μ¬μ©)
|
| 50 |
+
- `OLLAMA_BASE_URL`: Ollama μλ² URL (κΈ°λ³Έκ°: http://localhost:11434)
|
| 51 |
+
- `EMBEDDING_MODEL_NAME`: μλ² λ© λͺ¨λΈ μ΄λ¦
|
| 52 |
+
- `RERANKER_MODEL_NAME`: 리λ컀 λͺ¨λΈ μ΄λ¦
|
| 53 |
+
|
| 54 |
+
### 4. Dockerfile νμΈ
|
| 55 |
+
|
| 56 |
+
β
**μ΄λ―Έ μμ±λ¨**: νλ‘μ νΈ λ£¨νΈμ `Dockerfile`μ΄ μ΄λ―Έ μμ±λμ΄ μμ΅λλ€.
|
| 57 |
+
|
| 58 |
+
νμν κ²½μ° `Dockerfile`μ νμΈνκ±°λ μμ ν μ μμ΅λλ€.
|
| 59 |
+
|
| 60 |
+
### 5. README.md μμ±
|
| 61 |
+
|
| 62 |
+
Hugging Face Spacesμ© README.mdλ₯Ό μμ±νμΈμ:
|
| 63 |
+
|
| 64 |
+
```markdown
|
| 65 |
+
---
|
| 66 |
+
title: SOY NV AI
|
| 67 |
+
emoji: π
|
| 68 |
+
colorFrom: blue
|
| 69 |
+
colorTo: purple
|
| 70 |
+
sdk: docker
|
| 71 |
+
pinned: false
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
# SOY NV AI - μΉμμ€ μν κ°λ° μ΄μμ€ν΄νΈ
|
| 75 |
+
|
| 76 |
+
AIλ₯Ό νμ©ν μΉμμ€ μν κ°λ°μ μ§μνλ μ΄μμ€ν΄νΈμ
λλ€.
|
| 77 |
+
|
| 78 |
+
## κΈ°λ₯
|
| 79 |
+
|
| 80 |
+
- μΉμμ€ νμΌ μ
λ‘λ λ° λΆμ
|
| 81 |
+
- AI κΈ°λ° μν λΆμ λ° μμ½
|
| 82 |
+
- μΊλ¦ν° κ΄κ³ κ·Έλν μΆμΆ
|
| 83 |
+
- νμ°¨λ³ λΆμ
|
| 84 |
+
|
| 85 |
+
## μ¬μ© λ°©λ²
|
| 86 |
+
|
| 87 |
+
1. λ‘κ·ΈμΈ λλ νμκ°μ
|
| 88 |
+
2. μΉμμ€ νμΌ μ
λ‘λ
|
| 89 |
+
3. AI λΆμ μ€ν
|
| 90 |
+
4. κ²°κ³Ό νμΈ
|
| 91 |
+
|
| 92 |
+
## νκ²½ λ³μ
|
| 93 |
+
|
| 94 |
+
λ€μ νκ²½ λ³μλ₯Ό μ€μ ν΄μΌ ν©λλ€:
|
| 95 |
+
|
| 96 |
+
- `SECRET_KEY`: Flask μν¬λ¦Ώ ν€
|
| 97 |
+
- `GEMINI_API_KEY`: Google Gemini API ν€ (μ νμ¬ν)
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## μ£Όμμ¬ν
|
| 101 |
+
|
| 102 |
+
### 1. λ°μ΄ν°λ² μ΄μ€
|
| 103 |
+
|
| 104 |
+
- Hugging Face Spacesλ μμ μ μ₯μμ΄λ―λ‘, λ°μ΄ν°λ² μ΄μ€λ μꡬ μ μ₯μκ° μλλλ€.
|
| 105 |
+
- μ€μν λ°μ΄ν°λ μΈλΆ λ°μ΄ν°λ² μ΄μ€(PostgreSQL, MySQL λ±)λ₯Ό μ¬μ©νκ±°λ μ κΈ°μ μΌλ‘ λ°±μ
νμΈμ.
|
| 106 |
+
|
| 107 |
+
### 2. νμΌ μ
λ‘λ
|
| 108 |
+
|
| 109 |
+
- μ
λ‘λλ νμΌμ Spaces μ¬μμ μ μμ λ μ μμ΅λλ€.
|
| 110 |
+
- μꡬ μ μ₯μ΄ νμν κ²½μ° μΈλΆ μ€ν 리μ§(S3, Google Cloud Storage λ±)λ₯Ό μ¬μ©νμΈμ.
|
| 111 |
+
|
| 112 |
+
### 3. 리μμ€ μ ν
|
| 113 |
+
|
| 114 |
+
- Hugging Face Spacesλ λ¬΄λ£ ν°μ΄μμ 리μμ€ μ νμ΄ μμ΅λλ€.
|
| 115 |
+
- λμ©λ νμΌ μ²λ¦¬λ κΈ΄ μμ
μ νμμμλ μ μμ΅λλ€.
|
| 116 |
+
|
| 117 |
+
### 4. Ollama μ¬μ©
|
| 118 |
+
|
| 119 |
+
- Ollamaλ λ‘컬 μλ²μ΄λ―λ‘ Hugging Face Spacesμμ μ§μ μ¬μ©ν μ μμ΅λλ€.
|
| 120 |
+
- μΈλΆ Ollama μλ²λ₯Ό μ¬μ©νκ±°λ, Hugging Faceμ λͺ¨λΈ APIλ₯Ό μ¬μ©νμΈμ.
|
| 121 |
+
|
| 122 |
+
## λ°°ν¬ ν νμΈ
|
| 123 |
+
|
| 124 |
+
1. Spaces νμ΄μ§μμ "Logs" νμ νμΈνμ¬ μ€λ₯κ° μλμ§ νμΈ
|
| 125 |
+
2. μ ν리μΌμ΄μ
μ΄ μ μμ μΌλ‘ λ‘λλλμ§ νμΈ
|
| 126 |
+
3. λ‘κ·ΈμΈ λ° κΈ°λ³Έ κΈ°λ₯ ν
μ€νΈ
|
| 127 |
+
|
| 128 |
+
## λ¬Έμ ν΄κ²°
|
| 129 |
+
|
| 130 |
+
### λΉλ μ€ν¨
|
| 131 |
+
- `requirements.txt`μ ν¨ν€μ§ λ²μ νμΈ
|
| 132 |
+
- λ‘κ·Έμμ μ€λ₯ λ©μμ§ νμΈ
|
| 133 |
+
|
| 134 |
+
### λ°νμ μ€λ₯
|
| 135 |
+
- νκ²½ λ³μκ° μ¬λ°λ₯΄κ² μ€μ λμλμ§ νμΈ
|
| 136 |
+
- λ°μ΄ν°λ² μ΄μ€ κ²½λ‘ λ° κΆν νμΈ
|
| 137 |
+
|
| 138 |
+
### μ±λ₯ λ¬Έμ
|
| 139 |
+
- λ λμ νλμ¨μ΄ ν°μ΄λ‘ μ
κ·Έλ μ΄λ κ³ λ €
|
| 140 |
+
- λΆνμν μμ‘΄μ± μ κ±°
|
| 141 |
+
|
| 142 |
+
## μΆκ° 리μμ€
|
| 143 |
+
|
| 144 |
+
- [Hugging Face Spaces λ¬Έμ](https://huggingface.co/docs/hub/spaces)
|
| 145 |
+
- [Docker λ°°ν¬ κ°μ΄λ](https://huggingface.co/docs/hub/spaces-sdks-docker)
|
| 146 |
+
|
README_HF.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SOY NV AI
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# SOY NV AI - μΉμμ€ μν κ°λ° μ΄μμ€ν΄νΈ
|
| 12 |
+
|
| 13 |
+
AIλ₯Ό νμ©ν μΉμμ€ μν κ°λ°μ μ§μνλ μ΄μμ€ν΄νΈμ
λλ€.
|
| 14 |
+
|
| 15 |
+
## μ£Όμ κΈ°λ₯
|
| 16 |
+
|
| 17 |
+
- π μΉμμ€ νμΌ μ
λ‘λ λ° λΆμ
|
| 18 |
+
- π€ AI κΈ°λ° μν λΆμ λ° μμ½
|
| 19 |
+
- π μΊλ¦ν° κ΄κ³ κ·Έλν μΆμΆ
|
| 20 |
+
- π νμ°¨λ³ μμΈ λΆμ
|
| 21 |
+
- π¬ AI μ±ν
μ΄μμ€ν΄νΈ
|
| 22 |
+
|
| 23 |
+
## μ¬μ© λ°©λ²
|
| 24 |
+
|
| 25 |
+
1. **λ‘κ·ΈμΈ/νμκ°μ
**: μ²μ μ¬μ© μ κ³μ μ μμ±νμΈμ
|
| 26 |
+
2. **νμΌ μ
λ‘λ**: μΉμμ€ νμΌ(.txt, .md)μ μ
λ‘λνμΈμ
|
| 27 |
+
3. **AI λΆμ**: Parent Chunk, Episode Analysis, Graph Extractionμ μ€ννμΈμ
|
| 28 |
+
4. **κ²°κ³Ό νμΈ**: λΆμ κ²°κ³Όλ₯Ό νμΈνκ³ AIμ λννμΈμ
|
| 29 |
+
|
| 30 |
+
## νκ²½ λ³μ μ€μ
|
| 31 |
+
|
| 32 |
+
Settings > Repository secretsμμ λ€μ νκ²½ λ³μλ₯Ό μ€μ νμΈμ:
|
| 33 |
+
|
| 34 |
+
### νμ
|
| 35 |
+
- `SECRET_KEY`: Flask μν¬λ¦Ώ ν€ (λλ€ λ¬Έμμ΄)
|
| 36 |
+
|
| 37 |
+
### μ νμ¬ν
|
| 38 |
+
- `GEMINI_API_KEY`: Google Gemini API ν€
|
| 39 |
+
- `DATABASE_URL`: λ°μ΄ν°λ² μ΄μ€ URL (κΈ°λ³Έ: SQLite)
|
| 40 |
+
- `OLLAMA_BASE_URL`: Ollama μλ² URL
|
| 41 |
+
|
| 42 |
+
## κΈ°μ μ€ν
|
| 43 |
+
|
| 44 |
+
- **Backend**: Flask, SQLAlchemy
|
| 45 |
+
- **AI**: Google Gemini, Ollama
|
| 46 |
+
- **Vector DB**: ChromaDB
|
| 47 |
+
- **Embedding**: Sentence Transformers
|
| 48 |
+
|
| 49 |
+
## μ£Όμμ¬ν
|
| 50 |
+
|
| 51 |
+
β οΈ Hugging Face Spacesλ μμ μ μ₯μμ
λλ€. μ€μν λ°μ΄ν°λ μ κΈ°μ μΌλ‘ λ°±μ
νμΈμ.
|
| 52 |
+
|
| 53 |
+
## λΌμ΄μ μ€
|
| 54 |
+
|
| 55 |
+
MIT License
|
| 56 |
+
|
add_exaone_model.py
CHANGED
|
@@ -162,5 +162,6 @@ if __name__ == "__main__":
|
|
| 162 |
|
| 163 |
|
| 164 |
|
|
|
|
| 165 |
|
| 166 |
|
|
|
|
| 162 |
|
| 163 |
|
| 164 |
|
| 165 |
+
|
| 166 |
|
| 167 |
|
app.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Spacesμ© Flask μ ν리μΌμ΄μ
μ§μ
μ
|
| 3 |
+
"""
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
import logging
|
| 7 |
+
from logging.handlers import RotatingFileHandler
|
| 8 |
+
|
| 9 |
+
# UTF-8 μΈμ½λ© κ°μ μ€μ
|
| 10 |
+
if sys.platform == 'win32':
|
| 11 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 12 |
+
sys.stderr.reconfigure(encoding='utf-8')
|
| 13 |
+
|
| 14 |
+
from app import create_app
|
| 15 |
+
|
| 16 |
+
app = create_app()
|
| 17 |
+
|
| 18 |
+
# Hugging Face Spaces νκ²½ λ³μ μ€μ
|
| 19 |
+
# Spacesλ μλμΌλ‘ ν¬νΈλ₯Ό ν λΉνλ―λ‘ νκ²½ λ³μμμ κ°μ Έμ΄
|
| 20 |
+
port = int(os.environ.get('PORT', 7860))
|
| 21 |
+
host = os.environ.get('HOST', '0.0.0.0')
|
| 22 |
+
|
| 23 |
+
# λ‘κΉ
μ€μ
|
| 24 |
+
if not os.path.exists('logs'):
|
| 25 |
+
os.mkdir('logs')
|
| 26 |
+
|
| 27 |
+
# νμΌ νΈλ€λ¬ μ€μ
|
| 28 |
+
file_handler = RotatingFileHandler('logs/server.log', maxBytes=10240000, backupCount=10)
|
| 29 |
+
file_handler.setFormatter(logging.Formatter(
|
| 30 |
+
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
| 31 |
+
))
|
| 32 |
+
file_handler.setLevel(logging.INFO)
|
| 33 |
+
|
| 34 |
+
# μ½μ νΈλ€λ¬ μ€μ
|
| 35 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 36 |
+
console_handler.setFormatter(logging.Formatter(
|
| 37 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 38 |
+
))
|
| 39 |
+
console_handler.setLevel(logging.INFO)
|
| 40 |
+
|
| 41 |
+
# Flask μ± λ‘κ±° μ€μ
|
| 42 |
+
app.logger.setLevel(logging.INFO)
|
| 43 |
+
app.logger.addHandler(file_handler)
|
| 44 |
+
app.logger.addHandler(console_handler)
|
| 45 |
+
|
| 46 |
+
# λ£¨νΈ λ‘κ±° μ€μ
|
| 47 |
+
root_logger = logging.getLogger()
|
| 48 |
+
root_logger.setLevel(logging.INFO)
|
| 49 |
+
root_logger.addHandler(console_handler)
|
| 50 |
+
|
| 51 |
+
# Werkzeug λ‘κ±° μ€μ
|
| 52 |
+
werkzeug_logger = logging.getLogger('werkzeug')
|
| 53 |
+
werkzeug_logger.setLevel(logging.INFO)
|
| 54 |
+
werkzeug_logger.handlers.clear()
|
| 55 |
+
werkzeug_handler = logging.StreamHandler(sys.stdout)
|
| 56 |
+
werkzeug_handler.setFormatter(logging.Formatter(
|
| 57 |
+
'%(asctime)s - %(levelname)s - %(message)s'
|
| 58 |
+
))
|
| 59 |
+
werkzeug_logger.addHandler(werkzeug_handler)
|
| 60 |
+
|
| 61 |
+
app.logger.info(f'μλ² μμ - Host: {host}, Port: {port}')
|
| 62 |
+
|
| 63 |
+
if __name__ == '__main__':
|
| 64 |
+
try:
|
| 65 |
+
print(f"[{__name__}] μλ² μμ: http://{host}:{port}")
|
| 66 |
+
print(f"[{__name__}] λ‘κ·Έλ μ½μκ³Ό logs/server.log νμΌμ κΈ°λ‘λ©λλ€.")
|
| 67 |
+
app.run(host=host, port=port, debug=False, use_reloader=False)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"μλ² μμ μ€λ₯: {e}")
|
| 70 |
+
import traceback
|
| 71 |
+
traceback.print_exc()
|
| 72 |
+
|
app/database.py
CHANGED
|
@@ -41,6 +41,7 @@ class UploadedFile(db.Model):
|
|
| 41 |
file_path = db.Column(db.String(500), nullable=False)
|
| 42 |
file_size = db.Column(db.Integer, nullable=False)
|
| 43 |
model_name = db.Column(db.String(100), nullable=True) # μ°κ²°λ λͺ¨λΈ μ΄λ¦
|
|
|
|
| 44 |
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 45 |
uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
| 46 |
parent_file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=True) # μ΄μ΄μ μ
λ‘λν κ²½μ° μλ³Έ νμΌ ID
|
|
@@ -58,6 +59,7 @@ class UploadedFile(db.Model):
|
|
| 58 |
'original_filename': self.original_filename,
|
| 59 |
'file_size': self.file_size,
|
| 60 |
'model_name': self.model_name,
|
|
|
|
| 61 |
'uploaded_at': self.uploaded_at.isoformat() if self.uploaded_at else None,
|
| 62 |
'uploaded_by': self.uploaded_by,
|
| 63 |
'parent_file_id': self.parent_file_id,
|
|
|
|
| 41 |
file_path = db.Column(db.String(500), nullable=False)
|
| 42 |
file_size = db.Column(db.Integer, nullable=False)
|
| 43 |
model_name = db.Column(db.String(100), nullable=True) # μ°κ²°λ λͺ¨λΈ μ΄λ¦
|
| 44 |
+
is_public = db.Column(db.Boolean, default=False, nullable=False) # κ³΅κ° μ¬λΆ (κΈ°λ³Έκ°: 미곡κ°)
|
| 45 |
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 46 |
uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
| 47 |
parent_file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=True) # μ΄μ΄μ μ
λ‘λν κ²½μ° μλ³Έ νμΌ ID
|
|
|
|
| 59 |
'original_filename': self.original_filename,
|
| 60 |
'file_size': self.file_size,
|
| 61 |
'model_name': self.model_name,
|
| 62 |
+
'is_public': self.is_public,
|
| 63 |
'uploaded_at': self.uploaded_at.isoformat() if self.uploaded_at else None,
|
| 64 |
'uploaded_by': self.uploaded_by,
|
| 65 |
'parent_file_id': self.parent_file_id,
|
app/gemini_client.py
CHANGED
|
@@ -414,8 +414,38 @@ class GeminiClient:
|
|
| 414 |
elif rest_response.status_code != 200:
|
| 415 |
error_text_v1beta = rest_response.text[:1000] if rest_response.text else 'μμΈ μ 보 μμ'
|
| 416 |
raise Exception(f"REST API μ€λ₯ {rest_response.status_code}: {error_text_v1beta}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
else:
|
| 418 |
-
# 404κ° μλ λ€λ₯Έ μλ¬
|
| 419 |
error_text = json.dumps(error_info)
|
| 420 |
raise Exception(f"REST API μ€λ₯ {error_code}: {error_text}")
|
| 421 |
except ValueError:
|
|
@@ -497,8 +527,12 @@ class GeminiClient:
|
|
| 497 |
error_str = str(e).lower()
|
| 498 |
|
| 499 |
# μ¬μλ κ°λ₯ν μ€λ₯μΈμ§ νμΈ (νμμμ, λ€νΈμν¬ μ€λ₯ λ±)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
|
| 501 |
-
is_retryable = any(err in error_str for err in retryable_errors)
|
| 502 |
|
| 503 |
# deadline νμΈ
|
| 504 |
if time.time() >= deadline_time:
|
|
@@ -512,7 +546,10 @@ class GeminiClient:
|
|
| 512 |
wait_time = min(wait_time * multiplier, max_wait) # λ°°μλ‘ μ¦κ°, μ΅λκ° μ ν
|
| 513 |
else:
|
| 514 |
# μ¬μλ λΆκ°λ₯ν μ€λ₯
|
| 515 |
-
|
|
|
|
|
|
|
|
|
|
| 516 |
raise
|
| 517 |
|
| 518 |
# μλ΅ μκ° νμΈ
|
|
@@ -604,8 +641,12 @@ class GeminiClient:
|
|
| 604 |
error_str = str(e).lower()
|
| 605 |
|
| 606 |
# μ¬μλ κ°λ₯ν μ€λ₯μΈμ§ νμΈ (νμμμ, λ€νΈμν¬ μ€λ₯ λ±)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
|
| 608 |
-
is_retryable = any(err in error_str for err in retryable_errors)
|
| 609 |
|
| 610 |
# deadline νμΈ
|
| 611 |
if time.time() >= deadline_time:
|
|
@@ -619,7 +660,10 @@ class GeminiClient:
|
|
| 619 |
wait_time = min(wait_time * multiplier, max_wait) # λ°°μλ‘ μ¦κ°, μ΅λκ° μ ν
|
| 620 |
else:
|
| 621 |
# μ¬μλ λΆκ°λ₯ν μ€λ₯
|
| 622 |
-
|
|
|
|
|
|
|
|
|
|
| 623 |
raise
|
| 624 |
|
| 625 |
elapsed_time = time.time() - start_time
|
|
|
|
| 414 |
elif rest_response.status_code != 200:
|
| 415 |
error_text_v1beta = rest_response.text[:1000] if rest_response.text else 'μμΈ μ 보 μμ'
|
| 416 |
raise Exception(f"REST API μ€λ₯ {rest_response.status_code}: {error_text_v1beta}")
|
| 417 |
+
elif error_code == 429:
|
| 418 |
+
# 429 μ€λ₯: ν λΉλ μ΄κ³Ό (μ¬μλ λΆκ°λ₯)
|
| 419 |
+
print(f"[Gemini] β ν λΉλ μ΄κ³Ό μ€λ₯ (429) κ°μ§")
|
| 420 |
+
print(f"[Gemini] μ¬μλ λΆκ°λ₯ν μ€λ₯: REST API μ€λ₯ {error_code}: {json.dumps(error_info)}")
|
| 421 |
+
|
| 422 |
+
# μλ¬ λ©μμ§μμ κΆμ₯ λͺ¨λΈ μΆμΆ
|
| 423 |
+
recommended_model = None
|
| 424 |
+
if 'gemini-2.0-flash' in error_message.lower() or 'gemini 2.0' in error_message.lower():
|
| 425 |
+
recommended_model = "gemini-2.0-flash-exp"
|
| 426 |
+
|
| 427 |
+
# μ¬μ©μ μΉνμ μΈ μλ¬ λ©μμ§ μμ±
|
| 428 |
+
quota_error_msg = f"""Gemini API ν λΉλ μ΄κ³Ό (429)
|
| 429 |
+
|
| 430 |
+
νμ¬ μ¬μ© μ€μΈ λͺ¨λΈ '{model_name_clean}'μ μΌμΌ μμ² νλλ₯Ό μ΄κ³Όνμ΅λλ€.
|
| 431 |
+
|
| 432 |
+
ν΄κ²° λ°©λ²:
|
| 433 |
+
1. λ΄μΌκΉμ§ λκΈ° (ν λΉλμ΄ μλμΌλ‘ μ¬μ€μ λ©λλ€)
|
| 434 |
+
2. λ€λ₯Έ Gemini λͺ¨λΈλ‘ λ³κ²½:
|
| 435 |
+
- gemini-2.0-flash-exp (λ λμ ν λΉλ μ 곡)
|
| 436 |
+
- gemini-1.5-pro
|
| 437 |
+
- gemini-1.5-flash
|
| 438 |
+
3. Google AI Studioμμ ν λΉλ νμΈ:
|
| 439 |
+
https://aistudio.google.com/app/apikey
|
| 440 |
+
|
| 441 |
+
μμΈ μ€λ₯: {error_message[:200]}"""
|
| 442 |
+
|
| 443 |
+
if recommended_model:
|
| 444 |
+
quota_error_msg += f"\n\nκΆμ₯: {recommended_model} λͺ¨λΈλ‘ λ³κ²½μ κ³ λ €ν΄λ³΄μΈμ."
|
| 445 |
+
|
| 446 |
+
raise Exception(quota_error_msg)
|
| 447 |
else:
|
| 448 |
+
# 404, 429κ° μλ λ€λ₯Έ μλ¬
|
| 449 |
error_text = json.dumps(error_info)
|
| 450 |
raise Exception(f"REST API μ€λ₯ {error_code}: {error_text}")
|
| 451 |
except ValueError:
|
|
|
|
| 527 |
error_str = str(e).lower()
|
| 528 |
|
| 529 |
# μ¬μλ κ°λ₯ν μ€λ₯μΈμ§ νμΈ (νμμμ, λ€νΈμν¬ μ€λ₯ λ±)
|
| 530 |
+
# 429(ν λΉλ μ΄κ³Ό), 400(μλͺ»λ μμ²), 401(μΈμ¦ μ€ν¨), 403(κΆν μμ)μ μ¬μλ λΆκ°
|
| 531 |
+
non_retryable_errors = ['429', 'quota', 'exceeded', '400', '401', '403', 'api key', 'invalid', 'unauthorized', 'forbidden']
|
| 532 |
+
is_non_retryable = any(err in error_str for err in non_retryable_errors)
|
| 533 |
+
|
| 534 |
retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
|
| 535 |
+
is_retryable = any(err in error_str for err in retryable_errors) and not is_non_retryable
|
| 536 |
|
| 537 |
# deadline νμΈ
|
| 538 |
if time.time() >= deadline_time:
|
|
|
|
| 546 |
wait_time = min(wait_time * multiplier, max_wait) # λ°°μλ‘ μ¦κ°, μ΅λκ° μ ν
|
| 547 |
else:
|
| 548 |
# μ¬μλ λΆκ°λ₯ν μ€λ₯
|
| 549 |
+
if is_non_retryable:
|
| 550 |
+
print(f"[Gemini] μ¬μλ λΆκ°λ₯ν μ€λ₯ (ν λΉλ/μΈμ¦ μ€λ₯): {str(e)[:200]}")
|
| 551 |
+
else:
|
| 552 |
+
print(f"[Gemini] μ¬μλ λΆκ°λ₯ν μ€λ₯: {str(e)[:100]}")
|
| 553 |
raise
|
| 554 |
|
| 555 |
# μλ΅ μκ° νμΈ
|
|
|
|
| 641 |
error_str = str(e).lower()
|
| 642 |
|
| 643 |
# μ¬μλ κ°λ₯ν μ€λ₯μΈμ§ νμΈ (νμμμ, λ€νΈμν¬ μ€λ₯ λ±)
|
| 644 |
+
# 429(ν λΉλ μ΄κ³Ό), 400(μλͺ»λ μμ²), 401(μΈμ¦ μ€ν¨), 403(κΆν μμ)μ μ¬μλ λΆκ°
|
| 645 |
+
non_retryable_errors = ['429', 'quota', 'exceeded', '400', '401', '403', 'api key', 'invalid', 'unauthorized', 'forbidden']
|
| 646 |
+
is_non_retryable = any(err in error_str for err in non_retryable_errors)
|
| 647 |
+
|
| 648 |
retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
|
| 649 |
+
is_retryable = any(err in error_str for err in retryable_errors) and not is_non_retryable
|
| 650 |
|
| 651 |
# deadline νμΈ
|
| 652 |
if time.time() >= deadline_time:
|
|
|
|
| 660 |
wait_time = min(wait_time * multiplier, max_wait) # λ°°μλ‘ μ¦κ°, μ΅λκ° μ ν
|
| 661 |
else:
|
| 662 |
# μ¬μλ λΆκ°λ₯ν μ€λ₯
|
| 663 |
+
if is_non_retryable:
|
| 664 |
+
print(f"[Gemini] μ±ν
μ¬μλ λΆκ°λ₯ν μ€λ₯ (ν λΉλ/μΈμ¦ μ€λ₯): {str(e)[:200]}")
|
| 665 |
+
else:
|
| 666 |
+
print(f"[Gemini] μ±ν
μ¬μλ λΆκ°λ₯ν μ€λ₯: {str(e)[:100]}")
|
| 667 |
raise
|
| 668 |
|
| 669 |
elapsed_time = time.time() - start_time
|
app/routes.py
CHANGED
|
@@ -475,6 +475,8 @@ character_relationshipsλ μ΄ μ²ν¬μ λ±μ₯νλ μΈλ¬Όλ€ κ°μ νμ¬
|
|
| 475 |
else:
|
| 476 |
# Ollama API νΈμΆ
|
| 477 |
try:
|
|
|
|
|
|
|
| 478 |
ollama_response = requests.post(
|
| 479 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 480 |
json={
|
|
@@ -483,7 +485,8 @@ character_relationshipsλ μ΄ μ²ν¬μ λ±μ₯νλ μΈλ¬Όλ€ κ°μ νμ¬
|
|
| 483 |
'stream': False,
|
| 484 |
'options': {
|
| 485 |
'temperature': 0.3,
|
| 486 |
-
'num_predict': get_model_token_limit(model_name, 500) # μ μ₯λ ν ν° μ μ¬μ©
|
|
|
|
| 487 |
}
|
| 488 |
},
|
| 489 |
timeout=120 # 2λΆ νμμμ
|
|
@@ -659,6 +662,8 @@ def analyze_episode(episode_content, episode_title, full_content=None, parent_ch
|
|
| 659 |
else:
|
| 660 |
# Ollama API νΈμΆ
|
| 661 |
try:
|
|
|
|
|
|
|
| 662 |
ollama_response = requests.post(
|
| 663 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 664 |
json={
|
|
@@ -667,7 +672,8 @@ def analyze_episode(episode_content, episode_title, full_content=None, parent_ch
|
|
| 667 |
'stream': False,
|
| 668 |
'options': {
|
| 669 |
'temperature': 0.5,
|
| 670 |
-
'num_predict': get_model_token_limit(model_name, 3000) # μ μ₯λ ν ν° μ μ¬μ©
|
|
|
|
| 671 |
}
|
| 672 |
},
|
| 673 |
timeout=300 # 5λΆ νμμμ (νμ°¨ λΆμμ μκ°μ΄ μ€λ 걸릴 μ μμ)
|
|
@@ -770,6 +776,8 @@ def extract_graph_from_episode(episode_content, episode_title, file_id, full_con
|
|
| 770 |
else:
|
| 771 |
# Ollama API νΈμΆ
|
| 772 |
try:
|
|
|
|
|
|
|
| 773 |
ollama_response = requests.post(
|
| 774 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 775 |
json={
|
|
@@ -778,7 +786,8 @@ def extract_graph_from_episode(episode_content, episode_title, file_id, full_con
|
|
| 778 |
'stream': False,
|
| 779 |
'options': {
|
| 780 |
'temperature': 0.3,
|
| 781 |
-
'num_predict': 3000
|
|
|
|
| 782 |
}
|
| 783 |
},
|
| 784 |
timeout=300 # 5λΆ νμμμ
|
|
@@ -1213,6 +1222,8 @@ def create_parent_chunk_with_ai(file_id, content, model_name):
|
|
| 1213 |
print(f"[Parent Chunk μμ±] Ollama APIμ λΆμ μμ² μ μ‘ μ€... (λͺ¨λΈ: {model_name})")
|
| 1214 |
|
| 1215 |
try:
|
|
|
|
|
|
|
| 1216 |
ollama_response = requests.post(
|
| 1217 |
f'{OLLAMA_BASE_URL}/api/chat',
|
| 1218 |
json={
|
|
@@ -1223,7 +1234,10 @@ def create_parent_chunk_with_ai(file_id, content, model_name):
|
|
| 1223 |
'content': analysis_prompt
|
| 1224 |
}
|
| 1225 |
],
|
| 1226 |
-
'stream': False
|
|
|
|
|
|
|
|
|
|
| 1227 |
},
|
| 1228 |
timeout=300 # 5λΆ νμμμ
|
| 1229 |
)
|
|
@@ -2852,12 +2866,19 @@ def chat():
|
|
| 2852 |
response_text = 'μλ΅μ μμ±ν μ μμμ΅λλ€. λ€μ μλν΄μ£ΌμΈμ.'
|
| 2853 |
else:
|
| 2854 |
# Ollama API νΈμΆ
|
|
|
|
|
|
|
|
|
|
| 2855 |
ollama_response = requests.post(
|
| 2856 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 2857 |
json={
|
| 2858 |
'model': answer_model, # λ΅λ³ λͺ¨λΈ μ¬μ©
|
| 2859 |
'prompt': full_prompt,
|
| 2860 |
-
'stream': False
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2861 |
},
|
| 2862 |
timeout=120 # νμΌμ΄ λ§μ μ μμΌλ―λ‘ νμμμ μ¦κ°
|
| 2863 |
)
|
|
@@ -3176,6 +3197,7 @@ def upload_file():
|
|
| 3176 |
file_path=file_path,
|
| 3177 |
file_size=saved_file_size,
|
| 3178 |
model_name=model_name, # μ΄λ―Έ κ²μ¦λ¨
|
|
|
|
| 3179 |
uploaded_by=current_user.id,
|
| 3180 |
parent_file_id=parent_file_id if parent_file else None # μ΄μ΄μ μ
λ‘λμΈ κ²½μ°
|
| 3181 |
)
|
|
@@ -3259,11 +3281,16 @@ def get_files():
|
|
| 3259 |
"""μ
λ‘λλ νμΌ λͺ©λ‘ μ‘°ν"""
|
| 3260 |
try:
|
| 3261 |
model_name = request.args.get('model_name', None)
|
|
|
|
| 3262 |
|
| 3263 |
# μλ³Έ νμΌλ§ μ‘°ν (parent_file_idκ° NoneμΈ νμΌ)
|
| 3264 |
-
#
|
| 3265 |
-
|
| 3266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3267 |
|
| 3268 |
# λͺ¨λΈ νν°λ§ μ μ 체 νμΌ μ νμΈ
|
| 3269 |
total_before_filter = query.count()
|
|
@@ -3311,12 +3338,14 @@ def get_files():
|
|
| 3311 |
file_dict['child_files'] = child_files_dict
|
| 3312 |
files_with_children.append(file_dict)
|
| 3313 |
|
| 3314 |
-
# λͺ¨λΈλ³ ν΅κ³ μ 보 μΆκ° (μλ³Έ νμΌλ§
|
| 3315 |
model_stats = {}
|
| 3316 |
if not model_name:
|
| 3317 |
-
# λͺ¨λ λͺ¨λΈμ ν΅κ³ (μλ³Έ νμΌλ§)
|
| 3318 |
-
|
| 3319 |
-
|
|
|
|
|
|
|
| 3320 |
for file in all_files:
|
| 3321 |
model = file.model_name or 'λ―Έμ§μ '
|
| 3322 |
if model not in model_stats:
|
|
@@ -4081,6 +4110,29 @@ def delete_file(file_id):
|
|
| 4081 |
db.session.rollback()
|
| 4082 |
return jsonify({'error': f'νμΌ μμ μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}'}), 500
|
| 4083 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4084 |
@main_bp.route('/api/files/<int:file_id>/content', methods=['GET'])
|
| 4085 |
@login_required
|
| 4086 |
def get_file_content(file_id):
|
|
|
|
| 475 |
else:
|
| 476 |
# Ollama API νΈμΆ
|
| 477 |
try:
|
| 478 |
+
# μ
λ ₯ ν ν° μλ₯Ό num_ctxλ‘ μ¬μ©
|
| 479 |
+
num_ctx = get_model_token_limit_by_type(model_name, 100000, 'input')
|
| 480 |
ollama_response = requests.post(
|
| 481 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 482 |
json={
|
|
|
|
| 485 |
'stream': False,
|
| 486 |
'options': {
|
| 487 |
'temperature': 0.3,
|
| 488 |
+
'num_predict': get_model_token_limit(model_name, 500), # μ μ₯λ ν ν° μ μ¬μ©
|
| 489 |
+
'num_ctx': num_ctx # μ
λ ₯ ν ν° μλ₯Ό 컨ν
μ€νΈ μλμ°λ‘ μ¬μ©
|
| 490 |
}
|
| 491 |
},
|
| 492 |
timeout=120 # 2λΆ νμμμ
|
|
|
|
| 662 |
else:
|
| 663 |
# Ollama API νΈμΆ
|
| 664 |
try:
|
| 665 |
+
# μ
λ ₯ ν ν° μλ₯Ό num_ctxλ‘ μ¬μ©
|
| 666 |
+
num_ctx = get_model_token_limit_by_type(model_name, 100000, 'input')
|
| 667 |
ollama_response = requests.post(
|
| 668 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 669 |
json={
|
|
|
|
| 672 |
'stream': False,
|
| 673 |
'options': {
|
| 674 |
'temperature': 0.5,
|
| 675 |
+
'num_predict': get_model_token_limit(model_name, 3000), # μ μ₯λ ν ν° μ μ¬μ©
|
| 676 |
+
'num_ctx': num_ctx # μ
λ ₯ ν ν° μλ₯Ό 컨ν
μ€νΈ μλμ°λ‘ μ¬μ©
|
| 677 |
}
|
| 678 |
},
|
| 679 |
timeout=300 # 5λΆ νμμμ (νμ°¨ λΆμμ μκ°μ΄ μ€λ 걸릴 μ μμ)
|
|
|
|
| 776 |
else:
|
| 777 |
# Ollama API νΈμΆ
|
| 778 |
try:
|
| 779 |
+
# μ
λ ₯ ν ν° μλ₯Ό num_ctxλ‘ μ¬μ©
|
| 780 |
+
num_ctx = get_model_token_limit_by_type(model_name, 100000, 'input')
|
| 781 |
ollama_response = requests.post(
|
| 782 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 783 |
json={
|
|
|
|
| 786 |
'stream': False,
|
| 787 |
'options': {
|
| 788 |
'temperature': 0.3,
|
| 789 |
+
'num_predict': 3000,
|
| 790 |
+
'num_ctx': num_ctx # μ
λ ₯ ν ν° μλ₯Ό 컨ν
μ€νΈ μλμ°λ‘ μ¬μ©
|
| 791 |
}
|
| 792 |
},
|
| 793 |
timeout=300 # 5λΆ νμμμ
|
|
|
|
| 1222 |
print(f"[Parent Chunk μμ±] Ollama APIμ λΆμ μμ² μ μ‘ μ€... (λͺ¨λΈ: {model_name})")
|
| 1223 |
|
| 1224 |
try:
|
| 1225 |
+
# μ
λ ₯ ν ν° μλ₯Ό num_ctxλ‘ μ¬μ©
|
| 1226 |
+
num_ctx = get_model_token_limit_by_type(model_name, 100000, 'input')
|
| 1227 |
ollama_response = requests.post(
|
| 1228 |
f'{OLLAMA_BASE_URL}/api/chat',
|
| 1229 |
json={
|
|
|
|
| 1234 |
'content': analysis_prompt
|
| 1235 |
}
|
| 1236 |
],
|
| 1237 |
+
'stream': False,
|
| 1238 |
+
'options': {
|
| 1239 |
+
'num_ctx': num_ctx # μ
λ ₯ ν ν° μλ₯Ό 컨ν
μ€νΈ μλμ°λ‘ μ¬μ©
|
| 1240 |
+
}
|
| 1241 |
},
|
| 1242 |
timeout=300 # 5λΆ νμμμ
|
| 1243 |
)
|
|
|
|
| 2866 |
response_text = 'μλ΅μ μμ±ν μ μμμ΅λλ€. λ€μ μλν΄μ£ΌμΈμ.'
|
| 2867 |
else:
|
| 2868 |
# Ollama API νΈμΆ
|
| 2869 |
+
# μ
λ ₯ ν ν° μλ₯Ό num_ctxλ‘ μ¬μ©
|
| 2870 |
+
num_ctx = get_model_token_limit_by_type(answer_model, 100000, 'input')
|
| 2871 |
+
num_predict = get_model_token_limit_by_type(answer_model, 8192, 'output')
|
| 2872 |
ollama_response = requests.post(
|
| 2873 |
f'{OLLAMA_BASE_URL}/api/generate',
|
| 2874 |
json={
|
| 2875 |
'model': answer_model, # λ΅λ³ λͺ¨λΈ μ¬μ©
|
| 2876 |
'prompt': full_prompt,
|
| 2877 |
+
'stream': False,
|
| 2878 |
+
'options': {
|
| 2879 |
+
'num_ctx': num_ctx, # μ
λ ₯ ν ν° μλ₯Ό 컨ν
μ€νΈ μλμ°λ‘ μ¬μ©
|
| 2880 |
+
'num_predict': num_predict # μΆλ ₯ ν ν° μ
|
| 2881 |
+
}
|
| 2882 |
},
|
| 2883 |
timeout=120 # νμΌμ΄ λ§μ μ μμΌλ―λ‘ νμμμ μ¦κ°
|
| 2884 |
)
|
|
|
|
| 3197 |
file_path=file_path,
|
| 3198 |
file_size=saved_file_size,
|
| 3199 |
model_name=model_name, # μ΄λ―Έ κ²μ¦λ¨
|
| 3200 |
+
is_public=False, # κΈ°λ³Έκ°: 미곡κ°
|
| 3201 |
uploaded_by=current_user.id,
|
| 3202 |
parent_file_id=parent_file_id if parent_file else None # μ΄μ΄μ μ
λ‘λμΈ κ²½μ°
|
| 3203 |
)
|
|
|
|
| 3281 |
"""μ
λ‘λλ νμΌ λͺ©λ‘ μ‘°ν"""
|
| 3282 |
try:
|
| 3283 |
model_name = request.args.get('model_name', None)
|
| 3284 |
+
public_only = request.args.get('public_only', 'false').lower() == 'true' # κ³΅κ° νμΌλ§ μ‘°ν μ΅μ
|
| 3285 |
|
| 3286 |
# μλ³Έ νμΌλ§ μ‘°ν (parent_file_idκ° NoneμΈ νμΌ)
|
| 3287 |
+
# κ΄λ¦¬μκ° μλ κ²½μ° κ³΅κ° νμΌλ§ μ‘°ν, κ΄λ¦¬μλ λͺ¨λ νμΌ μ‘°ν κ°λ₯
|
| 3288 |
+
if public_only or (not current_user.is_admin):
|
| 3289 |
+
query = UploadedFile.query.filter_by(parent_file_id=None, is_public=True)
|
| 3290 |
+
print(f"[νμΌ μ‘°ν] κ³΅κ° νμΌλ§ μ‘°ν (μ¬μ©μ: {current_user.username}, κ΄λ¦¬μ: {current_user.is_admin})")
|
| 3291 |
+
else:
|
| 3292 |
+
query = UploadedFile.query.filter_by(parent_file_id=None)
|
| 3293 |
+
print(f"[νμΌ μ‘°ν] λͺ¨λ νμΌ μ‘°ν (μ¬μ©μ: {current_user.username}, κ΄λ¦¬μ: {current_user.is_admin})")
|
| 3294 |
|
| 3295 |
# λͺ¨λΈ νν°λ§ μ μ 체 νμΌ μ νμΈ
|
| 3296 |
total_before_filter = query.count()
|
|
|
|
| 3338 |
file_dict['child_files'] = child_files_dict
|
| 3339 |
files_with_children.append(file_dict)
|
| 3340 |
|
| 3341 |
+
# λͺ¨λΈλ³ ν΅κ³ μ 보 μΆκ° (μλ³Έ νμΌλ§ μΉ΄μ΄νΈ, κ³΅κ° νμΌλ§)
|
| 3342 |
model_stats = {}
|
| 3343 |
if not model_name:
|
| 3344 |
+
# λͺ¨λ λͺ¨λΈμ ν΅κ³ (μλ³Έ νμΌλ§, κ³΅κ° νμΌλ§)
|
| 3345 |
+
if public_only or (not current_user.is_admin):
|
| 3346 |
+
all_files = UploadedFile.query.filter_by(parent_file_id=None, is_public=True).all()
|
| 3347 |
+
else:
|
| 3348 |
+
all_files = UploadedFile.query.filter_by(parent_file_id=None).all()
|
| 3349 |
for file in all_files:
|
| 3350 |
model = file.model_name or 'λ―Έμ§μ '
|
| 3351 |
if model not in model_stats:
|
|
|
|
| 4110 |
db.session.rollback()
|
| 4111 |
return jsonify({'error': f'νμΌ μμ μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}'}), 500
|
| 4112 |
|
| 4113 |
+
@main_bp.route('/api/files/<int:file_id>/public', methods=['PUT'])
|
| 4114 |
+
@login_required
|
| 4115 |
+
@admin_required
|
| 4116 |
+
def toggle_file_public(file_id):
|
| 4117 |
+
"""νμΌ κ³΅κ° μ¬λΆ λ³κ²½ (κ΄λ¦¬μλ§ κ°λ₯)"""
|
| 4118 |
+
try:
|
| 4119 |
+
file = UploadedFile.query.get_or_404(file_id)
|
| 4120 |
+
|
| 4121 |
+
data = request.get_json()
|
| 4122 |
+
is_public = data.get('is_public', False)
|
| 4123 |
+
|
| 4124 |
+
file.is_public = is_public
|
| 4125 |
+
db.session.commit()
|
| 4126 |
+
|
| 4127 |
+
return jsonify({
|
| 4128 |
+
'message': f'νμΌμ΄ {"곡κ°" if is_public else "λΉκ³΅κ°"}λ‘ μ€μ λμμ΅λλ€.',
|
| 4129 |
+
'file': file.to_dict()
|
| 4130 |
+
}), 200
|
| 4131 |
+
|
| 4132 |
+
except Exception as e:
|
| 4133 |
+
db.session.rollback()
|
| 4134 |
+
return jsonify({'error': f'νμΌ κ³΅κ° μ¬λΆ λ³κ²½ μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}'}), 500
|
| 4135 |
+
|
| 4136 |
@main_bp.route('/api/files/<int:file_id>/content', methods=['GET'])
|
| 4137 |
@login_required
|
| 4138 |
def get_file_content(file_id):
|
download_exaone_model.py
CHANGED
|
@@ -90,5 +90,6 @@ if __name__ == "__main__":
|
|
| 90 |
|
| 91 |
|
| 92 |
|
|
|
|
| 93 |
|
| 94 |
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
+
|
| 94 |
|
| 95 |
|
install_exaone_direct.py
CHANGED
|
@@ -95,5 +95,6 @@ if __name__ == "__main__":
|
|
| 95 |
|
| 96 |
|
| 97 |
|
|
|
|
| 98 |
|
| 99 |
|
|
|
|
| 95 |
|
| 96 |
|
| 97 |
|
| 98 |
+
|
| 99 |
|
| 100 |
|
install_exaone_simple.py
CHANGED
|
@@ -72,5 +72,6 @@ if __name__ == "__main__":
|
|
| 72 |
|
| 73 |
|
| 74 |
|
|
|
|
| 75 |
|
| 76 |
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
|
| 75 |
+
|
| 76 |
|
| 77 |
|
migrate_add_is_public.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
λ°μ΄ν°λ² μ΄μ€ λ§μ΄κ·Έλ μ΄μ
μ€ν¬λ¦½νΈ
|
| 3 |
+
uploaded_file ν
μ΄λΈμ is_public 컬λΌμ μΆκ°ν©λλ€.
|
| 4 |
+
"""
|
| 5 |
+
import sqlite3
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# λ°μ΄ν°λ² μ΄μ€ κ²½λ‘
|
| 10 |
+
db_path = Path(__file__).parent / 'instance' / 'finance_analysis.db'
|
| 11 |
+
|
| 12 |
+
def migrate_database():
|
| 13 |
+
"""λ°μ΄ν°λ² μ΄μ€μ is_public μ»¬λΌ μΆκ°"""
|
| 14 |
+
if not db_path.exists():
|
| 15 |
+
print(f"λ°μ΄ν°λ² μ΄μ€ νμΌμ΄ μμ΅λλ€: {db_path}")
|
| 16 |
+
print("μ±μ μ€ννλ©΄ μλμΌλ‘ μμ±λ©λλ€.")
|
| 17 |
+
return
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
conn = sqlite3.connect(str(db_path))
|
| 21 |
+
cursor = conn.cursor()
|
| 22 |
+
|
| 23 |
+
# uploaded_file ν
μ΄λΈμ is_public 컬λΌμ΄ μλμ§ νμΈ
|
| 24 |
+
cursor.execute("PRAGMA table_info(uploaded_file)")
|
| 25 |
+
columns = [column[1] for column in cursor.fetchall()]
|
| 26 |
+
|
| 27 |
+
if 'is_public' in columns:
|
| 28 |
+
print("is_public 컬λΌμ΄ μ΄λ―Έ μ‘΄μ¬ν©λλ€.")
|
| 29 |
+
conn.close()
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
# is_public μ»¬λΌ μΆκ° (κΈ°λ³Έκ°: 0 = False)
|
| 33 |
+
print("is_public 컬λΌμ μΆκ°νλ μ€...")
|
| 34 |
+
cursor.execute("ALTER TABLE uploaded_file ADD COLUMN is_public BOOLEAN DEFAULT 0")
|
| 35 |
+
conn.commit()
|
| 36 |
+
print("is_public 컬λΌμ΄ μ±κ³΅μ μΌλ‘ μΆκ°λμμ΅λλ€.")
|
| 37 |
+
|
| 38 |
+
# κΈ°μ‘΄ λ°μ΄ν°μ is_public κ°μ 0(False)μΌλ‘ μ€μ
|
| 39 |
+
cursor.execute("UPDATE uploaded_file SET is_public = 0 WHERE is_public IS NULL")
|
| 40 |
+
conn.commit()
|
| 41 |
+
print("κΈ°μ‘΄ λ°μ΄ν°μ is_public κ°μ 0(False)μΌλ‘ μ€μ νμ΅λλ€.")
|
| 42 |
+
|
| 43 |
+
conn.close()
|
| 44 |
+
print("λ§μ΄κ·Έλ μ΄μ
μ΄ μλ£λμμ΅λλ€.")
|
| 45 |
+
|
| 46 |
+
except sqlite3.OperationalError as e:
|
| 47 |
+
print(f"μ€λ₯ λ°μ: {e}")
|
| 48 |
+
if "duplicate column name" in str(e).lower() or "already exists" in str(e).lower():
|
| 49 |
+
print("is_public 컬λΌμ΄ μ΄λ―Έ μ‘΄μ¬ν©λλ€.")
|
| 50 |
+
else:
|
| 51 |
+
raise
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"μμμΉ λͺ»ν μ€λ₯: {e}")
|
| 54 |
+
raise
|
| 55 |
+
|
| 56 |
+
if __name__ == '__main__':
|
| 57 |
+
print("=" * 60)
|
| 58 |
+
print("λ°μ΄ν°λ² μ΄μ€ λ§μ΄κ·Έλ μ΄μ
: is_public μ»¬λΌ μΆκ°")
|
| 59 |
+
print("=" * 60)
|
| 60 |
+
migrate_database()
|
| 61 |
+
|
templates/admin.html
CHANGED
|
@@ -43,6 +43,126 @@
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
.btn {
|
| 47 |
padding: 8px 16px;
|
| 48 |
border: none;
|
|
@@ -443,6 +563,7 @@
|
|
| 443 |
<span>π€</span>
|
| 444 |
<span>SOY NV AI κ΄λ¦¬μ νμ΄μ§</span>
|
| 445 |
</div>
|
|
|
|
| 446 |
<div class="header-actions">
|
| 447 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 448 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
|
@@ -455,6 +576,26 @@
|
|
| 455 |
</div>
|
| 456 |
</div>
|
| 457 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
<div class="container">
|
| 459 |
<div class="page-header">
|
| 460 |
<h1>μ¬μ©μ κ΄λ¦¬</h1>
|
|
@@ -558,6 +699,24 @@
|
|
| 558 |
<script>
|
| 559 |
let currentEditUserId = null;
|
| 560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
function showAlert(message, type = 'success') {
|
| 562 |
const container = document.getElementById('alertContainer');
|
| 563 |
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
|
|
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
| 46 |
+
.menu-toggle {
|
| 47 |
+
display: none;
|
| 48 |
+
background: none;
|
| 49 |
+
border: none;
|
| 50 |
+
font-size: 24px;
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
padding: 8px;
|
| 53 |
+
color: #202124;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.mobile-menu {
|
| 57 |
+
display: none;
|
| 58 |
+
position: fixed;
|
| 59 |
+
top: 0;
|
| 60 |
+
left: 0;
|
| 61 |
+
right: 0;
|
| 62 |
+
bottom: 0;
|
| 63 |
+
background: rgba(0, 0, 0, 0.5);
|
| 64 |
+
z-index: 1000;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.mobile-menu.active {
|
| 68 |
+
display: block;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.mobile-menu-content {
|
| 72 |
+
position: fixed;
|
| 73 |
+
top: 0;
|
| 74 |
+
right: -100%;
|
| 75 |
+
width: 280px;
|
| 76 |
+
max-width: 80%;
|
| 77 |
+
height: 100%;
|
| 78 |
+
background: white;
|
| 79 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 80 |
+
transition: right 0.3s ease;
|
| 81 |
+
overflow-y: auto;
|
| 82 |
+
z-index: 1001;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.mobile-menu.active .mobile-menu-content {
|
| 86 |
+
right: 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.mobile-menu-header {
|
| 90 |
+
padding: 16px 20px;
|
| 91 |
+
border-bottom: 1px solid #dadce0;
|
| 92 |
+
display: flex;
|
| 93 |
+
justify-content: space-between;
|
| 94 |
+
align-items: center;
|
| 95 |
+
background: white;
|
| 96 |
+
position: sticky;
|
| 97 |
+
top: 0;
|
| 98 |
+
z-index: 10;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.mobile-menu-title {
|
| 102 |
+
font-size: 18px;
|
| 103 |
+
font-weight: 500;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.mobile-menu-close {
|
| 107 |
+
background: none;
|
| 108 |
+
border: none;
|
| 109 |
+
font-size: 24px;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
color: #202124;
|
| 112 |
+
padding: 0;
|
| 113 |
+
width: 32px;
|
| 114 |
+
height: 32px;
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
justify-content: center;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.mobile-menu-items {
|
| 121 |
+
padding: 8px 0;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.mobile-menu-item {
|
| 125 |
+
display: block;
|
| 126 |
+
padding: 12px 20px;
|
| 127 |
+
color: #202124;
|
| 128 |
+
text-decoration: none;
|
| 129 |
+
border-bottom: 1px solid #f1f3f4;
|
| 130 |
+
transition: background 0.2s;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.mobile-menu-item:hover {
|
| 134 |
+
background: #f8f9fa;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.mobile-menu-user {
|
| 138 |
+
padding: 16px 20px;
|
| 139 |
+
border-bottom: 1px solid #dadce0;
|
| 140 |
+
color: #5f6368;
|
| 141 |
+
font-size: 14px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
@media (max-width: 768px) {
|
| 145 |
+
.header {
|
| 146 |
+
padding: 12px 16px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header-title {
|
| 150 |
+
font-size: 18px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.header-title span:first-child {
|
| 154 |
+
display: none;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.menu-toggle {
|
| 158 |
+
display: block;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.header-actions {
|
| 162 |
+
display: none;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
.btn {
|
| 167 |
padding: 8px 16px;
|
| 168 |
border: none;
|
|
|
|
| 563 |
<span>π€</span>
|
| 564 |
<span>SOY NV AI κ΄λ¦¬μ νμ΄μ§</span>
|
| 565 |
</div>
|
| 566 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 567 |
<div class="header-actions">
|
| 568 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 569 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
|
|
|
| 576 |
</div>
|
| 577 |
</div>
|
| 578 |
|
| 579 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 580 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 581 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 582 |
+
<div class="mobile-menu-header">
|
| 583 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 584 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 585 |
+
</div>
|
| 586 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 587 |
+
<div class="mobile-menu-items">
|
| 588 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μΉμμ€ κ΄λ¦¬</a>
|
| 589 |
+
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νμΌ λͺ©λ‘</a>
|
| 590 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μμ§ νμΈ</a>
|
| 591 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ν둬ννΈ κ΄λ¦¬</a>
|
| 592 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI μ€μ </a>
|
| 593 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μΈμΌλ‘</a>
|
| 594 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">οΏ½οΏ½κ·Έμμ</a>
|
| 595 |
+
</div>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
|
| 599 |
<div class="container">
|
| 600 |
<div class="page-header">
|
| 601 |
<h1>μ¬μ©μ κ΄λ¦¬</h1>
|
|
|
|
| 699 |
<script>
|
| 700 |
let currentEditUserId = null;
|
| 701 |
|
| 702 |
+
function toggleMobileMenu() {
|
| 703 |
+
const menu = document.getElementById('mobileMenu');
|
| 704 |
+
menu.classList.toggle('active');
|
| 705 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
function closeMobileMenu() {
|
| 709 |
+
const menu = document.getElementById('mobileMenu');
|
| 710 |
+
menu.classList.remove('active');
|
| 711 |
+
document.body.style.overflow = '';
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 715 |
+
if (event.target.id === 'mobileMenu') {
|
| 716 |
+
closeMobileMenu();
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
function showAlert(message, type = 'success') {
|
| 721 |
const container = document.getElementById('alertContainer');
|
| 722 |
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
|
templates/admin_files.html
CHANGED
|
@@ -43,6 +43,126 @@
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
.btn {
|
| 47 |
padding: 8px 16px;
|
| 48 |
border: none;
|
|
@@ -345,17 +465,39 @@
|
|
| 345 |
<span>π€</span>
|
| 346 |
<span>SOY NV AI κ΄λ¦¬μ νμ΄μ§</span>
|
| 347 |
</div>
|
|
|
|
| 348 |
<div class="header-actions">
|
| 349 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 350 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 351 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
| 352 |
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©μμ§ νμΈ</a>
|
| 353 |
<a href="{{ url_for('main.admin_prompts') }}" class="btn btn-secondary">ν둬ννΈ κ΄λ¦¬</a>
|
|
|
|
| 354 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 355 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 356 |
</div>
|
| 357 |
</div>
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
<div class="container">
|
| 360 |
<div class="page-header">
|
| 361 |
<h1>νμΌ λͺ©λ‘</h1>
|
|
@@ -523,6 +665,24 @@
|
|
| 523 |
<script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
|
| 524 |
|
| 525 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
function escapeHtml(text) {
|
| 527 |
const div = document.createElement('div');
|
| 528 |
div.textContent = text;
|
|
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
| 46 |
+
.menu-toggle {
|
| 47 |
+
display: none;
|
| 48 |
+
background: none;
|
| 49 |
+
border: none;
|
| 50 |
+
font-size: 24px;
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
padding: 8px;
|
| 53 |
+
color: #202124;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.mobile-menu {
|
| 57 |
+
display: none;
|
| 58 |
+
position: fixed;
|
| 59 |
+
top: 0;
|
| 60 |
+
left: 0;
|
| 61 |
+
right: 0;
|
| 62 |
+
bottom: 0;
|
| 63 |
+
background: rgba(0, 0, 0, 0.5);
|
| 64 |
+
z-index: 1000;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.mobile-menu.active {
|
| 68 |
+
display: block;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.mobile-menu-content {
|
| 72 |
+
position: fixed;
|
| 73 |
+
top: 0;
|
| 74 |
+
right: -100%;
|
| 75 |
+
width: 280px;
|
| 76 |
+
max-width: 80%;
|
| 77 |
+
height: 100%;
|
| 78 |
+
background: white;
|
| 79 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 80 |
+
transition: right 0.3s ease;
|
| 81 |
+
overflow-y: auto;
|
| 82 |
+
z-index: 1001;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.mobile-menu.active .mobile-menu-content {
|
| 86 |
+
right: 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.mobile-menu-header {
|
| 90 |
+
padding: 16px 20px;
|
| 91 |
+
border-bottom: 1px solid #dadce0;
|
| 92 |
+
display: flex;
|
| 93 |
+
justify-content: space-between;
|
| 94 |
+
align-items: center;
|
| 95 |
+
background: white;
|
| 96 |
+
position: sticky;
|
| 97 |
+
top: 0;
|
| 98 |
+
z-index: 10;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.mobile-menu-title {
|
| 102 |
+
font-size: 18px;
|
| 103 |
+
font-weight: 500;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.mobile-menu-close {
|
| 107 |
+
background: none;
|
| 108 |
+
border: none;
|
| 109 |
+
font-size: 24px;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
color: #202124;
|
| 112 |
+
padding: 0;
|
| 113 |
+
width: 32px;
|
| 114 |
+
height: 32px;
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
justify-content: center;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.mobile-menu-items {
|
| 121 |
+
padding: 8px 0;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.mobile-menu-item {
|
| 125 |
+
display: block;
|
| 126 |
+
padding: 12px 20px;
|
| 127 |
+
color: #202124;
|
| 128 |
+
text-decoration: none;
|
| 129 |
+
border-bottom: 1px solid #f1f3f4;
|
| 130 |
+
transition: background 0.2s;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.mobile-menu-item:hover {
|
| 134 |
+
background: #f8f9fa;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.mobile-menu-user {
|
| 138 |
+
padding: 16px 20px;
|
| 139 |
+
border-bottom: 1px solid #dadce0;
|
| 140 |
+
color: #5f6368;
|
| 141 |
+
font-size: 14px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
@media (max-width: 768px) {
|
| 145 |
+
.header {
|
| 146 |
+
padding: 12px 16px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header-title {
|
| 150 |
+
font-size: 18px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.header-title span:first-child {
|
| 154 |
+
display: none;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.menu-toggle {
|
| 158 |
+
display: block;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.header-actions {
|
| 162 |
+
display: none;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
.btn {
|
| 167 |
padding: 8px 16px;
|
| 168 |
border: none;
|
|
|
|
| 465 |
<span>π€</span>
|
| 466 |
<span>SOY NV AI κ΄λ¦¬μ νμ΄μ§</span>
|
| 467 |
</div>
|
| 468 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 469 |
<div class="header-actions">
|
| 470 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 471 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 472 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
| 473 |
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©μμ§ νμΈ</a>
|
| 474 |
<a href="{{ url_for('main.admin_prompts') }}" class="btn btn-secondary">ν둬ννΈ κ΄λ¦¬</a>
|
| 475 |
+
<a href="{{ url_for('main.admin_settings') }}" class="btn btn-secondary">AI μ€μ </a>
|
| 476 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 477 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 478 |
</div>
|
| 479 |
</div>
|
| 480 |
|
| 481 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 482 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 483 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 484 |
+
<div class="mobile-menu-header">
|
| 485 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 486 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 487 |
+
</div>
|
| 488 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 489 |
+
<div class="mobile-menu-items">
|
| 490 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ¬μ©μ κ΄λ¦¬</a>
|
| 491 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μΉμμ€ κ΄λ¦¬</a>
|
| 492 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μμ§ νμΈ</a>
|
| 493 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ν둬ννΈ κ΄λ¦¬</a>
|
| 494 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI μ€μ </a>
|
| 495 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μΈμΌλ‘</a>
|
| 496 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
<div class="container">
|
| 502 |
<div class="page-header">
|
| 503 |
<h1>νμΌ λͺ©λ‘</h1>
|
|
|
|
| 665 |
<script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
|
| 666 |
|
| 667 |
<script>
|
| 668 |
+
function toggleMobileMenu() {
|
| 669 |
+
const menu = document.getElementById('mobileMenu');
|
| 670 |
+
menu.classList.toggle('active');
|
| 671 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
function closeMobileMenu() {
|
| 675 |
+
const menu = document.getElementById('mobileMenu');
|
| 676 |
+
menu.classList.remove('active');
|
| 677 |
+
document.body.style.overflow = '';
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 681 |
+
if (event.target.id === 'mobileMenu') {
|
| 682 |
+
closeMobileMenu();
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
function escapeHtml(text) {
|
| 687 |
const div = document.createElement('div');
|
| 688 |
div.textContent = text;
|
templates/admin_messages.html
CHANGED
|
@@ -43,6 +43,126 @@
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
.btn {
|
| 47 |
padding: 8px 16px;
|
| 48 |
border: none;
|
|
@@ -346,16 +466,39 @@
|
|
| 346 |
<span>π€</span>
|
| 347 |
<span>SOY NV AI λ©μμ§ νμΈ</span>
|
| 348 |
</div>
|
|
|
|
| 349 |
<div class="header-actions">
|
| 350 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 351 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 352 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
|
|
|
| 353 |
<a href="{{ url_for('main.admin_prompts') }}" class="btn btn-secondary">ν둬ννΈ κ΄λ¦¬</a>
|
|
|
|
| 354 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 355 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 356 |
</div>
|
| 357 |
</div>
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
<div class="container">
|
| 360 |
<div class="page-header">
|
| 361 |
<h1>μ 체 λ©μμ§ νμΈ</h1>
|
|
@@ -460,6 +603,24 @@
|
|
| 460 |
</div>
|
| 461 |
|
| 462 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
let currentSessionsPage = 1;
|
| 464 |
let currentMessagesPage = 1;
|
| 465 |
let selectedSessionId = null;
|
|
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
| 46 |
+
.menu-toggle {
|
| 47 |
+
display: none;
|
| 48 |
+
background: none;
|
| 49 |
+
border: none;
|
| 50 |
+
font-size: 24px;
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
padding: 8px;
|
| 53 |
+
color: #202124;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.mobile-menu {
|
| 57 |
+
display: none;
|
| 58 |
+
position: fixed;
|
| 59 |
+
top: 0;
|
| 60 |
+
left: 0;
|
| 61 |
+
right: 0;
|
| 62 |
+
bottom: 0;
|
| 63 |
+
background: rgba(0, 0, 0, 0.5);
|
| 64 |
+
z-index: 1000;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.mobile-menu.active {
|
| 68 |
+
display: block;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.mobile-menu-content {
|
| 72 |
+
position: fixed;
|
| 73 |
+
top: 0;
|
| 74 |
+
right: -100%;
|
| 75 |
+
width: 280px;
|
| 76 |
+
max-width: 80%;
|
| 77 |
+
height: 100%;
|
| 78 |
+
background: white;
|
| 79 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 80 |
+
transition: right 0.3s ease;
|
| 81 |
+
overflow-y: auto;
|
| 82 |
+
z-index: 1001;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.mobile-menu.active .mobile-menu-content {
|
| 86 |
+
right: 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.mobile-menu-header {
|
| 90 |
+
padding: 16px 20px;
|
| 91 |
+
border-bottom: 1px solid #dadce0;
|
| 92 |
+
display: flex;
|
| 93 |
+
justify-content: space-between;
|
| 94 |
+
align-items: center;
|
| 95 |
+
background: white;
|
| 96 |
+
position: sticky;
|
| 97 |
+
top: 0;
|
| 98 |
+
z-index: 10;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.mobile-menu-title {
|
| 102 |
+
font-size: 18px;
|
| 103 |
+
font-weight: 500;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.mobile-menu-close {
|
| 107 |
+
background: none;
|
| 108 |
+
border: none;
|
| 109 |
+
font-size: 24px;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
color: #202124;
|
| 112 |
+
padding: 0;
|
| 113 |
+
width: 32px;
|
| 114 |
+
height: 32px;
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
justify-content: center;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.mobile-menu-items {
|
| 121 |
+
padding: 8px 0;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.mobile-menu-item {
|
| 125 |
+
display: block;
|
| 126 |
+
padding: 12px 20px;
|
| 127 |
+
color: #202124;
|
| 128 |
+
text-decoration: none;
|
| 129 |
+
border-bottom: 1px solid #f1f3f4;
|
| 130 |
+
transition: background 0.2s;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.mobile-menu-item:hover {
|
| 134 |
+
background: #f8f9fa;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.mobile-menu-user {
|
| 138 |
+
padding: 16px 20px;
|
| 139 |
+
border-bottom: 1px solid #dadce0;
|
| 140 |
+
color: #5f6368;
|
| 141 |
+
font-size: 14px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
@media (max-width: 768px) {
|
| 145 |
+
.header {
|
| 146 |
+
padding: 12px 16px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header-title {
|
| 150 |
+
font-size: 18px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.header-title span:first-child {
|
| 154 |
+
display: none;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.menu-toggle {
|
| 158 |
+
display: block;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.header-actions {
|
| 162 |
+
display: none;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
.btn {
|
| 167 |
padding: 8px 16px;
|
| 168 |
border: none;
|
|
|
|
| 466 |
<span>π€</span>
|
| 467 |
<span>SOY NV AI λ©μμ§ νμΈ</span>
|
| 468 |
</div>
|
| 469 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 470 |
<div class="header-actions">
|
| 471 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 472 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 473 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
| 474 |
+
<a href="{{ url_for('main.admin_files') }}" class="btn btn-secondary">νμΌ λͺ©λ‘</a>
|
| 475 |
<a href="{{ url_for('main.admin_prompts') }}" class="btn btn-secondary">ν둬ννΈ κ΄λ¦¬</a>
|
| 476 |
+
<a href="{{ url_for('main.admin_settings') }}" class="btn btn-secondary">AI μ€μ </a>
|
| 477 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 478 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 479 |
</div>
|
| 480 |
</div>
|
| 481 |
|
| 482 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 483 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 484 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 485 |
+
<div class="mobile-menu-header">
|
| 486 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 487 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 490 |
+
<div class="mobile-menu-items">
|
| 491 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ¬μ©μ κ΄λ¦¬</a>
|
| 492 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μΉμμ€ κ΄λ¦¬</a>
|
| 493 |
+
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νμΌ λͺ©λ‘</a>
|
| 494 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ν둬ννΈ κ΄λ¦¬</a>
|
| 495 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI μ€μ </a>
|
| 496 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μΈμΌλ‘</a>
|
| 497 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
|
| 502 |
<div class="container">
|
| 503 |
<div class="page-header">
|
| 504 |
<h1>μ 체 λ©μμ§ νμΈ</h1>
|
|
|
|
| 603 |
</div>
|
| 604 |
|
| 605 |
<script>
|
| 606 |
+
function toggleMobileMenu() {
|
| 607 |
+
const menu = document.getElementById('mobileMenu');
|
| 608 |
+
menu.classList.toggle('active');
|
| 609 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
function closeMobileMenu() {
|
| 613 |
+
const menu = document.getElementById('mobileMenu');
|
| 614 |
+
menu.classList.remove('active');
|
| 615 |
+
document.body.style.overflow = '';
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 619 |
+
if (event.target.id === 'mobileMenu') {
|
| 620 |
+
closeMobileMenu();
|
| 621 |
+
}
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
let currentSessionsPage = 1;
|
| 625 |
let currentMessagesPage = 1;
|
| 626 |
let selectedSessionId = null;
|
templates/admin_prompts.html
CHANGED
|
@@ -45,6 +45,126 @@
|
|
| 45 |
gap: 8px;
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
.btn {
|
| 49 |
padding: 8px 16px;
|
| 50 |
border: none;
|
|
@@ -200,16 +320,39 @@
|
|
| 200 |
<span>π</span>
|
| 201 |
<span>ν둬ννΈ κ΄λ¦¬</span>
|
| 202 |
</div>
|
|
|
|
| 203 |
<div class="header-actions">
|
| 204 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 205 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 206 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
|
|
|
| 207 |
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©μμ§ νμΈ</a>
|
|
|
|
| 208 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 209 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 210 |
</div>
|
| 211 |
</div>
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
<div class="container">
|
| 214 |
<div class="page-header">
|
| 215 |
<h1>μμ€ν
ν둬ννΈ κ΄λ¦¬</h1>
|
|
@@ -243,6 +386,24 @@
|
|
| 243 |
</div>
|
| 244 |
|
| 245 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
// νμ΄μ§ λ‘λ μ ν둬ννΈ λΆλ¬μ€κΈ°
|
| 247 |
window.addEventListener('DOMContentLoaded', () => {
|
| 248 |
loadPrompt();
|
|
|
|
| 45 |
gap: 8px;
|
| 46 |
}
|
| 47 |
|
| 48 |
+
.menu-toggle {
|
| 49 |
+
display: none;
|
| 50 |
+
background: none;
|
| 51 |
+
border: none;
|
| 52 |
+
font-size: 24px;
|
| 53 |
+
cursor: pointer;
|
| 54 |
+
padding: 8px;
|
| 55 |
+
color: #202124;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.mobile-menu {
|
| 59 |
+
display: none;
|
| 60 |
+
position: fixed;
|
| 61 |
+
top: 0;
|
| 62 |
+
left: 0;
|
| 63 |
+
right: 0;
|
| 64 |
+
bottom: 0;
|
| 65 |
+
background: rgba(0, 0, 0, 0.5);
|
| 66 |
+
z-index: 1000;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.mobile-menu.active {
|
| 70 |
+
display: block;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.mobile-menu-content {
|
| 74 |
+
position: fixed;
|
| 75 |
+
top: 0;
|
| 76 |
+
right: -100%;
|
| 77 |
+
width: 280px;
|
| 78 |
+
max-width: 80%;
|
| 79 |
+
height: 100%;
|
| 80 |
+
background: white;
|
| 81 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 82 |
+
transition: right 0.3s ease;
|
| 83 |
+
overflow-y: auto;
|
| 84 |
+
z-index: 1001;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.mobile-menu.active .mobile-menu-content {
|
| 88 |
+
right: 0;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.mobile-menu-header {
|
| 92 |
+
padding: 16px 20px;
|
| 93 |
+
border-bottom: 1px solid #dadce0;
|
| 94 |
+
display: flex;
|
| 95 |
+
justify-content: space-between;
|
| 96 |
+
align-items: center;
|
| 97 |
+
background: white;
|
| 98 |
+
position: sticky;
|
| 99 |
+
top: 0;
|
| 100 |
+
z-index: 10;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.mobile-menu-title {
|
| 104 |
+
font-size: 18px;
|
| 105 |
+
font-weight: 500;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.mobile-menu-close {
|
| 109 |
+
background: none;
|
| 110 |
+
border: none;
|
| 111 |
+
font-size: 24px;
|
| 112 |
+
cursor: pointer;
|
| 113 |
+
color: #202124;
|
| 114 |
+
padding: 0;
|
| 115 |
+
width: 32px;
|
| 116 |
+
height: 32px;
|
| 117 |
+
display: flex;
|
| 118 |
+
align-items: center;
|
| 119 |
+
justify-content: center;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.mobile-menu-items {
|
| 123 |
+
padding: 8px 0;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.mobile-menu-item {
|
| 127 |
+
display: block;
|
| 128 |
+
padding: 12px 20px;
|
| 129 |
+
color: #202124;
|
| 130 |
+
text-decoration: none;
|
| 131 |
+
border-bottom: 1px solid #f1f3f4;
|
| 132 |
+
transition: background 0.2s;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.mobile-menu-item:hover {
|
| 136 |
+
background: #f8f9fa;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.mobile-menu-user {
|
| 140 |
+
padding: 16px 20px;
|
| 141 |
+
border-bottom: 1px solid #dadce0;
|
| 142 |
+
color: #5f6368;
|
| 143 |
+
font-size: 14px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
@media (max-width: 768px) {
|
| 147 |
+
.header {
|
| 148 |
+
padding: 12px 16px;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.header-title {
|
| 152 |
+
font-size: 18px;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.header-title span:first-child {
|
| 156 |
+
display: none;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.menu-toggle {
|
| 160 |
+
display: block;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.header-actions {
|
| 164 |
+
display: none;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
.btn {
|
| 169 |
padding: 8px 16px;
|
| 170 |
border: none;
|
|
|
|
| 320 |
<span>π</span>
|
| 321 |
<span>ν둬ννΈ κ΄λ¦¬</span>
|
| 322 |
</div>
|
| 323 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 324 |
<div class="header-actions">
|
| 325 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 326 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 327 |
<a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">μΉμμ€ κ΄λ¦¬</a>
|
| 328 |
+
<a href="{{ url_for('main.admin_files') }}" class="btn btn-secondary">νμΌ λͺ©λ‘</a>
|
| 329 |
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©μμ§ νμΈ</a>
|
| 330 |
+
<a href="{{ url_for('main.admin_settings') }}" class="btn btn-secondary">AI μ€μ </a>
|
| 331 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 332 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 333 |
</div>
|
| 334 |
</div>
|
| 335 |
|
| 336 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 337 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 338 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 339 |
+
<div class="mobile-menu-header">
|
| 340 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 341 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 342 |
+
</div>
|
| 343 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 344 |
+
<div class="mobile-menu-items">
|
| 345 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ¬μ©μ κ΄λ¦¬</a>
|
| 346 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μΉμμ€ κ΄λ¦¬</a>
|
| 347 |
+
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νμΌ λͺ©λ‘</a>
|
| 348 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μμ§ νμΈ</a>
|
| 349 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI μ€μ </a>
|
| 350 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μΈμΌλ‘</a>
|
| 351 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
<div class="container">
|
| 357 |
<div class="page-header">
|
| 358 |
<h1>μμ€ν
ν둬ννΈ κ΄λ¦¬</h1>
|
|
|
|
| 386 |
</div>
|
| 387 |
|
| 388 |
<script>
|
| 389 |
+
function toggleMobileMenu() {
|
| 390 |
+
const menu = document.getElementById('mobileMenu');
|
| 391 |
+
menu.classList.toggle('active');
|
| 392 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
function closeMobileMenu() {
|
| 396 |
+
const menu = document.getElementById('mobileMenu');
|
| 397 |
+
menu.classList.remove('active');
|
| 398 |
+
document.body.style.overflow = '';
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 402 |
+
if (event.target.id === 'mobileMenu') {
|
| 403 |
+
closeMobileMenu();
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
// νμ΄μ§ λ‘λ μ ν둬ννΈ λΆλ¬μ€κΈ°
|
| 408 |
window.addEventListener('DOMContentLoaded', () => {
|
| 409 |
loadPrompt();
|
templates/admin_settings.html
CHANGED
|
@@ -43,6 +43,126 @@
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
.btn {
|
| 47 |
padding: 8px 16px;
|
| 48 |
border: none;
|
|
@@ -163,6 +283,7 @@
|
|
| 163 |
<span>βοΈ</span>
|
| 164 |
<span>AI μ€μ κ΄λ¦¬</span>
|
| 165 |
</div>
|
|
|
|
| 166 |
<div class="header-actions">
|
| 167 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 168 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
|
@@ -175,6 +296,26 @@
|
|
| 175 |
</div>
|
| 176 |
</div>
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
<div class="container">
|
| 179 |
<div class="page-header">
|
| 180 |
<h1>AI μ€μ κ΄λ¦¬</h1>
|
|
@@ -220,6 +361,24 @@
|
|
| 220 |
</div>
|
| 221 |
|
| 222 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
function showAlert(message, type = 'success') {
|
| 224 |
const container = document.getElementById('alertContainer');
|
| 225 |
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
|
|
|
|
| 43 |
align-items: center;
|
| 44 |
}
|
| 45 |
|
| 46 |
+
.menu-toggle {
|
| 47 |
+
display: none;
|
| 48 |
+
background: none;
|
| 49 |
+
border: none;
|
| 50 |
+
font-size: 24px;
|
| 51 |
+
cursor: pointer;
|
| 52 |
+
padding: 8px;
|
| 53 |
+
color: #202124;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.mobile-menu {
|
| 57 |
+
display: none;
|
| 58 |
+
position: fixed;
|
| 59 |
+
top: 0;
|
| 60 |
+
left: 0;
|
| 61 |
+
right: 0;
|
| 62 |
+
bottom: 0;
|
| 63 |
+
background: rgba(0, 0, 0, 0.5);
|
| 64 |
+
z-index: 1000;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.mobile-menu.active {
|
| 68 |
+
display: block;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.mobile-menu-content {
|
| 72 |
+
position: fixed;
|
| 73 |
+
top: 0;
|
| 74 |
+
right: -100%;
|
| 75 |
+
width: 280px;
|
| 76 |
+
max-width: 80%;
|
| 77 |
+
height: 100%;
|
| 78 |
+
background: white;
|
| 79 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 80 |
+
transition: right 0.3s ease;
|
| 81 |
+
overflow-y: auto;
|
| 82 |
+
z-index: 1001;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.mobile-menu.active .mobile-menu-content {
|
| 86 |
+
right: 0;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.mobile-menu-header {
|
| 90 |
+
padding: 16px 20px;
|
| 91 |
+
border-bottom: 1px solid #dadce0;
|
| 92 |
+
display: flex;
|
| 93 |
+
justify-content: space-between;
|
| 94 |
+
align-items: center;
|
| 95 |
+
background: white;
|
| 96 |
+
position: sticky;
|
| 97 |
+
top: 0;
|
| 98 |
+
z-index: 10;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.mobile-menu-title {
|
| 102 |
+
font-size: 18px;
|
| 103 |
+
font-weight: 500;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.mobile-menu-close {
|
| 107 |
+
background: none;
|
| 108 |
+
border: none;
|
| 109 |
+
font-size: 24px;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
color: #202124;
|
| 112 |
+
padding: 0;
|
| 113 |
+
width: 32px;
|
| 114 |
+
height: 32px;
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
justify-content: center;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.mobile-menu-items {
|
| 121 |
+
padding: 8px 0;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.mobile-menu-item {
|
| 125 |
+
display: block;
|
| 126 |
+
padding: 12px 20px;
|
| 127 |
+
color: #202124;
|
| 128 |
+
text-decoration: none;
|
| 129 |
+
border-bottom: 1px solid #f1f3f4;
|
| 130 |
+
transition: background 0.2s;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.mobile-menu-item:hover {
|
| 134 |
+
background: #f8f9fa;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.mobile-menu-user {
|
| 138 |
+
padding: 16px 20px;
|
| 139 |
+
border-bottom: 1px solid #dadce0;
|
| 140 |
+
color: #5f6368;
|
| 141 |
+
font-size: 14px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
@media (max-width: 768px) {
|
| 145 |
+
.header {
|
| 146 |
+
padding: 12px 16px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header-title {
|
| 150 |
+
font-size: 18px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.header-title span:first-child {
|
| 154 |
+
display: none;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.menu-toggle {
|
| 158 |
+
display: block;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.header-actions {
|
| 162 |
+
display: none;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
.btn {
|
| 167 |
padding: 8px 16px;
|
| 168 |
border: none;
|
|
|
|
| 283 |
<span>βοΈ</span>
|
| 284 |
<span>AI μ€μ κ΄λ¦¬</span>
|
| 285 |
</div>
|
| 286 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 287 |
<div class="header-actions">
|
| 288 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 289 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
|
|
|
| 296 |
</div>
|
| 297 |
</div>
|
| 298 |
|
| 299 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 300 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 301 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 302 |
+
<div class="mobile-menu-header">
|
| 303 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 304 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 305 |
+
</div>
|
| 306 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 307 |
+
<div class="mobile-menu-items">
|
| 308 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ¬μ©μ κ΄λ¦¬</a>
|
| 309 |
+
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μΉμμ€ κ΄λ¦¬</a>
|
| 310 |
+
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νμΌ λͺ©λ‘</a>
|
| 311 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μμ§ νμΈ</a>
|
| 312 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ν둬ννΈ κ΄λ¦¬</a>
|
| 313 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μΈμΌλ‘</a>
|
| 314 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
|
| 319 |
<div class="container">
|
| 320 |
<div class="page-header">
|
| 321 |
<h1>AI μ€μ κ΄λ¦¬</h1>
|
|
|
|
| 361 |
</div>
|
| 362 |
|
| 363 |
<script>
|
| 364 |
+
function toggleMobileMenu() {
|
| 365 |
+
const menu = document.getElementById('mobileMenu');
|
| 366 |
+
menu.classList.toggle('active');
|
| 367 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
function closeMobileMenu() {
|
| 371 |
+
const menu = document.getElementById('mobileMenu');
|
| 372 |
+
menu.classList.remove('active');
|
| 373 |
+
document.body.style.overflow = '';
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 377 |
+
if (event.target.id === 'mobileMenu') {
|
| 378 |
+
closeMobileMenu();
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
function showAlert(message, type = 'success') {
|
| 383 |
const container = document.getElementById('alertContainer');
|
| 384 |
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
|
templates/admin_webnovels.html
CHANGED
|
@@ -46,6 +46,126 @@
|
|
| 46 |
flex-wrap: wrap;
|
| 47 |
}
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
.btn {
|
| 50 |
padding: 8px 16px;
|
| 51 |
border: none;
|
|
@@ -160,6 +280,7 @@
|
|
| 160 |
border-radius: 6px;
|
| 161 |
margin-bottom: 16px;
|
| 162 |
font-size: 14px;
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
.alert.error {
|
|
@@ -172,6 +293,40 @@
|
|
| 172 |
color: #137333;
|
| 173 |
}
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
/* νμΌ μ
λ‘λ μμ */
|
| 176 |
.file-upload-section {
|
| 177 |
margin-top: 24px;
|
|
@@ -454,17 +609,39 @@
|
|
| 454 |
<span>π</span>
|
| 455 |
<span>μΉμμ€ κ΄λ¦¬</span>
|
| 456 |
</div>
|
|
|
|
| 457 |
<div class="header-actions">
|
| 458 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 459 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 460 |
<a href="{{ url_for('main.admin_files') }}" class="btn btn-secondary">νμΌ λͺ©λ‘</a>
|
| 461 |
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©μμ§ νμΈ</a>
|
| 462 |
<a href="{{ url_for('main.admin_prompts') }}" class="btn btn-secondary">ν둬ννΈ κ΄λ¦¬</a>
|
|
|
|
| 463 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 464 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 465 |
</div>
|
| 466 |
</div>
|
| 467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
<div class="container">
|
| 469 |
<div class="page-header">
|
| 470 |
<h1>μΉμμ€ νμ΅ νμΌ κ΄λ¦¬</h1>
|
|
@@ -557,12 +734,13 @@
|
|
| 557 |
<th>μ°κ²°λ λͺ¨λΈ</th>
|
| 558 |
<th>ν¬κΈ°</th>
|
| 559 |
<th>μ
λ‘λμΌ</th>
|
|
|
|
| 560 |
<th>μμ
</th>
|
| 561 |
</tr>
|
| 562 |
</thead>
|
| 563 |
<tbody id="filesTableBody">
|
| 564 |
<tr>
|
| 565 |
-
<td colspan="
|
| 566 |
</tr>
|
| 567 |
</tbody>
|
| 568 |
</table>
|
|
@@ -584,12 +762,60 @@
|
|
| 584 |
</div>
|
| 585 |
|
| 586 |
<script>
|
| 587 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
const container = document.getElementById('alertContainer');
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
}
|
| 594 |
|
| 595 |
// νμΌ μ
λ‘λ κ΄λ ¨
|
|
@@ -748,11 +974,22 @@
|
|
| 748 |
fileNameDisplay += ` <span style="color: #1a73e8; font-size: 12px;">(${childCount}κ° νμ°¨)</span>`;
|
| 749 |
}
|
| 750 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
row.innerHTML = `
|
| 752 |
<td>${fileNameDisplay}</td>
|
| 753 |
<td>${modelName}</td>
|
| 754 |
<td class="file-size">${fileSize}</td>
|
| 755 |
<td>${uploadDate}</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
<td>
|
| 757 |
<div class="file-actions">
|
| 758 |
${(file.has_parent_chunk !== undefined && file.has_parent_chunk) ?
|
|
@@ -773,6 +1010,11 @@
|
|
| 773 |
const childFileSize = formatFileSize(childFile.file_size);
|
| 774 |
const childUploadDate = new Date(childFile.uploaded_at).toLocaleString('ko-KR');
|
| 775 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
childRow.innerHTML = `
|
| 777 |
<td style="padding-left: 32px; color: #5f6368;">
|
| 778 |
<span style="margin-right: 8px;">ββ</span>${escapeHtml(childFile.original_filename)}
|
|
@@ -780,6 +1022,12 @@
|
|
| 780 |
<td>${modelName}</td>
|
| 781 |
<td class="file-size">${childFileSize}</td>
|
| 782 |
<td>${childUploadDate}</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
<td>
|
| 784 |
<div class="file-actions">
|
| 785 |
<button class="btn btn-secondary" onclick="deleteFile(${childFile.id})" style="padding: 4px 8px; font-size: 12px;">μμ </button>
|
|
@@ -975,8 +1223,13 @@
|
|
| 975 |
console.log(`[λ¨κ³ 1] fetch νΈμΆ μμ: /api/upload`);
|
| 976 |
console.log(`[λ¨κ³ 1] FormData νλͺ©:`, Array.from(formData.entries()).map(([k, v]) => [k, v instanceof File ? v.name : v]));
|
| 977 |
|
| 978 |
-
// νμμμμ΄ μλ fetch λνΌ (
|
| 979 |
-
const fetchWithTimeout = (url, options, timeout =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
return Promise.race([
|
| 981 |
fetch(url, options),
|
| 982 |
new Promise((_, reject) =>
|
|
@@ -989,7 +1242,7 @@
|
|
| 989 |
method: 'POST',
|
| 990 |
body: formData,
|
| 991 |
credentials: 'include' // μΏ ν€ ν¬ν¨ (μΈμ
μΈμ¦)
|
| 992 |
-
},
|
| 993 |
|
| 994 |
console.log(`[λ¨κ³ 1] fetch μλ΅ μμ : ${response.status} ${response.statusText}`);
|
| 995 |
|
|
@@ -1049,42 +1302,101 @@
|
|
| 1049 |
itemElement.style.opacity = '0.7';
|
| 1050 |
console.log(`[μ
λ‘λ μ±κ³΅] ${file.name} β λͺ¨λΈ: ${modelName}`);
|
| 1051 |
} else {
|
| 1052 |
-
// λ¨κ³λ³ μ²λ¦¬ ν¨μ
|
| 1053 |
-
const processStep = async (stepName, stepNumber, url, timeout, detailText = '') => {
|
| 1054 |
updateProgressStatus(i, stepNumber, stepName, files.length, detailText);
|
| 1055 |
updateOverallStatus(i + 1, files.length, stepNumber, detailText);
|
| 1056 |
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
}
|
| 1066 |
-
|
| 1067 |
-
return await stepResponse.json();
|
| 1068 |
};
|
| 1069 |
|
|
|
|
|
|
|
|
|
|
| 1070 |
try {
|
| 1071 |
-
// λ¨κ³ 1: Parent Chunk μμ± (
|
| 1072 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
|
| 1074 |
-
// λ¨κ³ 2: Chunk μμ± (
|
| 1075 |
-
|
| 1076 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
|
| 1078 |
-
// λ¨κ³ 3: νμ°¨ λΆμ (
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1082 |
|
| 1083 |
-
// λ¨κ³ 4: Graph Extraction (
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
|
|
|
|
| 1088 |
successCount++;
|
| 1089 |
const modelName = data.model_name || 'μ μ μμ';
|
| 1090 |
|
|
@@ -1093,12 +1405,21 @@
|
|
| 1093 |
updateOverallStatus(i + 1, files.length, 9);
|
| 1094 |
|
| 1095 |
statusElement.className = 'progress-item-status success';
|
|
|
|
|
|
|
|
|
|
| 1096 |
statusElement.innerHTML = `β μλ£ (νμΌ ${i + 1}/${files.length})`;
|
| 1097 |
-
statusElement.title =
|
| 1098 |
itemElement.style.opacity = '0.7';
|
| 1099 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1100 |
} catch (stepError) {
|
| 1101 |
-
//
|
|
|
|
| 1102 |
throw stepError;
|
| 1103 |
}
|
| 1104 |
}
|
|
@@ -1234,6 +1555,31 @@
|
|
| 1234 |
}
|
| 1235 |
|
| 1236 |
// νμΌ μμ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
async function deleteFile(fileId) {
|
| 1238 |
if (!confirm('μ΄ νμΌμ μμ νμκ² μ΅λκΉ?\nμλ³Έ νμΌμΈ κ²½μ° μ΄μ΄μ μ
λ‘λν λͺ¨λ νμ°¨λ ν¨κ» μμ λ©λλ€.')) {
|
| 1239 |
return;
|
|
|
|
| 46 |
flex-wrap: wrap;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
.menu-toggle {
|
| 50 |
+
display: none;
|
| 51 |
+
background: none;
|
| 52 |
+
border: none;
|
| 53 |
+
font-size: 24px;
|
| 54 |
+
cursor: pointer;
|
| 55 |
+
padding: 8px;
|
| 56 |
+
color: #202124;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.mobile-menu {
|
| 60 |
+
display: none;
|
| 61 |
+
position: fixed;
|
| 62 |
+
top: 0;
|
| 63 |
+
left: 0;
|
| 64 |
+
right: 0;
|
| 65 |
+
bottom: 0;
|
| 66 |
+
background: rgba(0, 0, 0, 0.5);
|
| 67 |
+
z-index: 1000;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.mobile-menu.active {
|
| 71 |
+
display: block;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.mobile-menu-content {
|
| 75 |
+
position: fixed;
|
| 76 |
+
top: 0;
|
| 77 |
+
right: -100%;
|
| 78 |
+
width: 280px;
|
| 79 |
+
max-width: 80%;
|
| 80 |
+
height: 100%;
|
| 81 |
+
background: white;
|
| 82 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 83 |
+
transition: right 0.3s ease;
|
| 84 |
+
overflow-y: auto;
|
| 85 |
+
z-index: 1001;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.mobile-menu.active .mobile-menu-content {
|
| 89 |
+
right: 0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.mobile-menu-header {
|
| 93 |
+
padding: 16px 20px;
|
| 94 |
+
border-bottom: 1px solid #dadce0;
|
| 95 |
+
display: flex;
|
| 96 |
+
justify-content: space-between;
|
| 97 |
+
align-items: center;
|
| 98 |
+
background: white;
|
| 99 |
+
position: sticky;
|
| 100 |
+
top: 0;
|
| 101 |
+
z-index: 10;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.mobile-menu-title {
|
| 105 |
+
font-size: 18px;
|
| 106 |
+
font-weight: 500;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.mobile-menu-close {
|
| 110 |
+
background: none;
|
| 111 |
+
border: none;
|
| 112 |
+
font-size: 24px;
|
| 113 |
+
cursor: pointer;
|
| 114 |
+
color: #202124;
|
| 115 |
+
padding: 0;
|
| 116 |
+
width: 32px;
|
| 117 |
+
height: 32px;
|
| 118 |
+
display: flex;
|
| 119 |
+
align-items: center;
|
| 120 |
+
justify-content: center;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.mobile-menu-items {
|
| 124 |
+
padding: 8px 0;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.mobile-menu-item {
|
| 128 |
+
display: block;
|
| 129 |
+
padding: 12px 20px;
|
| 130 |
+
color: #202124;
|
| 131 |
+
text-decoration: none;
|
| 132 |
+
border-bottom: 1px solid #f1f3f4;
|
| 133 |
+
transition: background 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.mobile-menu-item:hover {
|
| 137 |
+
background: #f8f9fa;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.mobile-menu-user {
|
| 141 |
+
padding: 16px 20px;
|
| 142 |
+
border-bottom: 1px solid #dadce0;
|
| 143 |
+
color: #5f6368;
|
| 144 |
+
font-size: 14px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
@media (max-width: 768px) {
|
| 148 |
+
.header {
|
| 149 |
+
padding: 12px 16px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.header-title {
|
| 153 |
+
font-size: 18px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.header-title span:first-child {
|
| 157 |
+
display: none;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.menu-toggle {
|
| 161 |
+
display: block;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.header-actions {
|
| 165 |
+
display: none;
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
.btn {
|
| 170 |
padding: 8px 16px;
|
| 171 |
border: none;
|
|
|
|
| 280 |
border-radius: 6px;
|
| 281 |
margin-bottom: 16px;
|
| 282 |
font-size: 14px;
|
| 283 |
+
position: relative;
|
| 284 |
}
|
| 285 |
|
| 286 |
.alert.error {
|
|
|
|
| 293 |
color: #137333;
|
| 294 |
}
|
| 295 |
|
| 296 |
+
.alert.warning {
|
| 297 |
+
background: #fef7e0;
|
| 298 |
+
color: #b06000;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.alert-close {
|
| 302 |
+
position: absolute;
|
| 303 |
+
top: 8px;
|
| 304 |
+
right: 8px;
|
| 305 |
+
background: none;
|
| 306 |
+
border: none;
|
| 307 |
+
font-size: 18px;
|
| 308 |
+
cursor: pointer;
|
| 309 |
+
color: inherit;
|
| 310 |
+
opacity: 0.7;
|
| 311 |
+
padding: 0;
|
| 312 |
+
width: 24px;
|
| 313 |
+
height: 24px;
|
| 314 |
+
display: flex;
|
| 315 |
+
align-items: center;
|
| 316 |
+
justify-content: center;
|
| 317 |
+
border-radius: 4px;
|
| 318 |
+
transition: opacity 0.2s, background-color 0.2s;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.alert-close:hover {
|
| 322 |
+
opacity: 1;
|
| 323 |
+
background-color: rgba(0, 0, 0, 0.1);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.alert-persistent {
|
| 327 |
+
padding-right: 40px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
/* νμΌ μ
λ‘λ μμ */
|
| 331 |
.file-upload-section {
|
| 332 |
margin-top: 24px;
|
|
|
|
| 609 |
<span>π</span>
|
| 610 |
<span>μΉμμ€ κ΄λ¦¬</span>
|
| 611 |
</div>
|
| 612 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 613 |
<div class="header-actions">
|
| 614 |
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
|
| 615 |
<a href="{{ url_for('main.admin') }}" class="btn btn-secondary">μ¬μ©μ κ΄λ¦¬</a>
|
| 616 |
<a href="{{ url_for('main.admin_files') }}" class="btn btn-secondary">νμΌ λͺ©λ‘</a>
|
| 617 |
<a href="{{ url_for('main.admin_messages') }}" class="btn btn-secondary">λ©μμ§ νμΈ</a>
|
| 618 |
<a href="{{ url_for('main.admin_prompts') }}" class="btn btn-secondary">ν둬ννΈ κ΄λ¦¬</a>
|
| 619 |
+
<a href="{{ url_for('main.admin_settings') }}" class="btn btn-secondary">AI μ€μ </a>
|
| 620 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">λ©μΈμΌλ‘</a>
|
| 621 |
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">λ‘κ·Έμμ</a>
|
| 622 |
</div>
|
| 623 |
</div>
|
| 624 |
|
| 625 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 626 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 627 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 628 |
+
<div class="mobile-menu-header">
|
| 629 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 630 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 631 |
+
</div>
|
| 632 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 633 |
+
<div class="mobile-menu-items">
|
| 634 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ¬μ©μ κ΄λ¦¬</a>
|
| 635 |
+
<a href="{{ url_for('main.admin_files') }}" class="mobile-menu-item" onclick="closeMobileMenu()">νμΌ λͺ©λ‘</a>
|
| 636 |
+
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μμ§ νμΈ</a>
|
| 637 |
+
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ν둬ννΈ κ΄λ¦¬</a>
|
| 638 |
+
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI μ€μ </a>
|
| 639 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ©μΈμΌλ‘</a>
|
| 640 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 641 |
+
</div>
|
| 642 |
+
</div>
|
| 643 |
+
</div>
|
| 644 |
+
|
| 645 |
<div class="container">
|
| 646 |
<div class="page-header">
|
| 647 |
<h1>μΉμμ€ νμ΅ νμΌ κ΄λ¦¬</h1>
|
|
|
|
| 734 |
<th>μ°κ²°λ λͺ¨λΈ</th>
|
| 735 |
<th>ν¬κΈ°</th>
|
| 736 |
<th>μ
λ‘λμΌ</th>
|
| 737 |
+
<th>κ³΅κ° μ¬λΆ</th>
|
| 738 |
<th>μμ
</th>
|
| 739 |
</tr>
|
| 740 |
</thead>
|
| 741 |
<tbody id="filesTableBody">
|
| 742 |
<tr>
|
| 743 |
+
<td colspan="6" style="text-align: center; padding: 20px; color: #5f6368;">νμΌ λͺ©λ‘μ λΆλ¬μ€λ μ€...</td>
|
| 744 |
</tr>
|
| 745 |
</tbody>
|
| 746 |
</table>
|
|
|
|
| 762 |
</div>
|
| 763 |
|
| 764 |
<script>
|
| 765 |
+
function toggleMobileMenu() {
|
| 766 |
+
const menu = document.getElementById('mobileMenu');
|
| 767 |
+
menu.classList.toggle('active');
|
| 768 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
function closeMobileMenu() {
|
| 772 |
+
const menu = document.getElementById('mobileMenu');
|
| 773 |
+
menu.classList.remove('active');
|
| 774 |
+
document.body.style.overflow = '';
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 778 |
+
if (event.target.id === 'mobileMenu') {
|
| 779 |
+
closeMobileMenu();
|
| 780 |
+
}
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
function showAlert(message, type = 'success', autoClose = true) {
|
| 784 |
+
const container = document.getElementById('alertContainer');
|
| 785 |
+
const alertId = `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 786 |
+
const closeButton = autoClose ? '' : '<button class="alert-close" onclick="closeAlert(\'' + alertId + '\')" title="λ«κΈ°">×</button>';
|
| 787 |
+
const persistentClass = autoClose ? '' : ' alert-persistent';
|
| 788 |
+
container.innerHTML = `<div id="${alertId}" class="alert ${type}${persistentClass}">${closeButton}${message}</div>`;
|
| 789 |
+
|
| 790 |
+
if (autoClose) {
|
| 791 |
+
setTimeout(() => {
|
| 792 |
+
const alertElement = document.getElementById(alertId);
|
| 793 |
+
if (alertElement) {
|
| 794 |
+
alertElement.remove();
|
| 795 |
+
}
|
| 796 |
+
}, 5000);
|
| 797 |
+
}
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
function closeAlert(alertId) {
|
| 801 |
+
const alertElement = document.getElementById(alertId);
|
| 802 |
+
if (alertElement) {
|
| 803 |
+
alertElement.remove();
|
| 804 |
+
}
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
function showPersistentError(message, stepName = '') {
|
| 808 |
const container = document.getElementById('alertContainer');
|
| 809 |
+
const alertId = `alert-error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 810 |
+
const title = stepName ? `<strong>${stepName}:</strong> ` : '';
|
| 811 |
+
const alertElement = document.createElement('div');
|
| 812 |
+
alertElement.id = alertId;
|
| 813 |
+
alertElement.className = 'alert error alert-persistent';
|
| 814 |
+
alertElement.innerHTML = `
|
| 815 |
+
<button class="alert-close" onclick="closeAlert('${alertId}')" title="λ«κΈ°">×</button>
|
| 816 |
+
${title}${message}
|
| 817 |
+
`;
|
| 818 |
+
container.appendChild(alertElement);
|
| 819 |
}
|
| 820 |
|
| 821 |
// νμΌ μ
λ‘λ κ΄λ ¨
|
|
|
|
| 974 |
fileNameDisplay += ` <span style="color: #1a73e8; font-size: 12px;">(${childCount}κ° νμ°¨)</span>`;
|
| 975 |
}
|
| 976 |
|
| 977 |
+
const isPublic = file.is_public !== undefined ? file.is_public : false;
|
| 978 |
+
const publicStatus = isPublic ? '<span style="color: #137333; font-weight: 500;">곡κ°</span>' : '<span style="color: #5f6368;">λΉκ³΅κ°</span>';
|
| 979 |
+
const toggleButtonText = isPublic ? 'λΉκ³΅κ°λ‘' : '곡κ°λ‘';
|
| 980 |
+
const toggleButtonClass = isPublic ? 'btn-warning' : 'btn-success';
|
| 981 |
+
|
| 982 |
row.innerHTML = `
|
| 983 |
<td>${fileNameDisplay}</td>
|
| 984 |
<td>${modelName}</td>
|
| 985 |
<td class="file-size">${fileSize}</td>
|
| 986 |
<td>${uploadDate}</td>
|
| 987 |
+
<td>
|
| 988 |
+
${publicStatus}
|
| 989 |
+
<button class="btn ${toggleButtonClass}" onclick="toggleFilePublic(${file.id}, ${!isPublic})" style="padding: 4px 8px; font-size: 12px; margin-left: 8px;">
|
| 990 |
+
${toggleButtonText}
|
| 991 |
+
</button>
|
| 992 |
+
</td>
|
| 993 |
<td>
|
| 994 |
<div class="file-actions">
|
| 995 |
${(file.has_parent_chunk !== undefined && file.has_parent_chunk) ?
|
|
|
|
| 1010 |
const childFileSize = formatFileSize(childFile.file_size);
|
| 1011 |
const childUploadDate = new Date(childFile.uploaded_at).toLocaleString('ko-KR');
|
| 1012 |
|
| 1013 |
+
const childIsPublic = childFile.is_public !== undefined ? childFile.is_public : false;
|
| 1014 |
+
const childPublicStatus = childIsPublic ? '<span style="color: #137333; font-weight: 500;">곡κ°</span>' : '<span style="color: #5f6368;">λΉκ³΅κ°</span>';
|
| 1015 |
+
const childToggleButtonText = childIsPublic ? 'λΉκ³΅κ°λ‘' : '곡κ°λ‘';
|
| 1016 |
+
const childToggleButtonClass = childIsPublic ? 'btn-warning' : 'btn-success';
|
| 1017 |
+
|
| 1018 |
childRow.innerHTML = `
|
| 1019 |
<td style="padding-left: 32px; color: #5f6368;">
|
| 1020 |
<span style="margin-right: 8px;">ββ</span>${escapeHtml(childFile.original_filename)}
|
|
|
|
| 1022 |
<td>${modelName}</td>
|
| 1023 |
<td class="file-size">${childFileSize}</td>
|
| 1024 |
<td>${childUploadDate}</td>
|
| 1025 |
+
<td>
|
| 1026 |
+
${childPublicStatus}
|
| 1027 |
+
<button class="btn ${childToggleButtonClass}" onclick="toggleFilePublic(${childFile.id}, ${!childIsPublic})" style="padding: 4px 8px; font-size: 12px; margin-left: 8px;">
|
| 1028 |
+
${childToggleButtonText}
|
| 1029 |
+
</button>
|
| 1030 |
+
</td>
|
| 1031 |
<td>
|
| 1032 |
<div class="file-actions">
|
| 1033 |
<button class="btn btn-secondary" onclick="deleteFile(${childFile.id})" style="padding: 4px 8px; font-size: 12px;">μμ </button>
|
|
|
|
| 1223 |
console.log(`[λ¨κ³ 1] fetch νΈμΆ μμ: /api/upload`);
|
| 1224 |
console.log(`[λ¨κ³ 1] FormData νλͺ©:`, Array.from(formData.entries()).map(([k, v]) => [k, v instanceof File ? v.name : v]));
|
| 1225 |
|
| 1226 |
+
// νμμμμ΄ μλ fetch λνΌ (νμμμ 무μ ν)
|
| 1227 |
+
const fetchWithTimeout = (url, options, timeout = null) => {
|
| 1228 |
+
// timeoutμ΄ 0μ΄κ±°λ null/undefinedμ΄λ©΄ νμμμ μμ΄ μ€ν
|
| 1229 |
+
if (!timeout || timeout === 0) {
|
| 1230 |
+
return fetch(url, options);
|
| 1231 |
+
}
|
| 1232 |
+
// νμμμμ΄ μ€μ λ κ²½μ°μλ§ Promise.race μ¬μ©
|
| 1233 |
return Promise.race([
|
| 1234 |
fetch(url, options),
|
| 1235 |
new Promise((_, reject) =>
|
|
|
|
| 1242 |
method: 'POST',
|
| 1243 |
body: formData,
|
| 1244 |
credentials: 'include' // μΏ ν€ ν¬ν¨ (μΈμ
μΈμ¦)
|
| 1245 |
+
}, null); // νμμμ 무μ ν
|
| 1246 |
|
| 1247 |
console.log(`[λ¨κ³ 1] fetch μλ΅ μμ : ${response.status} ${response.statusText}`);
|
| 1248 |
|
|
|
|
| 1302 |
itemElement.style.opacity = '0.7';
|
| 1303 |
console.log(`[μ
λ‘λ μ±κ³΅] ${file.name} β λͺ¨λΈ: ${modelName}`);
|
| 1304 |
} else {
|
| 1305 |
+
// λ¨κ³λ³ μ²λ¦¬ ν¨μ (μλ¬κ° λ°μν΄λ κ³μ μ§ν)
|
| 1306 |
+
const processStep = async (stepName, stepNumber, url, timeout, detailText = '', allowSkip = false) => {
|
| 1307 |
updateProgressStatus(i, stepNumber, stepName, files.length, detailText);
|
| 1308 |
updateOverallStatus(i + 1, files.length, stepNumber, detailText);
|
| 1309 |
|
| 1310 |
+
try {
|
| 1311 |
+
const stepResponse = await fetchWithTimeout(url, {
|
| 1312 |
+
method: 'POST',
|
| 1313 |
+
credentials: 'include'
|
| 1314 |
+
}, timeout);
|
| 1315 |
+
|
| 1316 |
+
if (!stepResponse.ok) {
|
| 1317 |
+
const stepError = await stepResponse.json().catch(() => ({ error: `HTTP ${stepResponse.status}` }));
|
| 1318 |
+
const errorMsg = stepError.error || 'μ μ μλ μ€λ₯';
|
| 1319 |
+
|
| 1320 |
+
// "μ΄λ―Έ μλ£" λλ "μ΄λ―Έ μ‘΄μ¬" κ°μ λ©μμ§λ μ±κ³΅μΌλ‘ μ²λ¦¬
|
| 1321 |
+
if (allowSkip && (
|
| 1322 |
+
errorMsg.includes('μ΄λ―Έ') ||
|
| 1323 |
+
errorMsg.includes('μ‘΄μ¬') ||
|
| 1324 |
+
errorMsg.includes('μλ£') ||
|
| 1325 |
+
errorMsg.toLowerCase().includes('already') ||
|
| 1326 |
+
errorMsg.toLowerCase().includes('exists')
|
| 1327 |
+
)) {
|
| 1328 |
+
console.log(`[${stepName}] μ΄λ―Έ μλ£λ μμ
μΌλ‘ 건λλ: ${errorMsg}`);
|
| 1329 |
+
return { skipped: true, message: errorMsg };
|
| 1330 |
+
}
|
| 1331 |
+
|
| 1332 |
+
throw new Error(`${stepName} μ€ν¨: ${errorMsg}`);
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
+
return await stepResponse.json();
|
| 1336 |
+
} catch (error) {
|
| 1337 |
+
// νμμμμ΄λ λ€νΈμν¬ μ€λ₯κ° μλ κ²½μ°μλ§ μλ¬λ‘ μ²λ¦¬
|
| 1338 |
+
if (allowSkip && error.message && (
|
| 1339 |
+
error.message.includes('μ΄λ―Έ') ||
|
| 1340 |
+
error.message.includes('μ‘΄μ¬') ||
|
| 1341 |
+
error.message.includes('μλ£')
|
| 1342 |
+
)) {
|
| 1343 |
+
console.log(`[${stepName}] μ΄λ―Έ μλ£λ μμ
μΌλ‘ 건λλ: ${error.message}`);
|
| 1344 |
+
return { skipped: true, message: error.message };
|
| 1345 |
+
}
|
| 1346 |
+
throw error;
|
| 1347 |
}
|
|
|
|
|
|
|
| 1348 |
};
|
| 1349 |
|
| 1350 |
+
let chunkCount = 0;
|
| 1351 |
+
const stepErrors = [];
|
| 1352 |
+
|
| 1353 |
try {
|
| 1354 |
+
// λ¨κ³ 1: Parent Chunk μμ± (νμμμ 무μ ν, μ€ν¨ν΄λ κ³μ μ§ν)
|
| 1355 |
+
try {
|
| 1356 |
+
await processStep('Parent Chunk μμ±', 5, `/api/files/${fileId}/process/parent-chunk`, null, 'AI λΆμ μ€...', true);
|
| 1357 |
+
} catch (stepError) {
|
| 1358 |
+
const errorMsg = stepError.message || 'μ μ μλ μ€λ₯';
|
| 1359 |
+
console.warn(`[Parent Chunk μμ±] κ²½κ³ : ${errorMsg}`);
|
| 1360 |
+
stepErrors.push(`Parent Chunk: ${errorMsg}`);
|
| 1361 |
+
showPersistentError(`${errorMsg}`, `[${file.name}] Parent Chunk μμ±`);
|
| 1362 |
+
}
|
| 1363 |
|
| 1364 |
+
// λ¨κ³ 2: Chunk μμ± (νμμμ 무μ ν, μ€ν¨ν΄λ κ³μ μ§ν)
|
| 1365 |
+
try {
|
| 1366 |
+
const chunksData = await processStep('Child Chunk μμ±', 6, `/api/files/${fileId}/process/chunks`, null, 'μΉμ
λΆν μ€...', true);
|
| 1367 |
+
if (chunksData && !chunksData.skipped) {
|
| 1368 |
+
chunkCount = chunksData.chunk_count || 0;
|
| 1369 |
+
}
|
| 1370 |
+
} catch (stepError) {
|
| 1371 |
+
const errorMsg = stepError.message || 'μ μ μλ μ€λ₯';
|
| 1372 |
+
console.warn(`[Child Chunk μμ±] κ²½κ³ : ${errorMsg}`);
|
| 1373 |
+
stepErrors.push(`Child Chunk: ${errorMsg}`);
|
| 1374 |
+
showPersistentError(`${errorMsg}`, `[${file.name}] Child Chunk μμ±`);
|
| 1375 |
+
}
|
| 1376 |
|
| 1377 |
+
// λ¨κ³ 3: νμ°¨ λΆμ (νμμμ 무μ ν, μ€ν¨ν΄λ κ³μ μ§ν)
|
| 1378 |
+
try {
|
| 1379 |
+
const episodeDetail = episodeCount > 0 ? `μ΄ ${episodeCount}νμ°¨ μ€ λΆμ μ€...` : 'νμ°¨ λΆμ μ€...';
|
| 1380 |
+
await processStep('νμ°¨ λΆμ', 7, `/api/files/${fileId}/process/episode-analysis`, null, episodeDetail, true);
|
| 1381 |
+
} catch (stepError) {
|
| 1382 |
+
const errorMsg = stepError.message || 'μ μ μλ μ€λ₯';
|
| 1383 |
+
console.warn(`[νμ°¨ λΆμ] κ²½κ³ : ${errorMsg}`);
|
| 1384 |
+
stepErrors.push(`νμ°¨ λΆμ: ${errorMsg}`);
|
| 1385 |
+
showPersistentError(`${errorMsg}`, `[${file.name}] νμ°¨ λΆμ`);
|
| 1386 |
+
}
|
| 1387 |
|
| 1388 |
+
// λ¨κ³ 4: Graph Extraction (νμμμ 무μ ν, μ€ν¨ν΄λ κ³μ μ§ν)
|
| 1389 |
+
try {
|
| 1390 |
+
const graphDetail = episodeCount > 0 ? `μ΄ ${episodeCount}νμ°¨ μ€ μΆμΆ μ€...` : 'Graph Extraction μ€...';
|
| 1391 |
+
await processStep('Graph Extraction', 8, `/api/files/${fileId}/process/graph`, null, graphDetail, true);
|
| 1392 |
+
} catch (stepError) {
|
| 1393 |
+
const errorMsg = stepError.message || 'μ μ μλ μ€λ₯';
|
| 1394 |
+
console.warn(`[Graph Extraction] κ²½κ³ : ${errorMsg}`);
|
| 1395 |
+
stepErrors.push(`Graph Extraction: ${errorMsg}`);
|
| 1396 |
+
showPersistentError(`${errorMsg}`, `[${file.name}] Graph Extraction`);
|
| 1397 |
+
}
|
| 1398 |
|
| 1399 |
+
// λͺ¨λ λ¨κ³κ° μλ£λμκ±°λ μΌλΆλ§ μ€ν¨ν κ²½μ° μ±κ³΅μΌλ‘ μ²λ¦¬
|
| 1400 |
successCount++;
|
| 1401 |
const modelName = data.model_name || 'μ μ μμ';
|
| 1402 |
|
|
|
|
| 1405 |
updateOverallStatus(i + 1, files.length, 9);
|
| 1406 |
|
| 1407 |
statusElement.className = 'progress-item-status success';
|
| 1408 |
+
const statusTitle = stepErrors.length > 0
|
| 1409 |
+
? `λͺ¨λΈ: ${modelName}${chunkCount > 0 ? `, μ²ν¬: ${chunkCount}κ°` : ''} (μΌλΆ λ¨κ³ κ²½κ³ : ${stepErrors.length}κ°)`
|
| 1410 |
+
: `λͺ¨λΈ: ${modelName}${chunkCount > 0 ? `, μ²ν¬: ${chunkCount}κ°` : ''}`;
|
| 1411 |
statusElement.innerHTML = `β μλ£ (νμΌ ${i + 1}/${files.length})`;
|
| 1412 |
+
statusElement.title = statusTitle;
|
| 1413 |
itemElement.style.opacity = '0.7';
|
| 1414 |
+
|
| 1415 |
+
if (stepErrors.length > 0) {
|
| 1416 |
+
console.warn(`[μ
λ‘λ μλ£ (μΌλΆ κ²½κ³ )] ${file.name} β λͺ¨λΈ: ${modelName}, μ²ν¬: ${chunkCount}κ°, κ²½κ³ : ${stepErrors.join('; ')}`);
|
| 1417 |
+
} else {
|
| 1418 |
+
console.log(`[μ
λ‘λ μ±κ³΅] ${file.name} β λͺ¨λΈ: ${modelName}, μ²ν¬: ${chunkCount}κ°`);
|
| 1419 |
+
}
|
| 1420 |
} catch (stepError) {
|
| 1421 |
+
// μμμΉ λͺ»ν μΉλͺ
μ μ€λ₯λ§ μ€ν¨λ‘ μ²λ¦¬
|
| 1422 |
+
console.error(`[μ
λ‘λ μ²λ¦¬ μ€ μΉλͺ
μ μ€λ₯] ${file.name}:`, stepError);
|
| 1423 |
throw stepError;
|
| 1424 |
}
|
| 1425 |
}
|
|
|
|
| 1555 |
}
|
| 1556 |
|
| 1557 |
// νμΌ μμ
|
| 1558 |
+
// νμΌ κ³΅κ° μ¬λΆ ν κΈ
|
| 1559 |
+
async function toggleFilePublic(fileId, isPublic) {
|
| 1560 |
+
try {
|
| 1561 |
+
const response = await fetch(`/api/files/${fileId}/public`, {
|
| 1562 |
+
method: 'PUT',
|
| 1563 |
+
headers: {
|
| 1564 |
+
'Content-Type': 'application/json'
|
| 1565 |
+
},
|
| 1566 |
+
body: JSON.stringify({ is_public: isPublic })
|
| 1567 |
+
});
|
| 1568 |
+
|
| 1569 |
+
const data = await response.json();
|
| 1570 |
+
|
| 1571 |
+
if (response.ok) {
|
| 1572 |
+
alert(data.message || `νμΌμ΄ ${isPublic ? '곡κ°' : 'λΉκ³΅κ°'}λ‘ μ€μ λμμ΅λλ€.`);
|
| 1573 |
+
loadFiles(); // νμΌ λͺ©λ‘ μλ‘κ³ μΉ¨
|
| 1574 |
+
} else {
|
| 1575 |
+
alert(`μ€λ₯: ${data.error || 'νμΌ κ³΅κ° μ¬λΆ λ³κ²½μ μ€ν¨νμ΅λλ€.'}`);
|
| 1576 |
+
}
|
| 1577 |
+
} catch (error) {
|
| 1578 |
+
console.error('νμΌ κ³΅κ° μ¬λΆ λ³κ²½ μ€λ₯:', error);
|
| 1579 |
+
alert(`μ€λ₯: ${error.message || 'νμΌ κ³΅κ° μ¬λΆ λ³κ²½ μ€ μ€λ₯κ° λ°μνμ΅λλ€.'}`);
|
| 1580 |
+
}
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
async function deleteFile(fileId) {
|
| 1584 |
if (!confirm('μ΄ νμΌμ μμ νμκ² μ΅λκΉ?\nμλ³Έ νμΌμΈ κ²½μ° μ΄μ΄μ μ
λ‘λν λͺ¨λ νμ°¨λ ν¨κ» μμ λ©λλ€.')) {
|
| 1585 |
return;
|
templates/index.html
CHANGED
|
@@ -662,6 +662,122 @@
|
|
| 662 |
align-items: center;
|
| 663 |
}
|
| 664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
.btn-icon {
|
| 666 |
background: none;
|
| 667 |
border: none;
|
|
@@ -1180,6 +1296,22 @@
|
|
| 1180 |
max-width: 500px;
|
| 1181 |
}
|
| 1182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
/* λ°μν */
|
| 1184 |
@media (max-width: 768px) {
|
| 1185 |
.sidebar {
|
|
@@ -1188,6 +1320,13 @@
|
|
| 1188 |
top: 0;
|
| 1189 |
z-index: 1000;
|
| 1190 |
box-shadow: 2px 0 8px var(--shadow);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1191 |
}
|
| 1192 |
|
| 1193 |
.sidebar.collapsed {
|
|
@@ -1206,6 +1345,10 @@
|
|
| 1206 |
font-size: 18px;
|
| 1207 |
}
|
| 1208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
.chat-container {
|
| 1210 |
padding: 16px;
|
| 1211 |
}
|
|
@@ -1305,6 +1448,9 @@
|
|
| 1305 |
</div>
|
| 1306 |
</div>
|
| 1307 |
|
|
|
|
|
|
|
|
|
|
| 1308 |
<!-- λ©μΈ μ½ν
μΈ -->
|
| 1309 |
<div class="main-content">
|
| 1310 |
<!-- ν€λ -->
|
|
@@ -1317,6 +1463,7 @@
|
|
| 1317 |
</button>
|
| 1318 |
<span></span>
|
| 1319 |
</div>
|
|
|
|
| 1320 |
<div class="header-actions">
|
| 1321 |
<span style="margin-right: 8px; font-size: 14px; color: var(--text-secondary);">{{ current_user.nickname or current_user.username }}</span>
|
| 1322 |
<a href="{{ url_for('main.webnovels') }}" class="btn-text-icon" title="μμ μ 보">
|
|
@@ -1345,6 +1492,24 @@
|
|
| 1345 |
</div>
|
| 1346 |
</div>
|
| 1347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
<!-- μ±ν
μμ -->
|
| 1349 |
<div class="chat-container" id="chatContainer">
|
| 1350 |
<div class="empty-state" id="emptyState">
|
|
@@ -1446,6 +1611,24 @@
|
|
| 1446 |
</div>
|
| 1447 |
|
| 1448 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1449 |
const chatContainer = document.getElementById('chatContainer');
|
| 1450 |
const messageInput = document.getElementById('messageInput');
|
| 1451 |
const sendButton = document.getElementById('sendButton');
|
|
@@ -1490,9 +1673,10 @@
|
|
| 1490 |
async function loadNovels() {
|
| 1491 |
try {
|
| 1492 |
// λͺ¨λΈμ΄ μ νλμ΄ μμΌλ©΄ ν΄λΉ λͺ¨λΈμ νμΌλ§, μμΌλ©΄ λͺ¨λ νμΌ
|
|
|
|
| 1493 |
const url = selectedModel
|
| 1494 |
-
? `/api/files?model_name=${encodeURIComponent(selectedModel)}`
|
| 1495 |
-
: '/api/files';
|
| 1496 |
|
| 1497 |
console.log('[μΉμμ€ λͺ©λ‘] API μμ²:', url, 'μ νλ λͺ¨λΈ:', selectedModel || 'μμ (μ 체)');
|
| 1498 |
|
|
@@ -1518,11 +1702,11 @@
|
|
| 1518 |
let files = data.files || [];
|
| 1519 |
console.log('[μΉμμ€ λͺ©λ‘] νμΌ κ°μ:', files.length);
|
| 1520 |
|
| 1521 |
-
// λͺ¨λΈμ΄ μ νλμ΄ μλλ° ν΄λΉ λͺ¨λΈμ νμΌμ΄ μμΌλ©΄, λͺ¨λ νμΌ λ€μ μ‘°ν
|
| 1522 |
if (selectedModel && files.length === 0) {
|
| 1523 |
console.log('[μΉμμ€ λͺ©λ‘] μ νν λͺ¨λΈμ νμΌμ΄ μμ΄ μ 체 νμΌ μ‘°ν μ€...');
|
| 1524 |
try {
|
| 1525 |
-
const allFilesResponse = await fetch('/api/files', {
|
| 1526 |
credentials: 'include'
|
| 1527 |
});
|
| 1528 |
if (allFilesResponse.ok) {
|
|
@@ -1767,9 +1951,22 @@
|
|
| 1767 |
function toggleSidebar() {
|
| 1768 |
sidebar.classList.toggle('collapsed');
|
| 1769 |
const isCollapsed = sidebar.classList.contains('collapsed');
|
|
|
|
|
|
|
| 1770 |
if (window.innerWidth <= 768) {
|
| 1771 |
sidebarToggleBtn.style.display = isCollapsed ? 'flex' : 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1772 |
}
|
|
|
|
| 1773 |
// μ¬μ΄λλ° λ΄λΆ ν κΈ λ²νΌ μμ΄μ½ μ
λ°μ΄νΈ
|
| 1774 |
const sidebarToggle = sidebar.querySelector('.sidebar-toggle');
|
| 1775 |
if (sidebarToggle) {
|
|
@@ -1778,15 +1975,29 @@
|
|
| 1778 |
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l-6-6 6-6M21 18l-6-6 6-6"/></svg>';
|
| 1779 |
}
|
| 1780 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1781 |
|
| 1782 |
// λ°μν μ¬μ΄λλ° μ²λ¦¬
|
| 1783 |
function handleResize() {
|
|
|
|
| 1784 |
if (window.innerWidth <= 768) {
|
| 1785 |
sidebar.classList.add('collapsed');
|
| 1786 |
sidebarToggleBtn.style.display = 'flex';
|
|
|
|
|
|
|
| 1787 |
} else {
|
| 1788 |
sidebar.classList.remove('collapsed');
|
| 1789 |
sidebarToggleBtn.style.display = 'none';
|
|
|
|
| 1790 |
}
|
| 1791 |
}
|
| 1792 |
|
|
|
|
| 662 |
align-items: center;
|
| 663 |
}
|
| 664 |
|
| 665 |
+
.menu-toggle {
|
| 666 |
+
display: none;
|
| 667 |
+
background: none;
|
| 668 |
+
border: none;
|
| 669 |
+
font-size: 24px;
|
| 670 |
+
cursor: pointer;
|
| 671 |
+
padding: 8px;
|
| 672 |
+
color: var(--text-primary);
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.mobile-menu {
|
| 676 |
+
display: none;
|
| 677 |
+
position: fixed;
|
| 678 |
+
top: 0;
|
| 679 |
+
left: 0;
|
| 680 |
+
right: 0;
|
| 681 |
+
bottom: 0;
|
| 682 |
+
background: rgba(0, 0, 0, 0.5);
|
| 683 |
+
z-index: 1000;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.mobile-menu.active {
|
| 687 |
+
display: block;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.mobile-menu-content {
|
| 691 |
+
position: fixed;
|
| 692 |
+
top: 0;
|
| 693 |
+
right: -100%;
|
| 694 |
+
width: 280px;
|
| 695 |
+
max-width: 80%;
|
| 696 |
+
height: 100%;
|
| 697 |
+
background: var(--bg-primary);
|
| 698 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 699 |
+
transition: right 0.3s ease;
|
| 700 |
+
overflow-y: auto;
|
| 701 |
+
z-index: 1001;
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
.mobile-menu.active .mobile-menu-content {
|
| 705 |
+
right: 0;
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
.mobile-menu-header {
|
| 709 |
+
padding: 16px 20px;
|
| 710 |
+
border-bottom: 1px solid var(--border);
|
| 711 |
+
display: flex;
|
| 712 |
+
justify-content: space-between;
|
| 713 |
+
align-items: center;
|
| 714 |
+
background: var(--bg-primary);
|
| 715 |
+
position: sticky;
|
| 716 |
+
top: 0;
|
| 717 |
+
z-index: 10;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.mobile-menu-title {
|
| 721 |
+
font-size: 18px;
|
| 722 |
+
font-weight: 500;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.mobile-menu-close {
|
| 726 |
+
background: none;
|
| 727 |
+
border: none;
|
| 728 |
+
font-size: 24px;
|
| 729 |
+
cursor: pointer;
|
| 730 |
+
color: var(--text-primary);
|
| 731 |
+
padding: 0;
|
| 732 |
+
width: 32px;
|
| 733 |
+
height: 32px;
|
| 734 |
+
display: flex;
|
| 735 |
+
align-items: center;
|
| 736 |
+
justify-content: center;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.mobile-menu-items {
|
| 740 |
+
padding: 8px 0;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.mobile-menu-item {
|
| 744 |
+
display: block;
|
| 745 |
+
padding: 12px 20px;
|
| 746 |
+
color: var(--text-primary);
|
| 747 |
+
text-decoration: none;
|
| 748 |
+
border-bottom: 1px solid var(--bg-tertiary);
|
| 749 |
+
transition: background 0.2s;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.mobile-menu-item:hover {
|
| 753 |
+
background: var(--bg-secondary);
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.mobile-menu-user {
|
| 757 |
+
padding: 16px 20px;
|
| 758 |
+
border-bottom: 1px solid var(--border);
|
| 759 |
+
color: var(--text-secondary);
|
| 760 |
+
font-size: 14px;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
@media (max-width: 768px) {
|
| 764 |
+
.header {
|
| 765 |
+
padding: 12px 16px;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.header-title {
|
| 769 |
+
font-size: 18px;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.menu-toggle {
|
| 773 |
+
display: block;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.header-actions {
|
| 777 |
+
display: none;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
.btn-icon {
|
| 782 |
background: none;
|
| 783 |
border: none;
|
|
|
|
| 1296 |
max-width: 500px;
|
| 1297 |
}
|
| 1298 |
|
| 1299 |
+
/* μ¬μ΄λλ° μ€λ²λ μ΄ (λͺ¨λ°μΌ) */
|
| 1300 |
+
.sidebar-overlay {
|
| 1301 |
+
display: none;
|
| 1302 |
+
position: fixed;
|
| 1303 |
+
top: 0;
|
| 1304 |
+
left: 0;
|
| 1305 |
+
right: 0;
|
| 1306 |
+
bottom: 0;
|
| 1307 |
+
background: rgba(0, 0, 0, 0.5);
|
| 1308 |
+
z-index: 999;
|
| 1309 |
+
}
|
| 1310 |
+
|
| 1311 |
+
.sidebar-overlay.active {
|
| 1312 |
+
display: block;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
/* λ°μν */
|
| 1316 |
@media (max-width: 768px) {
|
| 1317 |
.sidebar {
|
|
|
|
| 1320 |
top: 0;
|
| 1321 |
z-index: 1000;
|
| 1322 |
box-shadow: 2px 0 8px var(--shadow);
|
| 1323 |
+
width: 0;
|
| 1324 |
+
overflow: hidden;
|
| 1325 |
+
transition: width 0.3s ease;
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
.sidebar:not(.collapsed) {
|
| 1329 |
+
width: 280px;
|
| 1330 |
}
|
| 1331 |
|
| 1332 |
.sidebar.collapsed {
|
|
|
|
| 1345 |
font-size: 18px;
|
| 1346 |
}
|
| 1347 |
|
| 1348 |
+
#sidebarToggleBtn {
|
| 1349 |
+
display: flex !important;
|
| 1350 |
+
}
|
| 1351 |
+
|
| 1352 |
.chat-container {
|
| 1353 |
padding: 16px;
|
| 1354 |
}
|
|
|
|
| 1448 |
</div>
|
| 1449 |
</div>
|
| 1450 |
|
| 1451 |
+
<!-- μ¬μ΄λλ° μ€λ²λ μ΄ (λͺ¨λ°μΌ) -->
|
| 1452 |
+
<div class="sidebar-overlay" id="sidebarOverlay" onclick="closeSidebarOnMobile()"></div>
|
| 1453 |
+
|
| 1454 |
<!-- λ©μΈ μ½ν
μΈ -->
|
| 1455 |
<div class="main-content">
|
| 1456 |
<!-- ν€λ -->
|
|
|
|
| 1463 |
</button>
|
| 1464 |
<span></span>
|
| 1465 |
</div>
|
| 1466 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 1467 |
<div class="header-actions">
|
| 1468 |
<span style="margin-right: 8px; font-size: 14px; color: var(--text-secondary);">{{ current_user.nickname or current_user.username }}</span>
|
| 1469 |
<a href="{{ url_for('main.webnovels') }}" class="btn-text-icon" title="μμ μ 보">
|
|
|
|
| 1492 |
</div>
|
| 1493 |
</div>
|
| 1494 |
|
| 1495 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 1496 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 1497 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 1498 |
+
<div class="mobile-menu-header">
|
| 1499 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 1500 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 1501 |
+
</div>
|
| 1502 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 1503 |
+
<div class="mobile-menu-items">
|
| 1504 |
+
<a href="{{ url_for('main.webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μμ μ 보</a>
|
| 1505 |
+
{% if current_user.is_admin %}
|
| 1506 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">κ΄λ¦¬μ νμ΄μ§</a>
|
| 1507 |
+
{% endif %}
|
| 1508 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 1509 |
+
</div>
|
| 1510 |
+
</div>
|
| 1511 |
+
</div>
|
| 1512 |
+
|
| 1513 |
<!-- μ±ν
μμ -->
|
| 1514 |
<div class="chat-container" id="chatContainer">
|
| 1515 |
<div class="empty-state" id="emptyState">
|
|
|
|
| 1611 |
</div>
|
| 1612 |
|
| 1613 |
<script>
|
| 1614 |
+
function toggleMobileMenu() {
|
| 1615 |
+
const menu = document.getElementById('mobileMenu');
|
| 1616 |
+
menu.classList.toggle('active');
|
| 1617 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 1618 |
+
}
|
| 1619 |
+
|
| 1620 |
+
function closeMobileMenu() {
|
| 1621 |
+
const menu = document.getElementById('mobileMenu');
|
| 1622 |
+
menu.classList.remove('active');
|
| 1623 |
+
document.body.style.overflow = '';
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 1627 |
+
if (event.target.id === 'mobileMenu') {
|
| 1628 |
+
closeMobileMenu();
|
| 1629 |
+
}
|
| 1630 |
+
}
|
| 1631 |
+
|
| 1632 |
const chatContainer = document.getElementById('chatContainer');
|
| 1633 |
const messageInput = document.getElementById('messageInput');
|
| 1634 |
const sendButton = document.getElementById('sendButton');
|
|
|
|
| 1673 |
async function loadNovels() {
|
| 1674 |
try {
|
| 1675 |
// λͺ¨λΈμ΄ μ νλμ΄ μμΌλ©΄ ν΄λΉ λͺ¨λΈμ νμΌλ§, μμΌλ©΄ λͺ¨λ νμΌ
|
| 1676 |
+
// κ³΅κ° νμΌλ§ μ‘°ν (public_only=true)
|
| 1677 |
const url = selectedModel
|
| 1678 |
+
? `/api/files?model_name=${encodeURIComponent(selectedModel)}&public_only=true`
|
| 1679 |
+
: '/api/files?public_only=true';
|
| 1680 |
|
| 1681 |
console.log('[μΉμμ€ λͺ©λ‘] API μμ²:', url, 'μ νλ λͺ¨λΈ:', selectedModel || 'μμ (μ 체)');
|
| 1682 |
|
|
|
|
| 1702 |
let files = data.files || [];
|
| 1703 |
console.log('[μΉμμ€ λͺ©λ‘] νμΌ κ°μ:', files.length);
|
| 1704 |
|
| 1705 |
+
// λͺ¨λΈμ΄ μ νλμ΄ μλλ° ν΄λΉ λͺ¨λΈμ νμΌμ΄ μμΌλ©΄, λͺ¨λ νμΌ λ€μ μ‘°ν (κ³΅κ° νμΌλ§)
|
| 1706 |
if (selectedModel && files.length === 0) {
|
| 1707 |
console.log('[μΉμμ€ λͺ©λ‘] μ νν λͺ¨λΈμ νμΌμ΄ μμ΄ μ 체 νμΌ μ‘°ν μ€...');
|
| 1708 |
try {
|
| 1709 |
+
const allFilesResponse = await fetch('/api/files?public_only=true', {
|
| 1710 |
credentials: 'include'
|
| 1711 |
});
|
| 1712 |
if (allFilesResponse.ok) {
|
|
|
|
| 1951 |
function toggleSidebar() {
|
| 1952 |
sidebar.classList.toggle('collapsed');
|
| 1953 |
const isCollapsed = sidebar.classList.contains('collapsed');
|
| 1954 |
+
const overlay = document.getElementById('sidebarOverlay');
|
| 1955 |
+
|
| 1956 |
if (window.innerWidth <= 768) {
|
| 1957 |
sidebarToggleBtn.style.display = isCollapsed ? 'flex' : 'none';
|
| 1958 |
+
// λͺ¨λ°μΌμμ μ¬μ΄λλ°κ° μ΄λ¦΄ λ λ°°κ²½ μ€λ²λ μ΄ μΆκ°
|
| 1959 |
+
if (!isCollapsed) {
|
| 1960 |
+
if (overlay) overlay.classList.add('active');
|
| 1961 |
+
document.body.style.overflow = 'hidden';
|
| 1962 |
+
} else {
|
| 1963 |
+
if (overlay) overlay.classList.remove('active');
|
| 1964 |
+
document.body.style.overflow = '';
|
| 1965 |
+
}
|
| 1966 |
+
} else {
|
| 1967 |
+
if (overlay) overlay.classList.remove('active');
|
| 1968 |
}
|
| 1969 |
+
|
| 1970 |
// μ¬μ΄λλ° λ΄λΆ ν κΈ λ²νΌ μμ΄μ½ μ
λ°μ΄νΈ
|
| 1971 |
const sidebarToggle = sidebar.querySelector('.sidebar-toggle');
|
| 1972 |
if (sidebarToggle) {
|
|
|
|
| 1975 |
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l-6-6 6-6M21 18l-6-6 6-6"/></svg>';
|
| 1976 |
}
|
| 1977 |
}
|
| 1978 |
+
|
| 1979 |
+
function closeSidebarOnMobile() {
|
| 1980 |
+
if (window.innerWidth <= 768) {
|
| 1981 |
+
sidebar.classList.add('collapsed');
|
| 1982 |
+
const overlay = document.getElementById('sidebarOverlay');
|
| 1983 |
+
if (overlay) overlay.classList.remove('active');
|
| 1984 |
+
document.body.style.overflow = '';
|
| 1985 |
+
sidebarToggleBtn.style.display = 'flex';
|
| 1986 |
+
}
|
| 1987 |
+
}
|
| 1988 |
|
| 1989 |
// λ°μν μ¬μ΄λλ° μ²λ¦¬
|
| 1990 |
function handleResize() {
|
| 1991 |
+
const overlay = document.getElementById('sidebarOverlay');
|
| 1992 |
if (window.innerWidth <= 768) {
|
| 1993 |
sidebar.classList.add('collapsed');
|
| 1994 |
sidebarToggleBtn.style.display = 'flex';
|
| 1995 |
+
if (overlay) overlay.classList.remove('active');
|
| 1996 |
+
document.body.style.overflow = '';
|
| 1997 |
} else {
|
| 1998 |
sidebar.classList.remove('collapsed');
|
| 1999 |
sidebarToggleBtn.style.display = 'none';
|
| 2000 |
+
if (overlay) overlay.classList.remove('active');
|
| 2001 |
}
|
| 2002 |
}
|
| 2003 |
|
templates/webnovels.html
CHANGED
|
@@ -56,6 +56,126 @@
|
|
| 56 |
align-items: center;
|
| 57 |
}
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
.btn {
|
| 60 |
padding: 8px 16px;
|
| 61 |
border: none;
|
|
@@ -477,6 +597,7 @@
|
|
| 477 |
<span>π</span>
|
| 478 |
<span>μ
λ‘λλ μΉμμ€</span>
|
| 479 |
</div>
|
|
|
|
| 480 |
<div class="header-actions">
|
| 481 |
<span style="margin-right: 12px; color: var(--text-secondary);">{{ current_user.nickname or current_user.username }}</span>
|
| 482 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">μ°½μ μ΄μμ€ν΄νΈ</a>
|
|
@@ -487,6 +608,24 @@
|
|
| 487 |
</div>
|
| 488 |
</div>
|
| 489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
<div class="container">
|
| 491 |
<div class="page-header">
|
| 492 |
<h1>μ
λ‘λλ μΉμμ€</h1>
|
|
@@ -694,6 +833,24 @@
|
|
| 694 |
</div>
|
| 695 |
|
| 696 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
function escapeHtml(text) {
|
| 698 |
const div = document.createElement('div');
|
| 699 |
div.textContent = text;
|
|
@@ -754,7 +911,10 @@
|
|
| 754 |
listContainer.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 24px;">μΉμμ€ λͺ©λ‘μ λΆλ¬μ€λ μ€...</div>';
|
| 755 |
|
| 756 |
try {
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
| 758 |
const response = await fetch(url, {
|
| 759 |
credentials: 'include'
|
| 760 |
});
|
|
|
|
| 56 |
align-items: center;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
.menu-toggle {
|
| 60 |
+
display: none;
|
| 61 |
+
background: none;
|
| 62 |
+
border: none;
|
| 63 |
+
font-size: 24px;
|
| 64 |
+
cursor: pointer;
|
| 65 |
+
padding: 8px;
|
| 66 |
+
color: var(--text-primary);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.mobile-menu {
|
| 70 |
+
display: none;
|
| 71 |
+
position: fixed;
|
| 72 |
+
top: 0;
|
| 73 |
+
left: 0;
|
| 74 |
+
right: 0;
|
| 75 |
+
bottom: 0;
|
| 76 |
+
background: rgba(0, 0, 0, 0.5);
|
| 77 |
+
z-index: 1000;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.mobile-menu.active {
|
| 81 |
+
display: block;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.mobile-menu-content {
|
| 85 |
+
position: fixed;
|
| 86 |
+
top: 0;
|
| 87 |
+
right: -100%;
|
| 88 |
+
width: 280px;
|
| 89 |
+
max-width: 80%;
|
| 90 |
+
height: 100%;
|
| 91 |
+
background: var(--bg-primary);
|
| 92 |
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
| 93 |
+
transition: right 0.3s ease;
|
| 94 |
+
overflow-y: auto;
|
| 95 |
+
z-index: 1001;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.mobile-menu.active .mobile-menu-content {
|
| 99 |
+
right: 0;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.mobile-menu-header {
|
| 103 |
+
padding: 16px 20px;
|
| 104 |
+
border-bottom: 1px solid var(--border);
|
| 105 |
+
display: flex;
|
| 106 |
+
justify-content: space-between;
|
| 107 |
+
align-items: center;
|
| 108 |
+
background: var(--bg-primary);
|
| 109 |
+
position: sticky;
|
| 110 |
+
top: 0;
|
| 111 |
+
z-index: 10;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.mobile-menu-title {
|
| 115 |
+
font-size: 18px;
|
| 116 |
+
font-weight: 500;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.mobile-menu-close {
|
| 120 |
+
background: none;
|
| 121 |
+
border: none;
|
| 122 |
+
font-size: 24px;
|
| 123 |
+
cursor: pointer;
|
| 124 |
+
color: var(--text-primary);
|
| 125 |
+
padding: 0;
|
| 126 |
+
width: 32px;
|
| 127 |
+
height: 32px;
|
| 128 |
+
display: flex;
|
| 129 |
+
align-items: center;
|
| 130 |
+
justify-content: center;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.mobile-menu-items {
|
| 134 |
+
padding: 8px 0;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.mobile-menu-item {
|
| 138 |
+
display: block;
|
| 139 |
+
padding: 12px 20px;
|
| 140 |
+
color: var(--text-primary);
|
| 141 |
+
text-decoration: none;
|
| 142 |
+
border-bottom: 1px solid var(--bg-tertiary);
|
| 143 |
+
transition: background 0.2s;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.mobile-menu-item:hover {
|
| 147 |
+
background: var(--bg-secondary);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.mobile-menu-user {
|
| 151 |
+
padding: 16px 20px;
|
| 152 |
+
border-bottom: 1px solid var(--border);
|
| 153 |
+
color: var(--text-secondary);
|
| 154 |
+
font-size: 14px;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
@media (max-width: 768px) {
|
| 158 |
+
.header {
|
| 159 |
+
padding: 12px 16px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.header-title {
|
| 163 |
+
font-size: 18px;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.header-title span:first-child {
|
| 167 |
+
display: none;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.menu-toggle {
|
| 171 |
+
display: block;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.header-actions {
|
| 175 |
+
display: none;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
.btn {
|
| 180 |
padding: 8px 16px;
|
| 181 |
border: none;
|
|
|
|
| 597 |
<span>π</span>
|
| 598 |
<span>μ
λ‘λλ μΉμμ€</span>
|
| 599 |
</div>
|
| 600 |
+
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="λ©λ΄ μ΄κΈ°">β°</button>
|
| 601 |
<div class="header-actions">
|
| 602 |
<span style="margin-right: 12px; color: var(--text-secondary);">{{ current_user.nickname or current_user.username }}</span>
|
| 603 |
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">μ°½μ μ΄μμ€ν΄νΈ</a>
|
|
|
|
| 608 |
</div>
|
| 609 |
</div>
|
| 610 |
|
| 611 |
+
<!-- λͺ¨λ°μΌ λ©λ΄ -->
|
| 612 |
+
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
|
| 613 |
+
<div class="mobile-menu-content" onclick="event.stopPropagation()">
|
| 614 |
+
<div class="mobile-menu-header">
|
| 615 |
+
<div class="mobile-menu-title">λ©λ΄</div>
|
| 616 |
+
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="λ©λ΄ λ«κΈ°">×</button>
|
| 617 |
+
</div>
|
| 618 |
+
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
|
| 619 |
+
<div class="mobile-menu-items">
|
| 620 |
+
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">μ°½μ μ΄μμ€ν΄νΈ</a>
|
| 621 |
+
{% if current_user.is_admin %}
|
| 622 |
+
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">κ΄λ¦¬μ νμ΄μ§</a>
|
| 623 |
+
{% endif %}
|
| 624 |
+
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">λ‘κ·Έμμ</a>
|
| 625 |
+
</div>
|
| 626 |
+
</div>
|
| 627 |
+
</div>
|
| 628 |
+
|
| 629 |
<div class="container">
|
| 630 |
<div class="page-header">
|
| 631 |
<h1>μ
λ‘λλ μΉμμ€</h1>
|
|
|
|
| 833 |
</div>
|
| 834 |
|
| 835 |
<script>
|
| 836 |
+
function toggleMobileMenu() {
|
| 837 |
+
const menu = document.getElementById('mobileMenu');
|
| 838 |
+
menu.classList.toggle('active');
|
| 839 |
+
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
function closeMobileMenu() {
|
| 843 |
+
const menu = document.getElementById('mobileMenu');
|
| 844 |
+
menu.classList.remove('active');
|
| 845 |
+
document.body.style.overflow = '';
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
function closeMobileMenuOnBackdrop(event) {
|
| 849 |
+
if (event.target.id === 'mobileMenu') {
|
| 850 |
+
closeMobileMenu();
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
function escapeHtml(text) {
|
| 855 |
const div = document.createElement('div');
|
| 856 |
div.textContent = text;
|
|
|
|
| 911 |
listContainer.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 24px;">μΉμμ€ λͺ©λ‘μ λΆλ¬μ€λ μ€...</div>';
|
| 912 |
|
| 913 |
try {
|
| 914 |
+
// κ³΅κ° νμΌλ§ μ‘°ν (public_only=true)
|
| 915 |
+
const url = modelName
|
| 916 |
+
? `/api/files?model_name=${encodeURIComponent(modelName)}&public_only=true`
|
| 917 |
+
: '/api/files?public_only=true';
|
| 918 |
const response = await fetch(url, {
|
| 919 |
credentials: 'include'
|
| 920 |
});
|
upload_to_hf.ps1
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces μ
λ‘λ μ€ν¬λ¦½νΈ
|
| 2 |
+
# μ¬μ©λ²: .\upload_to_hf.ps1 -HFSpacePath "C:\path\to\huggingface\space"
|
| 3 |
+
|
| 4 |
+
param(
|
| 5 |
+
[Parameter(Mandatory=$true)]
|
| 6 |
+
[string]$HFSpacePath
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
Write-Host "========================================" -ForegroundColor Cyan
|
| 10 |
+
Write-Host "Hugging Face Spaces μ
λ‘λ μ€λΉ" -ForegroundColor Cyan
|
| 11 |
+
Write-Host "========================================" -ForegroundColor Cyan
|
| 12 |
+
|
| 13 |
+
# κ²½λ‘ νμΈ
|
| 14 |
+
if (-not (Test-Path $HFSpacePath)) {
|
| 15 |
+
Write-Host "μ€λ₯: Hugging Face Spaces λλ ν 리λ₯Ό μ°Ύμ μ μμ΅λλ€: $HFSpacePath" -ForegroundColor Red
|
| 16 |
+
Write-Host "λ¨Όμ 'git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME' λͺ
λ ΉμΌλ‘ ν΄λ‘ νμΈμ." -ForegroundColor Yellow
|
| 17 |
+
exit 1
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
Write-Host "`nλμ λλ ν 리: $HFSpacePath" -ForegroundColor Green
|
| 21 |
+
|
| 22 |
+
# νμ¬ λλ ν 리 νμΈ
|
| 23 |
+
$currentDir = Get-Location
|
| 24 |
+
Write-Host "νμ¬ λλ ν 리: $currentDir" -ForegroundColor Green
|
| 25 |
+
|
| 26 |
+
# νμ νμΌ νμΈ
|
| 27 |
+
$requiredFiles = @("app.py", "Dockerfile", "requirements.txt", "README_HF.md")
|
| 28 |
+
$missingFiles = @()
|
| 29 |
+
|
| 30 |
+
foreach ($file in $requiredFiles) {
|
| 31 |
+
if (-not (Test-Path $file)) {
|
| 32 |
+
$missingFiles += $file
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
if ($missingFiles.Count -gt 0) {
|
| 37 |
+
Write-Host "`nμ€λ₯: λ€μ νμ νμΌμ΄ μμ΅λλ€:" -ForegroundColor Red
|
| 38 |
+
foreach ($file in $missingFiles) {
|
| 39 |
+
Write-Host " - $file" -ForegroundColor Red
|
| 40 |
+
}
|
| 41 |
+
exit 1
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# νμ λλ ν 리 νμΈ
|
| 45 |
+
$requiredDirs = @("app", "templates", "static")
|
| 46 |
+
$missingDirs = @()
|
| 47 |
+
|
| 48 |
+
foreach ($dir in $requiredDirs) {
|
| 49 |
+
if (-not (Test-Path $dir)) {
|
| 50 |
+
$missingDirs += $dir
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if ($missingDirs.Count -gt 0) {
|
| 55 |
+
Write-Host "`nμ€λ₯: λ€μ νμ λλ ν λ¦¬κ° μμ΅λλ€:" -ForegroundColor Red
|
| 56 |
+
foreach ($dir in $missingDirs) {
|
| 57 |
+
Write-Host " - $dir" -ForegroundColor Red
|
| 58 |
+
}
|
| 59 |
+
exit 1
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
Write-Host "`nνμ νμΌ λ° λλ ν 리 νμΈ μλ£!" -ForegroundColor Green
|
| 63 |
+
|
| 64 |
+
# νμΌ λ³΅μ¬ μμ
|
| 65 |
+
Write-Host "`nνμΌ λ³΅μ¬ μμ..." -ForegroundColor Yellow
|
| 66 |
+
|
| 67 |
+
try {
|
| 68 |
+
# νμ νμΌ λ³΅μ¬
|
| 69 |
+
Write-Host "`n[1/4] νμ νμΌ λ³΅μ¬ μ€..." -ForegroundColor Cyan
|
| 70 |
+
Copy-Item "app.py" "$HFSpacePath\" -Force
|
| 71 |
+
Copy-Item "Dockerfile" "$HFSpacePath\" -Force
|
| 72 |
+
Copy-Item "requirements.txt" "$HFSpacePath\" -Force
|
| 73 |
+
Copy-Item "README_HF.md" "$HFSpacePath\README.md" -Force
|
| 74 |
+
Write-Host " β νμ νμΌ λ³΅μ¬ μλ£" -ForegroundColor Green
|
| 75 |
+
|
| 76 |
+
# app λλ ν 리 볡μ¬
|
| 77 |
+
Write-Host "`n[2/4] app λλ ν 리 λ³΅μ¬ μ€..." -ForegroundColor Cyan
|
| 78 |
+
if (Test-Path "$HFSpacePath\app") {
|
| 79 |
+
Remove-Item "$HFSpacePath\app" -Recurse -Force
|
| 80 |
+
}
|
| 81 |
+
Copy-Item -Recurse "app" "$HFSpacePath\app" -Force
|
| 82 |
+
Write-Host " β app λλ ν 리 λ³΅μ¬ μλ£" -ForegroundColor Green
|
| 83 |
+
|
| 84 |
+
# templates λλ ν 리 볡μ¬
|
| 85 |
+
Write-Host "`n[3/4] templates λλ ν 리 λ³΅μ¬ μ€..." -ForegroundColor Cyan
|
| 86 |
+
if (Test-Path "$HFSpacePath\templates") {
|
| 87 |
+
Remove-Item "$HFSpacePath\templates" -Recurse -Force
|
| 88 |
+
}
|
| 89 |
+
Copy-Item -Recurse "templates" "$HFSpacePath\templates" -Force
|
| 90 |
+
Write-Host " β templates λλ ν 리 λ³΅μ¬ μλ£" -ForegroundColor Green
|
| 91 |
+
|
| 92 |
+
# static λλ ν 리 볡μ¬
|
| 93 |
+
Write-Host "`n[4/4] static λλ ν 리 λ³΅μ¬ μ€..." -ForegroundColor Cyan
|
| 94 |
+
if (Test-Path "$HFSpacePath\static") {
|
| 95 |
+
Remove-Item "$HFSpacePath\static" -Recurse -Force
|
| 96 |
+
}
|
| 97 |
+
Copy-Item -Recurse "static" "$HFSpacePath\static" -Force
|
| 98 |
+
Write-Host " β static λλ ν 리 λ³΅μ¬ μλ£" -ForegroundColor Green
|
| 99 |
+
|
| 100 |
+
Write-Host "`n========================================" -ForegroundColor Cyan
|
| 101 |
+
Write-Host "νμΌ λ³΅μ¬ μλ£!" -ForegroundColor Green
|
| 102 |
+
Write-Host "========================================" -ForegroundColor Cyan
|
| 103 |
+
Write-Host "`nλ€μ λ¨κ³:" -ForegroundColor Yellow
|
| 104 |
+
Write-Host "1. Hugging Face Spaces λλ ν λ¦¬λ‘ μ΄λ: cd $HFSpacePath" -ForegroundColor White
|
| 105 |
+
Write-Host "2. Git 컀λ°: git add ." -ForegroundColor White
|
| 106 |
+
Write-Host "3. Git 컀λ°: git commit -m 'Initial deployment'" -ForegroundColor White
|
| 107 |
+
Write-Host "4. Git νΈμ: git push" -ForegroundColor White
|
| 108 |
+
Write-Host "`nλλ Hugging Face μΉ μΈν°νμ΄μ€μμ νμΌμ νμΈνκ³ μ»€λ°νμΈμ." -ForegroundColor White
|
| 109 |
+
|
| 110 |
+
} catch {
|
| 111 |
+
Write-Host "`nμ€λ₯ λ°μ: $_" -ForegroundColor Red
|
| 112 |
+
exit 1
|
| 113 |
+
}
|
| 114 |
+
|