Spaces:
Sleeping
Sleeping
Commit
ยท
e221c83
0
Parent(s):
Deploy clean snapshot of the repository
Browse filesThis view is limited to 50 files because it contains too many changes. ย
See raw diff
- .gitattributes +6 -0
- .github/workflows/sync-to-hub.yml +38 -0
- .gitignore +114 -0
- DEVELOPMENT.md +42 -0
- DEVLOG.md +138 -0
- Dockerfile +41 -0
- README.md +171 -0
- app_deps.txt +59 -0
- core_ml_deps.txt +26 -0
- cursor/mcp.json +12 -0
- notebooks/explore_data.py +160 -0
- notebooks/tabel.py +104 -0
- notebooks/text_cleaner.py +50 -0
- output.txt +22 -0
- package-lock.json +345 -0
- package.json +5 -0
- requirements.txt +76 -0
- run.py +13 -0
- scripts/evaluate_model.py +168 -0
- scripts/evaluate_step1.py +228 -0
- scripts/migrate_recommendations.py +85 -0
- scripts/newtrain.py +282 -0
- scripts/train_final.py +331 -0
- server.log +256 -0
- src/__init__.py +67 -0
- src/auth.py +78 -0
- src/emotion_engine.py +56 -0
- src/main.py +373 -0
- src/models.py +31 -0
- src/recommender.py +31 -0
- src/templates/_macros.html +30 -0
- src/templates/auth_combined.html +73 -0
- src/templates/base.html +43 -0
- src/templates/base_auth.html +19 -0
- src/templates/diary.html +81 -0
- src/templates/login.html +8 -0
- src/templates/main.html +159 -0
- src/templates/page.html +31 -0
- src/templates/signup.html +8 -0
- src/templates/static/css/base_auth.css +121 -0
- src/templates/static/css/diary.css +488 -0
- src/templates/static/css/main.css +651 -0
- src/templates/static/css/page.css +73 -0
- src/templates/static/css/style.css +49 -0
- src/templates/static/js/diary_logic.js +349 -0
- src/templates/static/js/macros_rec_tabs.js +13 -0
- src/templates/static/js/main_logic.js +311 -0
- src/templates/static/js/main_onboarding.js +91 -0
- src/templates/static/js/main_theme.js +88 -0
- src/templates/static/js/script.js +27 -0
.gitattributes
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git LFS๋ก ๊ด๋ฆฌํ ๋์ฉ๋ ํ์ผ ํ์ฅ์ ๋ชฉ๋ก
|
| 2 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/sync-to-hub.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face hub
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
sync-to-hub:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
steps:
|
| 11 |
+
# 1. GitHub ์ ์ฅ์์ ์ฝ๋๋ฅผ ๊ฐ์ ธ์ต๋๋ค. (LFS ํ์ผ ํฌํจ)
|
| 12 |
+
- uses: actions/checkout@v2 # ์์ ์ฑ์ ์ํด v2๋ฅผ ์ฌ์ฉ
|
| 13 |
+
with:
|
| 14 |
+
fetch-depth: 0
|
| 15 |
+
lfs: true
|
| 16 |
+
|
| 17 |
+
# 2. ๊นจ๋ํ ๋ฒ์ ์ ๋ด์ญ์ ๋ง๋ค์ด์ Hugging Face Hub์ ํธ์ํฉ๋๋ค.
|
| 18 |
+
- name: Push clean history to Hugging Face Hub
|
| 19 |
+
env:
|
| 20 |
+
HF_TOKEN: ${{ secrets.TOKEN }} # GitHub Secret ์ด๋ฆ์ TOKEN์ผ๋ก ์ ์ง
|
| 21 |
+
run: |
|
| 22 |
+
# Git ์ฌ์ฉ์ ์ ๋ณด ๋ฐ ์๊ฒฉ ์ ์ฅ์ ์ค์
|
| 23 |
+
git config --global user.email "hf@example.com"
|
| 24 |
+
git config --global user.name "Hugging Face"
|
| 25 |
+
git remote add hf "https://huggingface.co/spaces/taehoon222/emotion-chatbot-app"
|
| 26 |
+
git config --global credential.helper store
|
| 27 |
+
|
| 28 |
+
# ์ฌ๋ฐ๋ฅธ ๋ณ์ ์ด๋ฆ์ธ HF_TOKEN์ ์ฌ์ฉํ๋๋ก ์์
|
| 29 |
+
echo "https://user:${HF_TOKEN}@huggingface.co" > $HOME/.git-credentials
|
| 30 |
+
|
| 31 |
+
# .env ํ์ผ์ด ์กด์ฌํ ๊ฒฝ์ฐ ๊ฐ์ ์ญ์
|
| 32 |
+
rm -f .env
|
| 33 |
+
|
| 34 |
+
# ์๋ก์ด ๋ธ๋์น๋ฅผ ๋ง๋ค์ด ๊ณผ๊ฑฐ ๊ธฐ๋ก์ ๋ชจ๋ ์ ๊ฑฐํ๊ณ ํธ์
|
| 35 |
+
git checkout --orphan clean-history
|
| 36 |
+
git add -A
|
| 37 |
+
git commit -m "Deploy clean snapshot of the repository"
|
| 38 |
+
git push --force hf clean-history:main
|
.gitignore
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
pip-wheel-metadata/
|
| 24 |
+
share/python-wheels/
|
| 25 |
+
*.egg-info/
|
| 26 |
+
.installed.cfg
|
| 27 |
+
*.egg
|
| 28 |
+
MANIFEST
|
| 29 |
+
|
| 30 |
+
# PyInstaller
|
| 31 |
+
*.manifest
|
| 32 |
+
*.spec
|
| 33 |
+
|
| 34 |
+
# Installer logs
|
| 35 |
+
pip-log.txt
|
| 36 |
+
pip-delete-this-directory.txt
|
| 37 |
+
|
| 38 |
+
# Unit test / coverage reports
|
| 39 |
+
htmlcov/
|
| 40 |
+
.tox/
|
| 41 |
+
.nox/
|
| 42 |
+
.coverage
|
| 43 |
+
.coverage.*
|
| 44 |
+
.cache
|
| 45 |
+
nosetests.xml
|
| 46 |
+
coverage.xml
|
| 47 |
+
*.cover
|
| 48 |
+
.hypothesis/
|
| 49 |
+
.pytest_cache/
|
| 50 |
+
|
| 51 |
+
# Environments
|
| 52 |
+
.env
|
| 53 |
+
.venv
|
| 54 |
+
env/
|
| 55 |
+
venv/
|
| 56 |
+
ENV/
|
| 57 |
+
env.bak/
|
| 58 |
+
venv.bak/
|
| 59 |
+
|
| 60 |
+
# IDEs and editors
|
| 61 |
+
.vscode/
|
| 62 |
+
.idea/
|
| 63 |
+
.project
|
| 64 |
+
.pydevproject
|
| 65 |
+
.settings/
|
| 66 |
+
*.swp
|
| 67 |
+
*~
|
| 68 |
+
*.sublime-project
|
| 69 |
+
*.sublime-workspace
|
| 70 |
+
|
| 71 |
+
# Cursor
|
| 72 |
+
cursor/
|
| 73 |
+
|
| 74 |
+
# Supabase
|
| 75 |
+
supabase/.temp/
|
| 76 |
+
|
| 77 |
+
# Node.js
|
| 78 |
+
node_modules/
|
| 79 |
+
npm-debug.log*
|
| 80 |
+
yarn-debug.log*
|
| 81 |
+
yarn-error.log*
|
| 82 |
+
pnpm-debug.log*
|
| 83 |
+
|
| 84 |
+
# Database files
|
| 85 |
+
*.db
|
| 86 |
+
*.sqlite3
|
| 87 |
+
/src/database.db
|
| 88 |
+
|
| 89 |
+
# Model files
|
| 90 |
+
*.pt
|
| 91 |
+
*.bin
|
| 92 |
+
korean-emotion-final/
|
| 93 |
+
|
| 94 |
+
# Logs and results
|
| 95 |
+
logs/
|
| 96 |
+
results/
|
| 97 |
+
results1/
|
| 98 |
+
|
| 99 |
+
# OS-generated files
|
| 100 |
+
.DS_Store
|
| 101 |
+
.DS_Store?
|
| 102 |
+
._*
|
| 103 |
+
.Spotlight-V100
|
| 104 |
+
.Trashes
|
| 105 |
+
ehthumbs.db
|
| 106 |
+
Thumbs.db
|
| 107 |
+
|
| 108 |
+
# Data files
|
| 109 |
+
# ์ฃผ์: data/ ๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ํ์ผ์ด ๋ฌด์๋ฉ๋๋ค.
|
| 110 |
+
# ๋ง์ฝ ํ๋ก์ ํธ ์คํ์ ํ์ํ ๋ฐ์ดํฐ ํ์ผ์ด ์๋ค๋ฉด,
|
| 111 |
+
# ํด๋น ํ์ผ์ ๋ฒ์ ๊ด๋ฆฌ์ ํฌํจ์์ผ์ผ ํฉ๋๋ค.
|
| 112 |
+
# ์: !data/required_data.csv
|
| 113 |
+
data/
|
| 114 |
+
src/static/images/
|
DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ๐งโโ๏ธ ๊ฐ๋ฐ ๊ณผ์ ๋ฐ ๋ฌธ์ ํด๊ฒฐ (Development Journey)
|
| 2 |
+
|
| 3 |
+
์ด ๋ฌธ์์์๋ Emotion Diary ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ ๊ฒช์๋ ์ฃผ์ ๊ธฐ์ ์ ๋์ ๊ณผ์ ์ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๊ณผ์ ์ ์์ธํ ๊ธฐ๋กํฉ๋๋ค.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
### 1. ๋์ฉ๋ AI ๋ชจ๋ธ ๊ด๋ฆฌ ๋ฐ ๋ฐฐํฌ ์ ๋ต ์๋ฆฝ
|
| 8 |
+
- **๋ฌธ์ ์ **: 1GB๊ฐ ๋๋ AI ๋ชจ๋ธ ํ์ผ์ Git LFS๋ก ๊ด๋ฆฌํ์ผ๋, Hugging Face Spaces์ 1GB ์ ์ฅ ๊ณต๊ฐ ํ๊ณ(Storage limit reached)์ LFS ํ์ผ-ํฌ์ธํฐ ๋ถ์ผ์น(LFS pointer does not exist) ๋ฑ ๋ฐฐํฌ ๊ณผ์ ์์ ์ง์์ ์ธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.
|
| 9 |
+
- **ํด๊ฒฐ ๊ณผ์ **: ๋ชจ๋ธ๊ณผ ์ฑ ์ฝ๋์ ์์ ํ ๋ถ๋ฆฌ ์ ๋ต์ ์ฑํํ์ต๋๋ค.
|
| 10 |
+
- ๋์ฉ๋ ๋ชจ๋ธ์ Hugging Face Hub์ ๋ณ๋๋ก ์
๋ก๋ํ์ฌ ๋ฒ์ ๊ด๋ฆฌํฉ๋๋ค.
|
| 11 |
+
- GitHub ์ ์ฅ์์์๋ Git LFS ์ถ์ ์ ์์ ํ ์ ๊ฑฐํ๊ณ ์์ ์ฑ ์ฝ๋๋ง ๊ด๋ฆฌํ๋๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
|
| 12 |
+
- GitHub Actions ์ํฌํ๋ก์ฐ(`sync-to-hub.yml`)์ `lfs` ์ต์
์ `false`๋ก ์ค์ ํ์ฌ, ๋ฐฐํฌ ์์๋ ์ฑ ์ฝ๋๋ง Spaces๋ก ํธ์ํ๋๋ก ์์ ํ์ต๋๋ค.
|
| 13 |
+
- **๊ฒฐ๋ก **: Spaces ์ฑ ์คํ ์์ ์์ `emotion_engine.py`๊ฐ Hub๋ก๋ถํฐ ๋ชจ๋ธ์ ๋ค์ด๋ก๋ํ๋๋ก ๊ตฌํํ์ต๋๋ค. ์ด๋ฅผ ํตํด ์ ์ฅ ๊ณต๊ฐ ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ๊ณ , ์ฝ๋ ๋ณ๊ฒฝ ์ ๋ชจ๋ธ์ ๋ค์ ์
๋ก๋ํ ํ์๊ฐ ์๋ ํจ์จ์ ์ธ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ์ ์์ฑํ์ต๋๋ค.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
### 2. CI/CD ํ์ดํ๋ผ์ธ์ ๋ถ์ฐ ํ๊ฒฝ ์ธ์ฆ ๋ฌธ์ ํด๊ฒฐ
|
| 18 |
+
- **๋ฌธ์ ์ **: ๋ก์ปฌ์์ `git push`๋ก ํธ๋ฆฌ๊ฑฐ๋ GitHub Actions๊ฐ Hugging Face Spaces์ ์ ๊ทผํ ๋ `Invalid credentials` ์ธ์ฆ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ด๋ Spaces์ Secret๊ณผ GitHub Actions์ Secret ์ญํ ์ ๋ํ ํผ๋ ๋๋ฌธ์ด์์ต๋๋ค.
|
| 19 |
+
- **ํด๊ฒฐ ๊ณผ์ **: '๋ฐฐํฌ ๋ก๋ด(GitHub Actions)'๊ณผ '๋น๋ ๋ก๋ด(Spaces)'์ ๊ฐ๋
์ผ๋ก ์ญํ ์ ๋ช
ํํ ๋ถ๋ฆฌํ์ฌ ์ ๊ทผํ์ต๋๋ค.
|
| 20 |
+
- **GitHub Actions Secret (`HF_TOKEN`)**: '๋ฐฐํฌ ๋ก๋ด'์ด Hugging Face ์ ์ฅ์(Repository)์ ์ฝ๋๋ฅผ ํธ์ํ ๋ ํ์ํ `write` ๊ถํ ํ ํฐ์ ๋ฑ๋กํ์ต๋๋ค.
|
| 21 |
+
- **Hugging Face Spaces Secret (`HF_TOKEN`)**: '๋น๋ ๋ก๋ด'์ด ๋ด๋ถ์ ์ผ๋ก LFS ํ์ผ ์ฒ๋ฆฌ๋ ๋ค๋ฅธ private ์ ์ฅ์์ ์ ๊ทผํ ๋ ํ์ํ ํ ํฐ์ ๋ฑ๋กํ์ต๋๋ค. (์ด ํ๋ก์ ํธ์์๋ ๋ชจ๋ธ์ ๋ถ๋ฆฌํ๋ฉด์ Spaces Secret์ ํ์์ฑ์ ๋ฎ์์ก์ต๋๋ค.)
|
| 22 |
+
- **๊ฒฐ๋ก **: ๊ฐ๊ธฐ ๋ค๋ฅธ ์คํ ํ๊ฒฝ์์ ํ์ํ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ช
ํํ ๋ถ๋ฆฌํ๊ณ ์ฌ๋ฐ๋ฅธ ๊ถํ์ ํ ํฐ์ ์ ๊ณตํจ์ผ๋ก์จ CI/CD ํ์ดํ๋ผ์ธ์ ์ธ์ฆ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
### 3. Flask ์ ํ๋ฆฌ์ผ์ด์
๊ตฌ์กฐ ์ค๊ณ ๋ฐ ๋ฐํ์ ์ค๋ฅ ๋๋ฒ๊น
|
| 27 |
+
- **๋ฌธ์ ์ **: ๊ฐ๋ฐ ์ด๊ธฐ, ๋ชจ๋ ๋ก์ง์ด ๋ด๊ธด ๋จ์ผ ํ์ผ ๊ตฌ์กฐ(`app.py`)๋ก ์ธํด ์ํ ์ฐธ์กฐ(Circular Import) ๋ฐ `ModuleNotFoundError`๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ํ, API๊ฐ ํธ์ถ๋ ๋๋ง๋ค AI ๋ชจ๋ธ์ ๋ก๋ฉํ์ฌ ์๋ต ์๋๊ฐ ๋งค์ฐ ๋๋ ธ์ต๋๋ค.
|
| 28 |
+
- **ํด๊ฒฐ ๊ณผ์ **: Flask์ **Application Factory ํจํด**์ ๋์
ํ์ฌ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ์ฌ์ค๊ณํ์ต๋๋ค.
|
| 29 |
+
- `src/__init__.py`์ `create_app` ํจ์๋ฅผ ํตํด ์ฑ์ ๋ชจ๋ ๊ตฌ์ฑ์์(DB, ๋ธ๋ฃจํ๋ฆฐํธ, ์ค์ )๋ฅผ ์กฐ๋ฆฝํ๋๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
|
| 30 |
+
- ์ฑ์ด ์์๋๋ ์์ (`create_app` ๋ด๋ถ)์์ AI ๋ชจ๋ธ์ ๋จ ํ ๋ฒ๋ง ๋ก๋ํ์ฌ `app` ๊ฐ์ฒด์ ์ ์ฅ(`app.emotion_classifier`)ํ์ต๋๋ค.
|
| 31 |
+
- **๊ฒฐ๋ก **: ๊ฐ API ์์ฒญ์์๋ `current_app` ํ๋ก์๋ฅผ ํตํด ๋ฏธ๋ฆฌ ๋ก๋๋ ๋ชจ๋ธ์ ์ฐธ์กฐํ๊ฒ ํ์ฌ, ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ๊ณผ ์๋ต ์๋๋ฅผ ๊ทน๋ํํ์ต๋๋ค. ์ด๋ฅผ ํตํด ํ์ฅ ๊ฐ๋ฅํ๊ณ ์์ ์ ์ธ ๋ฐฑ์๋ ๊ตฌ์กฐ๋ฅผ ์์ฑํ ์ ์์์ต๋๋ค.
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
### 4. ๋ชจ๋ธ ์ฑ๋ฅ ํ๋ฝ ๋ฐ ์ ํ๋ ๊ฐ์ ๊ณผ์
|
| 36 |
+
- **๋ฌธ์ ์ **: ๊ฐ๋ฐ ๊ณผ์ ์์ ์๋ณธ ํ์ต ๋ฐ์ดํฐ๊ฐ ์ ์ค๋์ด ๋ชจ๋ธ์ ์ฌํ์ตํ์, ์ ํ๋๊ฐ ์ด๊ธฐ ๋ชจ๋ธ๋ณด๋ค ํ์ ํ ๋ฎ์์ง๋ ๋ฌธ์ ์ ์ง๋ฉดํ์ต๋๋ค.
|
| 37 |
+
- **ํด๊ฒฐ ๊ณผ์ **: ์ฑ๋ฅ์ ๋ณต์ํ๊ณ ๊ฐ์ ํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ฒด๊ณ์ ์ผ๋ก ์คํํ์ต๋๋ค.
|
| 38 |
+
- **ํผ๋ ํ๋ ฌ(Confusion Matrix) ๋ถ์**: ๋ชจ๋ธ์ด ์ด๋ค ๊ฐ์ ๋ค์ ์๋ก ํผ๋ํ๋์ง ํ์
ํ์ฌ ๋ฌธ์ ์ ์์ธ์ ์ง๋จํ์ต๋๋ค.
|
| 39 |
+
- **๋ ์ด๋ธ ์ฌ๊ตฌ์ฑ (Label Remapping)**: ์ธ๋ถํ๋ ๊ฐ์ ๋ ์ด๋ธ์ 6๊ฐ ๋๋ 4๊ฐ์ ์ฃผ์ ๊ฐ์ ์ผ๋ก ๊ทธ๋ฃนํํ์ฌ ๋ถ๋ฅ ๋ฌธ์ ์ ๋ณต์ก๋๋ฅผ ์กฐ์ ํ์ต๋๋ค.
|
| 40 |
+
- **๋ฐ์ดํฐ ๋ถ๊ท ํ ํด์**: ์์ ํด๋์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฆ๊ฐํ๋ ์ค๋ฒ์ํ๋ง(Oversampling) ๋ฐ ๊ฐ ํด๋์ค์ ๋ค๋ฅธ ์ค์๋๋ฅผ ๋ถ์ฌํ๋ ์๋ ํด๋์ค ๊ฐ์ค์น(Manual Class Weights)๋ฅผ ์ ์ฉํ์ต๋๋ค.
|
| 41 |
+
- **์ ์ด ํ์ต(Transfer Learning) ๊ฐํ**: ๊ฐ์ฑ ๋ถ์์ ํนํ๋ NSMC(Naver Movie Corpus) ๋ฐ์ดํฐ์
์ผ๋ก 1์ฐจ ์ฌ์ ํ์ตํ ๋ชจ๋ธ์ ๊ธฐ๋ฐ์ผ๋ก, ์ต์ข
๊ฐ์ ๋ฐ์ดํฐ์ 2์ฐจ ๋ฏธ์ธ์กฐ์ (Fine-tuning)์ ์ํํ์ต๋๋ค.
|
| 42 |
+
- **๊ฒฐ๋ก **: ์ฌ๋ฌ ์คํ ๊ฒฐ๊ณผ, 'ํ๊ตญ์ด ๊ฐ์ฑ๋ํ ๋ง๋ญ์น' ๋ฐ์ดํฐ์ ๋ ์ด๋ธ์ 6๊ฐ์ ์ฃผ์ ๊ฐ์ ์ผ๋ก ๋งคํํ๊ณ , ๋ฐ์ดํฐ ๋ถ๊ท ํ ๋ฌธ์ ๋ฅผ ํด๏ฟฝ๏ฟฝํ๊ธฐ ์ํด **์๋์ผ๋ก ํด๋์ค ๊ฐ์ค์น๋ฅผ ์ ๊ตํ๊ฒ ์กฐ์ **ํ์ฌ ํ์ตํ๋ ๋ฐฉ์์ด ์ฝ 80%์ ์ ํ๋๋ฅผ ํ๋ณตํ๋ฉฐ ๊ฐ์ฅ ์์ ์ ์ด๊ณ ํจ๊ณผ์ ์์ ํ์ธํ์ต๋๋ค.
|
DEVLOG.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
## ๐ ์๋ก์ด ์์ด๋์ด ๋ฐ ๊ฐ๋ฐ ์์ (Upcoming Tasks)
|
| 4 |
+
|
| 5 |
+
1. **๋ฌ๋ ฅ ๋์์ธ ๊ฐ์ :** ํฌ๋ช
์คํ์ผ์์ ๊ฐ๋
์ฑ ๋์ '๋ชจ๋ ์นด๋(Modern Card)' ์คํ์ผ๋ก ๋ณ๊ฒฝ.
|
| 6 |
+
2. **๊ฐ์ ์ด๋ชจ์ง ํค ๋งค์นญ:** ์ ์ฅ ์ ๋๋ฉ์ด์
๊ตฌ์ฌ(Orb) ์์์ ๊ฐ์ ๋ณ ๋ถ์๊ธฐ์ ๋ง๊ฒ ์ธ๋ถ ์กฐ์ .
|
| 7 |
+
3. **์ฉ์ด ์ง๊ดํ:** ์ถ์ฒ ์นดํ
๊ณ ๋ฆฌ ๋ช
์นญ ๋ณ๊ฒฝ ('์์ฉ/์ ํ' โ **'๊ณต๊ฐ/ํ๊ธฐ'**) ๋ฐ ๊ด๋ จ ๋ก์ง ๋๊ธฐํ.
|
| 8 |
+
4. **์ ์ฅ ์ ๋๋ฉ์ด์
๊ณ ๋ํ:** ๊ตฌ์ฌ์ด ํตํต ํ๊ธฐ๋ค(Bouncing) '๋์ ์ผ๊ธฐ' ๋ฉ๋ด๋ก ๊ฐ๋ ฅํ๊ฒ ๋ ์๊ฐ๋(Super Jump) ํจ๊ณผ ๊ตฌํ.
|
| 9 |
+
5. **๋ง์ดํ์ด์ง '๊ธฐ์ต์ ์ ๋ฆฌ๋ณ' ์๊ฐํ ๊ตฌํ.**
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## ๐
2025๋
11์ 27์ผ
|
| 14 |
+
### ์ฃผ์ : UI ๋ํ
์ผ ์์ฑ๋ ํฅ์ & ์ ๋๋ฉ์ด์
๊ณ ๋ํ (Polishing)
|
| 15 |
+
|
| 16 |
+
์ฌ์ฉ์ ๊ฒฝํ์ ์ง์ ๋์ด๊ธฐ ์ํด ๋ด๋น๊ฒ์ด์
, ๋ฌ๋ ฅ ๋ฑ ํต์ฌ UI ์์๋ฅผ ํ๋์ ์ธ ์คํ์ผ๋ก ๋ฆฌ๋์์ธํ๊ณ , ์ ์ฅ ์ ๋๋ฉ์ด์
์ ๋ฌผ๋ฆฌ์ ์ธ ์๋๊ฐ์ ๋ํ์ต๋๋ค.
|
| 17 |
+
|
| 18 |
+
#### 1. ๋งค์ง ๋ด๋น๊ฒ์ด์
๋ฐ (Magic Capsule Navbar)
|
| 19 |
+
- **๋์์ธ:** ๊ธฐ์กด ํ
์คํธ ๋์ด ๋ฐฉ์์ ํํผํ์ฌ, ๊ณต์ค์ ๋ ์๋ **์บก์ ํํ์ ๊ธ๋์ค๋ชจํผ์ฆ(Glassmorphism)** ๋์์ธ ์ ์ฉ.
|
| 20 |
+
- **์ธํฐ๋์
:** ๋ง์ฐ์ค์ ์์ง์์ ๋ฐ๋ผ ํ์์ ํ์ด๋ผ์ดํธ ๋ฐ(`nav-marker`)๊ฐ ๋ฉ๋ด ์ฌ์ด๋ฅผ ๋ฌผ ํ๋ฅด๋ฏ ๋ฐ๋ผ๋ค๋๋ **Sliding Magic Line** ํจ๊ณผ ๊ตฌํ.
|
| 21 |
+
|
| 22 |
+
#### 2. ๋์๋ณด๋ํ ๋ฌ๋ ฅ (Modern Card Calendar)
|
| 23 |
+
- **์คํ์ผ:** ํฌ๋ช
ํ ๋ฐฐ๊ฒฝ ๋์ ๊ฐ๋
์ฑ์ด ๋ฐ์ด๋ **'๋ชจ๋ ์นด๋(Modern Card)'** ์คํ์ผ๋ก ๋ณ๊ฒฝ. ํค๋์ ํ
๋ง ์์์ ์ ์ฉํ์ฌ ์ง์ค๋ ํฅ์.
|
| 24 |
+
- **๋ ์ด์์ ์ต์ ํ:** ์์ผ(Header)๊ณผ ๋ ์ง(Grid)์ ์ ๋ ฌ์ด ์ด๊ธ๋๋ ๋ฌธ์ ๋ฅผ CSS `flex-basis: 14.28%` ๊ฐ์ ์ ์ฉ์ ํตํด 1:1๋ก ์๋ฒฝํ๊ฒ ๋ง์ถค.
|
| 25 |
+
- **์๊ฐํ:** ๋ ์ง ์๋์ ํด๋น ์ผ๊ธฐ์ ๊ฐ์ ์ ๋ํ๋ด๋ **์์ ์ (Dot)**์ด ์๋์ผ๋ก ํ์๋๋๋ก ๊ตฌํ.
|
| 26 |
+
|
| 27 |
+
#### 3. ์ ์ฅ ์ ๋๋ฉ์ด์
๊ณ ๋ํ (Bouncing & Super Jump)
|
| 28 |
+
- **๋ฌผ๋ฆฌ ํจ๊ณผ:** ๋จ์ํ ๋ ์๊ฐ๋ ์ ๋๋ฉ์ด์
์ **"๋ฐ๋ฅ์ ํตํต ํ๊ธฐ๋ค๊ฐ(Bouncing) ์๋์ง๋ฅผ ๋ชจ์ ๋ชฉํ์ง์ ์ผ๋ก ์์
๋ ์๊ฐ๋(Super Jump)"** ์ญ๋์ ์ธ ์ํ์ค๋ก ์
๊ทธ๋ ์ด๋.
|
| 29 |
+
- **๋ฐ์ดํฐ ๋๊ธฐํ:** CSS ์ ๋๋ฉ์ด์
์๊ฐ(5.1์ด)๊ณผ ์๋ฒ ์ ์ฅ ์์ฒญ(`fetch`)์ `Promise.all`๋ก ๋๊ธฐํํ์ฌ, ์ ๋๋ฉ์ด์
๊ณผ ๋ฐ์ดํฐ ์ ์ฅ์ด ์์ ํ๊ฒ ์๋ฃ๋ ํ ํ์ด์ง๊ฐ ์ด๋๋๋๋ก ๋ก์ง ๊ฐํ.
|
| 30 |
+
|
| 31 |
+
#### 4. ๊ฐ์ฑ ๋ํ
์ผ ๋ฐ ์ฉ์ด ๊ฐ์
|
| 32 |
+
- **์ฉ์ด ์ง๊ดํ:** ์ถ์ฒ ์นดํ
๊ณ ๋ฆฌ ๋ช
์นญ์ '์์ฉ/์ ํ'์์ ๋ ๊ฐ์ฑ์ ์ธ **'๊ณต๊ฐ/ํ๊ธฐ'**๋ก ๋ณ๊ฒฝํ๊ณ , ํ๋ก ํธ์๋ ํ์ฑ ๋ก์ง์ ์ด์ ๋ง์ถฐ ์์ .
|
| 33 |
+
- **์ถ์ฒ ์์ฝ:** AI์ ๊ธด ์ถ์ฒ ์ด์ ๋ฅผ ์ ๊ฑฐํ๊ณ , ํต์ฌ ์ ๋ณด(์ข
๋ฅ, ์ ๋ชฉ)๋ง ๊น๋ํ๊ฒ ๋ณด์ฌ์ฃผ๋๋ก ํ
์คํธ ์ ์ ํจ์(`extractSummary`) ์ ์ฉ.
|
| 34 |
+
- **ํค ๋งค์นญ:** ์ ์ฅ ์ ๋๋ฉ์ด์
์ ์์ฑ๋๋ '๊ธฐ์ต ๊ตฌ์ฌ'์ ์์์ ๊ฐ ๊ฐ์ (๊ธฐ์จ=Gold, ์ฌํ=SteelBlue ๋ฑ)์ ๊ณ ์ ๋ถ์๊ธฐ์ ๋ง์ถฐ ์ธ๋ฐํ๊ฒ ์กฐ์ .
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## ๐
2025๋
11์ 25์ผ
|
| 39 |
+
### ์ฃผ์ : ์ ๋๋ฉ์ด์
ํตํฉ ๋ฐ ๋ฒ๊ทธ ์์
|
| 40 |
+
|
| 41 |
+
`test_animation.html`์์ ๊ฐ๋ฐ๋ ์ฑ
์ ๊ธฐ ๋ฐ ๊ตฌ์ฌ ๋ฐ์ด์ค ์ ๋๋ฉ์ด์
์ `main.html`์ ํตํฉํ๊ณ ๊ด๋ จ ๋ฒ๊ทธ๋ฅผ ์์ ํ์ต๋๋ค.
|
| 42 |
+
|
| 43 |
+
#### 1. ์ ๋๋ฉ์ด์
๊ธฐ๋ฅ ๊ฐ์ ๋ฐ ํตํฉ
|
| 44 |
+
- **์ ๋๋ฉ์ด์
์์ ๋ฌธ์ ํด๊ฒฐ**: `playFullAnimation` ํจ์์ ์ ๋ฌ๋๋ ์ ์๋์ง ์์ ๋ณ์(`rect`) ์ค๋ฅ๋ฅผ ์์ ํ์ฌ ์ ๋๋ฉ์ด์
์ด ์ ์์ ์ผ๋ก ์์๋๋๋ก ํ์ต๋๋ค.
|
| 45 |
+
- **๊ตฌ์ฌ ๋ฐ์ด์ค ์ ๊ตํ**: ์ค์์์ ์์ํ์ฌ ํ๋ฉด ์ค๋ฅธ์ชฝ ๊ฐ์ฅ์๋ฆฌ์์ ์ ํํ ๋ง๋ฌด๋ฆฌ๋๋ 6ํ ๋ฐ์ด์ค ์ ๋๋ฉ์ด์
์ ๊ตฌํํ์ต๋๋ค. ๊ฐ ๋ฐ์ด์ค์ ์ํ ์ด๋ ๊ฑฐ๋ฆฌ๋ฅผ ์กฐ์ ํ์ฌ ์์ฐ์ค๋ฌ์ด ์์ง์์ ๋ง๋ค์์ต๋๋ค.
|
| 46 |
+
- **์ฑ
โ ๊ตฌ์ฌ ๋ณ์ ์ธํธ๋ก ์ ๋๋ฉ์ด์
๊ตฌํ**:
|
| 47 |
+
- ์ฑ
์ด ๊ตฌ๊ฒจ์ง๋ฏ ์ค์์ผ๋ก ๋ชจ์ฌ ๊ตฌ์ฌ๋ก ๋ณํ๋ 1.5์ด(์ฑ
์์ถ) + 5์ด(๊ตฌ์ฌ ๋ฐ์ด์ค) = ์ด 6.5์ด ๊ธธ์ด์ ์ธํธ๋ก ์ ๋๋ฉ์ด์
์ ๊ตฌํํ์ต๋๋ค.
|
| 48 |
+
- ์ฑ
์ด ๋ชจ์ด๋ ๋์ ๊ตฌ์ฌ์ ๊ฐ์ ์์์ผ๋ก ๋ฐฐ๊ฒฝ์์ด ๋ณํ๋๋ก ํ์ฌ ์๊ฐ์ ์ฐ๊ฒฐ์ฑ์ ๊ฐํํ์ต๋๋ค.
|
| 49 |
+
- ์ฑ
์ด ๋ชจ์ด๋ ๋์์ '์ ํ๋' ๋์ ํ์ ์์ด '์ค์์ผ๋ก ์์ถ๋๋' ๋๋์ ์ฃผ๋๋ก `transform` ์์ฑ์ ์กฐ์ ํ์ต๋๋ค.
|
| 50 |
+
- ์์ถ๋๋ ์ฑ
์ด ์ต์ข
์ ์ผ๋ก ๊ตฌ์ฌ ๋ชจ์๊ณผ ์ ์ฐ๊ฒฐ๋๋๋ก `borderRadius` ์ ๋๋ฉ์ด์
์ ๋ค์ ๋์
ํ์ต๋๋ค.
|
| 51 |
+
- ์ฑ
์ด ์ฌ๋ผ์ง๋ ๋๋ ์์ด ๊ตฌ์ฌ๋ก ์ ํ๋๋๋ก, ์ฑ
์ด ์ฌ๋ผ์ง๋ ๋์ ๊ตฌ์ฌ์ด ๋์์ ๋ํ๋๋ ํฌ๋ก์คํ์ด๋(cross-fade) ๊ธฐ๋ฒ์ ์ฌ์ฉํ์ฌ ๋ถ๋๋ฌ์ด ๋ณ์ ์ ๊ตฌํํ์ต๋๋ค.
|
| 52 |
+
- **`main.html` ํตํฉ**: ๊ฐ๋ฐ๋ ์ ๋๋ฉ์ด์
๋ก์ง์ `src/templates/static/js/main_logic.js` ํ์ผ๋ก ์ด์ ํ์ต๋๋ค. ์ด ์ ๋๋ฉ์ด์
์ ์ด์ '์ผ๊ธฐ์ฅ์ ์ ์ฅํ๊ธฐ' ๋ฒํผ์ ํด๋ฆญํ ๋ ์คํ๋ฉ๋๋ค.
|
| 53 |
+
|
| 54 |
+
#### 2. ๋ฆฌ๋๋ ์
๋ฒ๊ทธ ์์
|
| 55 |
+
- **๋ฌธ์ **: ์ ๋๋ฉ์ด์
์๋ฃ ํ `main_logic.js`์์ `/diary` ๊ฒฝ๋ก๋ก ๋ฆฌ๋๋ ์
ํ ๋ 'Not Found' ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.
|
| 56 |
+
- **์์ธ**: ๋ฐฑ์๋์ `/diary` ๋ผ์ฐํธ๊ฐ ์ ์๋์ด ์์ง ์๊ณ , ์ค์ ์ผ๊ธฐ ๋ชฉ๋ก ํ์ด์ง๋ `/my_diary` ๋ผ์ฐํธ๋ฅผ ์ฌ์ฉํ๊ณ ์์์ต๋๋ค.
|
| 57 |
+
- **ํด๊ฒฐ**: `main_logic.js` ํ์ผ ๋ด `window.location.href`์ ๊ฒฝ๋ก๋ฅผ `/diary`์์ ์ฌ๋ฐ๋ฅธ ๊ฒฝ๋ก์ธ `/my_diary`๋ก ์์ ํ์ฌ ๋ฆฌ๋๋ ์
์ค๋ฅ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
## ๐
2025๋
11์ 23์ผ
|
| 61 |
+
### ์ฃผ์ : ๋ฉ์ธ ํ๋ฉด UI/UX ๋๊ท๋ชจ ๊ฐํธ & ์ธํฐ๋์
๊ฐํ
|
| 62 |
+
|
| 63 |
+
์ฌ์ฉ์๊ฐ ์ผ๊ธฐ๋ฅผ ์ฐ๋ ํ์์ ๋ ๊น์ด ๋ชฐ์
ํ ์ ์๋๋ก, ๋ฉ์ธ ํ๋ฉด์ ๋์์ธ ์ธ์ด๋ฅผ '๋์งํธ'์์ '์๋ ๋ก๊ทธ ๊ฐ์ฑ'์ผ๋ก ์ ๋ฉด ๊ต์ฒดํ๊ณ , ์ ์ฅ ๊ณผ์ ์ ์คํ ๋ฆฌํ
๋ง์ ๋ด์์ต๋๋ค.
|
| 64 |
+
|
| 65 |
+
#### 1. ํผ์ณ์ง ๋ค์ด์ด๋ฆฌ (Book Spread Layout)
|
| 66 |
+
- **๋ฌธ์ :** ๊ธฐ์กด์ ๋ถ๋ฆฌ๋ ์
๋ ฅ์ฐฝ(Left)๊ณผ ๊ฒฐ๊ณผ์ฐฝ(Right)์ ์์ ์ด ๋ถ์ฐ๋๊ณ , '์ผ๊ธฐ์ฅ'์ด๋ผ๋ ์ฑ์ ์ ์ฒด์ฑ์ ์๊ฐ์ ์ผ๋ก ์ ๋ฌํ๊ธฐ ๋ถ์กฑํจ.
|
| 67 |
+
- **ํด๊ฒฐ:** ํ๋ฉด ์ ์ฒด๋ฅผ ์์ฐ๋ฅด๋ **'ํผ์ณ์ง ๋ค์ด์ด๋ฆฌ'** ํํ์ ๋ ์ด์์ ๋์
.
|
| 68 |
+
- **๊ตฌํ ๋ํ
์ผ:**
|
| 69 |
+
- **Wide Layout:** `max-width: 1600px`, `min-width: 1200px`๋ฅผ ์ ์ฉํ์ฌ ์ค์ ์ฑ
์ ์์ ๋
ธํธ๋ฅผ ํผ์น ๋ฏํ ์์ํ ๊ฐ๋ฐฉ๊ฐ ํ๋ณด.
|
| 70 |
+
- **Clean UI:** ์ถ์ฒ ์ฝํ
์ธ ๊ฐ ๊ธธ์ด์ง ๊ฒฝ์ฐ ์คํฌ๋กค์ ๋์ง๋ง ์คํฌ๋กค๋ฐ(Bar)๋ ์จ๊ฒจ(`scrollbar-width: none`) ๊น๋ํจ ์ ์ง.
|
| 71 |
+
- **Line-clamp:** ์ถ์ฒ ํ
์คํธ๊ฐ ๊ธธ์ด์ง๋ฉด `line-clamp`๋ก ๋ง์ค์ ์ฒ๋ฆฌํ๊ณ , Hover ์ ์ ์ฒด ๋ด์ฉ์ ๋ณด์ฌ์ฃผ๋ ์ธํฐ๋์
์ถ๊ฐ.
|
| 72 |
+
|
| 73 |
+
#### 2. '๊ธฐ์ต์ ๊ตฌ์ฌ' ์ ์ฅ ์ ๋๋ฉ์ด์
(Memory Orb & Bouncing)
|
| 74 |
+
- **๊ธฐํ ์๋:** ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก ์ ์ก(Submit)ํ๋ ๊ฒ์ ๋์ด, **"๋์ ์์คํ ํ๋ฃจ๊ฐ ๊ธฐ์ต์ ๊ตฌ์ฌ์ด ๋์ด ๋ณด๊ด๋๋ค"**๋ ๋ฉํํฌ(์ํ *์ธ์ฌ์ด๋ ์์* ๋ชจํฐ๋ธ)๋ฅผ ์ ๋ฌํ๊ณ ์ ํจ.
|
| 75 |
+
- **์ ๋๋ฉ์ด์
์๋๋ฆฌ์ค:**
|
| 76 |
+
1. **์์ถ (Morph):** ์ ์ฅ ๋ฒํผ ํด๋ฆญ ์, ๊ฑฐ๋ํ๋ ๋ค์ด์ด๋ฆฌ๊ฐ ์์๊ฐ์ ์์์ง๋ฉฐ ๋๊ทธ๋ ๊ตฌ์ฌ๋ก ๋ณ์ .
|
| 77 |
+
2. **๊ฐ์ ์ ์:** AI๊ฐ ๋ถ์ํ ๊ฐ์ (๊ธฐ์จ=๋
ธ๋, ์ฌํ=ํ๋ ๋ฑ)์ ๋ง์ถฐ ๊ตฌ์ฌ์ ์์์ด ์ค์๊ฐ์ผ๋ก ๋ณํ.
|
| 78 |
+
3. **์ญ๋์ ์ด๋ (Bouncing):** ๊ตฌ์ฌ์ด ๋ฐ๋ฅ์ ํตํต ํ๊ธฐ๋ฉฐ(Squash & Stretch) ์๋์ง๋ฅผ ๋ชจ์ผ๋ค, ๋ง์ง๋ง์ ๋ฉ๋ด(๋ณด๊ดํจ) ์ชฝ์ผ๋ก ์์
๋ ์๊ฐ.
|
| 79 |
+
- **๊ธฐ์ ์ ๊ตฌํ:**
|
| 80 |
+
- **Clone Node:** ์๋ณธ DOM์ ๊ฑด๋๋ฆฌ๋ฉด ๋ ์ด์์์ด ๊นจ์ง๋ฏ๋ก, `cloneNode`๋ก ๋ค์ด์ด๋ฆฌ์ ๋ณต์ ๋ณธ์ ์์ฑํ์ฌ `fixed` ํฌ์ง์
์ผ๋ก ๋์ด ๋ค ์ ๋๋ฉ์ด์
์ ์ฉ.
|
| 81 |
+
- **Keyframes:** `0%`~`100%` ๊ตฌ๊ฐ์ ์ธ๋ฐํ๊ฒ ๋๋์ด ์์น ์ด๋(`translate`)๊ณผ ํํ ๋ณํ(`scale`, `border-radius`)์ ๋์์ ์ ์ด.
|
| 82 |
+
|
| 83 |
+
#### ๐ Next Step
|
| 84 |
+
- ์๋จ ๋ด๋น๊ฒ์ด์
๋ฐ(Navbar)์ 'Sliding Magic Line' ์ธํฐ๋์
์ ์ฉ ์์ .
|
| 85 |
+
- ๋ง์ดํ์ด์ง '๊ธฐ์ต์ ์ ๋ฆฌ๋ณ' ์๊ฐํ ๊ตฌํ.
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
## ๐
2025๋
11์ 21์ผ (UI/UX Refinement)
|
| 91 |
+
|
| 92 |
+
### Feature: ์๋ก์ด ํ๋จ ๋ด๋น๊ฒ์ด์
๋ฐ
|
| 93 |
+
- ๊ธฐ์กด ์๋จ ํค๋๋ฅผ ๋์ฒดํ๋ ์๋ก์ด SVG ๊ธฐ๋ฐ ํ๋จ ๋ด๋น๊ฒ์ด์
๋ฐ๋ฅผ ๊ตฌํํ์ต๋๋ค.
|
| 94 |
+
- ์ฃผ์ ๊ธฐ๋ฅ์ธ 'ํ', '๋์ ์ผ๊ธฐ', '๋ง์ดํ์ด์ง' ๋ฐ '๋ก๊ทธ์์' ๋ฒํผ์ ํฌํจํฉ๋๋ค.
|
| 95 |
+
- ๋ด๋น๊ฒ์ด์
๋ฐ๋ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ์ ๋๋ง ํ์๋๋๋ก ์ค์ ํ์ต๋๋ค.
|
| 96 |
+
|
| 97 |
+
### Refactor: ๋ด๋น๊ฒ์ด์
๋ฐ ์ธํฐ๋์
๊ฐ์
|
| 98 |
+
- **UI ์คํ์ผ:** ํฐ์ ๋ฐฐ๊ฒฝ, ์ด๋์ด ์์์ ์์ด์ฝ ๋ฐ ํ์ฑ ํ์ด์ง๋ฅผ ๋ํ๋ด๋ '๋น ํจ๊ณผ(tubelight)'๋ฅผ ์ ์ฉํ์ต๋๋ค. 4๊ฐ์ ์์ด์ฝ์ ๋ง๊ฒ ๋๋น๋ฅผ ์กฐ์ ํ์ต๋๋ค.
|
| 99 |
+
- **์์น ๋ฌธ์ ํด๊ฒฐ (CSS ๊ธฐ๋ฐ ๋ก์ง):** ํ์ด์ง ๋ก๋ ์ '๋น ํจ๊ณผ'์ ์์น๊ฐ ์ด๊ธ๋๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ๋ถ์์ ํ JavaScript ํ์ด๋ฐ ๋์ CSS๋ง์ผ๋ก ์์น๋ฅผ ์ ์ดํ๋ ๋ฐฉ์์ผ๋ก ๋ก์ง์ ์ ๋ฉด ์์ ํ์ต๋๋ค.
|
| 100 |
+
- ์๋ฒ์์ ํ์ฌ ํ์ด์ง ์ฃผ์(`request.endpoint`)์ ๋ฐ๋ผ `<nav>` ํ๊ทธ์ `active-main-home` ๊ณผ ๊ฐ์ ๋์ ํด๋์ค๋ฅผ ๋ถ์ฌํฉ๋๋ค.
|
| 101 |
+
- CSS๋ ์ด ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ '๋น ํจ๊ณผ'์ ์ด๊ธฐ `left` ์์น๋ฅผ ์ง์ ์ง์ ํ๋ฏ๋ก, ๋ ์ด์ ๋ ๋๋ง ์์ ์ ๊ฒฝ์ ์ํ(Race Condition) ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
|
| 102 |
+
- **์ ๋๋ฉ์ด์
ํผ๋๋ฐฑ (JS):** `navigation.js` ์คํฌ๋ฆฝํธ๋ ์ด์ ์ด๊ธฐ ์์น ๊ณ์ฐ์ ์ ํ ํ์ง ์๊ณ , ์ฌ์ฉ์๊ฐ ์์ด์ฝ์ ํด๋ฆญํ์ ๋ ์๊ฐ์ ํผ๋๋ฐฑ์ ์ฃผ๊ธฐ ์ํด ๋น์ ์์ง์ด๋ ๋จ์ํ ์ญํ ๋ง ์ํํ๋๋ก ๋ํญ ์ถ์ํ์ต๋๋ค.
|
| 103 |
+
|
| 104 |
+
### Feature: ํ์ด์ง ์ ํ ํจ๊ณผ (์ผ์์ ๋กค๋ฐฑ)
|
| 105 |
+
- ๋ถ๋๋ฌ์ด ํ์ด์ง ์ด๋ ๊ฒฝํ์ ์ํด ํ๋ฉด ์ ์ฒด๋ฅผ ๋ฎ๋ '์ปคํผ' ๋ฐฉ์์ ์ ํ ํจ๊ณผ๋ฅผ ๊ตฌํํ์ต๋๋ค.
|
| 106 |
+
- ํ์ง๋ง ๋ด๋น๊ฒ์ด์
๋ฐ ์ ๋๋ฉ์ด์
๊ณผ์ ์ง์์ ์ธ ์ถฉ๋ ๋ฐ ๋ถ์์ ์ฑ ๋ฌธ์ ๋ก, ์ฌ์ดํธ์ ์์ ์ฑ์ ํ๋ณดํ๊ธฐ ์ํด ์ด ๊ธฐ๋ฅ์ **์ผ์์ ์ผ๋ก ์ด์ ์ํ๋ก ๋๋๋ ธ์ต๋๋ค.** ํ์ฌ๋ ๋ธ๋ผ์ฐ์ ์ ๊ธฐ๋ณธ ํ์ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค.
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## ๐
2025๋
11์ 19์ผ
|
| 111 |
+
### ์ฃผ์ : AI ๋ชจ๋ธ ๊ฐ์ ์คํ & ์ด๊ธฐ UI/UX ๊ตฌ์ถ
|
| 112 |
+
|
| 113 |
+
#### 1. AI ๋ชจ๋ธ ๊ฐ์ ์ฌ์
|
| 114 |
+
**์ด๊ธฐ ๋ชจ๋ธ์ ํ๊ณ์ ์๋ก์ด ์์ด๋์ด**
|
| 115 |
+
- **๊ธฐ์กด ๋ชจ๋ธ ์ ํ๋:** `79.06%`๋ก, ๋ค์ ์์ฌ์ด ์ฑ๋ฅ.
|
| 116 |
+
- **์๋ก์ด ๊ฐ์ค:** NSMC๋ '๊ธ์ /๋ถ์ ' ๋ถ์์ ํนํ๋์ด ์์ด, ์ฐ๋ฆฌ ํ๋ก์ ํธ์ ๋ค์ค ๊ฐ์ ๋ถ๋ฅ(๋๋ถ๋ฅ)์๋ ์ ํฉํ์ง ์๋ค๊ณ ํ๋จ. NSMC๋ฅผ ์ ๊ฑฐํ๊ณ ์๋ ๊ฐ์ค์น๋ก๋ง ํ๋ จํ์ ๋, ์คํ๋ ค **์์ ๋ชจ๋ธ์ ์ฑ๋ฅ์ด ๊ฐ์ฅ ์ข์์.**
|
| 117 |
+
- **๋ฐฉํฅ ์ ํ:** ์ฌ์ฉ์์๊ฒ AI๊ฐ ์์ธกํ ๊ฐ์ ํ๋ฅ ์ ๋ณด์ฌ์ฃผ๊ณ , ๊ฐ์ฅ ๋์ ํ๋ฅ ์ ๊ฐ์ ์ ๊ธฐ๋ณธ์ผ๋ก ์ถ์ฒํ๋ **์ฌ์ฉ์๊ฐ ์ง์ ์์ ํ ์ ์๋ ์ ํ๊ถ**์ ์ ๊ณตํ๋ UX์ ํด๊ฒฐ์ฑ
๋์
.
|
| 118 |
+
|
| 119 |
+
#### 2. UI/UX ๊ฐ์ ์ฌํญ
|
| 120 |
+
**๋ก๊ทธ์ธ/ํ์๊ฐ์
UX ๊ฐ์ : Flip Animation**
|
| 121 |
+
- **ํด๊ฒฐ:** ๋ก๊ทธ์ธ๊ณผ ํ์๊ฐ์
ํ๋ฉด์ **ํ๋์ '์ฑ
(Book)'** ์ฝ์
ํธ๋ก ํตํฉ.
|
| 122 |
+
- **๊ตฌํ:** CSS `transform: rotateY(180deg)`๋ฅผ ํ์ฉํ 3D ์ฑ
๋๊น ์ ๋๋ฉ์ด์
๋ฐ ๋จ์ผ ํ์ด์ง ํด(Single Page Turn) ํจ๊ณผ ์ ์ฉ.
|
| 123 |
+
|
| 124 |
+
**๊ฐ์ ๋ถ์ ์ ํ๋ ๋ณด์: Emotion Selection UI**
|
| 125 |
+
- **ํด๊ฒฐ:** AI์ **ํ์ ๋(Confidence Score)**์ ๋ฐ๋ผ UI๋ฅผ ์ ์ฐํ๊ฒ ๋ถ๊ธฐ ์ฒ๋ฆฌ.
|
| 126 |
+
- **ํ์ (80% ์ด์):** AI๊ฐ ์์ธกํ ๊ฐ์ ์ ์ ๋ต์ผ๋ก ์๋ ์ฑํ.
|
| 127 |
+
- **๋ถํ์ค (80% ๋ฏธ๋ง):** ์์ 3๊ฐ ๊ฐ์ ๊ณผ ํ๋ฅ ์ **'๊ฐ์ ์นฉ(Chips)'** ํํ๋ก ์ ์ํ์ฌ ์ ํ๊ถ ๋ถ์ฌ.
|
| 128 |
+
|
| 129 |
+
**์ฌ์ฉ์ ๊ฒฝํ ๋ณดํธ: ๋ถ์๊ณผ ์ ์ฅ ๋จ๊ณ ๋ถ๋ฆฌ**
|
| 130 |
+
- **๊ตฌํ:** **`[๋ถ์] โ [ํ์ธ/์์ ] โ [์ ์ฅ]`** ํ๋ก์ฐ ๊ตฌ์ถ. ์ต์ข
์ ์ผ๋ก '์ ์ฅํ๊ธฐ' ๋ฒํผ์ ๋๋ ์ ๋๋ง DB์ ์ ์ฅ๋๋๋ก ๋ณ๊ฒฝํ๊ณ ๋ก๋ฉ UI ์ถ๊ฐ.
|
| 131 |
+
|
| 132 |
+
#### 3. KOTE ์ฌ์ ํ์ต ๋ชจ๋ธ ์คํ ๊ฒฐ๊ณผ
|
| 133 |
+
> Colab ํ๊ฒฝ์์ ์งํ๋์์ผ๋ฉฐ, ๊ฒฐ๊ณผ๊ฐ ๋ง์กฑ์ค๋ฝ์ง๋ ์์์.
|
| 134 |
+
|
| 135 |
+
- **์คํ 1 (๊ธฐ๋ณธ):** F1 Score `0.6154`
|
| 136 |
+
- **์คํ 2 (๊ฐ์ค์น ์ ์ฉ):** Accuracy `54.2%`
|
| 137 |
+
- **์คํ 3 (๊ฐ์ค์น ์ ๊ฑฐ):** Accuracy `58.45%`
|
| 138 |
+
- **๊ฒฐ๋ก :** KOTE ๋ฐ์ดํฐ์
์ ํ์ฉํ ์ฌ์ ํ์ต ๋ชจ๋ธ์ ์์ง ๊ธฐ๋๋งํผ์ ์ฑ๋ฅ์ ๋ณด์ฌ์ฃผ์ง ๋ชปํจ. ์ถํ ๊ฐ์ ๋ฐฉํฅ ์ฌ์ค์ ํ์.
|
Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1. ๋ฒ ์ด์ค ์ด๋ฏธ์ง ์ ํ (ํ์ด์ฌ 3.10 ๋ฒ์ )
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# 2. ์์
ํด๋ ์ค์
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 3. ํ์ํ ๋น๋ ๋๊ตฌ ๋ฐ ์์คํ
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
|
| 8 |
+
RUN apt-get update && \
|
| 9 |
+
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
| 10 |
+
build-essential libblas-dev liblapack-dev && \
|
| 11 |
+
rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# 4. ํ์ด์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น (๋ถ๋ฆฌํ์ฌ ์์ ์ฑ ๊ทน๋ํ)
|
| 14 |
+
|
| 15 |
+
# 4.1. ๋ ๊ฐ์ ์ข
์์ฑ ํ์ผ ๋ณต์ฌ
|
| 16 |
+
COPY core_ml_deps.txt core_ml_deps.txt
|
| 17 |
+
COPY app_deps.txt app_deps.txt
|
| 18 |
+
|
| 19 |
+
# 4.2. ๋์ฉ๋ ML ์ข
์์ฑ ๋จผ์ ์ค์น (์คํจ ์ํ์ ์ด ๋จ๊ณ์ ์ง์ค)
|
| 20 |
+
RUN pip install -r core_ml_deps.txt
|
| 21 |
+
|
| 22 |
+
# 4.3. ๋๋จธ์ง App ์ข
์์ฑ ์ค์น (์์ ๋ชจ๋์ ์ค์น ์ฑ๊ณต ๋ณด์ฅ)
|
| 23 |
+
RUN pip install -r app_deps.txt
|
| 24 |
+
|
| 25 |
+
RUN pip install google-generativeai
|
| 26 |
+
|
| 27 |
+
# 5. ์บ์ ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํด๋๋ฅผ ๋ฏธ๋ฆฌ ๋ง๋ค๊ณ ๊ถํ์ ๋ถ์ฌํฉ๋๋ค.
|
| 28 |
+
RUN mkdir -p /app/.cache /app/src && chmod -R 777 /app/.cache /app/src
|
| 29 |
+
|
| 30 |
+
# 6. ํ๋ก์ ํธ ์ ์ฒด ์ฝ๋ ๋ณต์ฌ (์บ์ฑ ํจ์จ์ ์ํด ๊ฐ์ฅ ๋ฆ๊ฒ ๋ฐฐ์น)
|
| 31 |
+
COPY . .
|
| 32 |
+
|
| 33 |
+
# 7. ํ๊ฒฝ ๋ณ์ ์ค์ (Hugging Face ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์บ์ ๊ฒฝ๋ก ์ง์ )
|
| 34 |
+
ENV HF_HOME=/app/.cache
|
| 35 |
+
ENV TRANSFORMERS_CACHE=/app/.cache
|
| 36 |
+
|
| 37 |
+
# 8. Hugging Face Spaces๊ฐ ์ฌ์ฉํ ํฌํธ ์ด๊ธฐ
|
| 38 |
+
EXPOSE 7860
|
| 39 |
+
|
| 40 |
+
# 9. ์ต์ข
์คํ ๋ช
๋ น์ด (Gunicorn ์์ปค ์๋ฅผ 2๊ฐ๋ก ์ ํ)
|
| 41 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "-w", "2", "--preload", "run:app"]
|
README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Emotion Chatbot
|
| 3 |
+
emoji: ๐ค
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
app_file: run.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
# Emotion Diary ๐ค
|
| 12 |
+
|
| 13 |
+
**ํ๋ฃจ๋ฅผ ๋ง๋ฌด๋ฆฌํ๋ฉฐ ์ฐ๋ ๋น์ ์ ์ผ๊ธฐ, ๊ทธ ์์ ์จ๊ฒจ์ง ์ง์ง ๊ฐ์ ์ ๋ฌด์์ผ๊น์?**
|
| 14 |
+
|
| 15 |
+
์ด ํ๋ก์ ํธ๋ AI๋ฅผ ํตํด ๋น์ ์ ๊ธ์ ์ดํดํ๊ณ , ๊ฐ์ ์ ๋ชฐ์
ํ๊ฑฐ๋ ํน์ ์๋ก์ด ํ๋ ฅ์ด ํ์ํ ๋ ๋ง์ถคํ ์ฝํ
์ธ ๋ฅผ ์ถ์ฒํด์ฃผ๋ ๋น์ ๋ง์ ๊ฐ์ฑ ๋น์์
๋๋ค.
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
### โจ Live Demo
|
| 20 |
+
|
| 21 |
+
๐ **[https://huggingface.co/spaces/taehoon222/emotion-chatbot-app](https://huggingface.co/spaces/taehoon222/emotion-chatbot-app)**
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
### ๐ธ Screenshots
|
| 26 |
+
|
| 27 |
+
*(์คํฌ๋ฆฐ์ท์ ์ฌ๊ธฐ์ ์ถ๊ฐํ์ธ์. ์: ๋ฉ์ธ ํ์ด์ง, ์ผ๊ธฐ ์์ฑ, ๊ฒฐ๊ณผ ํ๋ฉด)*
|
| 28 |
+
|
| 29 |
+

|
| 30 |
+

|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## ๐ ํต์ฌ ๊ธฐ๋ฅ
|
| 35 |
+
|
| 36 |
+
- **๐ค ํ
์คํธ ์ ๊ฐ์ ํ์**: `klue/roberta-base` ๋ชจ๋ธ์ ๊ธฐ๋ฐ์ผ๋ก, ์ผ๊ธฐ ์์ ๋ด๊ธด ๋ณตํฉ์ ์ธ ๊ฐ์ ์ 80% ์ด์์ ์ ํ๋๋ก ๋ถ์ํฉ๋๋ค.
|
| 37 |
+
- **๐ญ ๊ฐ์ฑ ๋ง์ถค ํ๋ ์ด์
**: ๋ถ์๋ ๊ฐ์ ์ ๋ฐ๋ผ '์์ฉ'๊ณผ '์ ํ' ๋ ๊ฐ์ง ์๋๋ฆฌ์ค์ ๋ง์ถฐ ์ํ, ์์
, ์ฑ
์ ์ถ์ฒํฉ๋๋ค.
|
| 38 |
+
- **๐ ๋๋ง์ ๊ฐ์ ๊ธฐ๋ก**: ์์ฑํ๋ ์ผ๊ธฐ์ AI์ ๊ฐ์ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฌ๋ ฅ ํํ๋ก ํ์ธํ๊ณ , ๊ณผ๊ฑฐ์ ๊ฐ์ ํ๋ฆ์ ์ธ์ ๋ ์ง ๋ค์ ๋์๋ณผ ์ ์์ต๋๋ค.
|
| 39 |
+
- **๐จ ์ปค์คํ
ํ
๋ง**: ๋ค์ํ ์์๊ณผ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง๋ก ์ฑ์ ๋ถ์๊ธฐ๋ฅผ ์ทจํฅ์ ๋ง๊ฒ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
|
| 40 |
+
- **๐ป ์ง๊ด์ ์ธ ๋ฐ์ํ UI**: Flask์ JavaScript๋ก ๊ตฌ์ถ๋ ๊ฐ๊ฒฐํ๊ณ ์ฌ์ฉํ๊ธฐ ์ฌ์ด ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## ๐ ๏ธ ๊ธฐ์ ์คํ
|
| 45 |
+
|
| 46 |
+
| ๊ตฌ๋ถ | ๊ธฐ์ |
|
| 47 |
+
| :--- | :--- |
|
| 48 |
+
| **Backend** | Python, Flask, Gunicorn, SQLAlchemy |
|
| 49 |
+
| **Frontend**| HTML, CSS, JavaScript |
|
| 50 |
+
| **AI / Data**| PyTorch, Hugging Face Transformers, Scikit-learn, Pandas |
|
| 51 |
+
| **Database**| Supabase (PostgreSQL) |
|
| 52 |
+
| **Deployment**| Docker, GitHub Actions (CI/CD), Hugging Face Spaces |
|
| 53 |
+
| **Version Control**| Git, GitHub, Git LFS |
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## ๐๏ธ ์ํคํ
์ฒ
|
| 58 |
+
|
| 59 |
+
๊ฐ๋ฒผ์ด ์ฑ ์ฝ๋์ ๋ฌด๊ฑฐ์ด AI ๋ชจ๋ธ์ ๋ถ๋ฆฌํ์ฌ ํจ์จ์ ์ธ CI/CD ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํ์ต๋๋ค.
|
| 60 |
+
|
| 61 |
+
```
|
| 62 |
+
[Local PC] --(git push)--> [GitHub] --(Action)--> [Hugging Face Spaces]
|
| 63 |
+
|
|
| 64 |
+
| (App Start)
|
| 65 |
+
V
|
| 66 |
+
[Hugging Face Hub] <--(Download Model)-- [Spaces Server]
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## ๐ ์์ํ๊ธฐ
|
| 72 |
+
|
| 73 |
+
### ์ฌ์ ์๊ตฌ์ฌํญ
|
| 74 |
+
|
| 75 |
+
- Python 3.10
|
| 76 |
+
- Anaconda (๊ถ์ฅ)
|
| 77 |
+
|
| 78 |
+
### ์ค์น ๋ฐ ์คํ
|
| 79 |
+
|
| 80 |
+
1. **ํ๋ก์ ํธ ๋ณต์ **
|
| 81 |
+
```bash
|
| 82 |
+
git clone https://github.com/kootaeng2/Emotion-Chatbot-App.git
|
| 83 |
+
cd Emotion-Chatbot-App
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
2. **๊ฐ์ํ๊ฒฝ ์์ฑ ๋ฐ ํ์ฑํ (Anaconda ์ฌ์ฉ)**
|
| 87 |
+
```bash
|
| 88 |
+
conda create -n emotion_env python=3.10
|
| 89 |
+
conda activate emotion_env
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
3. **ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น**
|
| 93 |
+
```bash
|
| 94 |
+
pip install -r requirements.txt
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
4. **ํ๊ฒฝ ๋ณ์ ์ค์ **
|
| 98 |
+
`.env` ํ์ผ์ ์์ฑํ๊ณ ์๋ ๋ด์ฉ์ ์ถ๊ฐํ์ธ์. Gemini API๋ฅผ ํตํ ์ถ์ฒ ๊ธฐ๋ฅ์ ํ์ํฉ๋๋ค.
|
| 99 |
+
```
|
| 100 |
+
GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
5. **์น ์ ํ๋ฆฌ์ผ์ด์
์คํ**
|
| 104 |
+
```bash
|
| 105 |
+
python run.py
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
6. **์๋ฒ ์ ์**
|
| 109 |
+
์น ๋ธ๋ผ์ฐ์ ์์ `http://127.0.0.1:5000` ์ฃผ์๋ก ์ ์ํ์ธ์.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## ๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
|
| 114 |
+
|
| 115 |
+
```
|
| 116 |
+
Emotion/
|
| 117 |
+
โ
|
| 118 |
+
โโโ .github/ # GitHub Actions ์ํฌํ๋ก์ฐ (CI/CD)
|
| 119 |
+
โโโ data/ # AI ๋ชจ๋ธ ํ์ต์ฉ ๋ฐ์ดํฐ
|
| 120 |
+
โโโ notebooks/ # ๋ฐ์ดํฐ ํ์ ๋ฐ ์ ์ฒ๋ฆฌ์ฉ Jupyter Notebook
|
| 121 |
+
โโโ results/ # ๋ชจ๋ธ ํ์ต ๊ฒฐ๊ณผ
|
| 122 |
+
โโโ scripts/ # ๋ชจ๋ธ ํ๋ จ, ํ๊ฐ์ฉ ์คํฌ๋ฆฝํธ
|
| 123 |
+
โโโ src/ # ํต์ฌ ์ ํ๋ฆฌ์ผ์ด์
์์ค ์ฝ๋
|
| 124 |
+
โ โโโ templates/ # HTML ํ
ํ๋ฆฟ
|
| 125 |
+
โ โโโ static/ # CSS, JS ํ์ผ
|
| 126 |
+
โ โโโ __init__.py # Flask ์ฑ ์ด๊ธฐํ (Application Factory)
|
| 127 |
+
โ โโโ auth.py # ์ธ์ฆ ๊ด๋ จ ๋ก์ง
|
| 128 |
+
โ โโโ emotion_engine.py # ๊ฐ์ ๋ถ์ ๋ชจ๋ธ ๋ก๋ฉ ๋ฐ ์์ธก
|
| 129 |
+
โ โโโ main.py # ๋ฉ์ธ ํ์ด์ง, ์ผ๊ธฐ/์ถ์ฒ ๊ธฐ๋ฅ
|
| 130 |
+
โ โโโ models.py # ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ
|
| 131 |
+
โ
|
| 132 |
+
โโโ supabase/ # Supabase DB ๋ง์ด๊ทธ๋ ์ด์
|
| 133 |
+
โโโ Dockerfile # ๋ฐฐํฌ์ฉ Docker ์ปจํ
์ด๋ ์ค์
|
| 134 |
+
โโโ requirements.txt # Python ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ข
์์ฑ
|
| 135 |
+
โโโ run.py # ์ ํ๋ฆฌ์ผ์ด์
์คํ ์คํฌ๋ฆฝํธ
|
| 136 |
+
```
|
| 137 |
+
<details>
|
| 138 |
+
<summary><strong>Frontend (Templates) ๊ตฌ์กฐ ์์ธ (ํด๋ฆญํ์ฌ ํผ์น๊ธฐ)</strong></summary>
|
| 139 |
+
|
| 140 |
+
`src/templates` ํด๋๋ Flask์ Jinja2 ํ
ํ๋ฆฟ ์์ง์ ์ฌ์ฉํ์ฌ UI๋ฅผ ๊ตฌ์ฑํฉ๋๋ค. ์ญํ ์ ๋ฐ๋ผ ํ์ผ์ด ๋ช
๏ฟฝ๏ฟฝํ๊ฒ ๋ถ๋ฆฌ๋์ด ์์ผ๋ฉฐ, ์์๊ณผ ๋งคํฌ๋ก๋ฅผ ํตํด ํจ์จ์ ์ผ๋ก UI๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
|
| 141 |
+
|
| 142 |
+
- **๊ธฐ๋ณธ ๋ ์ด์์ (`base.html`, `base_auth.html`)**: ์ ์ฒด ํ์ด์ง์ ๊ณตํต์ ์ธ ๋ผ๋(๋ค๋น๊ฒ์ด์
๋ฐ ๋ฑ)๋ฅผ ์ ๊ณตํฉ๋๋ค.
|
| 143 |
+
- **๊ฐ๋ณ ํ์ด์ง (`main.html`, `diary.html`, `page.html`, `login.html`, `signup.html`)**: ๊ฐ ๊ธฐ๋ฅ์ ๋ง๋ ์ค์ ํ์ด์ง UI๋ฅผ ๋ด๋นํ๋ฉฐ, ๊ธฐ๋ณธ ๋ ์ด์์์ ์์๋ฐ์ ์ฌ์ฉํฉ๋๋ค.
|
| 144 |
+
- **์ฌ์ฌ์ฉ ์ปดํฌ๋ํธ (`_macros.html`)**: ๋ก๊ทธ์ธ ํผ, ์ถ์ฒ ํญ ๋ฑ ๋ฐ๋ณต์ ์ผ๋ก ์ฌ์ฉ๋๋ UI ์กฐ๊ฐ์ ๋งคํฌ๋ก ํํ๋ก ์ ์ํ์ฌ ์ฝ๋ ์ค๋ณต์ ์ค์
๋๋ค.
|
| 145 |
+
- **์ ์ ํ์ผ (`static/`)**:
|
| 146 |
+
- **`css/`**: ๊ฐ ํ์ด์ง์ ํนํ๋ ์คํ์ผ์ํธ์ ์ ์ญ ์คํ์ผ์ ํฌํจํฉ๋๋ค.
|
| 147 |
+
- **`js/`**: ํ์ด์ง๋ณ ํต์ฌ ๋ก์ง(API ํต์ , ๋ฌ๋ ฅ ๊ธฐ๋ฅ), ํ
๋ง ๋ณ๊ฒฝ, ์จ๋ณด๋ฉ ๋ฑ ๋์ ์ธ ๊ธฐ๋ฅ์ ๋ด๋นํ๋ JavaScript ํ์ผ๋ค์ ํฌํจํฉ๋๋ค.
|
| 148 |
+
|
| 149 |
+
</details>
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
## ๐งโโ๏ธ ๊ฐ๋ฐ ๊ณผ์ ๋ฐ ๋ฌธ์ ํด๊ฒฐ
|
| 154 |
+
|
| 155 |
+
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ ๊ฒช์๋ ์ฃผ์ ๊ธฐ์ ์ ๋์ ๊ณผ ํด๊ฒฐ ๊ณผ์ ์ ๋ํ ์์ธํ ๋ด์ฉ์ **[DEVELOPMENT.md](DEVELOPMENT.md)** ํ์ผ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## ๐ ๋ชจ๋ธ ์ฑ๋ฅ
|
| 160 |
+
|
| 161 |
+
| Metric | Score |
|
| 162 |
+
| :------- | :----- |
|
| 163 |
+
| Accuracy | 0.7905 |
|
| 164 |
+
| F1 Score | 0.7910 |
|
| 165 |
+
| Loss | 0.6943 |
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
## ๐ ๋ผ์ด์ ์ค
|
| 170 |
+
|
| 171 |
+
This project is licensed under the MIT License.
|
app_deps.txt
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app_deps.txt
|
| 2 |
+
|
| 3 |
+
aiohappyeyeballs==2.6.1
|
| 4 |
+
aiohttp==3.12.15
|
| 5 |
+
aiosignal==1.4.0
|
| 6 |
+
async-timeout==5.0.1
|
| 7 |
+
attrs==25.3.0
|
| 8 |
+
blinker==1.9.0
|
| 9 |
+
certifi==2025.10.5
|
| 10 |
+
charset-normalizer==3.4.3
|
| 11 |
+
click==8.3.0
|
| 12 |
+
colorama==0.4.6
|
| 13 |
+
contourpy==1.3.2
|
| 14 |
+
cycler==0.12.1
|
| 15 |
+
dill==0.4.0
|
| 16 |
+
et_xmlfile==2.0.0
|
| 17 |
+
Flask==3.1.2
|
| 18 |
+
Flask-SQLAlchemy==3.1.1
|
| 19 |
+
Flask-Login==0.6.3
|
| 20 |
+
fonttools==4.60.0
|
| 21 |
+
frozenlist==1.7.0
|
| 22 |
+
fsspec==2025.9.0
|
| 23 |
+
git-filter-repo==2.47.0
|
| 24 |
+
greenlet==3.2.4
|
| 25 |
+
gunicorn==23.0.0
|
| 26 |
+
idna==3.10
|
| 27 |
+
itsdangerous==2.2.0
|
| 28 |
+
Jinja2==3.1.6
|
| 29 |
+
joblib==1.5.2
|
| 30 |
+
kiwisolver==1.4.9
|
| 31 |
+
MarkupSafe==3.0.3
|
| 32 |
+
matplotlib==3.10.6
|
| 33 |
+
mpmath==1.3.0
|
| 34 |
+
multidict==6.6.4
|
| 35 |
+
multiprocess==0.70.16
|
| 36 |
+
networkx==3.4.2
|
| 37 |
+
openpyxl==3.1.5
|
| 38 |
+
packaging==25.0
|
| 39 |
+
pillow==11.3.0
|
| 40 |
+
propcache==0.3.2
|
| 41 |
+
protobuf==6.32.1
|
| 42 |
+
psutil==7.1.0
|
| 43 |
+
psycopg2-binary==2.9.10
|
| 44 |
+
pyparsing==3.2.5
|
| 45 |
+
python-dateutil==2.9.0.post0
|
| 46 |
+
python-dotenv==1.1.1
|
| 47 |
+
pytz==2025.2
|
| 48 |
+
PyYAML==6.0.3
|
| 49 |
+
regex==2025.9.18
|
| 50 |
+
requests==2.32.5
|
| 51 |
+
six==1.17.0
|
| 52 |
+
SQLAlchemy==2.0.43
|
| 53 |
+
sympy==1.13.1
|
| 54 |
+
typing_extensions==4.15.0
|
| 55 |
+
tzdata==2025.2
|
| 56 |
+
urllib3==2.5.0
|
| 57 |
+
Werkzeug==3.1.3
|
| 58 |
+
xxhash==3.6.0
|
| 59 |
+
yarl==1.20.1
|
core_ml_deps.txt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core_ml_deps.txt
|
| 2 |
+
|
| 3 |
+
# PyTorch Ecosystem
|
| 4 |
+
torch==2.5.1
|
| 5 |
+
torchaudio==2.5.1
|
| 6 |
+
torchvision==0.20.1
|
| 7 |
+
|
| 8 |
+
# Hugging Face ๋ฐ ML ์ข
์์ฑ
|
| 9 |
+
accelerate==1.10.1
|
| 10 |
+
datasets==4.1.1
|
| 11 |
+
filelock==3.19.1
|
| 12 |
+
huggingface-hub==0.35.3
|
| 13 |
+
safetensors==0.6.2
|
| 14 |
+
sentencepiece==0.2.1
|
| 15 |
+
tiktoken==0.12.0
|
| 16 |
+
tokenizers==0.22.1
|
| 17 |
+
transformers==4.57.1
|
| 18 |
+
|
| 19 |
+
# ์์น ๊ณ์ฐ ๋ฐ ๋ฐ์ดํฐ ์ฒ๋ฆฌ (์ปดํ์ผ ํ์)
|
| 20 |
+
numpy==2.2.6
|
| 21 |
+
pandas==2.3.3
|
| 22 |
+
scikit-learn==1.7.2
|
| 23 |
+
scipy==1.15.3
|
| 24 |
+
seaborn==0.13.2
|
| 25 |
+
threadpoolctl==3.6.0
|
| 26 |
+
pyarrow==21.0.0
|
cursor/mcp.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"supabase": {
|
| 4 |
+
"command": "npx",
|
| 5 |
+
"args": [
|
| 6 |
+
"-y",
|
| 7 |
+
"@modelcontextprotocol/server-postgres",
|
| 8 |
+
"<local-db-url>"
|
| 9 |
+
]
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
}
|
notebooks/explore_data.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import seaborn as sns
|
| 6 |
+
|
| 7 |
+
# --- Matplotlib ํ๊ธ ํฐํธ ์ค์ (Windows: Malgun Gothic) ---
|
| 8 |
+
try:
|
| 9 |
+
plt.rcParams['font.family'] = 'Malgun Gothic'
|
| 10 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 11 |
+
except:
|
| 12 |
+
print("ํ๊ธ ํฐํธ ์ค์ ์ ์คํจํ์ต๋๋ค. ๊ทธ๋ํ์ ๋ผ๋ฒจ์ด ๊นจ์ง ์ ์์ต๋๋ค.")
|
| 13 |
+
|
| 14 |
+
# --- ๊ฐ์ ๋งคํ ํจ์ ์ ์ ---
|
| 15 |
+
def map_emotion_code(ecode):
|
| 16 |
+
"""
|
| 17 |
+
E์ฝ๋ ๋ฌธ์์ด์ ๋๋ถ๋ฅ ๊ฐ์ ๋ฌธ์์ด๋ก ๋งคํํฉ๋๋ค. (์: 'E11' -> '๋ถ๋
ธ')
|
| 18 |
+
"""
|
| 19 |
+
# E์ฝ๋ ๋ฌธ์์ด์ด ์๋๊ฑฐ๋ ํ์์ด ๋ง์ง ์์ผ๋ฉด None ๋ฐํ
|
| 20 |
+
if not isinstance(ecode, str) or len(ecode) < 2 or ecode[0] != 'E':
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
# 'E'๋ฅผ ์ ๊ฑฐํ๊ณ ์ซ์ ๋ถ๋ถ๋ง ์ถ์ถ
|
| 25 |
+
code_num = int(ecode[1:])
|
| 26 |
+
except ValueError:
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
if 10 <= code_num <= 19:
|
| 30 |
+
return '๋ถ๋
ธ'
|
| 31 |
+
elif 20 <= code_num <= 29:
|
| 32 |
+
return '์ฌํ'
|
| 33 |
+
elif 30 <= code_num <= 39:
|
| 34 |
+
return '๋ถ์'
|
| 35 |
+
elif 40 <= code_num <= 49:
|
| 36 |
+
return '์์ฒ'
|
| 37 |
+
elif 50 <= code_num <= 59:
|
| 38 |
+
return '๋นํฉ'
|
| 39 |
+
elif 60 <= code_num <= 69:
|
| 40 |
+
return '๊ธฐ์จ'
|
| 41 |
+
else:
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
# --- [Phase 1] ๋ฐ์ดํฐ ๋ก๋ฉ ๋ฐ ๋ณํฉ ---
|
| 45 |
+
print("---" + "[Phase 1] ๋ฐ์ดํฐ ๋ก๋ฉ ๋ฐ ๋ณํฉ ์์" + "---")
|
| 46 |
+
|
| 47 |
+
# ํ์ผ ๊ฒฝ๋ก ์ค์
|
| 48 |
+
data_path = 'data/'
|
| 49 |
+
train_text_path = data_path + 'training-origin.xlsx'
|
| 50 |
+
train_label_path = data_path + 'training-label.json'
|
| 51 |
+
val_text_path = data_path + 'validation-origin.xlsx'
|
| 52 |
+
val_label_path = data_path + 'test.json'
|
| 53 |
+
|
| 54 |
+
# 1. ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
|
| 55 |
+
try:
|
| 56 |
+
df_train_text = pd.read_excel(train_text_path, header=0)
|
| 57 |
+
df_val_text = pd.read_excel(val_text_path, header=0)
|
| 58 |
+
|
| 59 |
+
with open(train_label_path, 'r', encoding='utf-8') as f:
|
| 60 |
+
train_labels_raw = json.load(f)
|
| 61 |
+
with open(val_label_path, 'r', encoding='utf-8') as f:
|
| 62 |
+
val_labels_raw = json.load(f)
|
| 63 |
+
|
| 64 |
+
print("ํ์ผ ๋ก๋ฉ ์ฑ๊ณต!")
|
| 65 |
+
|
| 66 |
+
except FileNotFoundError as e:
|
| 67 |
+
print(f"ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {e}")
|
| 68 |
+
print("ํ์ผ ๊ฒฝ๋ก์ ํ์ผ ์ด๋ฆ์ ๋ค์ ํ์ธํด์ฃผ์ธ์.")
|
| 69 |
+
exit()
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# 2. ๋ผ๋ฒจ ๋ฐ์ดํฐ ์ ์ ๋ฐ ์ถ์ถ
|
| 73 |
+
def extract_emotions(raw_labels):
|
| 74 |
+
emotions = []
|
| 75 |
+
for dialogue in raw_labels:
|
| 76 |
+
try:
|
| 77 |
+
emotions.append(dialogue['profile']['emotion']['type'])
|
| 78 |
+
except KeyError:
|
| 79 |
+
emotions.append(None)
|
| 80 |
+
return emotions
|
| 81 |
+
|
| 82 |
+
df_train_labels = pd.DataFrame({'emotion': extract_emotions(train_labels_raw)})
|
| 83 |
+
df_val_labels = pd.DataFrame({'emotion': extract_emotions(val_labels_raw)})
|
| 84 |
+
|
| 85 |
+
# 3. ํ
์คํธ ๋ฐ์ดํฐ์ ๋ผ๋ฒจ ๋ฐ์ดํฐ ๋ณํฉ
|
| 86 |
+
def combine_dialogues(df):
|
| 87 |
+
dialogue_cols = [col for col in df.columns if '๋ฌธ์ฅ' in str(col)]
|
| 88 |
+
for col in dialogue_cols:
|
| 89 |
+
df[col] = df[col].astype(str).fillna('')
|
| 90 |
+
df['text'] = df[dialogue_cols].apply(lambda row: ' '.join(row), axis=1)
|
| 91 |
+
return df
|
| 92 |
+
|
| 93 |
+
df_train = pd.concat([df_train_text, df_train_labels], axis=1)
|
| 94 |
+
df_val = pd.concat([df_val_text, df_val_labels], axis=1)
|
| 95 |
+
df_train = combine_dialogues(df_train)
|
| 96 |
+
df_val = combine_dialogues(df_val)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ์๋ณธ E์ฝ๋(emotion)๋ฅผ ๋๋ถ๋ฅ ๊ฐ์ (major_emotion)์ผ๋ก ๋งคํํ๊ณ , ๋งคํ๋์ง ์์ ๋ฐ์ดํฐ๋ ์ ๊ฑฐํฉ๋๋ค.
|
| 100 |
+
df_train['major_emotion'] = df_train['emotion'].apply(map_emotion_code)
|
| 101 |
+
df_val['major_emotion'] = df_val['emotion'].apply(map_emotion_code)
|
| 102 |
+
|
| 103 |
+
df_train.dropna(subset=['major_emotion'], inplace=True)
|
| 104 |
+
df_val.dropna(subset=['major_emotion'], inplace=True)
|
| 105 |
+
|
| 106 |
+
# 4. ํ๋ จ ๋ฐ์ดํฐ์ ๊ฒ์ฆ ๋ฐ์ดํฐ ํตํฉ
|
| 107 |
+
df_combined = pd.concat([df_train, df_val], ignore_index=True)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
print("\n--- ํตํฉ ๋ฐ์ดํฐํ๋ ์์ ์ฒซ 5์ค (๋งคํ ํ) ---")
|
| 111 |
+
print(df_combined[['text', 'emotion', 'major_emotion']].head())
|
| 112 |
+
print("\n--- ํตํฉ ๋ฐ์ดํฐํ๋ ์ ํฌ๊ธฐ ---")
|
| 113 |
+
print(f"ํตํฉ ๋ฐ์ดํฐ: {df_combined.shape}")
|
| 114 |
+
print("--- [Phase 1] ์๋ฃ ---")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# --- [Phase 2] ๋ฐ์ดํฐ ํ์ ๋ฐ ์ ์ฒ๋ฆฌ ---
|
| 118 |
+
print("\n---" + "[Phase 2] ๋ฐ์ดํฐ ํ์ ๋ฐ ์ ์ฒ๋ฆฌ ์์" + "---")
|
| 119 |
+
|
| 120 |
+
# 1. ๋ฐ์ดํฐ ํ์ ๋ฐ ์๊ฐํ
|
| 121 |
+
print("\n---" + "ํตํฉ ๋ฐ์ดํฐ (ํ๋ จ + ๊ฒ์ฆ) ๊ฐ์ ๋ถํฌ" + "---")
|
| 122 |
+
emotion_counts = df_combined['major_emotion'].value_counts()
|
| 123 |
+
print(emotion_counts)
|
| 124 |
+
print("-------------------------------------------\n")
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# ๊ฐ์ ๋ถํฌ ์๊ฐํ
|
| 128 |
+
plt.figure(figsize=(10, 6))
|
| 129 |
+
sns.barplot(x=emotion_counts.values, y=emotion_counts.index, color='#2c7bb6')
|
| 130 |
+
for index, value in enumerate(emotion_counts.values):
|
| 131 |
+
plt.text(x=value + 100, y=index, s=f'{value:,}', va='center', ha='left', fontsize=12, color='black')
|
| 132 |
+
|
| 133 |
+
plt.title('ํ๋ จ + ๊ฒ์ฆ ๋ฐ์ดํฐ ํตํฉ ๊ฐ์ ๋ถํฌ ์๊ฐํ', fontsize=15)
|
| 134 |
+
plt.xlabel('๊ฐ์', fontsize=12)
|
| 135 |
+
plt.ylabel('๊ฐ์ ', fontsize=12)
|
| 136 |
+
plt.grid(axis='x', linestyle='--', alpha=0.7)
|
| 137 |
+
plt.xlim(0, 15000)
|
| 138 |
+
plt.ticklabel_format(style='plain', axis='x')
|
| 139 |
+
|
| 140 |
+
plt.show()
|
| 141 |
+
|
| 142 |
+
print("\n์๊ฐํ ์๋ฃ. ๊ทธ๋ํ ์ฐฝ์ ๋ซ์ผ๋ฉด ๋ค์ ๋จ๊ณ๊ฐ ์งํ๋ฉ๋๋ค.")
|
| 143 |
+
|
| 144 |
+
# 2. ํ
์คํธ ์ ์
|
| 145 |
+
print("\n---" + "ํ
์คํธ ์ ์ ์์" + "---")
|
| 146 |
+
|
| 147 |
+
def clean_text(text):
|
| 148 |
+
if not isinstance(text, str):
|
| 149 |
+
return ""
|
| 150 |
+
# ์ ๊ทํํ์์ ์ฌ์ฉํ์ฌ ํ๊ธ, ์์ด, ์ซ์, ๊ณต๋ฐฑ์ ์ ์ธํ ๋ชจ๋ ๋ฌธ์ ์ ๊ฑฐ
|
| 151 |
+
return re.sub(r'[^๊ฐ-ํฃa-zA-Z0-9 ]', '', text)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
df_combined['cleaned_text'] = df_combined['text'].apply(clean_text)
|
| 155 |
+
|
| 156 |
+
print("ํ
์คํธ ์ ์ ์๋ฃ.")
|
| 157 |
+
print(df_combined[['text', 'cleaned_text', 'major_emotion']].head())
|
| 158 |
+
print("--- [Phase 2] ์๋ฃ ---")
|
| 159 |
+
|
| 160 |
+
print("\n๋ชจ๋ ๊ณผ์ ์ด ์๋ฃ๋์์ต๋๋ค. ์ด์ ์ด ๋ฐ์ดํฐํ๋ ์(df_combined)์ผ๋ก ๋ถ์์ ๊ณ์ ์งํํ ์ ์์ต๋๋ค.")
|
notebooks/tabel.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.font_manager as fm
|
| 5 |
+
import platform
|
| 6 |
+
|
| 7 |
+
# --- Matplotlib ํ๊ธ ํฐํธ ์ค์ ---
|
| 8 |
+
try:
|
| 9 |
+
if platform.system() == 'Windows':
|
| 10 |
+
plt.rc('font', family='Malgun Gothic')
|
| 11 |
+
elif platform.system() == 'Darwin': # Mac OS
|
| 12 |
+
plt.rc('font', family='AppleGothic')
|
| 13 |
+
else: # Linux (์ฝ๋ฉ ๋ฑ)
|
| 14 |
+
# Colab ๋ฑ์์ ์คํ ์, ๋จผ์ !sudo apt-get install -y fonts-nanum ์คํ ํ์
|
| 15 |
+
plt.rc('font', family='NanumBarunGothic')
|
| 16 |
+
|
| 17 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 18 |
+
print("ํ๊ธ ํฐํธ๊ฐ ์ค์ ๋์์ต๋๋ค.")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"ํ๊ธ ํฐํธ ์ค์ ์ ์คํจํ์ต๋๋ค: {e}")
|
| 21 |
+
|
| 22 |
+
# --- 1. JSON ํ์ผ ๋ก๋ ๋ฐ ํ์ฑ ---
|
| 23 |
+
# [๋ณ๊ฒฝ] v2 ํ๋ จ์ trainer_state.json ๊ฒฝ๋ก๋ก ์์
|
| 24 |
+
file_path = r'E:\Emotion\results\emotion_model_v2_manual\checkpoint-29050\trainer_state.json'
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 28 |
+
data = json.load(f)
|
| 29 |
+
|
| 30 |
+
log_history = data.get('log_history', [])
|
| 31 |
+
|
| 32 |
+
# ํ์ต ๋ก๊ทธ์ ํ๊ฐ ๋ก๊ทธ ๋ถ๋ฆฌ
|
| 33 |
+
train_logs = []
|
| 34 |
+
eval_logs = []
|
| 35 |
+
|
| 36 |
+
for item in log_history:
|
| 37 |
+
if 'eval_loss' in item:
|
| 38 |
+
eval_logs.append(item)
|
| 39 |
+
elif 'loss' in item:
|
| 40 |
+
train_logs.append(item)
|
| 41 |
+
|
| 42 |
+
if not eval_logs:
|
| 43 |
+
print("ํ์ผ์์ ํ๊ฐ ๋ก๊ทธ('eval_loss' ํฌํจ)๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 44 |
+
else:
|
| 45 |
+
# DataFrame์ผ๋ก ๋ณํ
|
| 46 |
+
df_train = pd.DataFrame(train_logs).sort_values(by='epoch')
|
| 47 |
+
df_eval = pd.DataFrame(eval_logs).sort_values(by='epoch')
|
| 48 |
+
|
| 49 |
+
# --- 2. ์ต์ ๋ชจ๋ธ ์ ๋ณด ์ฐพ๊ธฐ ---
|
| 50 |
+
best_step = data.get('best_global_step')
|
| 51 |
+
best_epoch_log = next((item for item in eval_logs if item['step'] == best_step), None)
|
| 52 |
+
best_epoch = best_epoch_log['epoch'] if best_epoch_log else None
|
| 53 |
+
|
| 54 |
+
# --- 3. ๊ทธ๋ํ ์์ฑ (1ํ 2์ด) ---
|
| 55 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 7))
|
| 56 |
+
|
| 57 |
+
# --- 3-1. Loss ๊ทธ๋ํ (Train vs Validation) ---
|
| 58 |
+
ax1.plot(df_train['epoch'], df_train['loss'], 'o-', label='Train Loss (ํ์ต ์์ค)', alpha=0.7)
|
| 59 |
+
ax1.plot(df_eval['epoch'], df_eval['eval_loss'], 's--', label='Validation Loss (๊ฒ์ฆ ์์ค)', linewidth=2, markersize=8)
|
| 60 |
+
ax1.set_title('๋ชจ๋ธ ํ์ต ๊ณผ์ (Loss)', fontsize=16)
|
| 61 |
+
ax1.set_xlabel('Epoch', fontsize=12)
|
| 62 |
+
ax1.set_ylabel('Loss', fontsize=12)
|
| 63 |
+
ax1.legend(fontsize=11)
|
| 64 |
+
ax1.grid(True, linestyle=':')
|
| 65 |
+
|
| 66 |
+
if best_epoch:
|
| 67 |
+
ax1.axvline(x=best_epoch, color='red', linestyle=':', linewidth=2,
|
| 68 |
+
label=f'Best Model (Epoch {best_epoch:g})')
|
| 69 |
+
# ์ต๊ณ ์ ํ
์คํธ ์ถ๊ฐ (์ต์ Loss๊ฐ ์๋ Best Model์ Epoch ๊ธฐ์ค)
|
| 70 |
+
best_val_loss = next((e['eval_loss'] for e in eval_logs if e['epoch'] == best_epoch), None)
|
| 71 |
+
if best_val_loss:
|
| 72 |
+
ax1.plot(best_epoch, best_val_loss, 'r*', markersize=15) # ์ต๊ณ ์ง์ ๋ง์ปค
|
| 73 |
+
ax1.text(best_epoch, best_val_loss * 1.05, f'Best @ Epoch {best_epoch:g}',
|
| 74 |
+
color='red', horizontalalignment='center', fontsize=12)
|
| 75 |
+
|
| 76 |
+
# --- 3-2. Metrics ๊ทธ๋ํ (Validation Accuracy vs F1-Score) ---
|
| 77 |
+
ax2.plot(df_eval['epoch'], df_eval['eval_accuracy'] * 100, 'o-',
|
| 78 |
+
label='Validation Accuracy (๊ฒ์ฆ ์ ํ๋)', linewidth=2, markersize=8)
|
| 79 |
+
ax2.plot(df_eval['epoch'], df_eval['eval_f1'] * 100, 's--',
|
| 80 |
+
label='Validation F1-Score (๊ฒ์ฆ F1)', linewidth=2, markersize=8)
|
| 81 |
+
ax2.set_title('๋ชจ๋ธ ํ๊ฐ ์งํ (Accuracy & F1-Score)', fontsize=16)
|
| 82 |
+
ax2.set_xlabel('Epoch', fontsize=12)
|
| 83 |
+
ax2.set_ylabel('Score (%)', fontsize=12)
|
| 84 |
+
ax2.legend(fontsize=11)
|
| 85 |
+
ax2.grid(True, linestyle=':')
|
| 86 |
+
|
| 87 |
+
if best_epoch:
|
| 88 |
+
ax2.axvline(x=best_epoch, color='red', linestyle=':', linewidth=2,
|
| 89 |
+
label=f'Best Model (Epoch {best_epoch:g})')
|
| 90 |
+
# ์ต๊ณ ์ ํ
์คํธ ์ถ๊ฐ (Accuracy ๊ธฐ์ค)
|
| 91 |
+
best_acc = best_epoch_log['eval_accuracy'] * 100
|
| 92 |
+
ax2.plot(best_epoch, best_acc, 'r*', markersize=15) # ์ต๊ณ ์ง์ ๋ง์ปค
|
| 93 |
+
ax2.text(best_epoch, best_acc * 0.99, f'Best Acc: {best_acc:.2f}%',
|
| 94 |
+
color='red', horizontalalignment='center', verticalalignment='top', fontsize=12)
|
| 95 |
+
|
| 96 |
+
# ๊ทธ๋ํ ๋ ์ด์์ ์ ๋ฆฌ ๋ฐ ํ์ผ๋ก ์ ์ฅ
|
| 97 |
+
plt.tight_layout()
|
| 98 |
+
plt.savefig('training_visualization_graph.png', dpi=300)
|
| 99 |
+
print("\n'training_visualization_graph.png' ํ์ผ๋ก ๊ทธ๋ํ๊ฐ ์ ์ฅ๋์์ต๋๋ค.")
|
| 100 |
+
|
| 101 |
+
except FileNotFoundError:
|
| 102 |
+
print(f"์ค๋ฅ: '{file_path}' ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค. ๊ฒฝ๋ก๋ฅผ ๋ค์ ํ์ธํด์ฃผ์ธ์.")
|
| 103 |
+
except Exception as e:
|
| 104 |
+
print(f"๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}")
|
notebooks/text_cleaner.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# 1. ํ
์คํธ ์ ์ ํจ์ ์ ์ (train_final.py์ ๋ก์ง)
|
| 6 |
+
def clean_text(text: str) -> str:
|
| 7 |
+
"""ํ๊ธ, ์์ด, ์ซ์, ๊ณต๋ฐฑ์ ์ ์ธํ ๋ชจ๋ ํน์๋ฌธ์๋ฅผ ์ ๊ฑฐํฉ๋๋ค."""
|
| 8 |
+
return re.sub(r'[^๊ฐ-ํฃa-zA-Z0-9 ]', '', str(text))
|
| 9 |
+
|
| 10 |
+
# 2. ํ์ผ ๊ฒฝ๋ก ์ค์ ๋ฐ ๋ฐ์ดํฐ ๋ก๋ (ํ์ผ ๊ฒฝ๋ก๊ฐ data/์ ์๋ค๊ณ ๊ฐ์ )
|
| 11 |
+
file_path = './data/training-label.json'
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 15 |
+
training_data_raw = json.load(f)
|
| 16 |
+
|
| 17 |
+
print(f"โ
'{file_path}' ํ์ผ ๋ก๋ฉ ์ฑ๊ณต. ์ด {len(training_data_raw)}๊ฐ ๋ฐ์ดํฐ ์ค 10๊ฐ๋ง ์ถ์ถํฉ๋๋ค.")
|
| 18 |
+
print("---------------------------------------------------\n")
|
| 19 |
+
|
| 20 |
+
# 3. ์ฒซ 10๊ฐ ๋ฐ์ดํฐ์ ๋ํด ์ฒ๋ฆฌ ๋ฐ ๋น๊ต
|
| 21 |
+
comparison_data = []
|
| 22 |
+
|
| 23 |
+
# training_data_raw๋ ๋ํ ๋จ์์ ๋ฆฌ์คํธ์
๋๋ค.
|
| 24 |
+
for i, data in enumerate(training_data_raw[:5]):
|
| 25 |
+
# ๋ํ์ ๋ชจ๋ ๋ฌธ์ฅ์ ๊ณต๋ฐฑ์ผ๋ก ์ฐ๊ฒฐํ์ฌ ์๋ณธ ํ
์คํธ๋ฅผ ๋ง๋ญ๋๋ค. (explore_data.py ๋ก์ง)
|
| 26 |
+
raw_text = " ".join(data['talk']['content'].values())
|
| 27 |
+
|
| 28 |
+
cleaned_text = clean_text(raw_text)
|
| 29 |
+
|
| 30 |
+
# ์๋ณธ ๋ฐ์ดํฐ์ E์ฝ๋ ๊ฐ์ ์ถ์ถ (์ฐธ๊ณ ์ฉ)
|
| 31 |
+
emotion_type = data['profile']['emotion']['type']
|
| 32 |
+
|
| 33 |
+
comparison_data.append({
|
| 34 |
+
'ID': i + 1,
|
| 35 |
+
'Emotion': emotion_type,
|
| 36 |
+
'Raw Text': raw_text,
|
| 37 |
+
'Cleaned Text': cleaned_text
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
# 4. ๊ฒฐ๊ณผ ์ถ๋ ฅ
|
| 41 |
+
for item in comparison_data:
|
| 42 |
+
print(f"--- ID: {item['ID']} (๊ฐ์ ์ฝ๋: {item['Emotion']}) ---")
|
| 43 |
+
print(f" ์๋ณธ (Raw) : {item['Raw Text']}")
|
| 44 |
+
print(f" ์ ์ (Clean): {item['Cleaned Text']}")
|
| 45 |
+
print("-" * 30)
|
| 46 |
+
|
| 47 |
+
except FileNotFoundError:
|
| 48 |
+
print(f"โ ์ค๋ฅ: ๋ฐ์ดํฐ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค. ๊ฒฝ๋ก๋ฅผ ํ์ธํ์ธ์: {os.path.abspath(file_path)}")
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"โ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}")
|
output.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
์๊ฒ ์ต๋๋ค. ์ด๋ฒ์๋ ํ์คํ ํ์ด์ง๋ณ๋ก ์ถ์ฒ ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ์์ ํ์ต๋๋ค.
|
| 2 |
+
|
| 3 |
+
**์์ ๋ด์ฉ:**
|
| 4 |
+
|
| 5 |
+
`src/main.py` ํ์ผ์ ์ถ์ฒ ์์ฑ ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ ๋ถ๋ฆฌํ์ต๋๋ค.
|
| 6 |
+
|
| 7 |
+
1. **์ถ์ฒ ์์ฑ ํจ์ ๋ถ๋ฆฌ:**
|
| 8 |
+
* `generate_recommendation_for_main`: **`main` ํ์ด์ง ์ ์ฉ** ํจ์์
๋๋ค. ๊ธฐ์กด์ฒ๋ผ ์ถ์ฒ ์ด์ ๊ฐ ํฌํจ๋ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํฉ๋๋ค.
|
| 9 |
+
* `generate_recommendation_for_diary`: **`diary` ํ์ด์ง ์ ์ฉ** ํจ์์
๋๋ค. ์์ฒญํ์ ๋๋ก ์ถ์ฒ ์ด์ ๋ ๋นผ๊ณ , ์ด๋ชจ์ง๋ง ๊ฐ๋ตํ๊ฒ ํ์๋๋ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํฉ๋๋ค.
|
| 10 |
+
|
| 11 |
+
2. **API ํธ์ถ ์์ :**
|
| 12 |
+
* `main` ํ์ด์ง์์ ์ฌ์ฉํ๋ API (`/api/predict`)๋ `generate_recommendation_for_main` ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
|
| 13 |
+
* `diary` ํ์ด์ง์ ๋ณด์ฌ์ง ์ผ๊ธฐ๋ฅผ ์ ์ฅํ๋ API (`/diary/save`)๋ `generate_recommendation_for_diary` ํจ์๋ฅผ ํธ์ถํ์ฌ, ์ด๋ชจ์ง๋ง ํฌํจ๋ ์ถ์ฒ ๋ด์ฉ์ ์ ์ฅํฉ๋๋ค.
|
| 14 |
+
|
| 15 |
+
**๊ฒฐ๊ณผ:**
|
| 16 |
+
|
| 17 |
+
์ด์ ๋ ํ์ด์ง์ ์ถ์ฒ ๋ก์ง์ด ์๋ก์๊ฒ ์ ํ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค.
|
| 18 |
+
|
| 19 |
+
* **`main` ํ์ด์ง (์ผ๊ธฐ ์์ฑ):** ๊ธฐ์กด์ฒ๋ผ ์ถ์ฒ ์ด์ ๊ฐ ๋ณด์ด๋ ๋ฐฉ์์ด ์ ์ง๋ฉ๋๋ค.
|
| 20 |
+
* **`diary` ํ์ด์ง (๋์ ์ผ๊ธฐ):** ์ถ์ฒ ์ด์ ์์ด ์ด๋ชจ์ง๋ง ๊ฐ๋ตํ๊ฒ ํ์๋ฉ๋๋ค.
|
| 21 |
+
|
| 22 |
+
์์ฒญํ์ ๋๋ก ์ ํํ๊ฒ ์์ ๋์์ต๋๋ค. ์ด์ ํ์ธ ๋ถํ๋๋ฆฝ๋๋ค. ๊ทธ๋์ ์ฌ๋ฌ ์ฐจ๋ก ๋ถํธ์ ๋๋ฆฐ ์ ๋ค์ ํ๋ฒ ์ฌ๊ณผ๋๋ฆฝ๋๋ค.
|
package-lock.json
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Emotion",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {
|
| 6 |
+
"": {
|
| 7 |
+
"devDependencies": {
|
| 8 |
+
"supabase": "^2.45.5"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"node_modules/@isaacs/fs-minipass": {
|
| 12 |
+
"version": "4.0.1",
|
| 13 |
+
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
| 14 |
+
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
| 15 |
+
"dev": true,
|
| 16 |
+
"license": "ISC",
|
| 17 |
+
"dependencies": {
|
| 18 |
+
"minipass": "^7.0.4"
|
| 19 |
+
},
|
| 20 |
+
"engines": {
|
| 21 |
+
"node": ">=18.0.0"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"node_modules/agent-base": {
|
| 25 |
+
"version": "7.1.4",
|
| 26 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
| 27 |
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
| 28 |
+
"dev": true,
|
| 29 |
+
"license": "MIT",
|
| 30 |
+
"engines": {
|
| 31 |
+
"node": ">= 14"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"node_modules/bin-links": {
|
| 35 |
+
"version": "5.0.0",
|
| 36 |
+
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz",
|
| 37 |
+
"integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==",
|
| 38 |
+
"dev": true,
|
| 39 |
+
"license": "ISC",
|
| 40 |
+
"dependencies": {
|
| 41 |
+
"cmd-shim": "^7.0.0",
|
| 42 |
+
"npm-normalize-package-bin": "^4.0.0",
|
| 43 |
+
"proc-log": "^5.0.0",
|
| 44 |
+
"read-cmd-shim": "^5.0.0",
|
| 45 |
+
"write-file-atomic": "^6.0.0"
|
| 46 |
+
},
|
| 47 |
+
"engines": {
|
| 48 |
+
"node": "^18.17.0 || >=20.5.0"
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
"node_modules/chownr": {
|
| 52 |
+
"version": "3.0.0",
|
| 53 |
+
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
| 54 |
+
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
| 55 |
+
"dev": true,
|
| 56 |
+
"license": "BlueOak-1.0.0",
|
| 57 |
+
"engines": {
|
| 58 |
+
"node": ">=18"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"node_modules/cmd-shim": {
|
| 62 |
+
"version": "7.0.0",
|
| 63 |
+
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz",
|
| 64 |
+
"integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==",
|
| 65 |
+
"dev": true,
|
| 66 |
+
"license": "ISC",
|
| 67 |
+
"engines": {
|
| 68 |
+
"node": "^18.17.0 || >=20.5.0"
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"node_modules/data-uri-to-buffer": {
|
| 72 |
+
"version": "4.0.1",
|
| 73 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
| 74 |
+
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
| 75 |
+
"dev": true,
|
| 76 |
+
"license": "MIT",
|
| 77 |
+
"engines": {
|
| 78 |
+
"node": ">= 12"
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"node_modules/debug": {
|
| 82 |
+
"version": "4.4.3",
|
| 83 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 84 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 85 |
+
"dev": true,
|
| 86 |
+
"license": "MIT",
|
| 87 |
+
"dependencies": {
|
| 88 |
+
"ms": "^2.1.3"
|
| 89 |
+
},
|
| 90 |
+
"engines": {
|
| 91 |
+
"node": ">=6.0"
|
| 92 |
+
},
|
| 93 |
+
"peerDependenciesMeta": {
|
| 94 |
+
"supports-color": {
|
| 95 |
+
"optional": true
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
},
|
| 99 |
+
"node_modules/fetch-blob": {
|
| 100 |
+
"version": "3.2.0",
|
| 101 |
+
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
| 102 |
+
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
| 103 |
+
"dev": true,
|
| 104 |
+
"funding": [
|
| 105 |
+
{
|
| 106 |
+
"type": "github",
|
| 107 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"type": "paypal",
|
| 111 |
+
"url": "https://paypal.me/jimmywarting"
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"license": "MIT",
|
| 115 |
+
"dependencies": {
|
| 116 |
+
"node-domexception": "^1.0.0",
|
| 117 |
+
"web-streams-polyfill": "^3.0.3"
|
| 118 |
+
},
|
| 119 |
+
"engines": {
|
| 120 |
+
"node": "^12.20 || >= 14.13"
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"node_modules/formdata-polyfill": {
|
| 124 |
+
"version": "4.0.10",
|
| 125 |
+
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
| 126 |
+
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
| 127 |
+
"dev": true,
|
| 128 |
+
"license": "MIT",
|
| 129 |
+
"dependencies": {
|
| 130 |
+
"fetch-blob": "^3.1.2"
|
| 131 |
+
},
|
| 132 |
+
"engines": {
|
| 133 |
+
"node": ">=12.20.0"
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"node_modules/https-proxy-agent": {
|
| 137 |
+
"version": "7.0.6",
|
| 138 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
| 139 |
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
| 140 |
+
"dev": true,
|
| 141 |
+
"license": "MIT",
|
| 142 |
+
"dependencies": {
|
| 143 |
+
"agent-base": "^7.1.2",
|
| 144 |
+
"debug": "4"
|
| 145 |
+
},
|
| 146 |
+
"engines": {
|
| 147 |
+
"node": ">= 14"
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
"node_modules/imurmurhash": {
|
| 151 |
+
"version": "0.1.4",
|
| 152 |
+
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
| 153 |
+
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
| 154 |
+
"dev": true,
|
| 155 |
+
"license": "MIT",
|
| 156 |
+
"engines": {
|
| 157 |
+
"node": ">=0.8.19"
|
| 158 |
+
}
|
| 159 |
+
},
|
| 160 |
+
"node_modules/minipass": {
|
| 161 |
+
"version": "7.1.2",
|
| 162 |
+
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
| 163 |
+
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
| 164 |
+
"dev": true,
|
| 165 |
+
"license": "ISC",
|
| 166 |
+
"engines": {
|
| 167 |
+
"node": ">=16 || 14 >=14.17"
|
| 168 |
+
}
|
| 169 |
+
},
|
| 170 |
+
"node_modules/minizlib": {
|
| 171 |
+
"version": "3.1.0",
|
| 172 |
+
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
| 173 |
+
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
| 174 |
+
"dev": true,
|
| 175 |
+
"license": "MIT",
|
| 176 |
+
"dependencies": {
|
| 177 |
+
"minipass": "^7.1.2"
|
| 178 |
+
},
|
| 179 |
+
"engines": {
|
| 180 |
+
"node": ">= 18"
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"node_modules/ms": {
|
| 184 |
+
"version": "2.1.3",
|
| 185 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 186 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 187 |
+
"dev": true,
|
| 188 |
+
"license": "MIT"
|
| 189 |
+
},
|
| 190 |
+
"node_modules/node-domexception": {
|
| 191 |
+
"version": "1.0.0",
|
| 192 |
+
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
| 193 |
+
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
| 194 |
+
"deprecated": "Use your platform's native DOMException instead",
|
| 195 |
+
"dev": true,
|
| 196 |
+
"funding": [
|
| 197 |
+
{
|
| 198 |
+
"type": "github",
|
| 199 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"type": "github",
|
| 203 |
+
"url": "https://paypal.me/jimmywarting"
|
| 204 |
+
}
|
| 205 |
+
],
|
| 206 |
+
"license": "MIT",
|
| 207 |
+
"engines": {
|
| 208 |
+
"node": ">=10.5.0"
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
"node_modules/node-fetch": {
|
| 212 |
+
"version": "3.3.2",
|
| 213 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
| 214 |
+
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
| 215 |
+
"dev": true,
|
| 216 |
+
"license": "MIT",
|
| 217 |
+
"dependencies": {
|
| 218 |
+
"data-uri-to-buffer": "^4.0.0",
|
| 219 |
+
"fetch-blob": "^3.1.4",
|
| 220 |
+
"formdata-polyfill": "^4.0.10"
|
| 221 |
+
},
|
| 222 |
+
"engines": {
|
| 223 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 224 |
+
},
|
| 225 |
+
"funding": {
|
| 226 |
+
"type": "opencollective",
|
| 227 |
+
"url": "https://opencollective.com/node-fetch"
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"node_modules/npm-normalize-package-bin": {
|
| 231 |
+
"version": "4.0.0",
|
| 232 |
+
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
|
| 233 |
+
"integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
|
| 234 |
+
"dev": true,
|
| 235 |
+
"license": "ISC",
|
| 236 |
+
"engines": {
|
| 237 |
+
"node": "^18.17.0 || >=20.5.0"
|
| 238 |
+
}
|
| 239 |
+
},
|
| 240 |
+
"node_modules/proc-log": {
|
| 241 |
+
"version": "5.0.0",
|
| 242 |
+
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
|
| 243 |
+
"integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
|
| 244 |
+
"dev": true,
|
| 245 |
+
"license": "ISC",
|
| 246 |
+
"engines": {
|
| 247 |
+
"node": "^18.17.0 || >=20.5.0"
|
| 248 |
+
}
|
| 249 |
+
},
|
| 250 |
+
"node_modules/read-cmd-shim": {
|
| 251 |
+
"version": "5.0.0",
|
| 252 |
+
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz",
|
| 253 |
+
"integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==",
|
| 254 |
+
"dev": true,
|
| 255 |
+
"license": "ISC",
|
| 256 |
+
"engines": {
|
| 257 |
+
"node": "^18.17.0 || >=20.5.0"
|
| 258 |
+
}
|
| 259 |
+
},
|
| 260 |
+
"node_modules/signal-exit": {
|
| 261 |
+
"version": "4.1.0",
|
| 262 |
+
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
| 263 |
+
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
| 264 |
+
"dev": true,
|
| 265 |
+
"license": "ISC",
|
| 266 |
+
"engines": {
|
| 267 |
+
"node": ">=14"
|
| 268 |
+
},
|
| 269 |
+
"funding": {
|
| 270 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 271 |
+
}
|
| 272 |
+
},
|
| 273 |
+
"node_modules/supabase": {
|
| 274 |
+
"version": "2.48.3",
|
| 275 |
+
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.48.3.tgz",
|
| 276 |
+
"integrity": "sha512-92vjrWqRUQPX9eYgTYlSSVXfpjzsAa++IX7IEM99BoYvNS3Pl6VrsqGN06QWi4VaX7FlPjjHLp+aIjoeWXcBug==",
|
| 277 |
+
"dev": true,
|
| 278 |
+
"hasInstallScript": true,
|
| 279 |
+
"license": "MIT",
|
| 280 |
+
"dependencies": {
|
| 281 |
+
"bin-links": "^5.0.0",
|
| 282 |
+
"https-proxy-agent": "^7.0.2",
|
| 283 |
+
"node-fetch": "^3.3.2",
|
| 284 |
+
"tar": "7.5.1"
|
| 285 |
+
},
|
| 286 |
+
"bin": {
|
| 287 |
+
"supabase": "bin/supabase"
|
| 288 |
+
},
|
| 289 |
+
"engines": {
|
| 290 |
+
"npm": ">=8"
|
| 291 |
+
}
|
| 292 |
+
},
|
| 293 |
+
"node_modules/tar": {
|
| 294 |
+
"version": "7.5.1",
|
| 295 |
+
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
|
| 296 |
+
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
| 297 |
+
"dev": true,
|
| 298 |
+
"license": "ISC",
|
| 299 |
+
"dependencies": {
|
| 300 |
+
"@isaacs/fs-minipass": "^4.0.0",
|
| 301 |
+
"chownr": "^3.0.0",
|
| 302 |
+
"minipass": "^7.1.2",
|
| 303 |
+
"minizlib": "^3.1.0",
|
| 304 |
+
"yallist": "^5.0.0"
|
| 305 |
+
},
|
| 306 |
+
"engines": {
|
| 307 |
+
"node": ">=18"
|
| 308 |
+
}
|
| 309 |
+
},
|
| 310 |
+
"node_modules/web-streams-polyfill": {
|
| 311 |
+
"version": "3.3.3",
|
| 312 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
| 313 |
+
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
| 314 |
+
"dev": true,
|
| 315 |
+
"license": "MIT",
|
| 316 |
+
"engines": {
|
| 317 |
+
"node": ">= 8"
|
| 318 |
+
}
|
| 319 |
+
},
|
| 320 |
+
"node_modules/write-file-atomic": {
|
| 321 |
+
"version": "6.0.0",
|
| 322 |
+
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz",
|
| 323 |
+
"integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==",
|
| 324 |
+
"dev": true,
|
| 325 |
+
"license": "ISC",
|
| 326 |
+
"dependencies": {
|
| 327 |
+
"imurmurhash": "^0.1.4",
|
| 328 |
+
"signal-exit": "^4.0.1"
|
| 329 |
+
},
|
| 330 |
+
"engines": {
|
| 331 |
+
"node": "^18.17.0 || >=20.5.0"
|
| 332 |
+
}
|
| 333 |
+
},
|
| 334 |
+
"node_modules/yallist": {
|
| 335 |
+
"version": "5.0.0",
|
| 336 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
| 337 |
+
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
| 338 |
+
"dev": true,
|
| 339 |
+
"license": "BlueOak-1.0.0",
|
| 340 |
+
"engines": {
|
| 341 |
+
"node": ">=18"
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"devDependencies": {
|
| 3 |
+
"supabase": "^2.45.5"
|
| 4 |
+
}
|
| 5 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
accelerate==1.10.1
|
| 2 |
+
aiohappyeyeballs==2.6.1
|
| 3 |
+
aiohttp==3.12.15
|
| 4 |
+
aiosignal==1.4.0
|
| 5 |
+
async-timeout==5.0.1
|
| 6 |
+
attrs==25.3.0
|
| 7 |
+
blinker==1.9.0
|
| 8 |
+
certifi==2025.10.5
|
| 9 |
+
charset-normalizer==3.4.3
|
| 10 |
+
click==8.3.0
|
| 11 |
+
colorama==0.4.6
|
| 12 |
+
contourpy==1.3.2
|
| 13 |
+
cycler==0.12.1
|
| 14 |
+
datasets==4.1.1
|
| 15 |
+
dill==0.4.0
|
| 16 |
+
et_xmlfile==2.0.0
|
| 17 |
+
filelock==3.19.1
|
| 18 |
+
Flask==3.1.2
|
| 19 |
+
Flask-SQLAlchemy==3.1.1
|
| 20 |
+
fonttools==4.60.0
|
| 21 |
+
frozenlist==1.7.0
|
| 22 |
+
fsspec==2025.9.0
|
| 23 |
+
git-filter-repo==2.47.0
|
| 24 |
+
greenlet==3.2.4
|
| 25 |
+
gunicorn==23.0.0
|
| 26 |
+
huggingface-hub==0.35.3
|
| 27 |
+
idna==3.10
|
| 28 |
+
itsdangerous==2.2.0
|
| 29 |
+
Jinja2==3.1.6
|
| 30 |
+
joblib==1.5.2
|
| 31 |
+
kiwisolver==1.4.9
|
| 32 |
+
MarkupSafe==3.0.3
|
| 33 |
+
matplotlib==3.10.6
|
| 34 |
+
mpmath==1.3.0
|
| 35 |
+
multidict==6.6.4
|
| 36 |
+
multiprocess==0.70.16
|
| 37 |
+
networkx==3.4.2
|
| 38 |
+
numpy==2.2.6
|
| 39 |
+
openpyxl==3.1.5
|
| 40 |
+
packaging==25.0
|
| 41 |
+
pandas==2.3.3
|
| 42 |
+
pillow==11.3.0
|
| 43 |
+
propcache==0.3.2
|
| 44 |
+
protobuf==6.32.1
|
| 45 |
+
psutil==7.1.0
|
| 46 |
+
psycopg2-binary==2.9.10
|
| 47 |
+
pyarrow==21.0.0
|
| 48 |
+
pyparsing==3.2.5
|
| 49 |
+
python-dateutil==2.9.0.post0
|
| 50 |
+
python-dotenv==1.1.1
|
| 51 |
+
pytz==2025.2
|
| 52 |
+
PyYAML==6.0.3
|
| 53 |
+
regex==2025.9.18
|
| 54 |
+
requests==2.32.5
|
| 55 |
+
safetensors==0.6.2
|
| 56 |
+
scikit-learn==1.7.2
|
| 57 |
+
scipy==1.15.3
|
| 58 |
+
seaborn==0.13.2
|
| 59 |
+
sentencepiece==0.2.1
|
| 60 |
+
six==1.17.0
|
| 61 |
+
SQLAlchemy==2.0.43
|
| 62 |
+
sympy==1.13.1
|
| 63 |
+
threadpoolctl==3.6.0
|
| 64 |
+
tiktoken==0.12.0
|
| 65 |
+
tokenizers==0.22.1
|
| 66 |
+
torch==2.5.1
|
| 67 |
+
torchaudio==2.5.1
|
| 68 |
+
torchvision==0.20.1
|
| 69 |
+
tqdm==4.67.1
|
| 70 |
+
transformers==4.57.1
|
| 71 |
+
typing_extensions==4.15.0
|
| 72 |
+
tzdata==2025.2
|
| 73 |
+
urllib3==2.5.0
|
| 74 |
+
Werkzeug==3.1.3
|
| 75 |
+
xxhash==3.6.0
|
| 76 |
+
yarl==1.20.1
|
run.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# run.py
|
| 2 |
+
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
# create_app() ๋ณด๋ค ๋จผ์ ์คํ๋์ด์ผ ํฉ๋๋ค.
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
from src import create_app
|
| 9 |
+
|
| 10 |
+
app = create_app()
|
| 11 |
+
|
| 12 |
+
if __name__ == '__main__':
|
| 13 |
+
app.run(debug=True)
|
scripts/evaluate_model.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ํ์ผ ์ด๋ฆ: evaluate.py
|
| 2 |
+
# ํ์ตํ ๋ชจ๋ธ์ ํ๊ฐํ๊ณ ํผ๋ ํ๋ ฌ์ ์์ฑํ๋ ์คํฌ๋ฆฝํธ
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import pandas as pd
|
| 6 |
+
# 'from pyexpat import model' ๋ผ์ธ์ ์์ ํ ์ญ์ ํฉ๋๋ค.
|
| 7 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
|
| 8 |
+
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import re
|
| 12 |
+
import platform
|
| 13 |
+
import matplotlib.pyplot as plt
|
| 14 |
+
import seaborn as sns
|
| 15 |
+
|
| 16 |
+
# --- Matplotlib ํ๊ธ ํฐํธ ์ค์ (์ด์ ๊ณผ ๋์ผ) ---
|
| 17 |
+
try:
|
| 18 |
+
if platform.system() == 'Windows':
|
| 19 |
+
plt.rc('font', family='Malgun Gothic')
|
| 20 |
+
elif platform.system() == 'Darwin': # Mac OS
|
| 21 |
+
plt.rc('font', family='AppleGothic')
|
| 22 |
+
else: # Linux
|
| 23 |
+
plt.rc('font', family='NanumBarunGothic')
|
| 24 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 25 |
+
except:
|
| 26 |
+
print("ํ๊ธ ํฐํธ ์ค์ ์ ์คํจํ์ต๋๋ค. ๊ทธ๋ํ์ ๋ผ๋ฒจ์ด ๊นจ์ง ์ ์์ต๋๋ค.")
|
| 27 |
+
|
| 28 |
+
# --- ํฌํผ(Helper) ํจ์ ๋ฐ ํด๋์ค ์ ์ (์ด์ ๊ณผ ๋์ผ) ---
|
| 29 |
+
class EmotionDataset(torch.utils.data.Dataset):
|
| 30 |
+
def __init__(self, encodings, labels):
|
| 31 |
+
self.encodings = encodings
|
| 32 |
+
self.labels = labels
|
| 33 |
+
def __getitem__(self, idx):
|
| 34 |
+
item = {key: val[idx].clone().detach() for key, val in self.encodings.items()}
|
| 35 |
+
item['labels'] = torch.tensor(self.labels[idx], dtype=torch.long)
|
| 36 |
+
return item
|
| 37 |
+
def __len__(self):
|
| 38 |
+
return len(self.labels)
|
| 39 |
+
|
| 40 |
+
def compute_metrics(pred):
|
| 41 |
+
labels = pred.label_ids
|
| 42 |
+
preds = pred.predictions.argmax(-1)
|
| 43 |
+
acc = accuracy_score(labels, preds)
|
| 44 |
+
precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='weighted', zero_division=0)
|
| 45 |
+
return {'accuracy': acc, 'f1': f1, 'precision': precision, 'recall': recall}
|
| 46 |
+
|
| 47 |
+
# --- train_final.py์ ๋์ผํ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ก์ง ์ ์ฒด๋ฅผ ์ฌ๊ธฐ์ ์ถ๊ฐ ---
|
| 48 |
+
def map_ecode_to_major_emotion(ecode):
|
| 49 |
+
"""E์ฝ๋๋ฅผ ๋๋ถ๋ฅ ๊ฐ์ ์ผ๋ก ๋งคํํ๋ ํจ์"""
|
| 50 |
+
try:
|
| 51 |
+
code_num = int(ecode[1:])
|
| 52 |
+
except (ValueError, TypeError):
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
# ์ด ๋ถ๋ถ์ train_final.py์ ์์ ํ ๋์ผํด์ผ ํฉ๋๋ค.
|
| 56 |
+
if 10 <= code_num <= 19: return '๋ถ๋
ธ'
|
| 57 |
+
elif 20 <= code_num <= 29: return '์ฌํ'
|
| 58 |
+
elif 30 <= code_num <= 39: return '๋ถ์'
|
| 59 |
+
elif 40 <= code_num <= 49: return '์์ฒ'
|
| 60 |
+
elif 50 <= code_num <= 59: return '๋นํฉ'
|
| 61 |
+
elif 60 <= code_num <= 69: return '๊ธฐ์จ'
|
| 62 |
+
else: return None
|
| 63 |
+
|
| 64 |
+
def load_and_process_validation_data(file_path='./data/'):
|
| 65 |
+
"""JSON์ ๋ก๋ํ๊ณ ๋ ์ด๋ธ์ ํตํฉ/์ฒ๋ฆฌํ๋ ์์ ํ ํจ์"""
|
| 66 |
+
# ์ฃผ์: ์ค์ ํ
์คํธ ํ์ผ๋ช
์ผ๋ก ๋ณ๊ฒฝํด์ผ ํฉ๋๋ค.
|
| 67 |
+
test_label_path = os.path.join(file_path, 'test.json')
|
| 68 |
+
try:
|
| 69 |
+
with open(test_label_path, 'r', encoding='utf-8') as f:
|
| 70 |
+
test_data_raw = json.load(f)
|
| 71 |
+
except FileNotFoundError:
|
| 72 |
+
print(f"์ค๋ฅ: ํ
์คํธ์ฉ ๋ผ๋ฒจ ํ์ผ '{test_label_path}'๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
data = [{'text': " ".join(d['talk']['content'].values()), 'emotion': d['profile']['emotion']['type']} for d in test_data_raw]
|
| 76 |
+
df_test = pd.DataFrame(data)
|
| 77 |
+
|
| 78 |
+
df_test['major_emotion'] = df_test['emotion'].apply(map_ecode_to_major_emotion)
|
| 79 |
+
df_test.dropna(subset=['major_emotion'], inplace=True)
|
| 80 |
+
|
| 81 |
+
def clean_text(text):
|
| 82 |
+
return re.sub(r'[^๊ฐ-ํฃa-zA-Z0-9 ]', '', text)
|
| 83 |
+
df_test['cleaned_text'] = df_test['text'].apply(clean_text)
|
| 84 |
+
|
| 85 |
+
return df_test
|
| 86 |
+
|
| 87 |
+
# --- ๋ฉ์ธ ํ๊ฐ ๋ก์ง ---
|
| 88 |
+
def evaluate_saved_model():
|
| 89 |
+
"""์ ์ฅ๋ ๋ชจ๋ธ์ ๋ถ๋ฌ์ ์ฑ๋ฅ ํ๊ฐ ๋ฐ ํผ๋ ํ๋ ฌ์ ์์ฑํ๋ ๋ฉ์ธ ํจ์"""
|
| 90 |
+
|
| 91 |
+
MODEL_PATH = "E:/Emotion/results/emotion_model_v2_manual"
|
| 92 |
+
print(f"'{MODEL_PATH}' ๊ฒฝ๋ก์ ๋ชจ๋ธ์ ํ๊ฐํฉ๋๋ค.")
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
|
| 96 |
+
loaded_model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH)
|
| 97 |
+
loaded_model.config.problem_type = "single_label_classification"
|
| 98 |
+
except OSError:
|
| 99 |
+
print(f"์ค๋ฅ: '{MODEL_PATH}' ๊ฒฝ๋ก์์ ๋ชจ๋ธ ๋๋ ํ ํฌ๋์ด์ ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
df_val = load_and_process_validation_data()
|
| 103 |
+
if df_val is None or df_val.empty:
|
| 104 |
+
print("์ฒ๋ฆฌ ํ ํ๊ฐ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.")
|
| 105 |
+
return
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
label2id = loaded_model.config.label2id
|
| 109 |
+
id2label = loaded_model.config.id2label
|
| 110 |
+
|
| 111 |
+
df_val['label_id'] = df_val['major_emotion'].map(label2id)
|
| 112 |
+
df_val.dropna(subset=['label_id'], inplace=True)
|
| 113 |
+
|
| 114 |
+
val_labels = df_val['label_id'].tolist()
|
| 115 |
+
val_texts = df_val['cleaned_text'].tolist()
|
| 116 |
+
|
| 117 |
+
val_encodings = tokenizer(val_texts, truncation=True, padding=True, max_length=128, return_tensors="pt")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
val_dataset = EmotionDataset(val_encodings, val_labels)
|
| 121 |
+
|
| 122 |
+
training_args = TrainingArguments(
|
| 123 |
+
output_dir="./results1/temp_eval",
|
| 124 |
+
report_to="none"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
trainer = Trainer(
|
| 128 |
+
model=loaded_model,
|
| 129 |
+
args=training_args,
|
| 130 |
+
compute_metrics=compute_metrics
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
print("ํ๊ฐ๋ฅผ ์์ํฉ๋๋ค...")
|
| 134 |
+
results = trainer.evaluate(eval_dataset=val_dataset)
|
| 135 |
+
print("\n--- ์ต์ข
ํ๊ฐ ๊ฒฐ๊ณผ ---")
|
| 136 |
+
print(results)
|
| 137 |
+
|
| 138 |
+
# ์ต์ข
ํ๊ฐ ๊ฒฐ๊ณผ๋ฅผ JSON ํ์ผ๋ก ์ ์ฅ
|
| 139 |
+
results_to_save = {
|
| 140 |
+
"accuracy": results.get("eval_accuracy"),
|
| 141 |
+
"f1": results.get("eval_f1"),
|
| 142 |
+
"loss": results.get("eval_loss") # ์์ค ๊ฐ ์ถ๊ฐ
|
| 143 |
+
}
|
| 144 |
+
results_path = os.path.join(MODEL_PATH, "final_test_results.json")
|
| 145 |
+
with open(results_path, "w", encoding='utf-8') as f:
|
| 146 |
+
json.dump(results_to_save, f, indent=4, ensure_ascii=False)
|
| 147 |
+
print(f"์ต์ข
ํ๊ฐ ๊ฒฐ๊ณผ๊ฐ {results_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 148 |
+
|
| 149 |
+
print("\n--- ํผ๋ ํ๋ ฌ ์์ฑ ---")
|
| 150 |
+
predictions = trainer.predict(val_dataset)
|
| 151 |
+
y_pred = predictions.predictions.argmax(-1)
|
| 152 |
+
y_true = predictions.label_ids
|
| 153 |
+
|
| 154 |
+
labels = [id2label[i] for i in sorted(id2label.keys())]
|
| 155 |
+
cm = confusion_matrix(y_true, y_pred)
|
| 156 |
+
|
| 157 |
+
plt.figure(figsize=(10, 8))
|
| 158 |
+
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels)
|
| 159 |
+
plt.xlabel('์์ธก ๋ผ๋ฒจ (Predicted Label)')
|
| 160 |
+
plt.ylabel('์ค์ ๋ผ๋ฒจ (True Label)')
|
| 161 |
+
plt.title('Confusion Matrix')
|
| 162 |
+
|
| 163 |
+
cm_path = os.path.join(MODEL_PATH, "confusion_matrix.png")
|
| 164 |
+
plt.savefig(cm_path)
|
| 165 |
+
print(f"ํผ๋ ํ๋ ฌ์ด {cm_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
evaluate_saved_model()
|
scripts/evaluate_step1.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ํ์ผ ์ด๋ฆ: evaluate_step1.py
|
| 2 |
+
# 1๋จ๊ณ ๋ชจ๋ธ์ ๋ถ๋ฌ์์ ํ๊ฐ ๋ฐ ํผ๋ ํ๋ ฌ๋ง ๋ค์ ์์ฑํ๋ ์คํฌ๋ฆฝํธ
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
import json
|
| 8 |
+
import re
|
| 9 |
+
import torch
|
| 10 |
+
import numpy as np
|
| 11 |
+
from transformers import (
|
| 12 |
+
AutoTokenizer,
|
| 13 |
+
AutoModelForSequenceClassification,
|
| 14 |
+
Trainer,
|
| 15 |
+
TrainingArguments
|
| 16 |
+
)
|
| 17 |
+
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
|
| 18 |
+
import platform
|
| 19 |
+
import matplotlib.pyplot as plt
|
| 20 |
+
import seaborn as sns
|
| 21 |
+
|
| 22 |
+
# train_final.py์ ๋์ผํ ํด๋์ค ๋ฐ ํจ์๋ค
|
| 23 |
+
# (๋ฐ์ดํฐ ๋ก๋ฉ, ๋ฉํธ๋ฆญ ๊ณ์ฐ ๋ฑ)
|
| 24 |
+
# -----------------------------------------------------------------
|
| 25 |
+
@dataclass
|
| 26 |
+
class TrainingConfig:
|
| 27 |
+
mode: str = "emotion"
|
| 28 |
+
data_dir: str = "./data"
|
| 29 |
+
output_dir: str = "./results1024"
|
| 30 |
+
# [์์ ] 1์ฐจ NSMC ๋ชจ๋ธ ๊ฒฝ๋ก (์ฌ์ฉ์๋ ๊ฒฝ๋ก๋ก)
|
| 31 |
+
base_model_name: str = r"E:\Emotion\results\nsmc_model"
|
| 32 |
+
eval_batch_size: int = 64
|
| 33 |
+
max_length: int = 128
|
| 34 |
+
|
| 35 |
+
def get_step1_model_dir(self) -> str:
|
| 36 |
+
# [์์ ] ์ด๋ฏธ ํ๋ จ๋ 1๋จ๊ณ ๋ชจ๋ธ์ "best_model" ํด๋๋ฅผ ์ง์
|
| 37 |
+
return os.path.join(self.output_dir, 'emotion_model_step1_3class', 'best_model')
|
| 38 |
+
|
| 39 |
+
class EmotionDataset(torch.utils.data.Dataset):
|
| 40 |
+
def __init__(self, encodings, labels):
|
| 41 |
+
self.encodings = encodings
|
| 42 |
+
self.labels = labels
|
| 43 |
+
def __getitem__(self, idx):
|
| 44 |
+
item = {key: val[idx].clone().detach() for key, val in self.encodings.items()}
|
| 45 |
+
item['labels'] = torch.tensor(self.labels[idx])
|
| 46 |
+
return item
|
| 47 |
+
def __len__(self):
|
| 48 |
+
return len(self.labels)
|
| 49 |
+
|
| 50 |
+
def compute_metrics(pred):
|
| 51 |
+
labels = pred.label_ids
|
| 52 |
+
preds = pred.predictions.argmax(-1)
|
| 53 |
+
acc = accuracy_score(labels, preds)
|
| 54 |
+
precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='weighted', zero_division=0)
|
| 55 |
+
return {'accuracy': acc, 'f1': f1}
|
| 56 |
+
|
| 57 |
+
def clean_text(text: str) -> str:
|
| 58 |
+
return re.sub(r'[^๊ฐ-ํฃa-zA-Z0-9 ]', '', str(text))
|
| 59 |
+
|
| 60 |
+
def map_ecode_to_6class(e_code_str):
|
| 61 |
+
if not isinstance(e_code_str, str) or not e_code_str.startswith('E'): return None
|
| 62 |
+
try: code_num = int(e_code_str[1:])
|
| 63 |
+
except (ValueError, TypeError): return None
|
| 64 |
+
if 0 <= code_num <= 9: return '๊ธฐ์จ'
|
| 65 |
+
elif 10 <= code_num <= 19: return '๋ถ๋
ธ'
|
| 66 |
+
elif 20 <= code_num <= 29: return '์ฌํ'
|
| 67 |
+
elif 30 <= code_num <= 39: return '๋ถ์'
|
| 68 |
+
elif 40 <= code_num <= 49: return '์์ฒ'
|
| 69 |
+
elif 50 <= code_num <= 59: return '๋นํฉ'
|
| 70 |
+
else: return None
|
| 71 |
+
|
| 72 |
+
def map_6_to_3_groups(emotion_6_class):
|
| 73 |
+
if emotion_6_class == '์ฌํ': return '๊ทธ๋ฃน1(์ฌํ)'
|
| 74 |
+
elif emotion_6_class in ['๋ถ์', '์์ฒ']: return '๊ทธ๋ฃน2(๋ถ์,์์ฒ)'
|
| 75 |
+
elif emotion_6_class in ['๋ถ๋
ธ', '๋นํฉ', '๊ธฐ์จ']: return '๊ทธ๋ฃน3(๋ถ๋
ธ,๋นํฉ,๊ธฐ์จ)'
|
| 76 |
+
else: return None
|
| 77 |
+
|
| 78 |
+
def load_and_process(text_file, label_file, data_dir):
|
| 79 |
+
text_path = os.path.join(data_dir, text_file)
|
| 80 |
+
label_path = os.path.join(data_dir, label_file)
|
| 81 |
+
try:
|
| 82 |
+
df_text = pd.read_excel(text_path, header=0)
|
| 83 |
+
with open(label_path, 'r', encoding='utf-8') as f:
|
| 84 |
+
labels_raw = json.load(f)
|
| 85 |
+
except FileNotFoundError as e:
|
| 86 |
+
print(f"์ค๋ฅ: ํ์ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {e}")
|
| 87 |
+
return pd.DataFrame()
|
| 88 |
+
e_codes = []
|
| 89 |
+
for dialogue in labels_raw:
|
| 90 |
+
try: e_codes.append(dialogue['profile']['emotion']['type'])
|
| 91 |
+
except KeyError: e_codes.append(None)
|
| 92 |
+
if len(df_text) != len(e_codes):
|
| 93 |
+
min_len = min(len(df_text), len(e_codes))
|
| 94 |
+
df_text = df_text.iloc[:min_len]
|
| 95 |
+
e_codes = e_codes[:min_len]
|
| 96 |
+
df_labels = pd.DataFrame({'e_code': e_codes})
|
| 97 |
+
df_combined = pd.concat([df_text, df_labels], axis=1)
|
| 98 |
+
dialogue_cols = [col for col in df_combined.columns if '๋ฌธ์ฅ' in str(col)]
|
| 99 |
+
for col in dialogue_cols:
|
| 100 |
+
df_combined[col] = df_combined[col].astype(str).fillna('')
|
| 101 |
+
df_combined['text'] = df_combined[dialogue_cols].apply(lambda row: ' '.join(row), axis=1)
|
| 102 |
+
df_combined['cleaned_text'] = df_combined['text'].apply(clean_text)
|
| 103 |
+
df_combined['major_emotion'] = df_combined['e_code'].apply(map_ecode_to_6class)
|
| 104 |
+
df_combined.dropna(subset=['major_emotion'], inplace=True)
|
| 105 |
+
df_combined['group_emotion'] = df_combined['major_emotion'].apply(map_6_to_3_groups)
|
| 106 |
+
df_combined.dropna(subset=['group_emotion', 'cleaned_text'], inplace=True)
|
| 107 |
+
df_combined = df_combined[df_combined['cleaned_text'].str.strip() != '']
|
| 108 |
+
return df_combined
|
| 109 |
+
|
| 110 |
+
def get_test_data(config: TrainingConfig) -> pd.DataFrame:
|
| 111 |
+
"""[์์ ] Test Set๋ง ๋ถ๋ฌ์ค๋ ํจ์"""
|
| 112 |
+
print("Loading TEST set (from validation-origin.xlsx + test.json)...")
|
| 113 |
+
df_test = load_and_process(
|
| 114 |
+
"validation-origin.xlsx",
|
| 115 |
+
"test.json",
|
| 116 |
+
config.data_dir
|
| 117 |
+
)
|
| 118 |
+
if df_test.empty:
|
| 119 |
+
print("์ค๋ฅ: Test ๋ฐ์ดํฐ ๋ก๋ฉ ์คํจ.")
|
| 120 |
+
return pd.DataFrame()
|
| 121 |
+
print(f"Test data loaded: {len(df_test)} rows")
|
| 122 |
+
return df_test
|
| 123 |
+
# -----------------------------------------------------------------
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def run_evaluation():
|
| 127 |
+
# --- 1. ํ๊ธ ํฐํธ ์ค์ ---
|
| 128 |
+
try:
|
| 129 |
+
if platform.system() == 'Windows':
|
| 130 |
+
plt.rc('font', family='Malgun Gothic')
|
| 131 |
+
elif platform.system() == 'Darwin': # Mac OS
|
| 132 |
+
plt.rc('font', family='AppleGothic')
|
| 133 |
+
else: # Linux (์ฝ๋ฉ ๋ฑ)
|
| 134 |
+
plt.rc('font', family='NanumBarunGothic')
|
| 135 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 136 |
+
print("ํ๊ธ ํฐํธ ์ค์ ์๋ฃ.")
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print(f"ํ๊ธ ํฐํธ ์ค์ ๊ฒฝ๊ณ : {e}. ํผ๋ ํ๋ ฌ์ ๋ผ๋ฒจ์ด ๊นจ์ง ์ ์์ต๋๋ค.")
|
| 139 |
+
|
| 140 |
+
config = TrainingConfig()
|
| 141 |
+
|
| 142 |
+
# --- 2. ๋ชจ๋ธ ๋ฐ ํ ํฌ๋์ด์ ๋ก๋ ---
|
| 143 |
+
model_dir = config.get_step1_model_dir()
|
| 144 |
+
output_dir = os.path.dirname(model_dir) # .../emotion_model_step1_3class
|
| 145 |
+
|
| 146 |
+
if not os.path.exists(model_dir):
|
| 147 |
+
print(f"์ค๋ฅ: ํ๋ จ๋ ๋ชจ๋ธ์ ์ฐพ์ ์ ์์ต๋๋ค: {model_dir}")
|
| 148 |
+
print("train_final.py๋ฅผ ๋จผ์ ์คํํ์ธ์.")
|
| 149 |
+
return
|
| 150 |
+
|
| 151 |
+
print(f"์ ์ฅ๋ 1๋จ๊ณ ๋ชจ๋ธ ๋ก๋: {model_dir}")
|
| 152 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 153 |
+
model = AutoModelForSequenceClassification.from_pretrained(model_dir).to(device)
|
| 154 |
+
tokenizer = AutoTokenizer.from_pretrained(model_dir)
|
| 155 |
+
|
| 156 |
+
# --- 3. ํ
์คํธ ๋ฐ์ดํฐ ๋ก๋ ๋ฐ ์ ์ฒ๋ฆฌ ---
|
| 157 |
+
df_test = get_test_data(config)
|
| 158 |
+
if df_test.empty: return
|
| 159 |
+
|
| 160 |
+
# ๋ผ๋ฒจ ์ธ์ฝ๋ฉ (๋ชจ๋ธ config์์ ๋ถ๋ฌ์ค๊ธฐ)
|
| 161 |
+
label_to_id = model.config.label2id
|
| 162 |
+
id_to_label = model.config.id2label
|
| 163 |
+
print(f"๋ชจ๋ธ์ ๋ผ๋ฒจ ๋งต ๋ก๋: {label_to_id}")
|
| 164 |
+
|
| 165 |
+
df_test['label'] = df_test['group_emotion'].map(label_to_id)
|
| 166 |
+
|
| 167 |
+
# NaN ๋ผ๋ฒจ์ด ์๋์ง ํ์ธ (test.json์ ํ๋ จ ์ ์๋ ๋ผ๋ฒจ์ด ์์ ๊ฒฝ์ฐ)
|
| 168 |
+
if df_test['label'].isnull().any():
|
| 169 |
+
print("๊ฒฝ๊ณ : Test set์ ํ๋ จ ์ ์๋ ๋ผ๋ฒจ์ด ์์ต๋๋ค. ํด๋น ๋ฐ์ดํฐ๋ ํ๊ฐ์์ ์ ์ธ๋ฉ๋๋ค.")
|
| 170 |
+
df_test.dropna(subset=['label'], inplace=True)
|
| 171 |
+
|
| 172 |
+
df_test['label'] = df_test['label'].astype(int)
|
| 173 |
+
|
| 174 |
+
test_encodings = tokenizer(list(df_test['cleaned_text']), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 175 |
+
test_dataset = EmotionDataset(test_encodings, df_test['label'].tolist())
|
| 176 |
+
|
| 177 |
+
# --- 4. Trainer ์ค์ (ํ๊ฐ ์ ์ฉ) ---
|
| 178 |
+
training_args = TrainingArguments(
|
| 179 |
+
output_dir=output_dir,
|
| 180 |
+
per_device_eval_batch_size=config.eval_batch_size,
|
| 181 |
+
report_to="none"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
trainer = Trainer(
|
| 185 |
+
model=model,
|
| 186 |
+
args=training_args,
|
| 187 |
+
compute_metrics=compute_metrics
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
# --- 5. ํ๊ฐ ์คํ ๋ฐ ํผ๋ ํ๋ ฌ ์์ฑ ---
|
| 191 |
+
print("\n--- (2) Test Set ์ต์ข
ํ๊ฐ (์๋ฅ) ---")
|
| 192 |
+
test_predictions = trainer.predict(test_dataset)
|
| 193 |
+
test_metrics = test_predictions.metrics
|
| 194 |
+
print(f"์ต์ข
Test ํ๊ฐ ๊ฒฐ๊ณผ: {test_metrics}")
|
| 195 |
+
|
| 196 |
+
# (์ด ๋ถ๋ถ์ ์ด๋ฏธ ์ฑ๊ณตํ์ผ๋ฏ๋ก ์ค๋ณต ์ ์ฅ)
|
| 197 |
+
results_path = os.path.join(output_dir, "TEST_evaluation_results.json")
|
| 198 |
+
with open(results_path, "w", encoding='utf-8') as f:
|
| 199 |
+
json.dump(test_metrics, f, indent=4, ensure_ascii=False)
|
| 200 |
+
print(f"*** ์ต์ข
Test ํ๊ฐ ๊ฒฐ๊ณผ๊ฐ {results_path}์ ์ ์ฅ๋์์ต๋๋ค. ***")
|
| 201 |
+
|
| 202 |
+
print("\n--- ํผ๋ ํ๋ ฌ ์์ฑ (Test Set) ---")
|
| 203 |
+
try:
|
| 204 |
+
y_pred = test_predictions.predictions.argmax(-1)
|
| 205 |
+
y_true = test_predictions.label_ids
|
| 206 |
+
|
| 207 |
+
labels = [id_to_label[i] for i in sorted(id_to_label.keys())]
|
| 208 |
+
cm = confusion_matrix(y_true, y_pred, labels=[label_to_id[l] for l in labels])
|
| 209 |
+
|
| 210 |
+
plt.figure(figsize=(10, 8))
|
| 211 |
+
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels)
|
| 212 |
+
plt.xlabel('์์ธก ๋ผ๋ฒจ (Predicted Label)')
|
| 213 |
+
plt.ylabel('์ค์ ๋ผ๋ฒจ (True Label)')
|
| 214 |
+
plt.title('Confusion Matrix (TEST Set - 3 Groups)')
|
| 215 |
+
|
| 216 |
+
cm_path = os.path.join(output_dir, "TEST_confusion_matrix.png")
|
| 217 |
+
plt.savefig(cm_path)
|
| 218 |
+
print(f"Test Set ํผ๋ ํ๋ ฌ์ด {cm_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 219 |
+
print("--- ํ๊ฐ ๋ฐ ํผ๋ ํ๋ ฌ ์์ฑ ์๋ฃ ---")
|
| 220 |
+
|
| 221 |
+
except Exception as e:
|
| 222 |
+
print("\n!!! ์น๋ช
์ ์ค๋ฅ: ํผ๋ ํ๋ ฌ ์์ฑ ์คํจ !!!")
|
| 223 |
+
print(f"์ค๋ฅ ๋ฉ์์ง: {e}")
|
| 224 |
+
print("matplotlib, seaborn, ๋๋ ํ๊ธ ํฐํธ ์ค์ ์ ํ์ธํ์ธ์.")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
if __name__ == "__main__":
|
| 228 |
+
run_evaluation()
|
scripts/migrate_recommendations.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
# --- ํ๋ก์ ํธ ๋ฃจํธ ๊ฒฝ๋ก ์ค์ ---
|
| 6 |
+
# ์ด ์คํฌ๋ฆฝํธ๊ฐ 'scripts' ํด๋ ์์ ์์ผ๋ฏ๋ก, ๋ถ๋ชจ ๋๋ ํ ๋ฆฌ(ํ๋ก์ ํธ ๋ฃจํธ)๋ฅผ ๊ฒฝ๋ก์ ์ถ๊ฐํฉ๋๋ค.
|
| 7 |
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 8 |
+
if project_root not in sys.path:
|
| 9 |
+
sys.path.insert(0, project_root)
|
| 10 |
+
|
| 11 |
+
from src import create_app, db
|
| 12 |
+
from src.models import Diary
|
| 13 |
+
from src.main import generate_recommendation, recommender # generate_recommendation ์ฌ์ฉ
|
| 14 |
+
import datetime
|
| 15 |
+
|
| 16 |
+
# --- ๋ก๊น
์ค์ ---
|
| 17 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 18 |
+
|
| 19 |
+
def migrate_diaries_with_recommendations():
|
| 20 |
+
"""
|
| 21 |
+
๊ธฐ์กด์ ๋ชจ๋ ์ผ๊ธฐ๋ฅผ ์ํํ๋ฉฐ 'recommendation' ํ๋๊ฐ ๋น์ด์๋ ๊ฒฝ์ฐ,
|
| 22 |
+
์๋ก์ด ์ถ์ฒ์ ์์ฑํ์ฌ ์ฑ์๋ฃ์ต๋๋ค.
|
| 23 |
+
"""
|
| 24 |
+
app = create_app()
|
| 25 |
+
with app.app_context():
|
| 26 |
+
logging.info("๋ฐ์ดํฐ ๋ง์ด๊ทธ๋ ์ด์
์ ์์ํฉ๋๋ค...")
|
| 27 |
+
|
| 28 |
+
# ์ถ์ฒ์ด ๋น์ด์๋ ๋ชจ๋ ์ผ๊ธฐ๋ฅผ ์กฐํํฉ๋๋ค.
|
| 29 |
+
diaries_to_update = Diary.query.filter((Diary.recommendation == None) | (Diary.recommendation == '')).all()
|
| 30 |
+
|
| 31 |
+
if not diaries_to_update:
|
| 32 |
+
logging.info("์
๋ฐ์ดํธํ ์ผ๊ธฐ๊ฐ ์์ต๋๋ค. ๋ชจ๋ ์ผ๊ธฐ์ ์ถ์ฒ์ด ์ด๋ฏธ ์กด์ฌํฉ๋๋ค.")
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
logging.info(f"์ด {len(diaries_to_update)}๊ฐ์ ์ผ๊ธฐ๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.")
|
| 36 |
+
|
| 37 |
+
updated_count = 0
|
| 38 |
+
failed_count = 0
|
| 39 |
+
|
| 40 |
+
for diary in diaries_to_update:
|
| 41 |
+
try:
|
| 42 |
+
logging.info(f"ID: {diary.id} ์ผ๊ธฐ ์ฒ๋ฆฌ ์ค...")
|
| 43 |
+
|
| 44 |
+
# 1. Gemini API๋ฅผ ํตํด ์ถ์ฒ ์์ฑ ์๋
|
| 45 |
+
recommendation_text = generate_recommendation(diary.content, diary.emotion)
|
| 46 |
+
|
| 47 |
+
# 2. ์คํจ ์, Recommender ํด๋์ค๋ก ๋์ฒด
|
| 48 |
+
if recommendation_text is None:
|
| 49 |
+
logging.warning(f"ID: {diary.id} - Gemini ์ถ์ฒ ์คํจ. Recommender ํด๋์ค๋ก ๋์ฒดํฉ๋๋ค.")
|
| 50 |
+
su_yoong_recs = recommender.recommend(diary.emotion, '์์ฉ')
|
| 51 |
+
jeon_hwan_recs = recommender.recommend(diary.emotion, '์ ํ')
|
| 52 |
+
|
| 53 |
+
# diary_logic.js๊ฐ ํ์ฑํ ์ ์๋ ํ์์ผ๋ก ๋ง๋ญ๋๋ค.
|
| 54 |
+
recommendation_text = f"## [์์ฉ]\n"
|
| 55 |
+
for rec in su_yoong_recs:
|
| 56 |
+
recommendation_text += f"* {rec}\n"
|
| 57 |
+
|
| 58 |
+
recommendation_text += f"\n## [์ ํ]\n"
|
| 59 |
+
for rec in jeon_hwan_recs:
|
| 60 |
+
recommendation_text += f"* {rec}\n"
|
| 61 |
+
|
| 62 |
+
# 3. ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์
|
| 63 |
+
diary.recommendation = recommendation_text
|
| 64 |
+
updated_count += 1
|
| 65 |
+
logging.info(f"ID: {diary.id} - ์ถ์ฒ ์์ฑ ์๋ฃ.")
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
failed_count += 1
|
| 69 |
+
logging.error(f"ID: {diary.id} ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}")
|
| 70 |
+
|
| 71 |
+
if updated_count > 0:
|
| 72 |
+
try:
|
| 73 |
+
db.session.commit()
|
| 74 |
+
logging.info(f"์ฑ๊ณต: {updated_count}๊ฐ ์ผ๊ธฐ์ ์ถ์ฒ ์ ๋ณด ์
๋ฐ์ดํธ ์๋ฃ.")
|
| 75 |
+
except Exception as e:
|
| 76 |
+
db.session.rollback()
|
| 77 |
+
logging.error(f"๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฐ ์ค ์ค๋ฅ ๋ฐ์: {e}")
|
| 78 |
+
|
| 79 |
+
if failed_count > 0:
|
| 80 |
+
logging.warning(f"์คํจ: {failed_count}๊ฐ ์ผ๊ธฐ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์.")
|
| 81 |
+
|
| 82 |
+
logging.info("๋ง์ด๊ทธ๋ ์ด์
์๋ฃ.")
|
| 83 |
+
|
| 84 |
+
if __name__ == '__main__':
|
| 85 |
+
migrate_diaries_with_recommendations()
|
scripts/newtrain.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#newtrain.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import json
|
| 6 |
+
import re
|
| 7 |
+
import torch
|
| 8 |
+
import numpy as np
|
| 9 |
+
from transformers import (
|
| 10 |
+
AutoTokenizer,
|
| 11 |
+
AutoModelForSequenceClassification,
|
| 12 |
+
Trainer,
|
| 13 |
+
TrainingArguments
|
| 14 |
+
)
|
| 15 |
+
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
|
| 16 |
+
from sklearn.model_selection import train_test_split # ๋ฐ์ดํฐ ๋ถ๋ฆฌ
|
| 17 |
+
from sklearn.utils import class_weight
|
| 18 |
+
from torch.nn import CrossEntropyLoss
|
| 19 |
+
from typing import Dict, List, Tuple
|
| 20 |
+
from dataclasses import dataclass
|
| 21 |
+
import platform
|
| 22 |
+
import matplotlib.pyplot as plt
|
| 23 |
+
import seaborn as sns
|
| 24 |
+
|
| 25 |
+
# --- Matplotlib ํ๊ธ ํฐํธ ์ค์ (๋ก์ปฌ PC์ฉ) ---
|
| 26 |
+
try:
|
| 27 |
+
if platform.system() == 'Windows':
|
| 28 |
+
plt.rc('font', family='Malgun Gothic')
|
| 29 |
+
elif platform.system() == 'Darwin': # Mac OS
|
| 30 |
+
plt.rc('font', family='AppleGothic')
|
| 31 |
+
else: # Linux (์ฝ๋ฉ ๋ฑ)
|
| 32 |
+
plt.rc('font', family='NanumBarunGothic')
|
| 33 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 34 |
+
except:
|
| 35 |
+
print("ํ๊ธ ํฐํธ ์ค์ ์ ์คํจํ์ต๋๋ค. ํผ๋ ํ๋ ฌ์ ๋ผ๋ฒจ์ด ๊นจ์ง ์ ์์ต๋๋ค.")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# --- 1. ์ค์ ๋ถ ---
|
| 39 |
+
@dataclass
|
| 40 |
+
class TrainingConfig:
|
| 41 |
+
mode: str = "emotion"
|
| 42 |
+
data_dir: str = "./data"
|
| 43 |
+
output_dir: str = "./results"
|
| 44 |
+
base_model_name: str = "klue/roberta-base"
|
| 45 |
+
eval_batch_size: int = 64
|
| 46 |
+
num_train_epochs: int = 10
|
| 47 |
+
learning_rate: float = 1e-5
|
| 48 |
+
train_batch_size: int = 16
|
| 49 |
+
weight_decay: float = 0.01
|
| 50 |
+
max_length: int = 128
|
| 51 |
+
warmup_ratio: float = 0.1
|
| 52 |
+
|
| 53 |
+
def get_model_name(self) -> str:
|
| 54 |
+
return self.base_model_name
|
| 55 |
+
|
| 56 |
+
def get_output_dir(self) -> str:
|
| 57 |
+
# v2 ๋ชจ๋ธ ์ ์ฅ ๊ฒฝ๋ก
|
| 58 |
+
return os.path.join(self.output_dir, 'emotion_model_v2_manual')
|
| 59 |
+
|
| 60 |
+
# --- 2. ์ปค์คํ
ํด๋์ค ๋ฐ ํจ์ ---
|
| 61 |
+
class EmotionDataset(torch.utils.data.Dataset):
|
| 62 |
+
def __init__(self, encodings, labels):
|
| 63 |
+
self.encodings = encodings
|
| 64 |
+
self.labels = labels
|
| 65 |
+
def __getitem__(self, idx):
|
| 66 |
+
item = {key: val[idx].clone().detach() for key, val in self.encodings.items()}
|
| 67 |
+
item['labels'] = torch.tensor(self.labels[idx])
|
| 68 |
+
return item
|
| 69 |
+
def __len__(self):
|
| 70 |
+
return len(self.labels)
|
| 71 |
+
|
| 72 |
+
class CustomTrainer(Trainer):
|
| 73 |
+
def __init__(self, *args, class_weights=None, **kwargs):
|
| 74 |
+
super().__init__(*args, **kwargs)
|
| 75 |
+
if class_weights is not None:
|
| 76 |
+
self.loss_fct = CrossEntropyLoss(weight=class_weights)
|
| 77 |
+
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
| 78 |
+
labels = inputs.pop("labels")
|
| 79 |
+
outputs = model(**inputs)
|
| 80 |
+
logits = outputs.get("logits")
|
| 81 |
+
loss = self.loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1))
|
| 82 |
+
return (loss, outputs) if return_outputs else loss
|
| 83 |
+
|
| 84 |
+
def compute_metrics(pred):
|
| 85 |
+
labels = pred.label_ids
|
| 86 |
+
preds = pred.predictions.argmax(-1)
|
| 87 |
+
acc = accuracy_score(labels, preds)
|
| 88 |
+
precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='weighted', zero_division=0)
|
| 89 |
+
return {'accuracy': acc, 'f1': f1}
|
| 90 |
+
|
| 91 |
+
def clean_text(text: str) -> str:
|
| 92 |
+
return re.sub(r'[^๊ฐ-ํฃa-zA-Z0-9 ]', '', str(text))
|
| 93 |
+
|
| 94 |
+
# --- 3. ๋ฐ์ดํฐ ๋ก๋ ([๋ณ๊ฒฝ] Train/Val/Test ๋ถ๋ฆฌ) ---
|
| 95 |
+
def get_data(config: TrainingConfig) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
| 96 |
+
if config.mode == 'nsmc':
|
| 97 |
+
raise ValueError("์ด ์คํฌ๋ฆฝํธ๋ 'emotion' ๋ชจ๋ ์ ์ฉ์
๋๋ค.")
|
| 98 |
+
|
| 99 |
+
elif config.mode == 'emotion':
|
| 100 |
+
print("--- ๊ฐ์ ๋ฐ์ดํฐ ๋ก๋ฉ (Train/Val/Test ๋ถ๋ฆฌ) ---")
|
| 101 |
+
|
| 102 |
+
def load_and_map_labels(file_name):
|
| 103 |
+
def map_ecode_to_major_emotion(ecode):
|
| 104 |
+
try: code_num = int(ecode[1:])
|
| 105 |
+
except: return None
|
| 106 |
+
|
| 107 |
+
if 10 <= code_num <= 19: return '๋ถ๋
ธ'
|
| 108 |
+
elif 20 <= code_num <= 29: return '์ฌํ'
|
| 109 |
+
elif 30 <= code_num <= 39: return '๋ถ์'
|
| 110 |
+
elif 40 <= code_num <= 49: return '์์ฒ'
|
| 111 |
+
elif 50 <= code_num <= 59: return '๋นํฉ'
|
| 112 |
+
elif 60 <= code_num <= 69: return '๊ธฐ์จ'
|
| 113 |
+
else: return None
|
| 114 |
+
|
| 115 |
+
with open(os.path.join(config.data_dir, file_name), 'r', encoding='utf-8') as f:
|
| 116 |
+
raw = json.load(f)
|
| 117 |
+
data = [{'text': " ".join(d['talk']['content'].values()), 'emotion': d['profile']['emotion']['type']} for d in raw]
|
| 118 |
+
df = pd.DataFrame(data)
|
| 119 |
+
df['major_emotion'] = df['emotion'].apply(map_ecode_to_major_emotion)
|
| 120 |
+
df.dropna(subset=['major_emotion'], inplace=True)
|
| 121 |
+
df['cleaned_text'] = df['text'].apply(clean_text)
|
| 122 |
+
return df
|
| 123 |
+
|
| 124 |
+
# 1. Test Set ๋ก๋ (๊ธฐ์กด validation-label.json ์ฌ์ฉ)
|
| 125 |
+
df_test = load_and_map_labels("test.json")
|
| 126 |
+
|
| 127 |
+
# 2. Train Set ๋ก๋ (๊ธฐ์กด training-label.json ์ฌ์ฉ)
|
| 128 |
+
df_train_full = load_and_map_labels("training-label.json")
|
| 129 |
+
|
| 130 |
+
# 3. Train Set์ 9:1๋ก ๋ถ๋ฆฌ (์ ๊ท Train / ์ ๊ท Validation)
|
| 131 |
+
label_column_str = 'major_emotion'
|
| 132 |
+
|
| 133 |
+
df_train, df_val = train_test_split(
|
| 134 |
+
df_train_full,
|
| 135 |
+
test_size=0.1, # 10%๋ฅผ Validation์ผ๋ก ์ฌ์ฉ
|
| 136 |
+
random_state=42, # ๊ฒฐ๊ณผ ์ฌํ์ ์ํด ๊ณ ์
|
| 137 |
+
stratify=df_train_full[label_column_str] # ํด๋์ค ๋น์จ์ ์ ์งํ๋ฉฐ ๋ถ๋ฆฌ
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
print(f" ์ด ์๋ณธ ํ๋ จ ๋ฐ์ดํฐ: {len(df_train_full)}๊ฐ")
|
| 141 |
+
print(f" [์ ๊ท] ํ๋ จ(Train)์ฉ: {len(df_train)}๊ฐ (90%)")
|
| 142 |
+
print(f" [์ ๊ท] ๊ฒ์ฆ(Validation)์ฉ: {len(df_val)}๊ฐ (10%)")
|
| 143 |
+
print(f" [์ต์ข
] ํ
์คํธ(Test)์ฉ: {len(df_test)}๊ฐ ")
|
| 144 |
+
|
| 145 |
+
return df_train, df_val, df_test
|
| 146 |
+
else:
|
| 147 |
+
raise ValueError(f"์ง์ํ์ง ์๋ ๋ชจ๋์
๋๋ค: {config.mode}")
|
| 148 |
+
|
| 149 |
+
# --- 4. ๋ฉ์ธ ์คํ ํจ์ ---
|
| 150 |
+
def run_training():
|
| 151 |
+
config = TrainingConfig()
|
| 152 |
+
df_train, df_val, df_test = get_data(config)
|
| 153 |
+
|
| 154 |
+
text_column = 'cleaned_text'
|
| 155 |
+
label_column_str = 'major_emotion'
|
| 156 |
+
|
| 157 |
+
# 2. ํ ํฌ๋์ด์ ๋ฐ ๋ผ๋ฒจ ์ธ์ฝ๋ฉ
|
| 158 |
+
tokenizer = AutoTokenizer.from_pretrained(config.get_model_name())
|
| 159 |
+
unique_labels = sorted(df_train[label_column_str].unique())
|
| 160 |
+
label_to_id = {label: i for i, label in enumerate(unique_labels)}
|
| 161 |
+
id_to_label = {i: label for label, i in label_to_id.items()}
|
| 162 |
+
|
| 163 |
+
print("\n--- ์์ฑ๋ ๋ผ๋ฒจ ์์ (0~5) ---")
|
| 164 |
+
print(unique_labels) # ['๊ธฐ์จ', '๋นํฉ', '๋ถ๋
ธ', '๋ถ์', '์์ฒ', '์ฌํ']
|
| 165 |
+
print("------------------------------")
|
| 166 |
+
|
| 167 |
+
df_train['label'] = df_train[label_column_str].map(label_to_id)
|
| 168 |
+
df_val['label'] = df_val[label_column_str].map(label_to_id)
|
| 169 |
+
df_test['label'] = df_test[label_column_str].map(label_to_id)
|
| 170 |
+
|
| 171 |
+
# 3. ๋ฐ์ดํฐ์
์์ฑ ๋ฐ ํด๋์ค ๊ฐ์ค์น ๊ณ์ฐ
|
| 172 |
+
train_encodings = tokenizer(list(df_train[text_column]), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 173 |
+
val_encodings = tokenizer(list(df_val[text_column]), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 174 |
+
|
| 175 |
+
train_dataset = EmotionDataset(train_encodings, df_train['label'].tolist())
|
| 176 |
+
val_dataset = EmotionDataset(val_encodings, df_val['label'].tolist())
|
| 177 |
+
|
| 178 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 179 |
+
print(f"\nUsing device: {device}")
|
| 180 |
+
# ํด๋์ค ๊ฐ์ค์น ๊ณ์ฐ
|
| 181 |
+
manual_weights_list = [6.00, 4.50, 0.85, 1.80, 1.80, 0.92]
|
| 182 |
+
class_weights = torch.tensor(manual_weights_list, dtype=torch.float).to(device)
|
| 183 |
+
|
| 184 |
+
print(f"--- ์๋ ์ ์ฉ๋ ํด๋์ค ๊ฐ์ค์น ---")
|
| 185 |
+
print(f"{class_weights.tolist()}")
|
| 186 |
+
print(f"---------------------------------")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# 4. ๋ชจ๋ธ ๋ก๋ฉ
|
| 190 |
+
model = AutoModelForSequenceClassification.from_pretrained(
|
| 191 |
+
config.get_model_name(),
|
| 192 |
+
num_labels=len(unique_labels),
|
| 193 |
+
id2label=id_to_label,
|
| 194 |
+
label2id=label_to_id,
|
| 195 |
+
ignore_mismatched_sizes=True
|
| 196 |
+
).to(device)
|
| 197 |
+
|
| 198 |
+
# 5. ํ๋ จ ์คํ
|
| 199 |
+
training_args = TrainingArguments(
|
| 200 |
+
output_dir=config.get_output_dir(),
|
| 201 |
+
num_train_epochs=config.num_train_epochs,
|
| 202 |
+
per_device_train_batch_size=config.train_batch_size,
|
| 203 |
+
per_device_eval_batch_size=config.eval_batch_size,
|
| 204 |
+
learning_rate=config.learning_rate,
|
| 205 |
+
weight_decay=config.weight_decay,
|
| 206 |
+
warmup_ratio=config.warmup_ratio,
|
| 207 |
+
eval_strategy="epoch",
|
| 208 |
+
save_strategy="epoch",
|
| 209 |
+
load_best_model_at_end=True,
|
| 210 |
+
metric_for_best_model="accuracy",
|
| 211 |
+
lr_scheduler_type="cosine",
|
| 212 |
+
report_to="none"
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
trainer = CustomTrainer(
|
| 216 |
+
model=model,
|
| 217 |
+
args=training_args,
|
| 218 |
+
train_dataset=train_dataset,
|
| 219 |
+
eval_dataset=val_dataset,
|
| 220 |
+
compute_metrics=compute_metrics,
|
| 221 |
+
class_weights=class_weights
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
print(f"\n '[์ ๊ท ๋ถ๋ฆฌ ๋ฐ์ดํฐ]'๋ก ๋ชจ๋ธ ํ๋ จ์ ์์ํฉ๋๋ค...")
|
| 225 |
+
trainer.train()
|
| 226 |
+
print("\n ๋ชจ๋ธ ํ๋ จ ์๋ฃ!")
|
| 227 |
+
|
| 228 |
+
output_dir = config.get_output_dir()
|
| 229 |
+
trainer.save_model(output_dir)
|
| 230 |
+
tokenizer.save_pretrained(output_dir)
|
| 231 |
+
print(f"์ต์ข
๋ชจ๋ธ๊ณผ ํ ํฌ๋์ด์ ๊ฐ {output_dir} ๊ฒฝ๋ก์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 232 |
+
|
| 233 |
+
# ํ๋ จ ์ค ์ฌ์ฉํ ๊ฒ์ฆ ๋ฐ์ดํฐ(10%)์ ๋ํ ํ๊ฐ ๊ฒฐ๊ณผ
|
| 234 |
+
print("\n--- ์ ๊ท Validation Set(10%) ํ๊ฐ ๊ฒฐ๊ณผ (์ฐธ๊ณ ์ฉ) ---")
|
| 235 |
+
results = trainer.evaluate() # ๊ธฐ๋ณธ๊ฐ (eval_dataset)
|
| 236 |
+
print(results)
|
| 237 |
+
|
| 238 |
+
# --- ์ต์ข
Test Set์ผ๋ก '์ง์ง ์ฑ๋ฅ' ํ๊ฐ ---
|
| 239 |
+
print("\n" + "="*50)
|
| 240 |
+
print("--- ์ต์ข
Test Set์ผ๋ก '์ง์ง ์ฑ๋ฅ' ํ๊ฐ ์์ ---")
|
| 241 |
+
print("="*50)
|
| 242 |
+
|
| 243 |
+
# Test Set์ ์ํ ๋ฐ์ดํฐ์
์์ฑ
|
| 244 |
+
test_encodings = tokenizer(list(df_test[text_column]), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 245 |
+
test_dataset = EmotionDataset(test_encodings, df_test['label'].tolist())
|
| 246 |
+
|
| 247 |
+
# trainer.predict()๋ฅผ ์ฌ์ฉํ์ฌ Test Set์ ๋ํ ์์ธก ์ํ
|
| 248 |
+
test_predictions = trainer.predict(test_dataset)
|
| 249 |
+
|
| 250 |
+
# compute_metrics ํจ์๋ฅผ ๏ฟฝ๏ฟฝ๏ฟฝ์ฌ์ฉํ์ฌ '์ง์ง ์ฑ๋ฅ' ๊ณ์ฐ
|
| 251 |
+
final_metrics = compute_metrics(test_predictions)
|
| 252 |
+
|
| 253 |
+
print(f"*** ์ต์ข
Test Set '์ง์ง' ์ฑ๋ฅ ๊ฒฐ๊ณผ ***")
|
| 254 |
+
print(f" - ์ต์ข
Accuracy: {final_metrics['accuracy']:.4f}")
|
| 255 |
+
print(f" - ์ต์ข
F1-Score (Weighted): {final_metrics['f1']:.4f}")
|
| 256 |
+
print("="*50)
|
| 257 |
+
|
| 258 |
+
results_path = os.path.join(output_dir, "final_test_results.json")
|
| 259 |
+
with open(results_path, "w", encoding='utf-8') as f:
|
| 260 |
+
json.dump(final_metrics, f, indent=4, ensure_ascii=False)
|
| 261 |
+
print(f"์ต์ข
ํ
์คํธ ๊ฒฐ๊ณผ๊ฐ {results_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 262 |
+
|
| 263 |
+
# --- Test Set ๊ธฐ์ค ํผ๋ ํ๋ ฌ ์์ฑ ---
|
| 264 |
+
print("\n--- Test Set ๊ธฐ์ค ํผ๋ ํ๋ ฌ ์์ฑ ---")
|
| 265 |
+
y_pred = test_predictions.predictions.argmax(-1)
|
| 266 |
+
y_true = test_predictions.label_ids
|
| 267 |
+
|
| 268 |
+
labels = [id_to_label[i] for i in sorted(id_to_label.keys())]
|
| 269 |
+
cm = confusion_matrix(y_true, y_pred, labels=[label_to_id[l] for l in labels])
|
| 270 |
+
|
| 271 |
+
plt.figure(figsize=(10, 8))
|
| 272 |
+
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels)
|
| 273 |
+
plt.xlabel('์์ธก ๋ผ๋ฒจ (Predicted Label)')
|
| 274 |
+
plt.ylabel('์ค์ ๋ผ๋ฒจ (True Label)')
|
| 275 |
+
plt.title('Test Set Confusion Matrix')
|
| 276 |
+
|
| 277 |
+
cm_path = os.path.join(output_dir, "final_test_confusion_matrix.png")
|
| 278 |
+
plt.savefig(cm_path)
|
| 279 |
+
print(f"์ต์ข
ํผ๋ ํ๋ ฌ์ด {cm_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 280 |
+
|
| 281 |
+
if __name__ == "__main__":
|
| 282 |
+
run_training()
|
scripts/train_final.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ํ์ผ ์ด๋ฆ: train_final.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import json
|
| 6 |
+
import re
|
| 7 |
+
import torch
|
| 8 |
+
import numpy as np
|
| 9 |
+
from transformers import (
|
| 10 |
+
AutoTokenizer,
|
| 11 |
+
AutoModelForSequenceClassification,
|
| 12 |
+
Trainer,
|
| 13 |
+
TrainingArguments
|
| 14 |
+
)
|
| 15 |
+
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
|
| 16 |
+
from sklearn.model_selection import train_test_split
|
| 17 |
+
from sklearn.utils import resample
|
| 18 |
+
from typing import Dict, List, Tuple
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
import platform
|
| 21 |
+
import matplotlib.pyplot as plt
|
| 22 |
+
import seaborn as sns
|
| 23 |
+
|
| 24 |
+
# --- Matplotlib ํ๊ธ ํฐํธ ์ค์ ---
|
| 25 |
+
try:
|
| 26 |
+
if platform.system() == 'Windows':
|
| 27 |
+
plt.rc('font', family='Malgun Gothic')
|
| 28 |
+
elif platform.system() == 'Darwin': # Mac OS
|
| 29 |
+
plt.rc('font', family='AppleGothic')
|
| 30 |
+
else: # Linux (์ฝ๋ฉ ๋ฑ)
|
| 31 |
+
plt.rc('font', family='NanumBarunGothic')
|
| 32 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"ํ๊ธ ํฐํธ ์ค์ ๊ฒฝ๊ณ : {e}. ํผ๋ ํ๋ ฌ์ ๋ผ๋ฒจ์ด ๊นจ์ง ์ ์์ต๋๋ค.")
|
| 35 |
+
|
| 36 |
+
# --- 1. ์ค์ ๋ถ ---
|
| 37 |
+
@dataclass
|
| 38 |
+
class TrainingConfig:
|
| 39 |
+
mode: str = "emotion"
|
| 40 |
+
data_dir: str = "./data"
|
| 41 |
+
output_dir: str = "./results1024"
|
| 42 |
+
base_model_name: str = "klue/roberta-base"
|
| 43 |
+
eval_batch_size: int = 64
|
| 44 |
+
num_train_epochs: int = 3
|
| 45 |
+
learning_rate: float = 2e-5
|
| 46 |
+
train_batch_size: int = 16
|
| 47 |
+
weight_decay: float = 0.01
|
| 48 |
+
max_length: int = 128
|
| 49 |
+
warmup_ratio: float = 0.1
|
| 50 |
+
|
| 51 |
+
def get_model_name(self) -> str:
|
| 52 |
+
if self.mode == 'emotion':
|
| 53 |
+
if not os.path.exists(self.base_model_name):
|
| 54 |
+
print(f"๊ฒฝ๊ณ : 1์ฐจ ํ์ต๋ NSMC ๋ชจ๋ธ์ ์ฐพ์ ์ ์์ต๋๋ค ({self.base_model_name})")
|
| 55 |
+
print("๊ธฐ๋ณธ 'klue/roberta-base' ๋ชจ๋ธ๋ก ๋์ ํ์ต์ ์๋ํฉ๋๋ค.")
|
| 56 |
+
return "klue/roberta-base"
|
| 57 |
+
print(f"1์ฐจ ํ์ต๋ ๋ชจ๋ธ ๋ก๋: {self.base_model_name}")
|
| 58 |
+
return self.base_model_name
|
| 59 |
+
return "klue/roberta-base"
|
| 60 |
+
|
| 61 |
+
def get_output_dir(self) -> str:
|
| 62 |
+
# ๋ชจ๋ธ ์ ์ฅ ๊ฒฝ๋ก ์์
|
| 63 |
+
return os.path.join(self.output_dir, 'emotion_model_6class_oversampled')
|
| 64 |
+
|
| 65 |
+
# --- 2. ์ปค์คํ
ํด๋์ค ๋ฐ ํจ์ ---
|
| 66 |
+
class EmotionDataset(torch.utils.data.Dataset):
|
| 67 |
+
def __init__(self, encodings, labels):
|
| 68 |
+
self.encodings = encodings
|
| 69 |
+
self.labels = labels
|
| 70 |
+
def __getitem__(self, idx):
|
| 71 |
+
item = {key: val[idx].clone().detach() for key, val in self.encodings.items()}
|
| 72 |
+
item['labels'] = torch.tensor(self.labels[idx])
|
| 73 |
+
return item
|
| 74 |
+
def __len__(self):
|
| 75 |
+
return len(self.labels)
|
| 76 |
+
|
| 77 |
+
def compute_metrics(pred):
|
| 78 |
+
labels = pred.label_ids
|
| 79 |
+
preds = pred.predictions.argmax(-1)
|
| 80 |
+
acc = accuracy_score(labels, preds)
|
| 81 |
+
precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='weighted', zero_division=0)
|
| 82 |
+
return {'accuracy': acc, 'f1': f1}
|
| 83 |
+
|
| 84 |
+
def clean_text(text: str) -> str:
|
| 85 |
+
return re.sub(r'[^๊ฐ-ํฃa-zA-Z0-9 ]', '', str(text))
|
| 86 |
+
|
| 87 |
+
# --- 3. ๋ฐ์ดํฐ ๋ก๋ ---
|
| 88 |
+
|
| 89 |
+
def map_ecode_to_6class(e_code_str):
|
| 90 |
+
"""E์ฝ๋("E18")๋ฅผ 6-Class("๋ถ๋
ธ")๋ก ๋งคํํ๋ ํจ์"""
|
| 91 |
+
if not isinstance(e_code_str, str) or not e_code_str.startswith('E'): return None
|
| 92 |
+
try: code_num = int(e_code_str[1:])
|
| 93 |
+
except (ValueError, TypeError): return None
|
| 94 |
+
|
| 95 |
+
if 10 <= code_num <= 19: return '๋ถ๋
ธ'
|
| 96 |
+
elif 20 <= code_num <= 29: return '์ฌํ'
|
| 97 |
+
elif 30 <= code_num <= 39: return '๋ถ์'
|
| 98 |
+
elif 40 <= code_num <= 49: return '์์ฒ'
|
| 99 |
+
elif 50 <= code_num <= 59: return '๋นํฉ'
|
| 100 |
+
elif 60 <= code_num <= 69: return '๊ธฐ์จ'
|
| 101 |
+
else: return None
|
| 102 |
+
|
| 103 |
+
def load_and_process(text_file, label_file, data_dir):
|
| 104 |
+
"""Excel(ํ
์คํธ)๊ณผ JSON(๋ผ๋ฒจ)์ ๋ณํฉํ๊ณ ์ ์ฒ๋ฆฌํ๋ ํฌํผ ํจ์"""
|
| 105 |
+
text_path = os.path.join(data_dir, text_file)
|
| 106 |
+
label_path = os.path.join(data_dir, label_file)
|
| 107 |
+
try:
|
| 108 |
+
df_text = pd.read_excel(text_path, header=None)
|
| 109 |
+
with open(label_path, 'r', encoding='utf-8') as f:
|
| 110 |
+
labels_raw = json.load(f)
|
| 111 |
+
except FileNotFoundError as e:
|
| 112 |
+
print(f"์ค๋ฅ: ํ์ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {e}")
|
| 113 |
+
return pd.DataFrame()
|
| 114 |
+
|
| 115 |
+
e_codes = []
|
| 116 |
+
for dialogue in labels_raw:
|
| 117 |
+
try: e_codes.append(dialogue['profile']['emotion']['type']) # "E18"
|
| 118 |
+
except KeyError: e_codes.append(None)
|
| 119 |
+
|
| 120 |
+
if len(df_text) != len(e_codes):
|
| 121 |
+
min_len = min(len(df_text), len(e_codes))
|
| 122 |
+
print(f"๊ฒฝ๊ณ : {text_file}๊ณผ {label_file} ์ค ์ ๋ถ์ผ์น. {min_len}๊ฐ๋ก ์ถ์ํฉ๋๋ค.")
|
| 123 |
+
df_text = df_text.iloc[:min_len]
|
| 124 |
+
e_codes = e_codes[:min_len]
|
| 125 |
+
|
| 126 |
+
df_labels = pd.DataFrame({'e_code': e_codes})
|
| 127 |
+
df_combined = pd.concat([df_text, df_labels], axis=1)
|
| 128 |
+
|
| 129 |
+
dialogue_cols = [8, 9, 10, 11]
|
| 130 |
+
for col in dialogue_cols:
|
| 131 |
+
df_combined[col] = df_combined[col].astype(str).fillna('')
|
| 132 |
+
df_combined['text'] = df_combined[dialogue_cols].apply(lambda row: ' '.join(row), axis=1)
|
| 133 |
+
|
| 134 |
+
df_combined['cleaned_text'] = df_combined['text'].apply(clean_text)
|
| 135 |
+
|
| 136 |
+
df_combined['major_emotion'] = df_combined['e_code'].apply(map_ecode_to_6class)
|
| 137 |
+
|
| 138 |
+
df_combined.dropna(subset=['major_emotion', 'cleaned_text'], inplace=True)
|
| 139 |
+
df_combined = df_combined[df_combined['cleaned_text'].str.strip() != '']
|
| 140 |
+
|
| 141 |
+
return df_combined
|
| 142 |
+
|
| 143 |
+
def get_data(config: TrainingConfig) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
| 144 |
+
"""
|
| 145 |
+
Train/Val/Test 3-Set์ ๋ก๋ํ๊ณ , Train Set์ Oversampling์ ์ ์ฉ
|
| 146 |
+
"""
|
| 147 |
+
if config.mode != 'emotion':
|
| 148 |
+
print("์ด ์คํฌ๋ฆฝํธ๋ 'emotion' ๋ชจ๋ ์ ์ฉ์
๋๋ค.")
|
| 149 |
+
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 150 |
+
|
| 151 |
+
print("--- ๊ฐ์ ๋ฐ์ดํฐ ๋ก๋ฉ (Train/Validation/Test) ---")
|
| 152 |
+
|
| 153 |
+
df_full_train = load_and_process("training-origin.xlsx", "training-label.json", config.data_dir)
|
| 154 |
+
df_test = load_and_process("validation-origin.xlsx", "test.json", config.data_dir)
|
| 155 |
+
|
| 156 |
+
if df_full_train.empty or df_test.empty:
|
| 157 |
+
print("์ค๋ฅ: ๋ฐ์ดํฐ ๋ก๋ฉ ์คํจ.")
|
| 158 |
+
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 159 |
+
|
| 160 |
+
print(f"Full Train data loaded: {len(df_full_train)} rows")
|
| 161 |
+
print(f"Test data loaded: {len(df_test)} rows")
|
| 162 |
+
|
| 163 |
+
print("Splitting Full Train into New Train (90%) and New Validation (10%)...")
|
| 164 |
+
df_train, df_val = train_test_split(
|
| 165 |
+
df_full_train,
|
| 166 |
+
test_size=0.1,
|
| 167 |
+
random_state=42,
|
| 168 |
+
stratify=df_full_train['major_emotion']
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
print("\n--- [Oversampling] New Train 6-Class (์๋ณธ ๋ถํฌ) ---")
|
| 172 |
+
print(df_train['major_emotion'].value_counts())
|
| 173 |
+
|
| 174 |
+
# --- [์ค๋ฒ์ํ๋ง ์์] ---
|
| 175 |
+
print("\n--- '๊ธฐ์จ' ํด๋์ค ์ค๋ฒ์ํ๋ง ์ ์ฉ ์ค ---")
|
| 176 |
+
|
| 177 |
+
# 1. '๊ธฐ์จ'๊ณผ ๋๋จธ์ง ๋ถ๋ฆฌ
|
| 178 |
+
df_train_joy = df_train[df_train['major_emotion'] == '๊ธฐ์จ']
|
| 179 |
+
df_train_others = df_train[df_train['major_emotion'] != '๊ธฐ์จ']
|
| 180 |
+
|
| 181 |
+
# 2. '๊ธฐ์จ'์ ์ ์ธํ 5๊ฐ ํด๋์ค์ ํ๊ท ๊ฐ์ ๊ณ์ฐ
|
| 182 |
+
target_count = int(df_train_others['major_emotion'].value_counts().mean())
|
| 183 |
+
print(f" '๊ธฐ์จ' ์๋ณธ: {len(df_train_joy)}๊ฐ")
|
| 184 |
+
print(f" ๋ค๋ฅธ ํด๋์ค ํ๊ท (ํ๊ฒ): {target_count}๊ฐ")
|
| 185 |
+
|
| 186 |
+
# 3. '๊ธฐ์จ'์ ํ๊ฒ ๊ฐ์๋งํผ ๋ณต์ (with replacement)
|
| 187 |
+
df_joy_oversampled = resample(
|
| 188 |
+
df_train_joy,
|
| 189 |
+
replace=True,
|
| 190 |
+
n_samples=target_count, # ํ๊ฒ ๊ฐ์
|
| 191 |
+
random_state=42
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# 4. ๋๋จธ์ง ๋ฐ์ดํฐ์ ๋ณต์ ๋ '๊ธฐ์จ' ๋ฐ์ดํฐ๋ฅผ ๋ค์ ํฉ์นจ
|
| 195 |
+
df_train = pd.concat([df_train_others, df_joy_oversampled])
|
| 196 |
+
|
| 197 |
+
# 5. ๋ฐ์ดํฐ์
์ ๋ค์ ์์ด์ค
|
| 198 |
+
df_train = df_train.sample(frac=1, random_state=42).reset_index(drop=True)
|
| 199 |
+
|
| 200 |
+
print("\n--- [Oversampling] New Train 6-Class (์ต์ข
๋ถํฌ) ---")
|
| 201 |
+
print(df_train['major_emotion'].value_counts())
|
| 202 |
+
# --- [์ค๋ฒ์ํ๋ง ๋] ---
|
| 203 |
+
|
| 204 |
+
print(f"\nNew Train set (Oversampled) size: {len(df_train)}")
|
| 205 |
+
print(f"New Validation set (Original) size: {len(df_val)}")
|
| 206 |
+
|
| 207 |
+
return df_train, df_val, df_test
|
| 208 |
+
|
| 209 |
+
# --- 4. ๋ฉ์ธ ์คํ ํจ์ ---
|
| 210 |
+
def run_training():
|
| 211 |
+
config = TrainingConfig()
|
| 212 |
+
|
| 213 |
+
df_train, df_val, df_test = get_data(config)
|
| 214 |
+
|
| 215 |
+
if df_train.empty or df_val.empty or df_test.empty:
|
| 216 |
+
print("\n์ค๋ฅ: ๋ฐ์ดํฐ๊ฐ ๋น์ด์์ด ํ๋ จ์ ์ค๋จํฉ๋๋ค.")
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
text_column = 'cleaned_text'
|
| 220 |
+
label_column_str = 'major_emotion'
|
| 221 |
+
|
| 222 |
+
model_name_to_load = config.get_model_name()
|
| 223 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name_to_load)
|
| 224 |
+
|
| 225 |
+
unique_labels = sorted(df_train[label_column_str].unique())
|
| 226 |
+
label_to_id = {label: i for i, label in enumerate(unique_labels)}
|
| 227 |
+
id_to_label = {i: label for label, i in label_to_id.items()}
|
| 228 |
+
|
| 229 |
+
print(f"\n๋ผ๋ฒจ ์ธ์ฝ๋ฉ ๋งต (6-Class): {label_to_id}")
|
| 230 |
+
|
| 231 |
+
df_train['label'] = df_train[label_column_str].map(label_to_id)
|
| 232 |
+
df_val['label'] = df_val[label_column_str].map(label_to_id)
|
| 233 |
+
df_test['label'] = df_test[label_column_str].map(label_to_id)
|
| 234 |
+
|
| 235 |
+
print("๋ฐ์ดํฐ์
ํ ํฌ๋์ด์ง ์ค...")
|
| 236 |
+
train_encodings = tokenizer(list(df_train[text_column]), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 237 |
+
val_encodings = tokenizer(list(df_val[text_column]), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 238 |
+
test_encodings = tokenizer(list(df_test[text_column]), max_length=config.max_length, padding=True, truncation=True, return_tensors="pt")
|
| 239 |
+
|
| 240 |
+
train_dataset = EmotionDataset(train_encodings, df_train['label'].tolist())
|
| 241 |
+
val_dataset = EmotionDataset(val_encodings, df_val['label'].tolist())
|
| 242 |
+
test_dataset = EmotionDataset(test_encodings, df_test['label'].tolist())
|
| 243 |
+
|
| 244 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 245 |
+
print(f"\nUsing device: {device}")
|
| 246 |
+
|
| 247 |
+
print("๋ชจ๋ธ ๋ก๋ฉ ์ค...")
|
| 248 |
+
model = AutoModelForSequenceClassification.from_pretrained(
|
| 249 |
+
model_name_to_load,
|
| 250 |
+
num_labels=len(unique_labels),
|
| 251 |
+
id2label=id_to_label,
|
| 252 |
+
label2id=label_to_id,
|
| 253 |
+
ignore_mismatched_sizes=True
|
| 254 |
+
).to(device)
|
| 255 |
+
|
| 256 |
+
output_dir = config.get_output_dir()
|
| 257 |
+
training_args = TrainingArguments(
|
| 258 |
+
output_dir=output_dir,
|
| 259 |
+
num_train_epochs=config.num_train_epochs,
|
| 260 |
+
per_device_train_batch_size=config.train_batch_size,
|
| 261 |
+
per_device_eval_batch_size=config.eval_batch_size,
|
| 262 |
+
learning_rate=config.learning_rate,
|
| 263 |
+
weight_decay=config.weight_decay,
|
| 264 |
+
warmup_ratio=config.warmup_ratio,
|
| 265 |
+
eval_strategy="epoch",
|
| 266 |
+
save_strategy="epoch",
|
| 267 |
+
load_best_model_at_end=True,
|
| 268 |
+
metric_for_best_model="accuracy",
|
| 269 |
+
report_to="none"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
trainer = Trainer(
|
| 273 |
+
model=model,
|
| 274 |
+
args=training_args,
|
| 275 |
+
train_dataset=train_dataset,
|
| 276 |
+
eval_dataset=val_dataset,
|
| 277 |
+
compute_metrics=compute_metrics
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
print(f"\n 6-Class (Oversampled) ๋ชจ๋ธ ํ๋ จ์ ์์ํฉ๋๋ค...")
|
| 281 |
+
trainer.train()
|
| 282 |
+
print("\n ๋ชจ๋ธ ํ๋ จ ์๋ฃ!")
|
| 283 |
+
|
| 284 |
+
final_model_path = os.path.join(output_dir, "best_model")
|
| 285 |
+
trainer.save_model(final_model_path)
|
| 286 |
+
tokenizer.save_pretrained(final_model_path)
|
| 287 |
+
print(f"์ต์ข
๋ชจ๋ธ(Best)๊ณผ ํ ํฌ๋์ด์ ๊ฐ {final_model_path} ๊ฒฝ๋ก์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 288 |
+
|
| 289 |
+
print("\n--- (1) Validation Set ํ๊ฐ (๋ชจ์๊ณ ์ฌ) ---")
|
| 290 |
+
val_results = trainer.evaluate(eval_dataset=val_dataset)
|
| 291 |
+
print(f"์ต์ข
Validation ํ๊ฐ ๊ฒฐ๊ณผ: {val_results}")
|
| 292 |
+
|
| 293 |
+
results_path = os.path.join(output_dir, "validation_evaluation_results.json")
|
| 294 |
+
with open(results_path, "w", encoding='utf-8') as f:
|
| 295 |
+
json.dump(val_results, f, indent=4, ensure_ascii=False)
|
| 296 |
+
print(f"Validation ํ๊ฐ ๊ฒฐ๊ณผ๊ฐ {results_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 297 |
+
|
| 298 |
+
print("\n--- (2) Test Set ์ต์ข
ํ๊ฐ (์๋ฅ) ---")
|
| 299 |
+
test_predictions = trainer.predict(test_dataset)
|
| 300 |
+
test_metrics = test_predictions.metrics
|
| 301 |
+
print(f"์ต์ข
Test ํ๊ฐ ๊ฒฐ๊ณผ: {test_metrics}")
|
| 302 |
+
|
| 303 |
+
results_path = os.path.join(output_dir, "TEST_evaluation_results.json")
|
| 304 |
+
with open(results_path, "w", encoding='utf-8') as f:
|
| 305 |
+
json.dump(test_metrics, f, indent=4, ensure_ascii=False)
|
| 306 |
+
print(f"*** ์ต์ข
Test ํ๊ฐ ๊ฒฐ๊ณผ๊ฐ {results_path}์ ์ ์ฅ๋์์ต๋๋ค. ***")
|
| 307 |
+
|
| 308 |
+
print("\n--- ํผ๋ ํ๋ ฌ ์์ฑ (Test Set) ---")
|
| 309 |
+
try:
|
| 310 |
+
y_pred = test_predictions.predictions.argmax(-1)
|
| 311 |
+
y_true = test_predictions.label_ids
|
| 312 |
+
|
| 313 |
+
labels = [id_to_label[i] for i in sorted(id_to_label.keys())]
|
| 314 |
+
cm = confusion_matrix(y_true, y_pred, labels=[label_to_id[l] for l in labels])
|
| 315 |
+
|
| 316 |
+
plt.figure(figsize=(10, 8))
|
| 317 |
+
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels)
|
| 318 |
+
plt.xlabel('์์ธก ๋ผ๋ฒจ (Predicted Label)')
|
| 319 |
+
plt.ylabel('์ค์ ๋ผ๋ฒจ (True Label)')
|
| 320 |
+
plt.title('Confusion Matrix (TEST Set - 6-Class Oversampled)')
|
| 321 |
+
|
| 322 |
+
cm_path = os.path.join(output_dir, "TEST_confusion_matrix.png")
|
| 323 |
+
plt.savefig(cm_path)
|
| 324 |
+
print(f"Test Set ํผ๋ ํ๋ ฌ์ด {cm_path}์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 325 |
+
except Exception as e:
|
| 326 |
+
print(f"\n!!! ์ค๋ฅ: ํผ๋ ํ๋ ฌ ์์ฑ ์คํจ: {e} !!!")
|
| 327 |
+
print("matplotlib, seaborn ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ค์น๋์๋์ง ํ์ธํ์ธ์.")
|
| 328 |
+
|
| 329 |
+
if __name__ == "__main__":
|
| 330 |
+
print("--- 6-Class (Oversampling) ๊ฐ์ ๋ถ๋ฅ ๋ชจ๋ธ ํ์ต ์์ ---")
|
| 331 |
+
run_training()
|
server.log
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* Serving Flask app 'src'
|
| 2 |
+
* Debug mode: off
|
| 3 |
+
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
| 4 |
+
* Running on http://127.0.0.1:5000
|
| 5 |
+
Press CTRL+C to quit
|
| 6 |
+
127.0.0.1 - - [20/Oct/2025 01:09:21] "GET / HTTP/1.1" 200 -
|
| 7 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:21] "GET / HTTP/1.1" 200 -
|
| 8 |
+
127.0.0.1 - - [20/Oct/2025 01:09:35] "GET /auth/logout HTTP/1.1" 302 -
|
| 9 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:35] "[32mGET /auth/logout HTTP/1.1[0m" 302 -
|
| 10 |
+
127.0.0.1 - - [20/Oct/2025 01:09:35] "GET /auth/login HTTP/1.1" 200 -
|
| 11 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:35] "GET /auth/login HTTP/1.1" 200 -
|
| 12 |
+
127.0.0.1 - - [20/Oct/2025 01:09:38] "GET /auth/signup HTTP/1.1" 200 -
|
| 13 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:38] "GET /auth/signup HTTP/1.1" 200 -
|
| 14 |
+
WARNING:root:\u2705 DB ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ '๏ฟฝ๏ฟฝ'๏ฟฝ๏ฟฝ ๏ฟฝ฿ฐ๏ฟฝ๏ฟฝวพ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฯด๏ฟฝ.
|
| 15 |
+
127.0.0.1 - - [20/Oct/2025 01:09:43] "POST /auth/signup HTTP/1.1" 302 -
|
| 16 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:43] "[32mPOST /auth/signup HTTP/1.1[0m" 302 -
|
| 17 |
+
127.0.0.1 - - [20/Oct/2025 01:09:43] "GET /auth/login HTTP/1.1" 200 -
|
| 18 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:43] "GET /auth/login HTTP/1.1" 200 -
|
| 19 |
+
WARNING:root:--- ๏ฟฝฮฑ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝรต๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฺธ๏ฟฝ '๏ฟฝ๏ฟฝ' ---
|
| 20 |
+
127.0.0.1 - - [20/Oct/2025 01:09:46] "POST /auth/login HTTP/1.1" 302 -
|
| 21 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:46] "[32mPOST /auth/login HTTP/1.1[0m" 302 -
|
| 22 |
+
127.0.0.1 - - [20/Oct/2025 01:09:46] "GET / HTTP/1.1" 200 -
|
| 23 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:46] "GET / HTTP/1.1" 200 -
|
| 24 |
+
Device set to use cpu
|
| 25 |
+
127.0.0.1 - - [20/Oct/2025 01:09:52] "POST /api/predict HTTP/1.1" 200 -
|
| 26 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:52] "POST /api/predict HTTP/1.1" 200 -
|
| 27 |
+
127.0.0.1 - - [20/Oct/2025 01:09:53] "POST /diary/save HTTP/1.1" 200 -
|
| 28 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:53] "POST /diary/save HTTP/1.1" 200 -
|
| 29 |
+
127.0.0.1 - - [20/Oct/2025 01:09:54] "GET /my_diary HTTP/1.1" 200 -
|
| 30 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:54] "GET /my_diary HTTP/1.1" 200 -
|
| 31 |
+
127.0.0.1 - - [20/Oct/2025 01:09:55] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 32 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:09:55] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 33 |
+
127.0.0.1 - - [20/Oct/2025 01:10:11] "GET / HTTP/1.1" 200 -
|
| 34 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:10:11] "GET / HTTP/1.1" 200 -
|
| 35 |
+
127.0.0.1 - - [20/Oct/2025 01:10:16] "POST /api/predict HTTP/1.1" 200 -
|
| 36 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:10:16] "POST /api/predict HTTP/1.1" 200 -
|
| 37 |
+
127.0.0.1 - - [20/Oct/2025 01:10:16] "POST /diary/save HTTP/1.1" 200 -
|
| 38 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:10:16] "POST /diary/save HTTP/1.1" 200 -
|
| 39 |
+
127.0.0.1 - - [20/Oct/2025 01:10:17] "GET /my_diary HTTP/1.1" 200 -
|
| 40 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:10:17] "GET /my_diary HTTP/1.1" 200 -
|
| 41 |
+
127.0.0.1 - - [20/Oct/2025 01:10:18] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 42 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:10:18] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 43 |
+
127.0.0.1 - - [20/Oct/2025 01:34:01] "GET /my_diary HTTP/1.1" 200 -
|
| 44 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:01] "GET /my_diary HTTP/1.1" 200 -
|
| 45 |
+
127.0.0.1 - - [20/Oct/2025 01:34:01] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 46 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:01] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 47 |
+
127.0.0.1 - - [20/Oct/2025 01:34:04] "GET /auth/logout HTTP/1.1" 302 -
|
| 48 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:04] "[32mGET /auth/logout HTTP/1.1[0m" 302 -
|
| 49 |
+
127.0.0.1 - - [20/Oct/2025 01:34:04] "GET /auth/login HTTP/1.1" 200 -
|
| 50 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:04] "GET /auth/login HTTP/1.1" 200 -
|
| 51 |
+
127.0.0.1 - - [20/Oct/2025 01:34:04] "GET /favicon.ico HTTP/1.1" 404 -
|
| 52 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:04] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 53 |
+
127.0.0.1 - - [20/Oct/2025 01:34:06] "GET /auth/signup HTTP/1.1" 200 -
|
| 54 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:06] "GET /auth/signup HTTP/1.1" 200 -
|
| 55 |
+
WARNING:root:\u2705 DB ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ '๏ฟฝ๏ฟฝ'๏ฟฝ๏ฟฝ ๏ฟฝ฿ฐ๏ฟฝ๏ฟฝวพ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฯด๏ฟฝ.
|
| 56 |
+
127.0.0.1 - - [20/Oct/2025 01:34:09] "POST /auth/signup HTTP/1.1" 302 -
|
| 57 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:09] "[32mPOST /auth/signup HTTP/1.1[0m" 302 -
|
| 58 |
+
127.0.0.1 - - [20/Oct/2025 01:34:09] "GET /auth/login HTTP/1.1" 200 -
|
| 59 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:09] "GET /auth/login HTTP/1.1" 200 -
|
| 60 |
+
WARNING:root:--- ๏ฟฝฮฑ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝรต๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฺธ๏ฟฝ '๏ฟฝ๏ฟฝ' ---
|
| 61 |
+
127.0.0.1 - - [20/Oct/2025 01:34:11] "POST /auth/login HTTP/1.1" 302 -
|
| 62 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:11] "[32mPOST /auth/login HTTP/1.1[0m" 302 -
|
| 63 |
+
127.0.0.1 - - [20/Oct/2025 01:34:11] "GET / HTTP/1.1" 200 -
|
| 64 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:11] "GET / HTTP/1.1" 200 -
|
| 65 |
+
127.0.0.1 - - [20/Oct/2025 01:34:16] "POST /api/predict HTTP/1.1" 200 -
|
| 66 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:16] "POST /api/predict HTTP/1.1" 200 -
|
| 67 |
+
127.0.0.1 - - [20/Oct/2025 01:34:18] "POST /diary/save HTTP/1.1" 200 -
|
| 68 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:18] "POST /diary/save HTTP/1.1" 200 -
|
| 69 |
+
127.0.0.1 - - [20/Oct/2025 01:34:20] "GET /my_diary HTTP/1.1" 200 -
|
| 70 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:20] "GET /my_diary HTTP/1.1" 200 -
|
| 71 |
+
127.0.0.1 - - [20/Oct/2025 01:34:20] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 72 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:34:20] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 73 |
+
127.0.0.1 - - [20/Oct/2025 01:36:40] "GET / HTTP/1.1" 200 -
|
| 74 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:36:40] "GET / HTTP/1.1" 200 -
|
| 75 |
+
127.0.0.1 - - [20/Oct/2025 01:36:44] "POST /api/predict HTTP/1.1" 200 -
|
| 76 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:36:44] "POST /api/predict HTTP/1.1" 200 -
|
| 77 |
+
127.0.0.1 - - [20/Oct/2025 01:36:45] "POST /diary/save HTTP/1.1" 200 -
|
| 78 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:36:45] "POST /diary/save HTTP/1.1" 200 -
|
| 79 |
+
127.0.0.1 - - [20/Oct/2025 01:36:46] "GET /my_diary HTTP/1.1" 200 -
|
| 80 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:36:46] "GET /my_diary HTTP/1.1" 200 -
|
| 81 |
+
127.0.0.1 - - [20/Oct/2025 01:36:46] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 82 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:36:46] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 83 |
+
127.0.0.1 - - [20/Oct/2025 01:38:54] "GET / HTTP/1.1" 200 -
|
| 84 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:38:54] "GET / HTTP/1.1" 200 -
|
| 85 |
+
127.0.0.1 - - [20/Oct/2025 01:38:58] "POST /api/predict HTTP/1.1" 200 -
|
| 86 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:38:58] "POST /api/predict HTTP/1.1" 200 -
|
| 87 |
+
127.0.0.1 - - [20/Oct/2025 01:39:03] "POST /api/predict HTTP/1.1" 200 -
|
| 88 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:39:03] "POST /api/predict HTTP/1.1" 200 -
|
| 89 |
+
127.0.0.1 - - [20/Oct/2025 01:39:06] "POST /api/predict HTTP/1.1" 200 -
|
| 90 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:39:06] "POST /api/predict HTTP/1.1" 200 -
|
| 91 |
+
127.0.0.1 - - [20/Oct/2025 01:39:10] "POST /api/predict HTTP/1.1" 200 -
|
| 92 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:39:10] "POST /api/predict HTTP/1.1" 200 -
|
| 93 |
+
127.0.0.1 - - [20/Oct/2025 01:39:13] "POST /api/predict HTTP/1.1" 200 -
|
| 94 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:39:13] "POST /api/predict HTTP/1.1" 200 -
|
| 95 |
+
127.0.0.1 - - [20/Oct/2025 01:56:50] "GET /auth/logout HTTP/1.1" 302 -
|
| 96 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:50] "[32mGET /auth/logout HTTP/1.1[0m" 302 -
|
| 97 |
+
127.0.0.1 - - [20/Oct/2025 01:56:50] "GET /auth/login HTTP/1.1" 200 -
|
| 98 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:50] "GET /auth/login HTTP/1.1" 200 -
|
| 99 |
+
127.0.0.1 - - [20/Oct/2025 01:56:52] "GET /auth/login HTTP/1.1" 200 -
|
| 100 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:52] "GET /auth/login HTTP/1.1" 200 -
|
| 101 |
+
127.0.0.1 - - [20/Oct/2025 01:56:53] "GET /favicon.ico HTTP/1.1" 404 -
|
| 102 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:53] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 103 |
+
127.0.0.1 - - [20/Oct/2025 01:56:55] "GET /auth/signup HTTP/1.1" 200 -
|
| 104 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:55] "GET /auth/signup HTTP/1.1" 200 -
|
| 105 |
+
WARNING:root:\u2705 DB ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ '๏ฟฝ๏ฟฝ'๏ฟฝ๏ฟฝ ๏ฟฝ฿ฐ๏ฟฝ๏ฟฝวพ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฯด๏ฟฝ.
|
| 106 |
+
127.0.0.1 - - [20/Oct/2025 01:56:58] "POST /auth/signup HTTP/1.1" 302 -
|
| 107 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:58] "[32mPOST /auth/signup HTTP/1.1[0m" 302 -
|
| 108 |
+
127.0.0.1 - - [20/Oct/2025 01:56:58] "GET /auth/login HTTP/1.1" 200 -
|
| 109 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:58] "GET /auth/login HTTP/1.1" 200 -
|
| 110 |
+
WARNING:root:--- ๏ฟฝฮฑ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝรต๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฺธ๏ฟฝ '๏ฟฝ๏ฟฝ' ---
|
| 111 |
+
127.0.0.1 - - [20/Oct/2025 01:56:59] "POST /auth/login HTTP/1.1" 302 -
|
| 112 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:59] "[32mPOST /auth/login HTTP/1.1[0m" 302 -
|
| 113 |
+
127.0.0.1 - - [20/Oct/2025 01:56:59] "GET / HTTP/1.1" 200 -
|
| 114 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:56:59] "GET / HTTP/1.1" 200 -
|
| 115 |
+
127.0.0.1 - - [20/Oct/2025 01:57:04] "POST /api/predict HTTP/1.1" 200 -
|
| 116 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:57:04] "POST /api/predict HTTP/1.1" 200 -
|
| 117 |
+
127.0.0.1 - - [20/Oct/2025 01:57:04] "POST /diary/save HTTP/1.1" 200 -
|
| 118 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:57:04] "POST /diary/save HTTP/1.1" 200 -
|
| 119 |
+
127.0.0.1 - - [20/Oct/2025 01:57:06] "GET /my_diary HTTP/1.1" 200 -
|
| 120 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:57:06] "GET /my_diary HTTP/1.1" 200 -
|
| 121 |
+
127.0.0.1 - - [20/Oct/2025 01:57:06] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 122 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:57:06] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 123 |
+
127.0.0.1 - - [20/Oct/2025 01:57:19] "GET /api/diaries?year=2025&month=8 HTTP/1.1" 200 -
|
| 124 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 01:57:19] "GET /api/diaries?year=2025&month=8 HTTP/1.1" 200 -
|
| 125 |
+
127.0.0.1 - - [20/Oct/2025 02:04:20] "GET / HTTP/1.1" 200 -
|
| 126 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:04:20] "GET / HTTP/1.1" 200 -
|
| 127 |
+
127.0.0.1 - - [20/Oct/2025 02:04:25] "GET /auth/logout HTTP/1.1" 302 -
|
| 128 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:04:25] "[32mGET /auth/logout HTTP/1.1[0m" 302 -
|
| 129 |
+
127.0.0.1 - - [20/Oct/2025 02:04:25] "GET /auth/login HTTP/1.1" 200 -
|
| 130 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:04:25] "GET /auth/login HTTP/1.1" 200 -
|
| 131 |
+
127.0.0.1 - - [20/Oct/2025 02:04:27] "GET /auth/login HTTP/1.1" 200 -
|
| 132 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:04:27] "GET /auth/login HTTP/1.1" 200 -
|
| 133 |
+
127.0.0.1 - - [20/Oct/2025 02:04:27] "GET /favicon.ico HTTP/1.1" 404 -
|
| 134 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:04:27] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 135 |
+
127.0.0.1 - - [20/Oct/2025 02:05:13] "GET /auth/login HTTP/1.1" 200 -
|
| 136 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:13] "GET /auth/login HTTP/1.1" 200 -
|
| 137 |
+
127.0.0.1 - - [20/Oct/2025 02:05:14] "GET /favicon.ico HTTP/1.1" 404 -
|
| 138 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:14] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 139 |
+
127.0.0.1 - - [20/Oct/2025 02:05:15] "GET /auth/login HTTP/1.1" 200 -
|
| 140 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:15] "GET /auth/login HTTP/1.1" 200 -
|
| 141 |
+
127.0.0.1 - - [20/Oct/2025 02:05:15] "GET /favicon.ico HTTP/1.1" 404 -
|
| 142 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:15] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 143 |
+
127.0.0.1 - - [20/Oct/2025 02:05:15] "GET /auth/login HTTP/1.1" 200 -
|
| 144 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:15] "GET /auth/login HTTP/1.1" 200 -
|
| 145 |
+
127.0.0.1 - - [20/Oct/2025 02:05:16] "GET /auth/login HTTP/1.1" 200 -
|
| 146 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:16] "GET /auth/login HTTP/1.1" 200 -
|
| 147 |
+
127.0.0.1 - - [20/Oct/2025 02:05:16] "GET /auth/login HTTP/1.1" 200 -
|
| 148 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:16] "GET /auth/login HTTP/1.1" 200 -
|
| 149 |
+
127.0.0.1 - - [20/Oct/2025 02:05:16] "GET /favicon.ico HTTP/1.1" 404 -
|
| 150 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:16] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 151 |
+
127.0.0.1 - - [20/Oct/2025 02:05:17] "GET /auth/login HTTP/1.1" 200 -
|
| 152 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:17] "GET /auth/login HTTP/1.1" 200 -
|
| 153 |
+
127.0.0.1 - - [20/Oct/2025 02:05:17] "GET /favicon.ico HTTP/1.1" 404 -
|
| 154 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:17] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 155 |
+
127.0.0.1 - - [20/Oct/2025 02:05:18] "GET /auth/signup HTTP/1.1" 200 -
|
| 156 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:18] "GET /auth/signup HTTP/1.1" 200 -
|
| 157 |
+
127.0.0.1 - - [20/Oct/2025 02:05:19] "GET /auth/login HTTP/1.1" 200 -
|
| 158 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:05:19] "GET /auth/login HTTP/1.1" 200 -
|
| 159 |
+
127.0.0.1 - - [20/Oct/2025 02:20:20] "GET / HTTP/1.1" 302 -
|
| 160 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:20] "[32mGET / HTTP/1.1[0m" 302 -
|
| 161 |
+
127.0.0.1 - - [20/Oct/2025 02:20:20] "GET /auth/login HTTP/1.1" 200 -
|
| 162 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:20] "GET /auth/login HTTP/1.1" 200 -
|
| 163 |
+
127.0.0.1 - - [20/Oct/2025 02:20:21] "GET /auth/login HTTP/1.1" 200 -
|
| 164 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:21] "GET /auth/login HTTP/1.1" 200 -
|
| 165 |
+
127.0.0.1 - - [20/Oct/2025 02:20:22] "GET /favicon.ico HTTP/1.1" 404 -
|
| 166 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:22] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 167 |
+
127.0.0.1 - - [20/Oct/2025 02:20:22] "GET /auth/login HTTP/1.1" 200 -
|
| 168 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:22] "GET /auth/login HTTP/1.1" 200 -
|
| 169 |
+
127.0.0.1 - - [20/Oct/2025 02:20:22] "GET /auth/login HTTP/1.1" 200 -
|
| 170 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:22] "GET /auth/login HTTP/1.1" 200 -
|
| 171 |
+
127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /auth/login HTTP/1.1" 200 -
|
| 172 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /auth/login HTTP/1.1" 200 -
|
| 173 |
+
127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /auth/login HTTP/1.1" 200 -
|
| 174 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /auth/login HTTP/1.1" 200 -
|
| 175 |
+
127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /auth/login HTTP/1.1" 200 -
|
| 176 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /auth/login HTTP/1.1" 200 -
|
| 177 |
+
127.0.0.1 - - [20/Oct/2025 02:20:23] "GET /favicon.ico HTTP/1.1" 404 -
|
| 178 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:23] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
| 179 |
+
127.0.0.1 - - [20/Oct/2025 02:20:24] "GET /auth/signup HTTP/1.1" 200 -
|
| 180 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:24] "GET /auth/signup HTTP/1.1" 200 -
|
| 181 |
+
127.0.0.1 - - [20/Oct/2025 02:20:24] "GET /auth/login HTTP/1.1" 200 -
|
| 182 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:24] "GET /auth/login HTTP/1.1" 200 -
|
| 183 |
+
127.0.0.1 - - [20/Oct/2025 02:20:28] "GET / HTTP/1.1" 302 -
|
| 184 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:28] "[32mGET / HTTP/1.1[0m" 302 -
|
| 185 |
+
127.0.0.1 - - [20/Oct/2025 02:20:28] "GET /auth/login HTTP/1.1" 200 -
|
| 186 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:28] "GET /auth/login HTTP/1.1" 200 -
|
| 187 |
+
127.0.0.1 - - [20/Oct/2025 02:20:30] "GET /auth/signup HTTP/1.1" 200 -
|
| 188 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:30] "GET /auth/signup HTTP/1.1" 200 -
|
| 189 |
+
127.0.0.1 - - [20/Oct/2025 02:20:31] "GET /auth/login HTTP/1.1" 200 -
|
| 190 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:31] "GET /auth/login HTTP/1.1" 200 -
|
| 191 |
+
WARNING:root:--- ๏ฟฝฮฑ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝรต๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฺธ๏ฟฝ '๏ฟฝ๏ฟฝ' ---
|
| 192 |
+
127.0.0.1 - - [20/Oct/2025 02:20:37] "POST /auth/login HTTP/1.1" 302 -
|
| 193 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:37] "[32mPOST /auth/login HTTP/1.1[0m" 302 -
|
| 194 |
+
127.0.0.1 - - [20/Oct/2025 02:20:37] "GET / HTTP/1.1" 200 -
|
| 195 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:20:37] "GET / HTTP/1.1" 200 -
|
| 196 |
+
127.0.0.1 - - [20/Oct/2025 02:22:23] "GET / HTTP/1.1" 302 -
|
| 197 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:23] "[32mGET / HTTP/1.1[0m" 302 -
|
| 198 |
+
127.0.0.1 - - [20/Oct/2025 02:22:23] "GET /auth/login HTTP/1.1" 200 -
|
| 199 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:23] "GET /auth/login HTTP/1.1" 200 -
|
| 200 |
+
127.0.0.1 - - [20/Oct/2025 02:22:28] "GET /auth/signup HTTP/1.1" 200 -
|
| 201 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:28] "GET /auth/signup HTTP/1.1" 200 -
|
| 202 |
+
127.0.0.1 - - [20/Oct/2025 02:22:29] "GET /auth/signup HTTP/1.1" 200 -
|
| 203 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:29] "GET /auth/signup HTTP/1.1" 200 -
|
| 204 |
+
127.0.0.1 - - [20/Oct/2025 02:22:29] "GET /auth/signup HTTP/1.1" 200 -
|
| 205 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:29] "GET /auth/signup HTTP/1.1" 200 -
|
| 206 |
+
127.0.0.1 - - [20/Oct/2025 02:22:31] "GET /auth/signup HTTP/1.1" 200 -
|
| 207 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:31] "GET /auth/signup HTTP/1.1" 200 -
|
| 208 |
+
127.0.0.1 - - [20/Oct/2025 02:22:32] "GET /auth/signup HTTP/1.1" 200 -
|
| 209 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:32] "GET /auth/signup HTTP/1.1" 200 -
|
| 210 |
+
127.0.0.1 - - [20/Oct/2025 02:22:38] "GET /auth/login HTTP/1.1" 200 -
|
| 211 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:38] "GET /auth/login HTTP/1.1" 200 -
|
| 212 |
+
WARNING:root:--- ๏ฟฝฮฑ๏ฟฝ๏ฟฝ๏ฟฝ ๏ฟฝรต๏ฟฝ: ๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฺธ๏ฟฝ '๏ฟฝ๏ฟฝ' ---
|
| 213 |
+
127.0.0.1 - - [20/Oct/2025 02:22:41] "POST /auth/login HTTP/1.1" 302 -
|
| 214 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:41] "[32mPOST /auth/login HTTP/1.1[0m" 302 -
|
| 215 |
+
127.0.0.1 - - [20/Oct/2025 02:22:41] "GET / HTTP/1.1" 200 -
|
| 216 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:41] "GET / HTTP/1.1" 200 -
|
| 217 |
+
127.0.0.1 - - [20/Oct/2025 02:22:48] "POST /api/predict HTTP/1.1" 200 -
|
| 218 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:48] "POST /api/predict HTTP/1.1" 200 -
|
| 219 |
+
127.0.0.1 - - [20/Oct/2025 02:22:48] "POST /diary/save HTTP/1.1" 200 -
|
| 220 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:48] "POST /diary/save HTTP/1.1" 200 -
|
| 221 |
+
127.0.0.1 - - [20/Oct/2025 02:22:50] "GET /my_diary HTTP/1.1" 200 -
|
| 222 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:50] "GET /my_diary HTTP/1.1" 200 -
|
| 223 |
+
127.0.0.1 - - [20/Oct/2025 02:22:50] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 224 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:22:50] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 225 |
+
127.0.0.1 - - [20/Oct/2025 02:23:00] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -
|
| 226 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:00] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
| 227 |
+
127.0.0.1 - - [20/Oct/2025 02:23:26] "GET /my_diary HTTP/1.1" 200 -
|
| 228 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:26] "GET /my_diary HTTP/1.1" 200 -
|
| 229 |
+
127.0.0.1 - - [20/Oct/2025 02:23:26] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -
|
| 230 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:26] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
| 231 |
+
127.0.0.1 - - [20/Oct/2025 02:23:26] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 232 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:26] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 233 |
+
127.0.0.1 - - [20/Oct/2025 02:23:33] "GET /my_diary HTTP/1.1" 200 -
|
| 234 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:33] "GET /my_diary HTTP/1.1" 200 -
|
| 235 |
+
127.0.0.1 - - [20/Oct/2025 02:23:33] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -
|
| 236 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:33] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
| 237 |
+
127.0.0.1 - - [20/Oct/2025 02:23:33] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 238 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:33] "GET /api/diaries?year=2025&month=9 HTTP/1.1" 200 -
|
| 239 |
+
127.0.0.1 - - [20/Oct/2025 02:23:35] "GET / HTTP/1.1" 200 -
|
| 240 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:35] "GET / HTTP/1.1" 200 -
|
| 241 |
+
127.0.0.1 - - [20/Oct/2025 02:23:35] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -
|
| 242 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:35] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
| 243 |
+
127.0.0.1 - - [20/Oct/2025 02:23:37] "GET /auth/logout HTTP/1.1" 302 -
|
| 244 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:37] "[32mGET /auth/logout HTTP/1.1[0m" 302 -
|
| 245 |
+
127.0.0.1 - - [20/Oct/2025 02:23:37] "GET /auth/login HTTP/1.1" 200 -
|
| 246 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:37] "GET /auth/login HTTP/1.1" 200 -
|
| 247 |
+
127.0.0.1 - - [20/Oct/2025 02:23:37] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 -
|
| 248 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:23:37] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
| 249 |
+
127.0.0.1 - - [20/Oct/2025 02:26:18] "GET / HTTP/1.1" 302 -
|
| 250 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:26:18] "[32mGET / HTTP/1.1[0m" 302 -
|
| 251 |
+
127.0.0.1 - - [20/Oct/2025 02:26:18] "GET /auth/login HTTP/1.1" 200 -
|
| 252 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:26:18] "GET /auth/login HTTP/1.1" 200 -
|
| 253 |
+
127.0.0.1 - - [20/Oct/2025 02:26:22] "GET /auth/signup HTTP/1.1" 200 -
|
| 254 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:26:22] "GET /auth/signup HTTP/1.1" 200 -
|
| 255 |
+
127.0.0.1 - - [20/Oct/2025 02:26:23] "GET /auth/login HTTP/1.1" 200 -
|
| 256 |
+
INFO:werkzeug:127.0.0.1 - - [20/Oct/2025 02:26:23] "GET /auth/login HTTP/1.1" 200 -
|
src/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
+
from flask_login import LoginManager
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
db = SQLAlchemy()
|
| 7 |
+
login_manager = LoginManager()
|
| 8 |
+
login_manager.login_view = 'auth.login'
|
| 9 |
+
|
| 10 |
+
def create_app():
|
| 11 |
+
app = Flask(__name__, static_folder='templates/static')
|
| 12 |
+
|
| 13 |
+
# 1. ์ค์
|
| 14 |
+
app.config['SECRET_KEY'] = 'a-very-long-and-unique-secret-key-for-this-app'
|
| 15 |
+
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
| 16 |
+
app.config['SESSION_COOKIE_SECURE'] = True
|
| 17 |
+
|
| 18 |
+
db_uri = os.environ.get('DATABASE_URL')
|
| 19 |
+
if not db_uri:
|
| 20 |
+
db_uri = "sqlite:///emotion.db"
|
| 21 |
+
|
| 22 |
+
if db_uri and db_uri.startswith("postgres://"):
|
| 23 |
+
db_uri = db_uri.replace("postgres://", "postgresql://", 1)
|
| 24 |
+
|
| 25 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
|
| 26 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 27 |
+
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
| 28 |
+
|
| 29 |
+
# SQLAlchemy ์์ง ์ต์
์ค์ (pgbouncer Session ๋ชจ๋ ํธํ)
|
| 30 |
+
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
| 31 |
+
"pool_size": 15, # pgbouncer์ pool_size์ ์ผ์น์ํค๊ฑฐ๋ ๋ ์๊ฒ ์ค์
|
| 32 |
+
"max_overflow": 5, # pool_size ์ด๊ณผ ์ ์์๋ก ์ด ์ ์๋ ์ฐ๊ฒฐ ์
|
| 33 |
+
"pool_recycle": 3600, # 1์๊ฐ๋ง๋ค ์ฐ๊ฒฐ ์ฌํ์ฉ (์ค๋๋ ์ฐ๊ฒฐ ๋ฐฉ์ง)
|
| 34 |
+
"pool_pre_ping": True, # ์ฐ๊ฒฐ ์ฌ์ฉ ์ ์ ํจ์ฑ ๊ฒ์ฌ
|
| 35 |
+
"pool_timeout": 30, # ์ฐ๊ฒฐ ํ์์ ์ฐ๊ฒฐ์ ๊ธฐ๋ค๋ฆฌ๋ ์ต๋ ์๊ฐ (์ด)
|
| 36 |
+
# pgbouncer Session ๋ชจ๋์์๋ 'rollback'์ด ๊ธฐ๋ณธ๊ฐ์ด๋ฏ๋ก ๋ช
์์ ์ผ๋ก ์ค์ ํ์ง ์์๋ ๋จ
|
| 37 |
+
# "pool_reset_on_return": 'rollback'
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# ๋ก๊น
์ค์
|
| 41 |
+
import logging
|
| 42 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 43 |
+
|
| 44 |
+
# 2. DB ์ด๊ธฐํ ๋ฐ ํ
์ด๋ธ ์์ฑ
|
| 45 |
+
db.init_app(app)
|
| 46 |
+
with app.app_context():
|
| 47 |
+
from . import models
|
| 48 |
+
db.create_all()
|
| 49 |
+
|
| 50 |
+
@app.teardown_appcontext
|
| 51 |
+
def shutdown_session(exception=None):
|
| 52 |
+
db.session.remove()
|
| 53 |
+
|
| 54 |
+
# 3. AI ๋ชจ๋ธ ๋ก๋ฉ
|
| 55 |
+
from .emotion_engine import load_emotion_classifier
|
| 56 |
+
# ์ฑ ์ปจํ
์คํธ ์์์ ๋ชจ๋ธ์ ๋ก๋ํ๊ณ app ๊ฐ์ฒด์ ์ ์ฅํฉ๋๋ค.
|
| 57 |
+
with app.app_context():
|
| 58 |
+
app.emotion_classifier = load_emotion_classifier()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# 5. ๋ธ๋ฃจํ๋ฆฐํธ ๋ฑ๋ก
|
| 63 |
+
from . import main, auth
|
| 64 |
+
app.register_blueprint(main.bp)
|
| 65 |
+
app.register_blueprint(auth.bp)
|
| 66 |
+
|
| 67 |
+
return app
|
src/auth.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/ auth.py
|
| 2 |
+
# ๊ธฐ์กด app.py์์ auth์ ๊ด๋ จํด์ ๋ถ๋ฆฌ
|
| 3 |
+
# ๋ก๊ทธ์ธ์ด๋ ํ์๊ฐ์
์ธ์ฆ ๊ด๋ จํ ์คํฌ๋ฆฝํธ
|
| 4 |
+
|
| 5 |
+
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
| 6 |
+
import logging
|
| 7 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 8 |
+
from . import db
|
| 9 |
+
from .models import User
|
| 10 |
+
|
| 11 |
+
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
| 12 |
+
|
| 13 |
+
# ๋ก๊ทธ์ธ ํํธ
|
| 14 |
+
@bp.route('/login', methods=['GET', 'POST'])
|
| 15 |
+
def login():
|
| 16 |
+
if request.method == 'POST':
|
| 17 |
+
username = request.form['username']
|
| 18 |
+
password = request.form['password']
|
| 19 |
+
logging.warning(f"--- ๋ก๊ทธ์ธ ์๋: ์ฌ์ฉ์๋ช
'{username}' ---")
|
| 20 |
+
|
| 21 |
+
# ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
|
| 22 |
+
user = User.query.filter_by(username=username).first()
|
| 23 |
+
|
| 24 |
+
if not user or not user.check_password(password):
|
| 25 |
+
flash('๋ก๊ทธ์ธ ์ ๋ณด๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.')
|
| 26 |
+
# ๋ก๊ทธ์ธ ์คํจ ์ ๋ค์ ๋ก๊ทธ์ธ ํ๋ฉด(์๋ฉด)
|
| 27 |
+
return render_template('auth_combined.html')
|
| 28 |
+
|
| 29 |
+
# ๋ก๊ทธ์ธ ์ฑ๊ณต
|
| 30 |
+
session.clear()
|
| 31 |
+
session['user_id'] = user.id
|
| 32 |
+
session['username'] = user.username
|
| 33 |
+
return redirect(url_for('main.home'))
|
| 34 |
+
|
| 35 |
+
return render_template('auth_combined.html')
|
| 36 |
+
|
| 37 |
+
# ํ์๊ฐ์
ํํธ
|
| 38 |
+
@bp.route('/signup', methods=['GET', 'POST'])
|
| 39 |
+
def signup():
|
| 40 |
+
try:
|
| 41 |
+
if request.method == 'POST':
|
| 42 |
+
username = request.form['username']
|
| 43 |
+
password = request.form['password']
|
| 44 |
+
|
| 45 |
+
# 1. ์ค๋ณต ์ฌ์ฉ์ ํ์ธ
|
| 46 |
+
if User.query.filter_by(username=username).first():
|
| 47 |
+
flash('์ด๋ฏธ ์กด์ฌํ๋ ์ฌ์ฉ์์
๋๋ค.')
|
| 48 |
+
# [ํต์ฌ] ์ด๋ฏธ ์กด์ฌํ๋ฉด ์นด๋๊ฐ ๋ค์งํ ์ํ(ํ์๊ฐ์
ํ๋ฉด)๋ฅผ ์ ์งํ๊ธฐ ์ํด mode='signup'์ ์ ๋ฌ
|
| 49 |
+
return redirect(url_for('auth.login', mode='signup'))
|
| 50 |
+
|
| 51 |
+
# 2. ์ ์ฌ์ฉ์ ์์ฑ
|
| 52 |
+
new_user = User(username=username)
|
| 53 |
+
new_user.set_password(password)
|
| 54 |
+
|
| 55 |
+
db.session.add(new_user)
|
| 56 |
+
db.session.commit()
|
| 57 |
+
logging.warning("โ
DB ์ ์ฅ ์ฑ๊ณต: ์ฌ์ฉ์ '{}'๊ฐ ์ถ๊ฐ๋์์ต๋๋ค.".format(username))
|
| 58 |
+
|
| 59 |
+
# 3. ๊ฐ์
์ฑ๊ณต ์ ๋ก๊ทธ์ธ ํ๋ฉด(์๋ฉด)์ผ๋ก ์ด๋
|
| 60 |
+
flash('ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค. ๋ก๊ทธ์ธํด์ฃผ์ธ์.')
|
| 61 |
+
return redirect(url_for('auth.login'))
|
| 62 |
+
|
| 63 |
+
except Exception as e:
|
| 64 |
+
db.session.rollback()
|
| 65 |
+
logging.exception("๐ฅ๐ฅ๐ฅ signup ํจ์์์ DB ์ค๋ฅ ๋ฐ์! ๐ฅ๐ฅ๐ฅ")
|
| 66 |
+
return "Internal Server Error", 500
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
return render_template('auth_combined.html')
|
| 70 |
+
|
| 71 |
+
# ๋ก๊ทธ์์ part
|
| 72 |
+
@bp.route('/logout')
|
| 73 |
+
def logout():
|
| 74 |
+
|
| 75 |
+
# ์ธ์
์์ ์ฌ์ฉ์ ์ ๋ณด ์ ๊ฑฐ
|
| 76 |
+
session.clear()
|
| 77 |
+
# ๋ก๊ทธ์์ ํ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
|
| 78 |
+
return redirect(url_for('auth.login'))
|
src/emotion_engine.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline
|
| 3 |
+
import os
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
# ๋ชจ๋ธ์ ์ ์ฅํ ์ ์ญ ๋ณ์
|
| 7 |
+
_classifier = None
|
| 8 |
+
|
| 9 |
+
def load_emotion_classifier():
|
| 10 |
+
global _classifier
|
| 11 |
+
# ๋ชจ๋ธ์ด ์ด๋ฏธ ๋ก๋๋์๋ค๋ฉด, ์ฆ์ ๋ฐํ
|
| 12 |
+
if _classifier is not None:
|
| 13 |
+
return _classifier
|
| 14 |
+
|
| 15 |
+
# ๋ชจ๋ธ์ด ๋ก๋๋์ง ์์๋ค๋ฉด, ๋ก๋ ์์
|
| 16 |
+
MODEL_ID = "taehoon222/korean-emotion-classifier-final"
|
| 17 |
+
|
| 18 |
+
logging.info(f"Hugging Face Hub ๋ชจ๋ธ '{MODEL_ID}'์์ ๋ชจ๋ธ์ ๋ถ๋ฌ์ต๋๋ค...")
|
| 19 |
+
try:
|
| 20 |
+
logging.info("ํ ํฌ๋์ด์ ๋ก๋ฉ ์ค...")
|
| 21 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 22 |
+
logging.info("๋ชจ๋ธ ๋ก๋ฉ ์ค...")
|
| 23 |
+
model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID)
|
| 24 |
+
logging.info("Hugging Face Hub ๋ชจ๋ธ ๋ก๋ฉ ์ฑ๊ณต!")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
logging.error(f"๋ชจ๋ธ ๋ก๋ฉ ์ค ์ค๋ฅ: {e}")
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
device = 0 if torch.cuda.is_available() else -1
|
| 30 |
+
if device == 0:
|
| 31 |
+
logging.info("Device set to use cuda (GPU)")
|
| 32 |
+
else:
|
| 33 |
+
logging.info("Device set to use cpu")
|
| 34 |
+
|
| 35 |
+
# ๋ก๋๋ ๋ชจ๋ธ์ ์ ์ญ ๋ณ์์ ์ ์ฅ
|
| 36 |
+
_classifier = pipeline("text-classification", model=model, tokenizer=tokenizer, device=device)
|
| 37 |
+
return _classifier
|
| 38 |
+
|
| 39 |
+
def predict_emotion(text, top_k=3):
|
| 40 |
+
logging.info(f"predict_emotion ํจ์ ํธ์ถ๋จ. ํ
์คํธ ๊ธธ์ด: {len(text) if text else 0}, top_k={top_k}")
|
| 41 |
+
classifier = load_emotion_classifier()
|
| 42 |
+
if not text or not text.strip():
|
| 43 |
+
logging.warning("๋ถ์ํ ํ
์คํธ๊ฐ ๋น์ด์๊ฑฐ๋ ๊ณต๋ฐฑ์
๋๋ค.")
|
| 44 |
+
return []
|
| 45 |
+
if classifier is None:
|
| 46 |
+
logging.error("๊ฐ์ ๋ถ์ ์์ง์ด ์ค๋น๋์ง ์์์ต๋๋ค.")
|
| 47 |
+
return []
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
logging.info(f"๋ถ๋ฅ๊ธฐ ์คํ ์ค... ํ
์คํธ: {text[:50]}...")
|
| 51 |
+
results = classifier(text, top_k=top_k)
|
| 52 |
+
logging.info(f"๋ถ๋ฅ ๊ฒฐ๊ณผ (Top {top_k}): {results}")
|
| 53 |
+
return results
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logging.error(f"๊ฐ์ ๋ถ๋ฅ ์ค ์ค๋ฅ ๋ฐ์: {e}")
|
| 56 |
+
return []
|
src/main.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request, current_app
|
| 2 |
+
from sqlalchemy import extract, String # String ์ถ๊ฐ
|
| 3 |
+
import datetime
|
| 4 |
+
import time
|
| 5 |
+
from . import db
|
| 6 |
+
from .models import Diary, User
|
| 7 |
+
from .emotion_engine import predict_emotion
|
| 8 |
+
from .recommender import Recommender
|
| 9 |
+
import logging
|
| 10 |
+
import os
|
| 11 |
+
import google.generativeai as genai
|
| 12 |
+
|
| 13 |
+
bp = Blueprint('main', __name__)
|
| 14 |
+
recommender = Recommender()
|
| 15 |
+
|
| 16 |
+
# --- Gemini API ์ค์ ---
|
| 17 |
+
try:
|
| 18 |
+
api_key = os.environ.get('GEMINI_API_KEY')
|
| 19 |
+
if not api_key:
|
| 20 |
+
logging.warning("๐ฅ๐ฅ๐ฅ GEMINI_API_KEY ํ๊ฒฝ ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค. ๐ฅ๐ฅ๐ฅ")
|
| 21 |
+
genai.configure(api_key=api_key)
|
| 22 |
+
except Exception as e:
|
| 23 |
+
logging.error(f"๐ฅ๐ฅ๐ฅ Gemini API ์ค์ ์ค ์ค๋ฅ ๋ฐ์: {e} ๐ฅ๐ฅ๐ฅ")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ๊ฐ์ ๋ณ ์ด๋ชจ์ง ๋งต
|
| 27 |
+
emotion_emoji_map = {
|
| 28 |
+
'๋ถ๋
ธ': '๐ ', '๋ถ์': '๐', '์ฌํ': '๐ข',
|
| 29 |
+
'๋นํฉ': '๐ฎ', '๊ธฐ์จ': '๐', '์์ฒ': '๐',
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
default_recommendations = {
|
| 33 |
+
'๋ถ๋
ธ': 'ํ๊ฐ ๋ ๋๋ ์ ๋๋ ์์
์ ๋ฃ๊ฑฐ๋, ๊ฐ๋ฒผ์ด ์ฝ๋ฏธ๋ ์ํ๋ฅผ ๋ณด๋ฉฐ ๊ธฐ๋ถ์ ์ ํํด ๋ณด์ธ์.',
|
| 34 |
+
'๋ถ์': '๋ถ์ํ ๋๋ ์ฐจ๋ถํ ํด๋์ ์์
์ ๋ฃ๊ฑฐ๋, ๋ฐ๋ปํ ์ฐจ๋ฅผ ๋ง์๋ฉฐ ๋ช
์์ ํด๋ณด๋ ๊ฑด ์ด๋จ๊น์?',
|
| 35 |
+
'์ฌํ': '์ฌํ ๋๋ ์๋ก๊ฐ ๋๋ ์ํ๋ ์ฑ
์ ๋ณด๋ฉฐ ๊ฐ์ ์ ์ถฉ๋ถํ ๋๊ปด๋ณด๋ ๊ฒ๋ ์ข์์. ํน์ ์น๊ตฌ์ ๋ํ๋ฅผ ๋๋ ๋ณด์ธ์.',
|
| 36 |
+
'๋นํฉ': '๋นํฉ์ค๋ฌ์ธ ๋๋ ์ ์ ์จ์ ๊ณ ๋ฅด๊ณ , ์ข์ํ๋ ์์
์ ๋ค์ผ๋ฉฐ ๋ง์์ ์ง์ ์์ผ ๋ณด์ธ์.',
|
| 37 |
+
'๊ธฐ์จ': '๊ธฐ์ ๋๋ ์ ๋๋ ๋์ค ์์
๊ณผ ํจ๊ป ์ถค์ ์ถ๊ฑฐ๋, ์น๊ตฌ๋ค๊ณผ ๋ง๋ ์ฆ๊ฑฐ์์ ๋๋ ๋ณด์ธ์!',
|
| 38 |
+
'์์ฒ': '๋ง์์ ์์ฒ๋ฅผ ๋ฐ์์ ๋๋, ์๋ก๊ฐ ๋๋ ์์
์ ๋ฃ๊ฑฐ๋, ์กฐ์ฉํ ๊ณณ์์ ์ฑ
์ ์ฝ์ผ๋ฉฐ ๋ง์์ ๋ฌ๋๋ณด์ธ์.'
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def generate_recommendation(user_diary, predicted_emotion):
|
| 42 |
+
"""
|
| 43 |
+
์ฃผ์ด์ง ์ผ๊ธฐ ๋ด์ฉ๊ณผ ๊ฐ์ ์ ๋ฐํ์ผ๋ก Gemini API๋ฅผ ์ฌ์ฉํ์ฌ ๋ฌธํ์ํ ์ถ์ฒ์ ์์ฑํฉ๋๋ค.
|
| 44 |
+
"""
|
| 45 |
+
start_time = time.time()
|
| 46 |
+
logging.info("Gemini API ํธ์ถ ์์...")
|
| 47 |
+
try:
|
| 48 |
+
model = genai.GenerativeModel('gemini-flash-latest')
|
| 49 |
+
prompt = f"""
|
| 50 |
+
์ฌ์ฉ์์ ์ผ๊ธฐ ๋ด์ฉ๊ณผ ๊ฐ์ ์ ๋ฐํ์ผ๋ก ๋ฌธํ์ํ์ ์ถ์ฒํด์ค.
|
| 51 |
+
์ฌ์ฉ์๋ ํ์ฌ '{predicted_emotion}' ๊ฐ์ ์ ๋๋ผ๊ณ ์์ด.
|
| 52 |
+
|
| 53 |
+
์ผ๊ธฐ ๋ด์ฉ:
|
| 54 |
+
---
|
| 55 |
+
{user_diary}
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
์๋ ๋ ๊ฐ์ง ์๋๋ฆฌ์ค์ ๋ง์ถฐ ์ํ, ์์
, ๋์๋ง ์ถ์ฒํด์ค.
|
| 59 |
+
๊ฐ ์ถ์ฒ ํญ๋ชฉ์ "์ข
๋ฅ: ์ถ์ฒ ์ฝํ
์ธ ์ ๋ชฉ (์ํฐ์คํธ/๊ฐ๋
/์๊ฐ ๋ฑ)" ํ์์ผ๋ก ์์ฑํ๊ณ , ๊ฐ๋จํ ์ถ์ฒ ์ด์ ๋ฅผ ๋ง๋ถ์ฌ์ค.
|
| 60 |
+
๊ฒฐ๊ณผ๋ Markdown ํ์์ผ๋ก ๋ณด๊ธฐ ์ข๊ฒ ์ ๋ฆฌํด์ค.
|
| 61 |
+
|
| 62 |
+
## [์์ฉ]
|
| 63 |
+
ํ์ฌ ๊ฐ์ ์ ๋ ๊น์ด ๋๋ผ๊ฑฐ๋ ์๋ก๋ฐ๊ณ ์ถ์ ๋.
|
| 64 |
+
|
| 65 |
+
## [์ ํ]
|
| 66 |
+
ํ์ฌ ๊ฐ์ ์์ ๋ฒ์ด๋ ์๋ก์ด ํ๋ ฅ์ ์ป๊ณ ์ถ์ ๋.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
response = model.generate_content(prompt)
|
| 70 |
+
end_time = time.time()
|
| 71 |
+
logging.info(f"Gemini API ํธ์ถ ์๋ฃ. ์์ ์๊ฐ: {end_time - start_time:.2f}์ด")
|
| 72 |
+
return response.text
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logging.error(f"๐ฅ๐ฅ๐ฅ Gemini API ํธ์ถ ์ค ์ค๋ฅ ๋ฐ์: {e} ๐ฅ๐ฅ๐ฅ")
|
| 75 |
+
return default_recommendations.get(predicted_emotion, "์ค๋์ ์ข์ํ๋ ์์
์ ๋ค์ผ๋ฉฐ ํธ์ํ ํ๋ฃจ๋ฅผ ๋ณด๋ด๋ ๊ฑด ์ด๋ ์ธ์?")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@bp.route("/")
|
| 79 |
+
def home():
|
| 80 |
+
if 'user_id' not in session:
|
| 81 |
+
return redirect(url_for('auth.login'))
|
| 82 |
+
logged_in = 'user_id' in session
|
| 83 |
+
display_name = None
|
| 84 |
+
if logged_in:
|
| 85 |
+
user_id = session.get('user_id')
|
| 86 |
+
user = User.query.get(user_id)
|
| 87 |
+
if user:
|
| 88 |
+
display_name = user.nickname if user.nickname else user.username
|
| 89 |
+
else:
|
| 90 |
+
display_name = session.get('username') # Fallback if user not found
|
| 91 |
+
logging.info(f"๋ฉ์ธ ํ์ด์ง ์ ์: ๋ก๊ทธ์ธ ์ํ: {logged_in}, ์ฌ์ฉ์: {display_name}")
|
| 92 |
+
return render_template("main.html", logged_in=logged_in, display_name=display_name)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@bp.route("/api/predict", methods=["POST"])
|
| 96 |
+
def api_predict():
|
| 97 |
+
if 'user_id' not in session:
|
| 98 |
+
return jsonify({"error": "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค."}), 401
|
| 99 |
+
|
| 100 |
+
user_diary = request.json.get("diary")
|
| 101 |
+
if not user_diary:
|
| 102 |
+
return jsonify({"error": "์ผ๊ธฐ ๋ด์ฉ์ด ์์ต๋๋ค."}), 400
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
# 1. Predict top 3 emotions
|
| 106 |
+
emotion_results = predict_emotion(user_diary, top_k=3)
|
| 107 |
+
|
| 108 |
+
if not emotion_results:
|
| 109 |
+
logging.error("[/api/predict] ๊ฐ์ ๋ถ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.")
|
| 110 |
+
return jsonify({"error": "๊ฐ์ ์ ๋ถ์ํ ์ ์์ต๋๋ค."}), 500
|
| 111 |
+
|
| 112 |
+
# 2. Process results
|
| 113 |
+
top_emotion_data = emotion_results[0]
|
| 114 |
+
top_emotion_label = top_emotion_data['label']
|
| 115 |
+
top_emotion_score = top_emotion_data['score']
|
| 116 |
+
|
| 117 |
+
# 3. Create candidates list
|
| 118 |
+
candidates = []
|
| 119 |
+
for result in emotion_results:
|
| 120 |
+
emotion_label = result['label']
|
| 121 |
+
candidates.append({
|
| 122 |
+
'emotion': emotion_label,
|
| 123 |
+
'score': result['score'],
|
| 124 |
+
'emoji': emotion_emoji_map.get(emotion_label, '๐ค')
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
# 4. Generate recommendation ONLY for the top emotion initially
|
| 128 |
+
recommendation_text = generate_recommendation(user_diary, top_emotion_label)
|
| 129 |
+
|
| 130 |
+
# 5. Return the new structure
|
| 131 |
+
# Note: Diary is NOT saved here. It will be saved via a separate '/diary/save' call later.
|
| 132 |
+
return jsonify({
|
| 133 |
+
"top_emotion": top_emotion_label,
|
| 134 |
+
"top_score": top_emotion_score,
|
| 135 |
+
"candidates": candidates,
|
| 136 |
+
"recommendation": recommendation_text
|
| 137 |
+
})
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logging.error(f"[/api/predict] ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}")
|
| 140 |
+
db.session.rollback() # ํน์ ๋ชจ๋ฅผ ํธ๋์ญ์
๋กค๋ฐฑ
|
| 141 |
+
return jsonify({"error": "์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."}), 500
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@bp.route("/api/recommend", methods=["POST"])
|
| 145 |
+
def api_recommend():
|
| 146 |
+
logging.info("[/api/recommend] ์์ฒญ ์์ ๋จ.")
|
| 147 |
+
user_diary = request.json.get("diary")
|
| 148 |
+
predicted_emotion = request.json.get("emotion") # ๊ฐ์ ์ ์ง์ ๋ฐ์
|
| 149 |
+
|
| 150 |
+
if not user_diary or not predicted_emotion:
|
| 151 |
+
logging.warning("[/api/recommend] ์ผ๊ธฐ ๋ด์ฉ ๋๋ ๊ฐ์ ์ด ์์ต๋๋ค.")
|
| 152 |
+
return jsonify({"error": "์ผ๊ธฐ ๋ด์ฉ ๋๋ ๊ฐ์ ์ด ์์ต๋๋ค."}), 400
|
| 153 |
+
|
| 154 |
+
recommendation_text = generate_recommendation(user_diary, predicted_emotion)
|
| 155 |
+
|
| 156 |
+
response_data = {
|
| 157 |
+
"emotion": predicted_emotion,
|
| 158 |
+
"emoji": emotion_emoji_map.get(predicted_emotion, '๐ค'),
|
| 159 |
+
"recommendation": recommendation_text
|
| 160 |
+
}
|
| 161 |
+
return jsonify(response_data)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@bp.route('/api/diaries')
|
| 165 |
+
def api_diaries():
|
| 166 |
+
if 'user_id' not in session:
|
| 167 |
+
return jsonify({"error": "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค."}), 401
|
| 168 |
+
|
| 169 |
+
user_id = session['user_id']
|
| 170 |
+
year = request.args.get('year', type=int)
|
| 171 |
+
month = request.args.get('month', type=int)
|
| 172 |
+
|
| 173 |
+
logging.info(f"API ์์ฒญ: user_id={user_id}, year={year}, month={month}")
|
| 174 |
+
|
| 175 |
+
if not year or not month:
|
| 176 |
+
today = datetime.date.today()
|
| 177 |
+
year = today.year
|
| 178 |
+
month = today.month
|
| 179 |
+
|
| 180 |
+
start_date = datetime.date(year, month, 1)
|
| 181 |
+
if month == 12:
|
| 182 |
+
end_date = datetime.date(year + 1, 1, 1)
|
| 183 |
+
else:
|
| 184 |
+
end_date = datetime.date(year, month + 1, 1)
|
| 185 |
+
|
| 186 |
+
logging.info(f"DB ์ฟผ๋ฆฌ ๋ฒ์: {start_date} <= created_at < {end_date}")
|
| 187 |
+
|
| 188 |
+
user_diaries = Diary.query.filter(
|
| 189 |
+
Diary.user_id == user_id,
|
| 190 |
+
Diary.created_at >= start_date,
|
| 191 |
+
Diary.created_at < end_date
|
| 192 |
+
).order_by(Diary.created_at.asc()).all()
|
| 193 |
+
|
| 194 |
+
diaries_data = []
|
| 195 |
+
utc_tz = datetime.timezone.utc
|
| 196 |
+
kst_tz = datetime.timezone(datetime.timedelta(hours=9))
|
| 197 |
+
|
| 198 |
+
for diary in user_diaries:
|
| 199 |
+
created_at_utc = diary.created_at
|
| 200 |
+
|
| 201 |
+
# ํ์์กด ์ ๋ณด๊ฐ ์๋ ๊ฒฝ์ฐ UTC๋ก ๊ฐ์ฃผ
|
| 202 |
+
if created_at_utc.tzinfo is None:
|
| 203 |
+
created_at_utc = created_at_utc.replace(tzinfo=utc_tz)
|
| 204 |
+
|
| 205 |
+
created_at_kst = created_at_utc.astimezone(kst_tz)
|
| 206 |
+
|
| 207 |
+
diaries_data.append({
|
| 208 |
+
"id": diary.id,
|
| 209 |
+
"date": created_at_kst.strftime('%Y-%m-%d'),
|
| 210 |
+
"createdAt": created_at_kst.strftime('%Y-%m-%d %H:%M:%S'),
|
| 211 |
+
"content": diary.content,
|
| 212 |
+
"emotion": diary.emotion,
|
| 213 |
+
"recommendation": diary.recommendation
|
| 214 |
+
})
|
| 215 |
+
|
| 216 |
+
return jsonify(diaries_data)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
@bp.route('/api/diaries/counts')
|
| 220 |
+
def api_diaries_counts():
|
| 221 |
+
if 'user_id' not in session:
|
| 222 |
+
return jsonify({"error": "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค."}), 401
|
| 223 |
+
|
| 224 |
+
user_id = session['user_id']
|
| 225 |
+
year = request.args.get('year', type=int)
|
| 226 |
+
|
| 227 |
+
if not year:
|
| 228 |
+
year = datetime.date.today().year
|
| 229 |
+
|
| 230 |
+
# PostgreSQL์ extract๋ฅผ ์ง์ํ๋ฏ๋ก ์๋ ๋ก์ง์ผ๋ก ๋ณต์
|
| 231 |
+
counts = db.session.query(
|
| 232 |
+
extract('month', Diary.created_at).cast(String), # ์์ ๋ฌธ์์ด๋ก ์บ์คํ
|
| 233 |
+
db.func.count(Diary.id)
|
| 234 |
+
).filter(
|
| 235 |
+
Diary.user_id == user_id,
|
| 236 |
+
extract('year', Diary.created_at) == year
|
| 237 |
+
).group_by(
|
| 238 |
+
extract('month', Diary.created_at)
|
| 239 |
+
).all()
|
| 240 |
+
|
| 241 |
+
counts_dict = {month: count for month, count in counts}
|
| 242 |
+
|
| 243 |
+
return jsonify(counts_dict)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
@bp.route('/my_diary')
|
| 247 |
+
def my_diary():
|
| 248 |
+
if 'user_id' not in session:
|
| 249 |
+
return redirect(url_for('auth.login'))
|
| 250 |
+
|
| 251 |
+
user_id = session.get('user_id')
|
| 252 |
+
user = User.query.get(user_id)
|
| 253 |
+
display_name = user.nickname if user.nickname else user.username
|
| 254 |
+
|
| 255 |
+
return render_template('diary.html', display_name=display_name)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
@bp.route('/mypage')
|
| 259 |
+
def mypage():
|
| 260 |
+
if 'user_id' not in session:
|
| 261 |
+
return redirect(url_for('auth.login'))
|
| 262 |
+
|
| 263 |
+
user_id = session['user_id']
|
| 264 |
+
user = User.query.get(user_id)
|
| 265 |
+
|
| 266 |
+
user_info = {
|
| 267 |
+
'username': user.username,
|
| 268 |
+
'nickname': user.nickname,
|
| 269 |
+
'display_name': user.nickname if user.nickname else user.username
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
return render_template('page.html', user_info=user_info)
|
| 273 |
+
|
| 274 |
+
@bp.route('/update_nickname', methods=['POST'])
|
| 275 |
+
def update_nickname():
|
| 276 |
+
if 'user_id' not in session:
|
| 277 |
+
return redirect(url_for('auth.login'))
|
| 278 |
+
|
| 279 |
+
user_id = session['user_id']
|
| 280 |
+
user = User.query.get(user_id)
|
| 281 |
+
|
| 282 |
+
new_nickname = request.form.get('nickname')
|
| 283 |
+
|
| 284 |
+
# ๋๋ค์์ด ๋น์ด์๊ฑฐ๋, ๊ณต๋ฐฑ๋ง ์์ ๊ฒฝ์ฐ None์ผ๋ก ์ ์ฅ
|
| 285 |
+
if not new_nickname or not new_nickname.strip():
|
| 286 |
+
user.nickname = None
|
| 287 |
+
else:
|
| 288 |
+
user.nickname = new_nickname
|
| 289 |
+
|
| 290 |
+
db.session.commit()
|
| 291 |
+
|
| 292 |
+
# ์ธ์
์ ๋ณด ์
๋ฐ์ดํธ (์ ํ ์ฌํญ, ๋๋ค์์ ์ธ์
์ ์ ์ฅํ ๊ฒฝ์ฐ)
|
| 293 |
+
# session['nickname'] = user.nickname
|
| 294 |
+
|
| 295 |
+
return redirect(url_for('main.mypage'))
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
@bp.route('/diary/save', methods=['POST'])
|
| 300 |
+
def diary_save():
|
| 301 |
+
if 'user_id' not in session:
|
| 302 |
+
return jsonify({"error": "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค."}), 401
|
| 303 |
+
|
| 304 |
+
user_id = session['user_id']
|
| 305 |
+
diary_content = request.form.get('diary')
|
| 306 |
+
predicted_emotion = request.form.get('emotion')
|
| 307 |
+
|
| 308 |
+
if not diary_content or not predicted_emotion:
|
| 309 |
+
return jsonify({"error": "์ผ๊ธฐ ๋ด์ฉ์ด๋ ๊ฐ์ ์ด ์์ต๋๋ค."}), 400
|
| 310 |
+
|
| 311 |
+
try:
|
| 312 |
+
# ์ถ์ฒ ์์ฑ
|
| 313 |
+
recommendation_text = generate_recommendation(diary_content, predicted_emotion)
|
| 314 |
+
|
| 315 |
+
# Gemini API ์คํจ ์ Recommender ํด๋์ค๋ก ๋์ฒด
|
| 316 |
+
if recommendation_text is None:
|
| 317 |
+
logging.info("Gemini ์ถ์ฒ ์คํจ. Recommender ํด๋์ค๋ก ๋์ฒดํฉ๋๋ค.")
|
| 318 |
+
su_yoong_recs = recommender.recommend(predicted_emotion, '์์ฉ')
|
| 319 |
+
jeon_hwan_recs = recommender.recommend(predicted_emotion, '์ ํ')
|
| 320 |
+
|
| 321 |
+
# diary_logic.js๊ฐ ํ์ฑํ ์ ์๋ ํ์์ผ๋ก ๋ง๋ญ๋๋ค.
|
| 322 |
+
recommendation_text = f"## [์์ฉ]\n"
|
| 323 |
+
for rec in su_yoong_recs:
|
| 324 |
+
recommendation_text += f"* {rec}\n"
|
| 325 |
+
|
| 326 |
+
recommendation_text += f"\n## [์ ํ]\n"
|
| 327 |
+
for rec in jeon_hwan_recs:
|
| 328 |
+
recommendation_text += f"* {rec}\n"
|
| 329 |
+
|
| 330 |
+
# ์ผ๊ธฐ ์ ์ฅ
|
| 331 |
+
new_diary = Diary(
|
| 332 |
+
content=diary_content,
|
| 333 |
+
emotion=predicted_emotion,
|
| 334 |
+
recommendation=recommendation_text,
|
| 335 |
+
user_id=user_id
|
| 336 |
+
)
|
| 337 |
+
db.session.add(new_diary)
|
| 338 |
+
db.session.commit()
|
| 339 |
+
|
| 340 |
+
return jsonify({
|
| 341 |
+
"success": "์ผ๊ธฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ ์ฅ๋์์ต๋๋ค.",
|
| 342 |
+
"recommendation": recommendation_text # ํด๋ผ์ด์ธํธ์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋๋ก ์ถ์ฒ ๋ด์ฉ ๋ฐํ
|
| 343 |
+
}), 200
|
| 344 |
+
|
| 345 |
+
except Exception as e:
|
| 346 |
+
db.session.rollback()
|
| 347 |
+
logging.error(f"์ผ๊ธฐ ์ ์ฅ ์ค ์ค๋ฅ ๋ฐ์: {e}")
|
| 348 |
+
return jsonify({"error": "์ผ๊ธฐ ์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."}), 500
|
| 349 |
+
|
| 350 |
+
@bp.route('/diary/delete/<string:diary_id>', methods=['DELETE'])
|
| 351 |
+
def delete_diary(diary_id):
|
| 352 |
+
if 'user_id' not in session:
|
| 353 |
+
return jsonify({"error": "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค."}), 401
|
| 354 |
+
|
| 355 |
+
diary_to_delete = Diary.query.get(diary_id)
|
| 356 |
+
|
| 357 |
+
if not diary_to_delete:
|
| 358 |
+
return jsonify({"error": "์ผ๊ธฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."}), 404
|
| 359 |
+
|
| 360 |
+
if diary_to_delete.user_id != session['user_id']:
|
| 361 |
+
return jsonify({"error": "์ญ์ ๊ถํ์ด ์์ต๋๋ค."}), 403
|
| 362 |
+
|
| 363 |
+
try:
|
| 364 |
+
db.session.delete(diary_to_delete)
|
| 365 |
+
db.session.commit()
|
| 366 |
+
return jsonify({"success": "์ผ๊ธฐ๊ฐ ์ญ์ ๋์์ต๋๋ค."}), 200
|
| 367 |
+
except Exception as e:
|
| 368 |
+
db.session.rollback()
|
| 369 |
+
return jsonify({"error": "์ญ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."}), 500
|
| 370 |
+
|
| 371 |
+
@bp.route('/test/animation')
|
| 372 |
+
def test_animation():
|
| 373 |
+
return render_template('test_animation.html', display_name='ํ
์คํธ')
|
src/models.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from . import db
|
| 2 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 3 |
+
import uuid
|
| 4 |
+
from sqlalchemy.sql import func
|
| 5 |
+
import datetime
|
| 6 |
+
from datetime import timezone, timedelta
|
| 7 |
+
|
| 8 |
+
class User(db.Model):
|
| 9 |
+
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 10 |
+
username = db.Column(db.String(80), unique=True, nullable=False)
|
| 11 |
+
nickname = db.Column(db.String(80), nullable=True)
|
| 12 |
+
|
| 13 |
+
password_hash = db.Column(db.String(256), nullable=False)
|
| 14 |
+
|
| 15 |
+
diaries = db.relationship('Diary', backref='author', lazy=True, cascade="all, delete-orphan")
|
| 16 |
+
|
| 17 |
+
def set_password(self, password):
|
| 18 |
+
self.password_hash = generate_password_hash(password)
|
| 19 |
+
|
| 20 |
+
def check_password(self, password):
|
| 21 |
+
return check_password_hash(self.password_hash, password)
|
| 22 |
+
|
| 23 |
+
class Diary(db.Model):
|
| 24 |
+
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 25 |
+
user_id = db.Column(db.String(36), db.ForeignKey('user.id'), nullable=False)
|
| 26 |
+
content = db.Column(db.Text, nullable=False)
|
| 27 |
+
emotion = db.Column(db.String(20), nullable=False)
|
| 28 |
+
recommendation = db.Column(db.Text, nullable=True)
|
| 29 |
+
created_at = db.Column(db.DateTime(timezone=True), default=func.now())
|
| 30 |
+
|
| 31 |
+
__table_args__ = (db.Index('idx_diary_user_id_created_at', "user_id", "created_at"),)
|
src/recommender.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class Recommender:
|
| 2 |
+
def __init__(self):
|
| 3 |
+
self.recommendation_db = {
|
| 4 |
+
'๊ธฐ์จ': { # ๊ธฐ์จ
|
| 5 |
+
'์์ฉ': ["์์
: Pharrell Williams - Happy", "์ํ: ์ํฐ์ ์์์ ํ์ค์ด ๋๋ค"],
|
| 6 |
+
'์ ํ': ["์์
: ์ด๋ฃจ๋ง - River Flows In You", "์ํ: ์ผ์ํฌ ํ์ถ"]
|
| 7 |
+
},
|
| 8 |
+
'์ฌํ': { # ์ฌํ
|
| 9 |
+
'์์ฉ': ["์์
: ๋ฐํจ์ - ๋์ ๊ฝ", "์ํ: ์ดํฐ๋ ์ ์ค์ธ"],
|
| 10 |
+
'์ ํ': ["์์
: ๊ฑฐ๋ถ์ด - ๋นํ๊ธฐ", "์ํ: ์-E"]
|
| 11 |
+
},
|
| 12 |
+
'๋ถ๋
ธ': { # ๋ถ๋
ธ
|
| 13 |
+
'์์ฉ': ["์์
: ๋์ํ์ธ - Du Hast", "์ํ: ์กด ์
"],
|
| 14 |
+
'์ ํ': ["์์
: ๋
ธ๋ผ ์กด์ค - Don't Know Why", "์ํ: ๋ฆฌํ ํฌ๋ ์คํธ"]
|
| 15 |
+
},
|
| 16 |
+
'๋ถ์': { # ๋ถ์
|
| 17 |
+
'์์ฉ': ["์์
: ์๋ก๊ฐ ๋๋ ์ฐ์ฃผ๊ณก ํ๋ ์ด๋ฆฌ์คํธ", "์ํ: ์ธ์ฌ์ด๋ ์์"],
|
| 18 |
+
'์ ํ': ["์์
: Maroon 5 - Moves Like Jagger", "์ํ: ๊ทนํ์ง์
"]
|
| 19 |
+
},
|
| 20 |
+
'๋๋': { # ๋๋
|
| 21 |
+
'์์ฉ': ["์ํ: ์์ค ์ผ์ค", "์์
: ๋ฐ์ง์ - ์ด๋จธ๋์ด ๋๊ตฌ๋"],
|
| 22 |
+
'์ ํ': ["์์
: Bach - Air on G String", "์ฑ
: ๊ณ ์ํ ์๋ก ๋ฐ์์ง๋ ๊ฒ๋ค"]
|
| 23 |
+
},
|
| 24 |
+
'๋นํฉ': { # ๋นํฉ
|
| 25 |
+
'์์ฉ': ["์์
: ์์ํ Lo-fi ํ๋ ์ด๋ฆฌ์คํธ", "์ํ: ํจํฐ์จ"],
|
| 26 |
+
'์ ํ': ["์์
: Queen - Don't Stop Me Now", "์ํ: ์คํ์ด๋๋งจ: ๋ด ์ ๋๋ฒ์ค"]
|
| 27 |
+
},
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def recommend(self, emotion: str, choice: str) -> list:
|
| 31 |
+
return self.recommendation_db.get(emotion, {}).get(choice, ["๐ฅ ์์ฝ์ง๋ง, ์์ง ์ค๋น๋ ์ถ์ฒ์ด ์์ด์."])
|
src/templates/_macros.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% macro auth_form(title, form_action, bottom_link_url, bottom_link_text) %}
|
| 2 |
+
<div class="container">
|
| 3 |
+
<h1>{{ title }}</h1>
|
| 4 |
+
<form method="post" action="{{ form_action }}">
|
| 5 |
+
<div class="form-group">
|
| 6 |
+
<label for="username">์ฌ์ฉ์ ์ด๋ฆ</label>
|
| 7 |
+
<input type="text" id="username" name="username" required>
|
| 8 |
+
</div>
|
| 9 |
+
<div class="form-group">
|
| 10 |
+
<label for="password">๋น๋ฐ๋ฒํธ</label>
|
| 11 |
+
<input type="password" id="password" name="password" required>
|
| 12 |
+
</div>
|
| 13 |
+
<button type="submit">{{ title }}</button>
|
| 14 |
+
</form>
|
| 15 |
+
<p>{{ bottom_link_text }} <a href="{{ bottom_link_url }}">{{ '๋ก๊ทธ์ธ' if 'signup' in bottom_link_url else 'ํ์๊ฐ์
' }}</a></p>
|
| 16 |
+
</div>
|
| 17 |
+
{% endmacro %}
|
| 18 |
+
|
| 19 |
+
{% macro recommendation_tabs(acceptance_content, diversion_content, container_id) %}
|
| 20 |
+
<div class="rec-tabs">
|
| 21 |
+
<button class="rec-tab-btn active" data-tab="{{ container_id }}-acceptance">์์ฉ</button>
|
| 22 |
+
<button class="rec-tab-btn" data-tab="{{ container_id }}-diversion">์ ํ</button>
|
| 23 |
+
</div>
|
| 24 |
+
<div id="{{ container_id }}-acceptance" class="rec-content active">
|
| 25 |
+
{{ acceptance_content | safe }}
|
| 26 |
+
</div>
|
| 27 |
+
<div id="{{ container_id }}-diversion" class="rec-content">
|
| 28 |
+
{{ diversion_content | safe }}
|
| 29 |
+
</div>
|
| 30 |
+
{% endmacro %}
|
src/templates/auth_combined.html
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base_auth.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}๋ก๊ทธ์ธ / ํ์๊ฐ์
{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="book-container">
|
| 7 |
+
<div class="layer-base">
|
| 8 |
+
<div class="base-box left-form">
|
| 9 |
+
<h2>๋ก๊ทธ์ธ</h2>
|
| 10 |
+
<form method="post" action="{{ url_for('auth.login') }}">
|
| 11 |
+
<div class="input-group">
|
| 12 |
+
<label>์์ด๋</label>
|
| 13 |
+
<input type="text" name="username" required>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="input-group">
|
| 16 |
+
<label>๋น๋ฐ๋ฒํธ</label>
|
| 17 |
+
<input type="password" name="password" required>
|
| 18 |
+
</div>
|
| 19 |
+
<button type="submit" class="action-btn">๋ก๊ทธ์ธ</button>
|
| 20 |
+
</form>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="base-box right-form">
|
| 24 |
+
<h2>ํ์๊ฐ์
</h2>
|
| 25 |
+
<form method="post" action="{{ url_for('auth.signup') }}">
|
| 26 |
+
<div class="input-group">
|
| 27 |
+
<label>์์ด๋</label>
|
| 28 |
+
<input type="text" name="username" required>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="input-group">
|
| 31 |
+
<label>๋น๋ฐ๋ฒํธ</label>
|
| 32 |
+
<input type="password" name="password" required>
|
| 33 |
+
</div>
|
| 34 |
+
<button type="submit" class="action-btn">๊ฐ์
ํ๊ธฐ</button>
|
| 35 |
+
</form>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div class="layer-flipper" id="flipper">
|
| 40 |
+
|
| 41 |
+
<div class="page-face front-face info-side">
|
| 42 |
+
<div class="content-wrapper">
|
| 43 |
+
<h2>๋น์ ์ ์ค๋์<br>์ด๋ ๋์?</h2>
|
| 44 |
+
<p>๋ง์ ์ ์ด์ผ๊ธฐ๋ฅผ<br>๊ธฐ๋กํ๋ฌ ์ค์
จ๋์?</p>
|
| 45 |
+
<button class="ghost-btn" onclick="turnPage()">์ฒซ ์ผ๊ธฐ ์ฐ๋ฌ ๊ฐ๊ธฐ</button>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div class="page-face back-face info-side">
|
| 50 |
+
<div class="content-wrapper">
|
| 51 |
+
<h2>๊ธฐ๋ค๋ฆฌ๊ณ <br>์์์ด์!</h2>
|
| 52 |
+
<p>์ค๋ ํ๋ฃจ๋<br>์ ๋ง ์๊ณ ๋ง์ผ์
จ์ต๋๋ค.</p>
|
| 53 |
+
<button class="ghost-btn" onclick="turnPage()">๋ด ์ผ๊ธฐ์ฅ ์ด๊ธฐ</button>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<script>
|
| 60 |
+
// ํ์ด์ง ๋๊ธฐ๊ธฐ ํจ์
|
| 61 |
+
function turnPage() {
|
| 62 |
+
document.getElementById('flipper').classList.toggle('flipped');
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ํ์๊ฐ์
์คํจ ์(mode=signup) ์๋์ผ๋ก ํ์ด์ง ๋๊ฒจ๋๊ธฐ
|
| 66 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 67 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 68 |
+
if (urlParams.get('mode') === 'signup') {
|
| 69 |
+
document.getElementById('flipper').classList.add('flipped');
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
</script>
|
| 73 |
+
{% endblock %}
|
src/templates/base.html
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}Emotion Diary{% endblock %}</title>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 8 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ko.js"></script>
|
| 11 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 12 |
+
<script src="{{ url_for('static', filename='js/theme_loader.js') }}"></script>
|
| 13 |
+
{% block head %}{% endblock %}
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<header class="navbar-container">
|
| 17 |
+
<nav class="navbar">
|
| 18 |
+
<a href="{{ url_for('main.home') }}" class="navbar-brand">Emotion Diary</a>
|
| 19 |
+
|
| 20 |
+
<ul class="navbar-menu">
|
| 21 |
+
<div class="nav-marker"></div>
|
| 22 |
+
|
| 23 |
+
{% if 'user_id' in session or logged_in %}
|
| 24 |
+
<li class="nav-item"><a href="{{ url_for('main.home') }}">์ผ๊ธฐ ์ฐ๊ธฐ</a></li>
|
| 25 |
+
<li class="nav-item"><a href="{{ url_for('main.my_diary') }}">๋์ ์ผ๊ธฐ</a></li>
|
| 26 |
+
<li class="nav-item"><a href="{{ url_for('main.mypage') }}">๋ง์ดํ์ด์ง</a></li>
|
| 27 |
+
<li class="nav-item"><a href="{{ url_for('auth.logout') }}">๋ก๊ทธ์์</a></li>
|
| 28 |
+
{% else %}
|
| 29 |
+
<li class="nav-item"><a href="{{ url_for('auth.login') }}">๋ก๊ทธ์ธ</a></li>
|
| 30 |
+
<li class="nav-item"><a href="{{ url_for('auth.signup') }}">ํ์๊ฐ์
</a></li>
|
| 31 |
+
{% endif %}
|
| 32 |
+
</ul>
|
| 33 |
+
</nav>
|
| 34 |
+
</header>
|
| 35 |
+
|
| 36 |
+
<main class="container">
|
| 37 |
+
{% block content %}{% endblock %}
|
| 38 |
+
</main>
|
| 39 |
+
|
| 40 |
+
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
| 41 |
+
{% block scripts %}{% endblock %}
|
| 42 |
+
</body>
|
| 43 |
+
</html>
|
src/templates/base_auth.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>{% block title %}์ธ์ฆ{% endblock %}</title>
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/base_auth.css') }}">
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
{% block content %}{% endblock %}
|
| 10 |
+
|
| 11 |
+
{% with messages = get_flashed_messages() %}
|
| 12 |
+
{% if messages %}
|
| 13 |
+
<script>
|
| 14 |
+
alert("{{ messages[0] }}");
|
| 15 |
+
</script>
|
| 16 |
+
{% endif %}
|
| 17 |
+
{% endwith %}
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
src/templates/diary.html
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ display_name }}'s Dashboard{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block head %}
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/diary.css') }}">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
| 8 |
+
{% endblock %}
|
| 9 |
+
|
| 10 |
+
{% block content %}
|
| 11 |
+
<div class="dashboard-container">
|
| 12 |
+
<!-- 1์ด: ์ข์ธก ์ฌ์ด๋๋ฐ -->
|
| 13 |
+
<aside class="sidebar">
|
| 14 |
+
<div class="year-selector">
|
| 15 |
+
<button id="prev-year">โ</button>
|
| 16 |
+
<h2 id="current-year">2025</h2>
|
| 17 |
+
<button id="next-year">โถ</button>
|
| 18 |
+
</div>
|
| 19 |
+
<ul class="month-list">
|
| 20 |
+
<li class="month-item" data-month="0" style="display: flex; justify-content: space-between;"><span>Jan</span> <span class="diary-count"></span></li>
|
| 21 |
+
<li class="month-item" data-month="1" style="display: flex; justify-content: space-between;"><span>Feb</span> <span class="diary-count"></span></li>
|
| 22 |
+
<li class="month-item" data-month="2" style="display: flex; justify-content: space-between;"><span>Mar</span> <span class="diary-count"></span></li>
|
| 23 |
+
<li class="month-item" data-month="3" style="display: flex; justify-content: space-between;"><span>Apr</span> <span class="diary-count"></span></li>
|
| 24 |
+
<li class="month-item" data-month="4" style="display: flex; justify-content: space-between;"><span>May</span> <span class="diary-count"></span></li>
|
| 25 |
+
<li class="month-item" data-month="5" style="display: flex; justify-content: space-between;"><span>Jun</span> <span class="diary-count"></span></li>
|
| 26 |
+
<li class="month-item" data-month="6" style="display: flex; justify-content: space-between;"><span>Jul</span> <span class="diary-count"></span></li>
|
| 27 |
+
<li class="month-item" data-month="7" style="display: flex; justify-content: space-between;"><span>Aug</span> <span class="diary-count"></span></li>
|
| 28 |
+
<li class="month-item" data-month="8" style="display: flex; justify-content: space-between;"><span>Sep</span> <span class="diary-count"></span></li>
|
| 29 |
+
<li class="month-item" data-month="9" style="display: flex; justify-content: space-between;"><span>Oct</span> <span class="diary-count"></span></li>
|
| 30 |
+
<li class="month-item" data-month="10" style="display: flex; justify-content: space-between;"><span>Nov</span> <span class="diary-count"></span></li>
|
| 31 |
+
<li class="month-item" data-month="11" style="display: flex; justify-content: space-between;"><span>Dec</span> <span class="diary-count"></span></li>
|
| 32 |
+
</ul>
|
| 33 |
+
<div class="sidebar-footer">
|
| 34 |
+
<p>Emotion Diary</p>
|
| 35 |
+
</div>
|
| 36 |
+
</aside>
|
| 37 |
+
|
| 38 |
+
<!-- 2์ด: ์ค์ ๋ฌ๋ ฅ -->
|
| 39 |
+
<main class="calendar-area">
|
| 40 |
+
<div class="calendar-header">
|
| 41 |
+
<h2 id="calendar-month-title">November</h2>
|
| 42 |
+
<button class="add-btn" title="์ค๋ ๋ ์ง๋ก ์ ์ผ๊ธฐ ์์ฑ (๊ตฌํ ์์ )">+ Add</button>
|
| 43 |
+
</div>
|
| 44 |
+
<div id="calendar"></div>
|
| 45 |
+
</main>
|
| 46 |
+
|
| 47 |
+
<!-- 3์ด: ์ฐ์ธก ํ์๋ผ์ธ -->
|
| 48 |
+
<section class="timeline-area">
|
| 49 |
+
<h2 class="timeline-title">์ผ๊ธฐ ๋ชฉ๋ก</h2>
|
| 50 |
+
<div id="diary-list-container">
|
| 51 |
+
<div class="placeholder">
|
| 52 |
+
<p>๋ ์ง๋ฅผ ์ ํํ๋ฉด<br>์ด๊ณณ์ ์ผ๊ธฐ๊ฐ ํ์๋ฉ๋๋ค.</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</section>
|
| 56 |
+
</div>
|
| 57 |
+
<!-- Recommendation Modal -->
|
| 58 |
+
<div id="rec-modal-overlay" class="modal-overlay">
|
| 59 |
+
<div id="rec-modal-content" class="modal-content">
|
| 60 |
+
<button id="rec-modal-close" class="modal-close-btn">×</button>
|
| 61 |
+
<h2 id="rec-modal-title"></h2>
|
| 62 |
+
<div id="rec-modal-body"></div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<!-- Diary Detail Modal -->
|
| 67 |
+
<div id="diary-detail-modal-overlay" class="diary-detail-modal-overlay">
|
| 68 |
+
<div class="diary-detail-modal-content">
|
| 69 |
+
<button id="diary-detail-modal-close" class="diary-detail-modal-close">×</button>
|
| 70 |
+
<h2 id="diary-detail-title"></h2>
|
| 71 |
+
<div id="diary-detail-body">
|
| 72 |
+
<!-- ๋ด์ฉ์ JS๋ฅผ ํตํด ๋์ ์ผ๋ก ์ฑ์์ง๋๋ค -->
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
{% endblock %}
|
| 77 |
+
|
| 78 |
+
{% block scripts %}
|
| 79 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 80 |
+
<script src="{{ url_for('static', filename='js/diary_logic.js') }}"></script>
|
| 81 |
+
{% endblock %}
|
src/templates/login.html
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base_auth.html" %}
|
| 2 |
+
{% from "_macros.html" import auth_form %}
|
| 3 |
+
|
| 4 |
+
{% block title %}๋ก๊ทธ์ธ{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block content %}
|
| 7 |
+
{{ auth_form('๋ก๊ทธ์ธ', url_for('auth.login'), url_for('auth.signup'), '๊ณ์ ์ด ์์ผ์ ๊ฐ์?') }}
|
| 8 |
+
{% endblock %}
|
src/templates/main.html
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}๊ฐ์ ์ผ๊ธฐ{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block head %}
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
| 7 |
+
{% endblock %}
|
| 8 |
+
|
| 9 |
+
{% block content %}
|
| 10 |
+
<!-- Onboarding Modal -->
|
| 11 |
+
<div id="onboarding-overlay" class="onboarding-overlay">
|
| 12 |
+
<div class="onboarding-modal">
|
| 13 |
+
<div id="slide1" class="onboarding-slide active">
|
| 14 |
+
<h2>ํ์ํฉ๋๋ค!</h2>
|
| 15 |
+
<p>Emotion Diary๋ ๋น์ ์ ํ๋ฃจ๋ฅผ ๊ธฐ๋กํ๊ณ ๊ฐ์ ์ ๋ถ์ํด์ฃผ๋ ์๋น์ค์
๋๋ค.</p>
|
| 16 |
+
</div>
|
| 17 |
+
<div id="slide2" class="onboarding-slide">
|
| 18 |
+
<h2>์ผ๊ธฐ ์์ฑ</h2>
|
| 19 |
+
<p>์ค๋ ์์๋ ์ผ์ ์์ ๋กญ๊ฒ ์์ฑํด์ฃผ์ธ์. ๋น์ ์ ์ด์ผ๊ธฐ์ ๊ท ๊ธฐ์ธ์ผ๊ฒ์.</p>
|
| 20 |
+
</div>
|
| 21 |
+
<div id="slide3" class="onboarding-slide">
|
| 22 |
+
<h2>๊ฐ์ ๋ถ์๊ณผ ์์
์ถ์ฒ</h2>
|
| 23 |
+
<p>์์ฑ๋ ์ผ๊ธฐ๋ฅผ ๋ฐํ์ผ๋ก ๊ฐ์ ์ ๋ถ์ํ๊ณ , ์ง๊ธ ๋น์ ์๊ฒ ์ด์ธ๋ฆฌ๋ ์์
์ ์ถ์ฒํด๋๋ฆฝ๋๋ค.</p>
|
| 24 |
+
</div>
|
| 25 |
+
<div id="slide4" class="onboarding-slide">
|
| 26 |
+
<h2>์ ํ๋ 80%</h2>
|
| 27 |
+
<p>์ ํฌ ์๋น์ค๋ ํ
์คํธ ๊ฐ์ ๋ถ์์์ ์ฝ 80%์ ์ ํ๋๋ฅผ ์๋ํฉ๋๋ค.</p>
|
| 28 |
+
</div>
|
| 29 |
+
<div id="slide5" class="onboarding-slide">
|
| 30 |
+
<h2>๋๋ค์ ์ค์ </h2>
|
| 31 |
+
<p>์๋น์ค์์ ์ฌ์ฉํ์ค ๋๋ค์์ ์ค์ ํด์ฃผ์ธ์. (์ ํ ์ฌํญ)</p>
|
| 32 |
+
<input type="text" id="onboarding-nickname-input" placeholder="๋๋ค์์ ์
๋ ฅํ์ธ์" style="width: calc(100% - 20px); padding: 10px; margin-top: 15px; border: 1px solid #ddd; border-radius: 5px;">
|
| 33 |
+
</div>
|
| 34 |
+
<div class="onboarding-nav">
|
| 35 |
+
<button id="onboarding-prev" class="onboarding-btn" style="display: none;">์ด์ </button>
|
| 36 |
+
<button id="onboarding-close" class="onboarding-btn">๋ค์ ๋ณด์ง ์๊ธฐ</button>
|
| 37 |
+
<div class="onboarding-dots">
|
| 38 |
+
<span class="onboarding-dot active" data-slide="1"></span>
|
| 39 |
+
<span class="onboarding-dot" data-slide="2"></span>
|
| 40 |
+
<span class="onboarding-dot" data-slide="3"></span>
|
| 41 |
+
<span class="onboarding-dot" data-slide="4"></span>
|
| 42 |
+
<span class="onboarding-dot" data-slide="5"></span>
|
| 43 |
+
</div>
|
| 44 |
+
<button id="onboarding-next" class="onboarding-btn">๋ค์</button>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<!-- ํ
๋ง ์์ ๋ณ๊ฒฝ ํ๋ ํธ -->
|
| 50 |
+
<div class="theme-palette">
|
| 51 |
+
<div class="theme-option-wrapper">
|
| 52 |
+
<button class="theme-btn" data-color="default" data-theme-group="default" title="๊ธฐ๋ณธ ํ
๋ง">๐จ</button>
|
| 53 |
+
<div class="sub-options" data-theme-group="default">
|
| 54 |
+
<button class="sub-option-btn" data-color="current" title="๋ณด๋ผ์">๋ณด๋ผ์</button>
|
| 55 |
+
<button class="sub-option-btn" data-color="blue" title="ํ๋์">ํ๋์</button>
|
| 56 |
+
<button class="sub-option-btn" data-color="lightyellow" title="์ฐํ ๋
ธ๋์">๋
ธ๋์</button>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="theme-option-wrapper">
|
| 60 |
+
<button class="theme-btn" data-color="sunset" data-theme-group="sunset" title="์ ๋
๋
ธ์">๐</button>
|
| 61 |
+
<div class="sub-options" data-theme-group="sunset">
|
| 62 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sun1.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sun1.jpg" title="์ ๋
๋
ธ์ 1">
|
| 63 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sun2.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sun2.jpg" title="์ ๋
๋
ธ์ผ 2">
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="theme-option-wrapper">
|
| 67 |
+
<button class="theme-btn" data-color="forest" data-theme-group="forest" title="๊ณ ์ํ ์ฒ">๐ณ</button>
|
| 68 |
+
<div class="sub-options" data-theme-group="forest">
|
| 69 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/forest1.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/forest1.jpg" title="๊ณ ์ํ ์ฒ 1">
|
| 70 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/forest2.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/forest2.jpg" title="๊ณ ์ํ ์ฒ 2">
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="theme-option-wrapper">
|
| 74 |
+
<button class="theme-btn" data-color="sky" data-theme-group="sky" title="ํ๋ ํ
๋ง">โ๏ธ</button>
|
| 75 |
+
<div class="sub-options" data-theme-group="sky">
|
| 76 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sky1.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sky1.jpg" title="ํ๋ 1">
|
| 77 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sky2.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sky2.jpg" title="ํ๋ 2">
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
<div class="theme-option-wrapper">
|
| 81 |
+
<button class="theme-btn" data-color="night" data-theme-group="night" title="๋ณ๋น ๋ฐค">๐</button>
|
| 82 |
+
<div class="sub-options" data-theme-group="night">
|
| 83 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/city1.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/city1.jpg" title="๋ณ๋น ๋ฐค 1">
|
| 84 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/city2.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/city2.jpg" title="๋ณ๋น ๋ฐค 2">
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="theme-option-wrapper">
|
| 88 |
+
<button class="theme-btn" data-color="sea" data-theme-group="sea" title="ํธ๋ฅธ ๋ฐ๋ค">๐</button>
|
| 89 |
+
<div class="sub-options" data-theme-group="sea">
|
| 90 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sea1.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sea1.jpg" title="ํธ๋ฅธ ๋ฐ๋ค 1">
|
| 91 |
+
<img src="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sea2.jpg" data-theme-bg="https://huggingface.co/datasets/taehoon222/emotion-images/resolve/main/sea2.jpg" title="ํธ๋ฅธ ๋ฐ๋ค 2">
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<div class="main-content">
|
| 97 |
+
<div class="diary-book">
|
| 98 |
+
|
| 99 |
+
<div class="book-page left-page chat-container">
|
| 100 |
+
<div class="page-header">
|
| 101 |
+
<h2>To. Clobe โ๏ธ</h2>
|
| 102 |
+
<span class="date-label">Nov 21</span> </div>
|
| 103 |
+
|
| 104 |
+
<textarea id="diary" placeholder="์ค๋ ๋น์ ์ ํ๋ฃจ๋ ์ด๋ ๋์? ์์งํ ๋ง์์ ์ ์ด์ฃผ์ธ์."></textarea>
|
| 105 |
+
|
| 106 |
+
<div class="button-container">
|
| 107 |
+
<button id="submit-btn" disabled>๋ถ์ํ๊ธฐ</button>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div class="book-spine"></div>
|
| 112 |
+
|
| 113 |
+
<div class="book-page right-page" id="result-container">
|
| 114 |
+
<div class="page-header">
|
| 115 |
+
<h2>From. Cloub ๐</h2>
|
| 116 |
+
<div id="save-action-container" style="display:none; text-align: center; margin-top: 20px;">
|
| 117 |
+
<button id="final-save-btn" class="save-btn">์ผ๊ธฐ์ฅ์ ์ ์ฅํ๊ธฐ</button>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div id="emotion-chips" class="emotion-chips" style="display:none;"></div>
|
| 122 |
+
|
| 123 |
+
<div id="result">
|
| 124 |
+
<div class="empty-state">
|
| 125 |
+
<p>์ผ์ชฝ ํ์ด์ง์ ์ผ๊ธฐ๋ฅผ ์ฐ๊ณ <br>๋ถ์ ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์.</p>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
<div id="save-status" style="margin-top: 1rem; text-align: center;"></div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- ๊ฐ์ ์ ํ UI (์ด๊ธฐ์๋ ์จ๊น) -->
|
| 135 |
+
<div id="emotion-choice-container" style="display: none;">
|
| 136 |
+
<div class="notification">
|
| 137 |
+
<p>AI๊ฐ ๋น์ ์ ๊ฐ์ ์ ๋ณตํฉ์ ์ผ๋ก ํ์
ํ์ด์.<br>ํ์ฌ ๋๋ผ๋ ๊ฐ์ฅ ์ฃผ๋ ๊ฐ์ ์ ์ ํํด์ฃผ์ธ์.</p>
|
| 138 |
+
</div>
|
| 139 |
+
<div id="emotion-chips">
|
| 140 |
+
<!-- ๊ฐ์ ์นฉ์ JS๋ฅผ ํตํด ๋์ ์ผ๋ก ์์ฑ๋ฉ๋๋ค. -->
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<!-- ์ ์ฅ ๋ฒํผ ์ปจํ
์ด๋ (์ด๊ธฐ์๋ ์จ๊น) -->
|
| 145 |
+
<div class="save-button-container" style="display: none; text-align: center; margin-top: 20px;">
|
| 146 |
+
<button id="save-diary-btn">์ผ๊ธฐ ์ ์ฅํ๊ธฐ</button>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<div id="save-status" style="margin-top: 1rem; text-align: center;"></div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
{% endblock %}
|
| 153 |
+
|
| 154 |
+
{% block scripts %}
|
| 155 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 156 |
+
<script src="{{ url_for('static', filename='js/main_onboarding.js') }}"></script>
|
| 157 |
+
<script src="{{ url_for('static', filename='js/main_logic.js') }}"></script>
|
| 158 |
+
<script src="{{ url_for('static', filename='js/main_theme.js') }}"></script>
|
| 159 |
+
{% endblock %}
|
src/templates/page.html
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ user_info.display_name }}๋์ ๋ง์ดํ์ด์ง{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block head %}
|
| 6 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/page.css') }}">
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
| 8 |
+
{% endblock %}
|
| 9 |
+
|
| 10 |
+
{% block content %}
|
| 11 |
+
|
| 12 |
+
<div class="mypage-box">
|
| 13 |
+
<h1>{{ user_info.display_name }}๋์ ๋ง์ ํ๋ ํธ</h1>
|
| 14 |
+
|
| 15 |
+
<ul class="info-list">
|
| 16 |
+
<li>
|
| 17 |
+
<span class="label">์์ด๋</span>
|
| 18 |
+
<span class="value">{{ user_info.username }}</span>
|
| 19 |
+
</li>
|
| 20 |
+
<li>
|
| 21 |
+
<span class="label">๋๋ค์</span>
|
| 22 |
+
<div class="value">
|
| 23 |
+
<form action="{{ url_for('main.update_nickname') }}" method="post" class="nickname-form">
|
| 24 |
+
<input type="text" name="nickname" value="{{ user_info.nickname or '' }}" placeholder="๋๋ค์์ ์
๋ ฅํ์ธ์">
|
| 25 |
+
<button type="submit">๋ณ๊ฒฝ</button>
|
| 26 |
+
</form>
|
| 27 |
+
</div>
|
| 28 |
+
</li>
|
| 29 |
+
</ul>
|
| 30 |
+
</div>
|
| 31 |
+
{% endblock %}
|
src/templates/signup.html
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base_auth.html" %}
|
| 2 |
+
{% from "_macros.html" import auth_form %}
|
| 3 |
+
|
| 4 |
+
{% block title %}ํ์๊ฐ์
{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block content %}
|
| 7 |
+
{{ auth_form('ํ์๊ฐ์
', url_for('auth.signup'), url_for('auth.login'), '์ด๋ฏธ ๊ณ์ ์ด ์์ผ์ ๊ฐ์?') }}
|
| 8 |
+
{% endblock %}
|
src/templates/static/css/base_auth.css
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* [๊ธฐ๋ณธ ์ค์ ] */
|
| 2 |
+
body {
|
| 3 |
+
background: #f0f2f5;
|
| 4 |
+
display: flex;
|
| 5 |
+
justify-content: center;
|
| 6 |
+
align-items: center;
|
| 7 |
+
height: 100vh;
|
| 8 |
+
margin: 0;
|
| 9 |
+
font-family: "Spoqa Han Sans Neo", sans-serif;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* --- ๋ฉ์ธ ์ปจํ
์ด๋ --- */
|
| 13 |
+
.book-container {
|
| 14 |
+
width: 800px;
|
| 15 |
+
height: 500px;
|
| 16 |
+
position: relative;
|
| 17 |
+
perspective: 1500px; /* 3D ์๊ทผ๊ฐ */
|
| 18 |
+
background-color: #fff; /* ํผ ๋ฐฐ๊ฒฝ์ */
|
| 19 |
+
border-radius: 20px;
|
| 20 |
+
box-shadow: 0 14px 28px rgba(0,0,0,0.15), 0 10px 10px rgba(0,0,0,0.12);
|
| 21 |
+
overflow: hidden; /* ๋ฅ๊ทผ ๋ชจ์๋ฆฌ ๋ฐ์ผ๋ก ๋๊ฐ๋ ๊ฒ ๋ฐฉ์ง */
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* [๋ ์ด์ด 1] ๋ฐ๋ฅ์ ๊น๋ฆฐ ํผ๋ค */
|
| 25 |
+
.layer-base {
|
| 26 |
+
width: 100%;
|
| 27 |
+
height: 100%;
|
| 28 |
+
display: flex; /* ์ข์ฐ ๋ฐฐ์น */
|
| 29 |
+
position: absolute;
|
| 30 |
+
top: 0;
|
| 31 |
+
left: 0;
|
| 32 |
+
z-index: 1;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.base-box {
|
| 36 |
+
width: 50%;
|
| 37 |
+
height: 100%;
|
| 38 |
+
padding: 40px;
|
| 39 |
+
box-sizing: border-box;
|
| 40 |
+
display: flex;
|
| 41 |
+
flex-direction: column;
|
| 42 |
+
justify-content: center;
|
| 43 |
+
align-items: center;
|
| 44 |
+
text-align: center;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* [๋ ์ด์ด 2] ์์ง์ด๋ ํ์ด์ง (Flipper) */
|
| 48 |
+
.layer-flipper {
|
| 49 |
+
width: 50%; /* ์ ์ฒด ๋๋น์ ์ ๋ฐ */
|
| 50 |
+
height: 100%;
|
| 51 |
+
position: absolute;
|
| 52 |
+
top: 0;
|
| 53 |
+
left: 50%; /* ์ค๋ฅธ์ชฝ ์ ๋ฐ์์ ์์ */
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
transform-origin: left center;
|
| 57 |
+
transform-style: preserve-3d;
|
| 58 |
+
transition: transform 0.8s cubic-bezier(0.645, 0.045, 0.355, 1.000); /* ๋ถ๋๋ฌ์ด ์ข
์ด ๋๊น ํจ๊ณผ */
|
| 59 |
+
z-index: 10;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* ํ์ด์ง๊ฐ ๋์ด๊ฐ ์ํ (์ผ์ชฝ์ผ๋ก -180๋ ํ์ ) */
|
| 63 |
+
.layer-flipper.flipped {
|
| 64 |
+
transform: rotateY(-180deg);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* --- ํ์ด์ง์ ์๋ฉด/๋ท๋ฉด ๋์์ธ --- */
|
| 68 |
+
.page-face {
|
| 69 |
+
position: absolute;
|
| 70 |
+
width: 100%;
|
| 71 |
+
height: 100%;
|
| 72 |
+
top: 0;
|
| 73 |
+
left: 0;
|
| 74 |
+
backface-visibility: hidden; /* ๋ท๋ฉด ์ ๋ณด์ด๊ฒ */
|
| 75 |
+
display: flex;
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
justify-content: center;
|
| 78 |
+
align-items: center;
|
| 79 |
+
text-align: center;
|
| 80 |
+
padding: 40px;
|
| 81 |
+
box-sizing: border-box;
|
| 82 |
+
|
| 83 |
+
/* ์๋ด ๋ฌธ๊ตฌ ๋ฐฐ๊ฒฝ (ํ๋์ ๊ทธ๋ผ๋ฐ์ด์
) */
|
| 84 |
+
background: linear-gradient(135deg, #6598e5, #5540a3);
|
| 85 |
+
color: white;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* ์๋ฉด (ํ์ํฉ๋๋ค) - ๊ธฐ๋ณธ ์ํ */
|
| 89 |
+
.front-face {
|
| 90 |
+
z-index: 2;
|
| 91 |
+
transform: rotateY(0deg);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ๋ท๋ฉด (๋์์ค์
จ๊ตฐ์) - ๋ฏธ๋ฆฌ 180๋ ๋๋ ค๋์ */
|
| 95 |
+
.back-face {
|
| 96 |
+
z-index: 1;
|
| 97 |
+
transform: rotateY(180deg);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* --- ๋ด๋ถ ์์ ์คํ์ผ (๋ฒํผ, ์
๋ ฅ์ฐฝ ๋ฑ) --- */
|
| 101 |
+
h2 { margin-bottom: 20px; font-size: 2rem; font-weight: 700; }
|
| 102 |
+
.input-group { width: 100%; margin-bottom: 15px; text-align: left; }
|
| 103 |
+
.input-group label { display: block; margin-bottom: 5px; font-size: 0.9rem; color: #666; }
|
| 104 |
+
.input-group input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; background: #f4f4f4; box-sizing: border-box; }
|
| 105 |
+
|
| 106 |
+
/* ๋ก๊ทธ์ธ/๊ฐ์
์ก์
๋ฒํผ */
|
| 107 |
+
.action-btn {
|
| 108 |
+
width: 100%; padding: 12px; background-color: #5540a3; color: white;
|
| 109 |
+
border: none; border-radius: 8px; font-size: 1rem; font-weight: bold;
|
| 110 |
+
cursor: pointer; margin-top: 10px; transition: 0.3s;
|
| 111 |
+
}
|
| 112 |
+
.action-btn:hover { background-color: #443383; }
|
| 113 |
+
|
| 114 |
+
/* ์ ๋ น ๋ฒํผ (ํ์ด์ง ๋๊ธฐ๊ธฐ์ฉ) */
|
| 115 |
+
.ghost-btn {
|
| 116 |
+
background: transparent; border: 2px solid white; color: white;
|
| 117 |
+
padding: 10px 30px; border-radius: 25px; font-weight: bold;
|
| 118 |
+
cursor: pointer; margin-top: 20px; transition: 0.2s;
|
| 119 |
+
}
|
| 120 |
+
.ghost-btn:hover { background: white; color: #5540a3; }
|
| 121 |
+
.content-wrapper p { font-size: 1.1rem; margin-bottom: 20px; line-height: 1.5; }
|
src/templates/static/css/diary.css
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* --- ๊ฐ์ ๋ณ ์์ ๋ณ์ --- */
|
| 2 |
+
:root {
|
| 3 |
+
--primary-theme-color: #7b68ee; /* ๋ณด๋ผ์ ํฌ์ธํธ */
|
| 4 |
+
--background-light: #f4f7fc;
|
| 5 |
+
--text-dark: #333;
|
| 6 |
+
--text-light: #888;
|
| 7 |
+
|
| 8 |
+
--dot-joy: #ffdd57;
|
| 9 |
+
--dot-sad: #5390e3;
|
| 10 |
+
--dot-angry: #ff6b6b;
|
| 11 |
+
--dot-anxious: #9775fa;
|
| 12 |
+
--dot-surprised: #ffa94d;
|
| 13 |
+
--dot-hurt: #48d1cc;
|
| 14 |
+
--dot-default: #e0e0e0;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* --- ์ ์ฒด ๋ ์ด์์ --- */
|
| 18 |
+
.dashboard-container {
|
| 19 |
+
display: flex;
|
| 20 |
+
width: 95%;
|
| 21 |
+
max-width: 1400px;
|
| 22 |
+
height: 750px;
|
| 23 |
+
margin: 2rem auto;
|
| 24 |
+
margin-top: 5rem;
|
| 25 |
+
background-color: white;
|
| 26 |
+
border-radius: 20px;
|
| 27 |
+
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.1);
|
| 28 |
+
overflow: hidden;
|
| 29 |
+
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* --- 1. ์ข์ธก ์ฌ์ด๋๋ฐ --- */
|
| 33 |
+
.sidebar {
|
| 34 |
+
width: 200px;
|
| 35 |
+
background-color: var(--primary-theme-color);
|
| 36 |
+
color: white;
|
| 37 |
+
padding: 0.5rem 0;
|
| 38 |
+
display: flex;
|
| 39 |
+
flex-direction: column;
|
| 40 |
+
flex-shrink: 0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.year-selector {
|
| 44 |
+
display: flex;
|
| 45 |
+
justify-content: space-around;
|
| 46 |
+
align-items: center;
|
| 47 |
+
padding: 0 1.5rem;
|
| 48 |
+
margin-bottom: 2rem;
|
| 49 |
+
}
|
| 50 |
+
.year-selector h2 {
|
| 51 |
+
font-size: 1.5rem;
|
| 52 |
+
margin: 0;
|
| 53 |
+
}
|
| 54 |
+
.year-selector button {
|
| 55 |
+
background: none;
|
| 56 |
+
border: none;
|
| 57 |
+
color: white;
|
| 58 |
+
font-size: 1.2rem;
|
| 59 |
+
cursor: pointer;
|
| 60 |
+
opacity: 0.8;
|
| 61 |
+
transition: opacity 0.2s;
|
| 62 |
+
}
|
| 63 |
+
.year-selector button:hover {
|
| 64 |
+
opacity: 1;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.month-list {
|
| 68 |
+
list-style: none;
|
| 69 |
+
padding: 0;
|
| 70 |
+
margin: 0;
|
| 71 |
+
flex-grow: 1;
|
| 72 |
+
}
|
| 73 |
+
.month-item {
|
| 74 |
+
padding: 0.7rem 2rem;
|
| 75 |
+
font-size: 1.3rem;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: background-color 0.2s;
|
| 78 |
+
opacity: 0.7;
|
| 79 |
+
position: relative;
|
| 80 |
+
}
|
| 81 |
+
.month-item:hover, .month-item.active {
|
| 82 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 83 |
+
opacity: 1;
|
| 84 |
+
}
|
| 85 |
+
.month-item.active::before {
|
| 86 |
+
content: '';
|
| 87 |
+
position: absolute;
|
| 88 |
+
left: 0;
|
| 89 |
+
top: 0;
|
| 90 |
+
width: 4px;
|
| 91 |
+
height: 100%;
|
| 92 |
+
background-color: white;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.sidebar-footer {
|
| 96 |
+
text-align: center;
|
| 97 |
+
font-size: 0.9rem;
|
| 98 |
+
opacity: 0.6;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* --- 2. ์ค์ ๋ฌ๋ ฅ --- */
|
| 102 |
+
.calendar-area {
|
| 103 |
+
flex-grow: 1;
|
| 104 |
+
padding: 2rem 2.5rem;
|
| 105 |
+
background-color: #fff;
|
| 106 |
+
display: flex;
|
| 107 |
+
flex-direction: column;
|
| 108 |
+
}
|
| 109 |
+
.calendar-header {
|
| 110 |
+
display: flex;
|
| 111 |
+
justify-content: space-between;
|
| 112 |
+
align-items: center;
|
| 113 |
+
margin-bottom: 1.5rem;
|
| 114 |
+
}
|
| 115 |
+
.calendar-header h2 {
|
| 116 |
+
font-size: 2rem;
|
| 117 |
+
color: var(--text-dark);
|
| 118 |
+
margin: 0;
|
| 119 |
+
}
|
| 120 |
+
.add-btn {
|
| 121 |
+
background-color: var(--primary-theme-color);
|
| 122 |
+
color: white;
|
| 123 |
+
border: none;
|
| 124 |
+
width: 40px;
|
| 125 |
+
height: 40px;
|
| 126 |
+
border-radius: 50%;
|
| 127 |
+
font-size: 1.8rem;
|
| 128 |
+
cursor: pointer;
|
| 129 |
+
transition: transform 0.2s;
|
| 130 |
+
}
|
| 131 |
+
.add-btn:hover { transform: scale(1.1); }
|
| 132 |
+
|
| 133 |
+
/* Flatpickr ์ปค์คํ
*/
|
| 134 |
+
#calendar {
|
| 135 |
+
flex-grow: 1;
|
| 136 |
+
display: flex;
|
| 137 |
+
}
|
| 138 |
+
.flatpickr-calendar {
|
| 139 |
+
width: 100% !important;
|
| 140 |
+
max-width: none !important;
|
| 141 |
+
padding: 0 !important;
|
| 142 |
+
height: 100%;
|
| 143 |
+
background: transparent;
|
| 144 |
+
box-shadow: none;
|
| 145 |
+
font-family: inherit;
|
| 146 |
+
display: flex;
|
| 147 |
+
flex-direction: column;
|
| 148 |
+
}
|
| 149 |
+
.flatpickr-innerContainer {
|
| 150 |
+
height: 100%;
|
| 151 |
+
display: flex;
|
| 152 |
+
flex-direction: column;
|
| 153 |
+
flex-grow: 1;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.flatpickr-rContainer {
|
| 157 |
+
flex-grow: 1;
|
| 158 |
+
display: flex;
|
| 159 |
+
flex-direction: column;
|
| 160 |
+
width: 100%;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* --- ์์ผ ํค๋ ์ ๋ ฌ ์์ (Weekday Alignment Fix) --- */
|
| 164 |
+
|
| 165 |
+
.flatpickr-weekdays {
|
| 166 |
+
display: flex !important; /* Grid ๋์ Flex ์ฌ์ฉ */
|
| 167 |
+
width: 100% !important;
|
| 168 |
+
padding: 0 2.5rem !important;
|
| 169 |
+
box-sizing: border-box !important;
|
| 170 |
+
margin: 0 0 10px 0 !important;
|
| 171 |
+
overflow: hidden; /* ๋์น๋ ๊ฒ ๋ฐฉ์ง */
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
span.flatpickr-weekday {
|
| 175 |
+
/* 7๋ฑ๋ถ ๊ฐ์ ์ ์ฉ (100% / 7 = 14.2857%) */
|
| 176 |
+
flex: 1 0 14.28% !important;
|
| 177 |
+
max-width: 14.28% !important;
|
| 178 |
+
|
| 179 |
+
display: flex !important;
|
| 180 |
+
justify-content: center !important;
|
| 181 |
+
align-items: center !important;
|
| 182 |
+
|
| 183 |
+
color: #888 !important;
|
| 184 |
+
font-weight: 600 !important;
|
| 185 |
+
font-size: 0.9rem !important;
|
| 186 |
+
margin: 0 !important;
|
| 187 |
+
}
|
| 188 |
+
.flatpickr-days {
|
| 189 |
+
width: 100%;
|
| 190 |
+
flex-grow: 1;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.dayContainer {
|
| 194 |
+
display: grid !important;
|
| 195 |
+
grid-template-columns: repeat(7, 1fr) !important;
|
| 196 |
+
width: 100% !important;
|
| 197 |
+
min-width: 100% !important;
|
| 198 |
+
max-width: 100% !important;
|
| 199 |
+
height: 100%;
|
| 200 |
+
justify-items: center !important;
|
| 201 |
+
gap: 0 !important;
|
| 202 |
+
padding: 0 2.5rem !important; /* Match weekday padding */
|
| 203 |
+
box-sizing: border-box; /* Match weekday box-sizing */
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.flatpickr-day {
|
| 207 |
+
width: 100% !important;
|
| 208 |
+
max-width: none !important;
|
| 209 |
+
margin: 0 !important;
|
| 210 |
+
height: 80px !important; /* ์๋ ๋์ด ์ ์ง */
|
| 211 |
+
border-radius: 12px;
|
| 212 |
+
position: relative;
|
| 213 |
+
z-index: 1;
|
| 214 |
+
display: flex;
|
| 215 |
+
justify-content: center;
|
| 216 |
+
align-items: center;
|
| 217 |
+
transition: background-color 0.2s, color 0.2s; /* color transition ์ถ๊ฐ */
|
| 218 |
+
font-size: 1.4rem;
|
| 219 |
+
overflow: hidden; /* ๊ฐ์์์๊ฐ ๋ฅ๊ทผ ๋ชจ์๋ฆฌ๋ฅผ ๋์ง ์๋๋ก */
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* ๋ ์ง ์ซ์ ์คํ์ผ */
|
| 223 |
+
.flatpickr-day span.flatpickr-day-num {
|
| 224 |
+
position: relative;
|
| 225 |
+
z-index: 2; /* ๋ฐฐ๊ฒฝ๋ณด๋ค ์์ ์ค๋๋ก */
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* ์ผ๊ธฐ๊ฐ ์๋ ๋ ์ ๊ฐ์์์ (๋ฐฐ๊ฒฝ ์ญํ ) */
|
| 229 |
+
.flatpickr-day.has-diary::before {
|
| 230 |
+
content: '';
|
| 231 |
+
position: absolute;
|
| 232 |
+
top: 0;
|
| 233 |
+
left: 0;
|
| 234 |
+
width: 100%;
|
| 235 |
+
height: 100%;
|
| 236 |
+
z-index: 1; /* ์ซ์๋ณด๋ค ๋ค์ ์ค๋๋ก */
|
| 237 |
+
transition: background-color 0.2s;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.flatpickr-day.today {
|
| 241 |
+
border: 1px solid var(--primary-theme-color);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.flatpickr-day:not(.has-diary):hover { /* ์ผ๊ธฐ๊ฐ ์๋ ๋ ์ง์๋ง ์ ์ฉ */
|
| 245 |
+
background-color: #f0edff;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.flatpickr-day.has-diary:hover::before { /* ์ผ๊ธฐ๊ฐ ์๋ ๋ ์ง๋ ๋ฐ๊ธฐ๋ง ์กฐ์ */
|
| 249 |
+
filter: brightness(90%);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.flatpickr-day.selected {
|
| 253 |
+
border: 2px solid var(--primary-theme-color); /* ๋ฐฐ๊ฒฝ์ ๋์ ํ
๋๋ฆฌ๋ก ์ ํ ํ์ */
|
| 254 |
+
}
|
| 255 |
+
/* .flatpickr-day.selected::before ๊ท์น์ ์์ ํ ์ ๊ฑฐ๋จ */
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
.flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: #ccc; }
|
| 259 |
+
|
| 260 |
+
.flatpickr-day.has-diary {
|
| 261 |
+
color: white; /* ๊ธฐ๋ณธ ํ
์คํธ ์์์ ํฐ์์ผ๋ก ๋ณ๊ฒฝ */
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* ๊ฐ์ ๋ณ ๋ฐฐ๊ฒฝ์ (๊ฐ์์์์ ์ ์ฉ) */
|
| 265 |
+
.flatpickr-day.has-diary.bg-๊ธฐ์จ::before { background-color: var(--dot-joy); }
|
| 266 |
+
.flatpickr-day.has-diary.bg-์ฌํ::before { background-color: var(--dot-sad); }
|
| 267 |
+
.flatpickr-day.has-diary.bg-๋ถ๋
ธ::before { background-color: var(--dot-angry); }
|
| 268 |
+
.flatpickr-day.has-diary.bg-๋ถ์::before { background-color: var(--dot-anxious); }
|
| 269 |
+
.flatpickr-day.has-diary.bg-๋นํฉ::before { background-color: var(--dot-surprised); }
|
| 270 |
+
.flatpickr-day.has-diary.bg-์์ฒ::before { background-color: var(--dot-hurt); }
|
| 271 |
+
.flatpickr-day.has-diary.bg-default::before { background-color: var(--dot-default); }
|
| 272 |
+
|
| 273 |
+
.flatpickr-day.has-diary.bg-๊ธฐ์จ span.flatpickr-day-num,
|
| 274 |
+
.flatpickr-day.has-diary.bg-๋นํฉ span.flatpickr-day-num,
|
| 275 |
+
.flatpickr-day.has-diary.bg-default span.flatpickr-day-num {
|
| 276 |
+
color: #333; /* ๋ฐ์ ๋ฐฐ๊ฒฝ์์ ์ด๋์ด ๊ธ์์ ์ ์ฉ */
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
.flatpickr-day.today.has-diary {
|
| 281 |
+
border: none; /* ๋ฐฐ๊ฒฝ์์ด ์ฑ์์ง๋ฏ๋ก ํ
๋๋ฆฌ ์ ๊ฑฐ */
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
/* --- 3. ์ฐ์ธก ํ์๋ผ์ธ --- */
|
| 287 |
+
.timeline-area {
|
| 288 |
+
width: 350px;
|
| 289 |
+
background-color: #fcfcfc;
|
| 290 |
+
border-left: 1px solid #f0f0f0;
|
| 291 |
+
padding: 2rem;
|
| 292 |
+
overflow-y: auto;
|
| 293 |
+
flex-shrink: 0;
|
| 294 |
+
}
|
| 295 |
+
.timeline-title {
|
| 296 |
+
font-size: 1.5rem;
|
| 297 |
+
color: var(--text-dark);
|
| 298 |
+
margin: 0 0 2rem 0;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.timeline-area .placeholder {
|
| 302 |
+
text-align: center;
|
| 303 |
+
color: var(--text-light);
|
| 304 |
+
padding-top: 5rem;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
#diary-list-container { position: relative; }
|
| 308 |
+
|
| 309 |
+
.timeline-item {
|
| 310 |
+
position: relative;
|
| 311 |
+
padding-left: 30px;
|
| 312 |
+
padding-bottom: 25px;
|
| 313 |
+
border-left: 2px solid #e0e0e0;
|
| 314 |
+
}
|
| 315 |
+
/* ํ์๋ผ์ธ ๋ง์ง๋ง ์์ดํ
์ ์ ์ ๊ฑฐ */
|
| 316 |
+
.timeline-item:last-child { border-left: 2px solid transparent; }
|
| 317 |
+
|
| 318 |
+
/* ํ์๋ผ์ธ ์ */
|
| 319 |
+
.timeline-item::before {
|
| 320 |
+
content: '';
|
| 321 |
+
position: absolute;
|
| 322 |
+
left: -9px; /* (16px / 2) - 2px */
|
| 323 |
+
top: 0;
|
| 324 |
+
width: 16px;
|
| 325 |
+
height: 16px;
|
| 326 |
+
border-radius: 50%;
|
| 327 |
+
background-color: white;
|
| 328 |
+
border: 3px solid var(--primary-theme-color);
|
| 329 |
+
}
|
| 330 |
+
.timeline-item.item-๊ธฐ์จ::before { border-color: var(--dot-joy); }
|
| 331 |
+
.timeline-item.item-์ฌํ::before { border-color: var(--dot-sad); }
|
| 332 |
+
.timeline-item.item-๋ถ๋
ธ::before { border-color: var(--dot-angry); }
|
| 333 |
+
.timeline-item.item-๋ถ์::before { border-color: var(--dot-anxious); }
|
| 334 |
+
.timeline-item.item-๋นํฉ::before { border-color: var(--dot-surprised); }
|
| 335 |
+
.timeline-item.item-์์ฒ::before { border-color: var(--dot-hurt); }
|
| 336 |
+
.timeline-item.item-default::before { border-color: var(--dot-default); }
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
.item-header {
|
| 340 |
+
display: flex;
|
| 341 |
+
justify-content: space-between;
|
| 342 |
+
align-items: center;
|
| 343 |
+
margin-bottom: 10px;
|
| 344 |
+
}
|
| 345 |
+
.item-time {
|
| 346 |
+
font-weight: 600;
|
| 347 |
+
color: var(--text-dark);
|
| 348 |
+
}
|
| 349 |
+
.item-emotion {
|
| 350 |
+
font-size: 1.2rem;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.item-controls {
|
| 354 |
+
display: flex;
|
| 355 |
+
align-items: center;
|
| 356 |
+
gap: 10px; /* ์ด๋ชจ์ง์ ์ญ์ ๋ฒํผ ์ฌ์ด ๊ฐ๊ฒฉ */
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.delete-diary-btn {
|
| 360 |
+
background: none;
|
| 361 |
+
border: 1px solid #ff6b6b;
|
| 362 |
+
color: #ff6b6b;
|
| 363 |
+
padding: 2px 8px;
|
| 364 |
+
font-size: 0.8rem;
|
| 365 |
+
border-radius: 5px;
|
| 366 |
+
cursor: pointer;
|
| 367 |
+
transition: background-color 0.2s, color 0.2s;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.delete-diary-btn:hover {
|
| 371 |
+
background-color: #ff6b6b;
|
| 372 |
+
color: white;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.item-content {
|
| 376 |
+
font-size: 0.9rem;
|
| 377 |
+
line-height: 1.6;
|
| 378 |
+
color: #666;
|
| 379 |
+
background-color: white;
|
| 380 |
+
padding: 15px;
|
| 381 |
+
border-radius: 10px;
|
| 382 |
+
border: 1px solid #f0f0f0;
|
| 383 |
+
}
|
| 384 |
+
.item-content h3 {
|
| 385 |
+
font-size: 1rem;
|
| 386 |
+
margin: 0 0 10px 0;
|
| 387 |
+
}
|
| 388 |
+
.item-content p {
|
| 389 |
+
margin: 0;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.item-recommendations {
|
| 393 |
+
margin-top: 15px;
|
| 394 |
+
padding-top: 15px;
|
| 395 |
+
border-top: 1px solid #f0f0f0;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.item-recommendations h4 {
|
| 399 |
+
font-size: 0.9rem;
|
| 400 |
+
font-weight: 600;
|
| 401 |
+
color: var(--text-dark);
|
| 402 |
+
margin: 0 0 10px 0;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.item-recommendations ul {
|
| 406 |
+
list-style-type: '๐ต';
|
| 407 |
+
padding-left: 20px;
|
| 408 |
+
margin: 0;
|
| 409 |
+
font-size: 0.9rem;
|
| 410 |
+
color: #666;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.item-recommendations li {
|
| 414 |
+
padding-left: 8px;
|
| 415 |
+
margin-bottom: 5px;
|
| 416 |
+
}
|
| 417 |
+
.item-content ul {
|
| 418 |
+
padding-left: 20px;
|
| 419 |
+
margin: 10px 0 0 0;
|
| 420 |
+
}
|
| 421 |
+
.item-content li {
|
| 422 |
+
margin-bottom: 5px;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
/* --- ์์ธ ์ ๋ณด ๋ชจ๋ฌ ์คํ์ผ --- */
|
| 426 |
+
.diary-detail-modal-overlay {
|
| 427 |
+
position: fixed;
|
| 428 |
+
top: 0;
|
| 429 |
+
left: 0;
|
| 430 |
+
width: 100%;
|
| 431 |
+
height: 100%;
|
| 432 |
+
background-color: rgba(0, 0, 0, 0.6);
|
| 433 |
+
display: none; /* ๊ธฐ๋ณธ์ ์ผ๋ก ์จ๊น */
|
| 434 |
+
justify-content: center;
|
| 435 |
+
align-items: center;
|
| 436 |
+
z-index: 1001;
|
| 437 |
+
backdrop-filter: blur(5px);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.diary-detail-modal-content {
|
| 441 |
+
background-color: white;
|
| 442 |
+
padding: 30px 40px;
|
| 443 |
+
border-radius: 15px;
|
| 444 |
+
width: 90%;
|
| 445 |
+
max-width: 600px;
|
| 446 |
+
max-height: 80vh;
|
| 447 |
+
overflow-y: auto;
|
| 448 |
+
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
| 449 |
+
position: relative;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.diary-detail-modal-close {
|
| 453 |
+
position: absolute;
|
| 454 |
+
top: 15px;
|
| 455 |
+
right: 20px;
|
| 456 |
+
font-size: 2rem;
|
| 457 |
+
color: #aaa;
|
| 458 |
+
background: none;
|
| 459 |
+
border: none;
|
| 460 |
+
cursor: pointer;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
#diary-detail-title {
|
| 464 |
+
font-size: 1.8rem;
|
| 465 |
+
margin-bottom: 20px;
|
| 466 |
+
color: var(--text-dark);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
#diary-detail-body {
|
| 470 |
+
font-size: 1rem;
|
| 471 |
+
line-height: 1.7;
|
| 472 |
+
color: #444;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
#diary-detail-body .diary-content-section {
|
| 476 |
+
margin-bottom: 25px;
|
| 477 |
+
padding-bottom: 20px;
|
| 478 |
+
border-bottom: 1px solid #eee;
|
| 479 |
+
}
|
| 480 |
+
#diary-detail-body .diary-content-section h3 {
|
| 481 |
+
font-size: 1.2rem;
|
| 482 |
+
margin-bottom: 10px;
|
| 483 |
+
color: var(--primary-theme-color);
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
#diary-detail-body #rec-container h3 {
|
| 487 |
+
margin-bottom: 15px;
|
| 488 |
+
}
|
src/templates/static/css/main.css
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* --- ๐ธ ๋งค์ง ์บก์ ๋ด๋น๊ฒ์ด์
๋ฐ (Magic Capsule Navbar) --- */
|
| 2 |
+
|
| 3 |
+
.navbar-container {
|
| 4 |
+
position: fixed;
|
| 5 |
+
top: 20px;
|
| 6 |
+
left: 0;
|
| 7 |
+
width: 100%;
|
| 8 |
+
display: flex;
|
| 9 |
+
justify-content: center;
|
| 10 |
+
z-index: 1000;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.navbar {
|
| 14 |
+
display: flex;
|
| 15 |
+
align-items: center;
|
| 16 |
+
gap: 30px;
|
| 17 |
+
padding: 8px 15px;
|
| 18 |
+
border-radius: 50px; /* ๋ฅ๊ทผ ์บก์ ๋ชจ์ */
|
| 19 |
+
|
| 20 |
+
/* ๊ธ๋์ค๋ชจํผ์ฆ (์ ๋ฆฌ ํจ๊ณผ) */
|
| 21 |
+
background: rgba(255, 255, 255, 0.7);
|
| 22 |
+
backdrop-filter: blur(15px);
|
| 23 |
+
-webkit-backdrop-filter: blur(15px);
|
| 24 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
|
| 25 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.navbar-brand {
|
| 29 |
+
font-weight: 800;
|
| 30 |
+
font-size: 1.1rem;
|
| 31 |
+
color: var(--primary-color);
|
| 32 |
+
text-decoration: none;
|
| 33 |
+
padding: 0 15px;
|
| 34 |
+
letter-spacing: -0.5px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* ๋ฉ๋ด ๋ฆฌ์คํธ */
|
| 38 |
+
.navbar-menu {
|
| 39 |
+
position: relative; /* ๋ง์ปค์ ๊ธฐ์ค์ */
|
| 40 |
+
display: flex;
|
| 41 |
+
flex-direction: row; /* Added */
|
| 42 |
+
align-items: center; /* Added */
|
| 43 |
+
list-style: none;
|
| 44 |
+
margin: 0;
|
| 45 |
+
padding: 5px;
|
| 46 |
+
background: rgba(0,0,0,0.03); /* ๋ฉ๋ด ๋ค ์ฐํ ํธ๋ */
|
| 47 |
+
border-radius: 30px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.nav-item {
|
| 51 |
+
position: relative;
|
| 52 |
+
z-index: 2; /* ๋ง์ปค๋ณด๋ค ์์ ์์น */
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.nav-item a {
|
| 56 |
+
display: block;
|
| 57 |
+
padding: 8px 20px;
|
| 58 |
+
text-decoration: none;
|
| 59 |
+
color: #666;
|
| 60 |
+
font-weight: 600;
|
| 61 |
+
font-size: 0.9rem;
|
| 62 |
+
border-radius: 30px;
|
| 63 |
+
transition: color 0.3s;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* ๋ง์ฐ์ค ์ฌ๋ ธ์ ๋ ๊ธ์์ ๋ณ๊ฒฝ */
|
| 67 |
+
.nav-item a:hover {
|
| 68 |
+
color: var(--primary-color);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* โจ ์์ง์ด๋ ํ์ด๋ผ์ดํธ (Marker) */
|
| 72 |
+
.nav-marker {
|
| 73 |
+
position: absolute;
|
| 74 |
+
top: 5px;
|
| 75 |
+
left: 5px;
|
| 76 |
+
width: 0; /* JS๋ก ์ ์ด */
|
| 77 |
+
height: calc(100% - 10px);
|
| 78 |
+
background: white;
|
| 79 |
+
border-radius: 25px;
|
| 80 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
| 81 |
+
transition: all 0.3s cubic-bezier(0.5, 0, 0, 1); /* ๋ถ๋๋ฌ์ด ์ด๋ */
|
| 82 |
+
z-index: 1;
|
| 83 |
+
opacity: 0; /* ์ฒ์์ ์จ๊น */
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.copyright-dropdown {
|
| 87 |
+
position: relative;
|
| 88 |
+
margin-left: 1rem;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.copyright-toggle {
|
| 92 |
+
cursor: pointer;
|
| 93 |
+
font-size: 0.9rem;
|
| 94 |
+
color: var(--light-text-color);
|
| 95 |
+
border: 1px solid #ddd;
|
| 96 |
+
padding: 4px 8px;
|
| 97 |
+
border-radius: 6px;
|
| 98 |
+
transition: background-color 0.2s;
|
| 99 |
+
}
|
| 100 |
+
.copyright-toggle:hover {
|
| 101 |
+
background-color: #f5f5f5;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.copyright-content {
|
| 105 |
+
display: none;
|
| 106 |
+
position: absolute;
|
| 107 |
+
top: 120%;
|
| 108 |
+
left: 0;
|
| 109 |
+
background-color: white;
|
| 110 |
+
border-radius: 8px;
|
| 111 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
| 112 |
+
padding: 1rem;
|
| 113 |
+
z-index: 1001; /* Onboarding is 1000 */
|
| 114 |
+
width: 220px;
|
| 115 |
+
border: 1px solid #eee;
|
| 116 |
+
opacity: 0;
|
| 117 |
+
transform: translateY(-10px);
|
| 118 |
+
transition: opacity 0.2s, transform 0.2s;
|
| 119 |
+
pointer-events: none;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.copyright-content p {
|
| 123 |
+
margin: 0.5rem 0;
|
| 124 |
+
font-size: 0.85rem;
|
| 125 |
+
color: #555;
|
| 126 |
+
}
|
| 127 |
+
.copyright-content p:first-child {
|
| 128 |
+
margin-top: 0;
|
| 129 |
+
}
|
| 130 |
+
.copyright-content p:last-child {
|
| 131 |
+
margin-bottom: 0;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.copyright-dropdown:hover .copyright-content {
|
| 135 |
+
display: block;
|
| 136 |
+
opacity: 1;
|
| 137 |
+
transform: translateY(0);
|
| 138 |
+
pointer-events: auto;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* --- ๋ฉ์ธ ์ฝํ
์ธ ์คํ์ผ --- */
|
| 142 |
+
/* --- ๐ ํผ์ณ์ง ๋ค์ด์ด๋ฆฌ ์คํ์ผ (Book Spread Design) --- */
|
| 143 |
+
|
| 144 |
+
/* 1. ๋ฉ์ธ ๋ ์ด์์ */
|
| 145 |
+
.main-content {
|
| 146 |
+
display: flex;
|
| 147 |
+
justify-content: center;
|
| 148 |
+
align-items: center;
|
| 149 |
+
min-height: 80vh; /* ํ๋ฉด ์ค์ ์ ๋ ฌ */
|
| 150 |
+
margin-top: 1.5rem;
|
| 151 |
+
padding: 10px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* 2. ๋ค์ด์ด๋ฆฌ ์ ์ฒด ํ๋ ์ */
|
| 155 |
+
/* --- 1. ๋ค์ด์ด๋ฆฌ ํฌ๊ธฐ ํ๋ (Big Size) --- */
|
| 156 |
+
.diary-book {
|
| 157 |
+
display: flex;
|
| 158 |
+
width: 70vw; /* ํ๋ฉด ๋๋น์ 70%๊น์ง ์ฌ์ฉ */
|
| 159 |
+
max-width: 1600px; /* ์ต๋ ๋๋น 1600px */
|
| 160 |
+
min-width: 1200px; /* ์ต์ ๋๋น 1200px ๊ฐ์ */
|
| 161 |
+
height: 750px; /* ๋์ด ๊ณ ์ */
|
| 162 |
+
margin-top: 5rem;
|
| 163 |
+
background-color: #fdfbf7;
|
| 164 |
+
border-radius: 20px;
|
| 165 |
+
box-shadow: 0 30px 60px rgba(0,0,0,0.15), 0 0 0 12px #5d4037;
|
| 166 |
+
overflow: hidden;
|
| 167 |
+
position: relative;
|
| 168 |
+
|
| 169 |
+
margin: 0 auto; /* ์ํ ์ค์ ์ ๋ ฌ */
|
| 170 |
+
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* --- 2. ํ์ด์ง ๋ด๋ถ ์คํฌ๋กค ์ถ๊ฐ (์๋ฆผ ๋ฐฉ์ง) --- */
|
| 174 |
+
.book-page {
|
| 175 |
+
flex: 1;
|
| 176 |
+
/* ํจ๋ฉ์ ์กฐ๊ธ ๋ ๋๋ ค์ ์์ํ๊ฒ (40px -> 60px) */
|
| 177 |
+
padding: 40px 50px;
|
| 178 |
+
display: flex;
|
| 179 |
+
flex-direction: column;
|
| 180 |
+
position: relative;
|
| 181 |
+
background-image: linear-gradient(to right, rgba(0,0,0,0.02) 0%, rgba(0,0,0,0) 5%);
|
| 182 |
+
|
| 183 |
+
/* ์คํฌ๋กค๋ฐ ์จ๊น ์ ์ง */
|
| 184 |
+
overflow-y: auto;
|
| 185 |
+
scrollbar-width: none;
|
| 186 |
+
-ms-overflow-style: none;
|
| 187 |
+
min-width:0;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
/* (์ ํ) ์คํฌ๋กค๋ฐ๋ฅผ ์์๊ฒ ๊พธ๋ฏธ๊ธฐ */
|
| 191 |
+
.book-page::-webkit-scrollbar {
|
| 192 |
+
display: none;
|
| 193 |
+
}
|
| 194 |
+
.book-page::-webkit-scrollbar-track {
|
| 195 |
+
background: transparent;
|
| 196 |
+
}
|
| 197 |
+
.book-page::-webkit-scrollbar-thumb {
|
| 198 |
+
background-color: #e0e0e0;
|
| 199 |
+
border-radius: 4px;
|
| 200 |
+
}
|
| 201 |
+
.book-page::-webkit-scrollbar-thumb:hover {
|
| 202 |
+
background-color: #ccc;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.page-header {
|
| 206 |
+
margin-bottom: 0.5rem;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* --- 3. ์
๋ ฅ์ฐฝ ํฌ๊ธฐ ๋์ --- */
|
| 210 |
+
textarea {
|
| 211 |
+
flex-grow: 1;
|
| 212 |
+
border: none;
|
| 213 |
+
background: transparent;
|
| 214 |
+
font-size: 1.1rem;
|
| 215 |
+
|
| 216 |
+
/* [์์ ] ์ค ๋์ด์ ๋ฐฐ๊ฒฝ ํจํด ๊ฐ๊ฒฉ์ ์ค์ฌ์ ๋ ๋ง์ด ์ฐ๊ฒ ํจ */
|
| 217 |
+
line-height: 2rem; /* ์ค ๊ฐ๊ฒฉ (๊ธฐ์กด ์ ์งํ๊ฑฐ๋ 1.8rem์ผ๋ก ์ค์ฌ๋ ๋จ) */
|
| 218 |
+
|
| 219 |
+
resize: none;
|
| 220 |
+
outline: none;
|
| 221 |
+
|
| 222 |
+
/* ๋ฐฐ๊ฒฝ ์ค๋ฌด๋ฌ ๊ฐ๊ฒฉ๋ line-height์ ๋๊ฐ์ด ๋ง์ถฐ์ผ ํจ */
|
| 223 |
+
background-image: linear-gradient(transparent 95%, #e0e0e0 95%);
|
| 224 |
+
background-size: 100% 2rem; /* 2rem ๊ฐ๊ฒฉ */
|
| 225 |
+
|
| 226 |
+
padding: 0 10px; /* ์ข์ฐ ์ฌ๋ฐฑ */
|
| 227 |
+
font-family: inherit;
|
| 228 |
+
|
| 229 |
+
/* ์คํฌ๋กค๋ฐ ์จ๊น */
|
| 230 |
+
scrollbar-width: none;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* 9. ์ค๋ฅธ์ชฝ ๋น ํ๋ฉด ์ํ */
|
| 234 |
+
.empty-state {
|
| 235 |
+
display: flex;
|
| 236 |
+
justify-content: center;
|
| 237 |
+
align-items: center;
|
| 238 |
+
height: 100%;
|
| 239 |
+
color: #aaa;
|
| 240 |
+
text-align: center;
|
| 241 |
+
font-size: 0.95rem;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/* --- ๊ธฐ์กด ๊ธฐ๋ฅ ํธํ์ฑ ์ ์ง --- */
|
| 245 |
+
/* ์ฐข์ด์ง๋ ์ ๋๋ฉ์ด์
์ฉ ํด๋์ค ์ฌ์กฐ์ */
|
| 246 |
+
|
| 247 |
+
#submit-btn {
|
| 248 |
+
width: 100%;
|
| 249 |
+
background-color: var(--primary-color);
|
| 250 |
+
color: white;
|
| 251 |
+
padding: 14px;
|
| 252 |
+
border-radius: 8px;
|
| 253 |
+
border: none;
|
| 254 |
+
font-size: 18px;
|
| 255 |
+
font-weight: bold;
|
| 256 |
+
cursor: pointer;
|
| 257 |
+
transition: background-color 0.2s;
|
| 258 |
+
}
|
| 259 |
+
#submit-btn:hover {
|
| 260 |
+
background-color: var(--primary-hover-color);
|
| 261 |
+
}
|
| 262 |
+
#submit-btn:disabled {
|
| 263 |
+
background-color: #cccccc;
|
| 264 |
+
cursor: not-allowed;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
#result {
|
| 268 |
+
background: #ffffff;
|
| 269 |
+
border-radius: 15px; /* ๋ฅ๊ธ๊ฒ */
|
| 270 |
+
padding: 25px; /* ๋ด๋ถ ์ฌ๋ฐฑ ๋๋ํ๊ฒ */
|
| 271 |
+
margin-top: 15px; /* ํค๋("AI์ ๋ต์ฅ")์ ๊ฐ๊ฒฉ ๋์ฐ๊ธฐ */
|
| 272 |
+
/* ๊ทธ๋ฆผ์ + ํ
๋๋ฆฌ๋ก ์นด๋ ๋๋ ๋ด๊ธฐ */
|
| 273 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
| 274 |
+
border: 1px solid #eee;
|
| 275 |
+
|
| 276 |
+
font-size: 1rem;
|
| 277 |
+
color: #444;
|
| 278 |
+
line-height: 1.7;
|
| 279 |
+
width:100%;
|
| 280 |
+
box-sizing: border-box;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.rec-tabs {
|
| 284 |
+
display: flex;
|
| 285 |
+
margin-top: 1.5rem;
|
| 286 |
+
margin-bottom: 1rem;
|
| 287 |
+
border-bottom: 1px solid #ddd;
|
| 288 |
+
}
|
| 289 |
+
.rec-tab-btn {
|
| 290 |
+
padding: 10px 15px;
|
| 291 |
+
cursor: pointer;
|
| 292 |
+
border: none;
|
| 293 |
+
background-color: transparent;
|
| 294 |
+
font-size: 1rem;
|
| 295 |
+
color: var(--light-text-color);
|
| 296 |
+
border-bottom: 2px solid transparent;
|
| 297 |
+
margin-bottom: -1px;
|
| 298 |
+
}
|
| 299 |
+
.rec-tab-btn.active {
|
| 300 |
+
color: var(--primary-color);
|
| 301 |
+
border-bottom-color: var(--primary-color);
|
| 302 |
+
font-weight: 600;
|
| 303 |
+
}
|
| 304 |
+
.rec-content {
|
| 305 |
+
display: none;
|
| 306 |
+
}
|
| 307 |
+
.rec-content.active {
|
| 308 |
+
display: block;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/* --- ์จ๋ณด๋ฉ ์ฌ๋ผ์ด๋ ์คํ์ผ --- */
|
| 312 |
+
.onboarding-overlay {
|
| 313 |
+
position: fixed;
|
| 314 |
+
top: 0;
|
| 315 |
+
left: 0;
|
| 316 |
+
width: 100%;
|
| 317 |
+
height: 100%;
|
| 318 |
+
background-color: rgba(0, 0, 0, 0.6);
|
| 319 |
+
display: flex;
|
| 320 |
+
justify-content: center;
|
| 321 |
+
align-items: center;
|
| 322 |
+
z-index: 1000;
|
| 323 |
+
opacity: 0;
|
| 324 |
+
visibility: hidden;
|
| 325 |
+
transition: opacity 0.3s, visibility 0.3s;
|
| 326 |
+
}
|
| 327 |
+
.onboarding-overlay.active {
|
| 328 |
+
opacity: 1;
|
| 329 |
+
visibility: visible;
|
| 330 |
+
}
|
| 331 |
+
.onboarding-modal {
|
| 332 |
+
background-color: white;
|
| 333 |
+
padding: 30px 40px;
|
| 334 |
+
border-radius: 15px;
|
| 335 |
+
width: 90%;
|
| 336 |
+
max-width: 500px;
|
| 337 |
+
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
| 338 |
+
transform: scale(0.9);
|
| 339 |
+
transition: transform 0.3s;
|
| 340 |
+
}
|
| 341 |
+
.onboarding-overlay.active .onboarding-modal {
|
| 342 |
+
transform: scale(1);
|
| 343 |
+
}
|
| 344 |
+
.onboarding-slide {
|
| 345 |
+
display: none;
|
| 346 |
+
text-align: center;
|
| 347 |
+
}
|
| 348 |
+
.onboarding-slide.active {
|
| 349 |
+
display: block;
|
| 350 |
+
}
|
| 351 |
+
.onboarding-slide h2 {
|
| 352 |
+
color: var(--primary-color);
|
| 353 |
+
margin-bottom: 15px;
|
| 354 |
+
}
|
| 355 |
+
.onboarding-slide p {
|
| 356 |
+
font-size: 1rem;
|
| 357 |
+
color: var(--light-text-color);
|
| 358 |
+
line-height: 1.6;
|
| 359 |
+
}
|
| 360 |
+
.onboarding-nav {
|
| 361 |
+
display: flex;
|
| 362 |
+
justify-content: space-between;
|
| 363 |
+
align-items: center;
|
| 364 |
+
margin-top: 25px;
|
| 365 |
+
}
|
| 366 |
+
.onboarding-dots {
|
| 367 |
+
display: flex;
|
| 368 |
+
gap: 8px;
|
| 369 |
+
}
|
| 370 |
+
.onboarding-dot {
|
| 371 |
+
width: 10px;
|
| 372 |
+
height: 10px;
|
| 373 |
+
background-color: #ccc;
|
| 374 |
+
border-radius: 50%;
|
| 375 |
+
cursor: pointer;
|
| 376 |
+
}
|
| 377 |
+
.onboarding-dot.active {
|
| 378 |
+
background-color: var(--primary-color);
|
| 379 |
+
}
|
| 380 |
+
.onboarding-btn {
|
| 381 |
+
background-color: var(--primary-color);
|
| 382 |
+
color: white;
|
| 383 |
+
border: none;
|
| 384 |
+
padding: 10px 20px;
|
| 385 |
+
border-radius: 5px;
|
| 386 |
+
cursor: pointer;
|
| 387 |
+
font-weight: 500;
|
| 388 |
+
}
|
| 389 |
+
.onboarding-btn:hover {
|
| 390 |
+
background-color: var(--primary-hover-color);
|
| 391 |
+
}
|
| 392 |
+
#onboarding-close {
|
| 393 |
+
background-color: transparent;
|
| 394 |
+
color: var(--light-text-color);
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
/* --- ํ
๋ง ํ๋ ํธ ์คํ์ผ --- */
|
| 398 |
+
.theme-palette {
|
| 399 |
+
position: fixed;
|
| 400 |
+
bottom: 20px;
|
| 401 |
+
right: 20px;
|
| 402 |
+
display: flex;
|
| 403 |
+
flex-direction: column;
|
| 404 |
+
gap: 10px;
|
| 405 |
+
z-index: 100;
|
| 406 |
+
}
|
| 407 |
+
.theme-btn {
|
| 408 |
+
width: 50px;
|
| 409 |
+
height: 50px;
|
| 410 |
+
border-radius: 50%;
|
| 411 |
+
border: 2px solid white;
|
| 412 |
+
cursor: pointer;
|
| 413 |
+
font-size: 24px;
|
| 414 |
+
display: flex;
|
| 415 |
+
justify-content: center;
|
| 416 |
+
align-items: center;
|
| 417 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
| 418 |
+
transition: transform 0.2s;
|
| 419 |
+
}
|
| 420 |
+
.theme-btn:hover {
|
| 421 |
+
transform: scale(1.1);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.theme-option-wrapper {
|
| 425 |
+
position: relative;
|
| 426 |
+
display: flex;
|
| 427 |
+
justify-content: flex-end;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.sub-options {
|
| 431 |
+
display: none;
|
| 432 |
+
position: absolute;
|
| 433 |
+
right: 60px; /* theme-btn width + gap */
|
| 434 |
+
top: 50%;
|
| 435 |
+
transform: translateY(-50%);
|
| 436 |
+
align-items: center;
|
| 437 |
+
gap: 10px;
|
| 438 |
+
background-color: rgba(255, 255, 255, 0.8);
|
| 439 |
+
padding: 8px;
|
| 440 |
+
border-radius: 10px;
|
| 441 |
+
backdrop-filter: blur(4px);
|
| 442 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.theme-option-wrapper.active .sub-options {
|
| 446 |
+
display: flex;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.sub-options img, .sub-option-btn {
|
| 450 |
+
width: 40px;
|
| 451 |
+
height: 40px;
|
| 452 |
+
border-radius: 6px;
|
| 453 |
+
cursor: pointer;
|
| 454 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 455 |
+
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
| 456 |
+
object-fit: cover;
|
| 457 |
+
}
|
| 458 |
+
.sub-option-btn {
|
| 459 |
+
background-color: white;
|
| 460 |
+
font-size: 12px;
|
| 461 |
+
padding: 0;
|
| 462 |
+
}
|
| 463 |
+
.rec-btn {
|
| 464 |
+
background-color: #f0f0f0;
|
| 465 |
+
border: 1px solid #ddd;
|
| 466 |
+
padding: 8px 12px;
|
| 467 |
+
border-radius: 6px;
|
| 468 |
+
cursor: pointer;
|
| 469 |
+
margin-right: 10px;
|
| 470 |
+
font-size: 14px;
|
| 471 |
+
}
|
| 472 |
+
.rec-btn.active {
|
| 473 |
+
background-color: var(--primary-color);
|
| 474 |
+
color: white;
|
| 475 |
+
border-color: var(--primary-color);
|
| 476 |
+
}
|
| 477 |
+
#result table {
|
| 478 |
+
width: 100%; /* ๋ถ๋ชจ ๋ฐ์ค ๋๋น์ ๋ฑ ๋ง์ถค */
|
| 479 |
+
table-layout: fixed; /* [ํต์ฌ] ๋ด์ฉ์ด ๊ธธ์ด๋ ํ
์ด๋ธ์ด ๋์ด๋์ง ์๊ฒ ๊ณ ์ */
|
| 480 |
+
border-collapse: collapse;
|
| 481 |
+
margin-top: 15px;
|
| 482 |
+
font-size: 0.95rem;
|
| 483 |
+
background-color: #fafafa; /* ํ ๋ฐฐ๊ฒฝ์ */
|
| 484 |
+
border-radius: 8px;
|
| 485 |
+
overflow: hidden; /* ๋ฅ๊ทผ ๋ชจ์๋ฆฌ ์ ์ฉ */
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
#result th, #result td {
|
| 489 |
+
padding: 12px 15px;
|
| 490 |
+
border-bottom: 1px solid #eee;
|
| 491 |
+
word-break: keep-all; /* ํ๊ธ ๋จ์ด ์ ๋๊ธฐ๊ฒ */
|
| 492 |
+
vertical-align: middle; /* ์ธ๋ก ์ค์ ์ ๋ ฌ */
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
/* ์ฒซ ๋ฒ์งธ ์นธ(์ข
๋ฅ) ๋๋น ์ค์ด๊ธฐ */
|
| 496 |
+
#result th:first-child, #result td:first-child {
|
| 497 |
+
width: 60px; /* '์ข
๋ฅ' ์นธ์ ์ข๊ฒ */
|
| 498 |
+
text-align: center;
|
| 499 |
+
font-weight: bold;
|
| 500 |
+
color: var(--primary-color);
|
| 501 |
+
background-color: #f0f5ff; /* ํค๋ ๋ฐฐ๊ฒฝ์ */
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
/* ๋ ๋ฒ์งธ ์นธ(์ถ์ฒ ๋ด์ฉ) */
|
| 505 |
+
#result td:last-child {
|
| 506 |
+
text-align: left;
|
| 507 |
+
font-size: 0.9rem;
|
| 508 |
+
color: #555;
|
| 509 |
+
|
| 510 |
+
/* [ํต์ฌ] 2์ค ๋์ด๊ฐ๋ฉด ... ์ฒ๋ฆฌ */
|
| 511 |
+
display: -webkit-box;
|
| 512 |
+
-webkit-line-clamp: 2;
|
| 513 |
+
line-clamp: 2;
|
| 514 |
+
-webkit-box-orient: vertical;
|
| 515 |
+
overflow: hidden;
|
| 516 |
+
text-overflow: ellipsis;
|
| 517 |
+
|
| 518 |
+
/* ๋ถ๋๋ฌ์ด ์ ํ */
|
| 519 |
+
transition: all 0.3s ease;
|
| 520 |
+
cursor: help; /* ๋ง์ฐ์ค ์ฌ๋ฆฌ๋ฉด ๋ฌผ์ํ ์ปค์ */
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
#result td:last-child:hover {
|
| 524 |
+
-webkit-line-clamp: unset; /* ์ค ์ ํ ํด์ */
|
| 525 |
+
line-clamp: unset;
|
| 526 |
+
background-color: #fff; /* ์ฝ๊ธฐ ํธํ๊ฒ ๋ฐฐ๊ฒฝ ๋ฐ๊ฒ */
|
| 527 |
+
color: #000;
|
| 528 |
+
z-index: 10;
|
| 529 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1); /* ์ด์ง ๋์ฐ๊ธฐ */
|
| 530 |
+
border-radius: 5px;
|
| 531 |
+
position: relative; /* ์๋ก ๋จ๊ฒ ํ๊ธฐ ์ํด */
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* --- ๊ฐ์ ์ ํ UI ์คํ์ผ --- */
|
| 535 |
+
#emotion-choice-container {
|
| 536 |
+
background-color: #f0f8ff;
|
| 537 |
+
border-radius: 8px;
|
| 538 |
+
padding: 20px;
|
| 539 |
+
margin-top: 20px;
|
| 540 |
+
border: 1px solid #bde0fe;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
#emotion-chips {
|
| 544 |
+
display: flex;
|
| 545 |
+
justify-content: center;
|
| 546 |
+
gap: 15px;
|
| 547 |
+
flex-wrap: wrap;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.emotion-chip {
|
| 551 |
+
display: flex;
|
| 552 |
+
align-items: center;
|
| 553 |
+
background-color: #f0f0f0;
|
| 554 |
+
border: 1px solid #ddd;
|
| 555 |
+
border-radius: 20px; /* ์บก์ ๋ชจ์ */
|
| 556 |
+
padding: 8px 16px;
|
| 557 |
+
font-size: 1rem;
|
| 558 |
+
cursor: pointer;
|
| 559 |
+
transition: background-color 0.2s, border-color 0.2s, transform 0.1s;
|
| 560 |
+
user-select: none;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.emotion-chip:hover {
|
| 564 |
+
background-color: #e0e0e0;
|
| 565 |
+
transform: translateY(-2px);
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.emotion-chip.active {
|
| 569 |
+
background-color: var(--primary-color);
|
| 570 |
+
color: white;
|
| 571 |
+
border-color: var(--primary-hover-color);
|
| 572 |
+
font-weight: bold;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.score-badge {
|
| 576 |
+
margin-left: 8px;
|
| 577 |
+
background-color: rgba(0, 0, 0, 0.1);
|
| 578 |
+
color: #333;
|
| 579 |
+
padding: 3px 6px;
|
| 580 |
+
font-size: 0.75rem;
|
| 581 |
+
font-weight: bold;
|
| 582 |
+
border-radius: 8px;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.emotion-chip.active .score-badge {
|
| 586 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 587 |
+
color: white;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
/* --- ์ผ๊ธฐ ์ ์ฅํ๊ธฐ ๋ฒํผ ์์น ์กฐ์ --- */
|
| 591 |
+
#save-action-container {
|
| 592 |
+
/* ๋ฒํผ์ด ์ค๋ฅธ์ชฝ ์ ๋ ฌ๋๋๋ก */
|
| 593 |
+
display: flex;
|
| 594 |
+
justify-content: flex-end;
|
| 595 |
+
margin:0;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.save-btn {
|
| 599 |
+
/* ๋ฒํผ ๋์์ธ ์กฐ๊ธ ๋ ์์๊ฒ */
|
| 600 |
+
background-color: var(--primary-color);
|
| 601 |
+
color: white;
|
| 602 |
+
padding: 10px 20px;
|
| 603 |
+
border-radius: 50px;
|
| 604 |
+
border: none;
|
| 605 |
+
font-weight: bold;
|
| 606 |
+
box-shadow: 0 4px 10px rgba(101, 152, 229, 0.4);
|
| 607 |
+
transition: transform 0.2s;
|
| 608 |
+
}
|
| 609 |
+
.save-btn:hover {
|
| 610 |
+
transform: translateY(-2px);
|
| 611 |
+
}
|
| 612 |
+
/* --- ๊ฒฐ๊ณผ ํ์ ํค๋ --- */
|
| 613 |
+
.result-header {
|
| 614 |
+
display: flex;
|
| 615 |
+
justify-content: space-between;
|
| 616 |
+
align-items: flex-start; /* Align to the top */
|
| 617 |
+
margin-bottom: 1rem;
|
| 618 |
+
padding-bottom: 1rem;
|
| 619 |
+
border-bottom: 1px solid #eee;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.result-title p {
|
| 623 |
+
margin: 0;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
/* Make the save button container part of the flex layout */
|
| 627 |
+
.result-header .save-button-container {
|
| 628 |
+
margin-top: 0;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
/* --- ๋ก๋ฉ ํ๋ก๊ทธ๋ ์ค ๋ฐ --- */
|
| 632 |
+
.progress-bar-container {
|
| 633 |
+
width: 100%;
|
| 634 |
+
background-color: #e0e0e0;
|
| 635 |
+
border-radius: 4px;
|
| 636 |
+
margin: 20px 0;
|
| 637 |
+
overflow: hidden; /* Ensures the inner bar stays within the rounded corners */
|
| 638 |
+
}
|
| 639 |
+
.progress-bar {
|
| 640 |
+
width: 0%;
|
| 641 |
+
height: 10px;
|
| 642 |
+
background-color: var(--primary-color);
|
| 643 |
+
border-radius: 4px;
|
| 644 |
+
transition: width 0.2s ease-in-out;
|
| 645 |
+
}
|
| 646 |
+
.loading-text {
|
| 647 |
+
text-align: center;
|
| 648 |
+
margin-top: 10px;
|
| 649 |
+
color: #555;
|
| 650 |
+
font-size: 0.9rem;
|
| 651 |
+
}
|
src/templates/static/css/page.css
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* mypage css part*/
|
| 2 |
+
.mypage-box {
|
| 3 |
+
width: 100%;
|
| 4 |
+
max-width: 600px;
|
| 5 |
+
margin: 0 auto;
|
| 6 |
+
padding: 4rem 2rem 2rem;
|
| 7 |
+
margin-top: 5rem;
|
| 8 |
+
background-color: white;
|
| 9 |
+
border-radius: 8px;
|
| 10 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
h1 {
|
| 14 |
+
|
| 15 |
+
font-weight: 700;
|
| 16 |
+
color: var(--primary-color);
|
| 17 |
+
font-size: 2rem;
|
| 18 |
+
text-align: center;
|
| 19 |
+
margin-bottom: 0.1rem;
|
| 20 |
+
padding-bottom: 0.5rem;
|
| 21 |
+
border-bottom: 1px solid #000000;
|
| 22 |
+
}
|
| 23 |
+
.info-list {
|
| 24 |
+
list-style: none;
|
| 25 |
+
padding: 0;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.info-list li {
|
| 29 |
+
display: flex;
|
| 30 |
+
justify-content: space-between;
|
| 31 |
+
align-items: center;
|
| 32 |
+
padding: 1rem 0;
|
| 33 |
+
border-bottom: 1px solid #000000;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.info-list li:last-child {
|
| 37 |
+
border-bottom: none;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.info-list .label {
|
| 41 |
+
font-weight: 500;
|
| 42 |
+
color: var(--light-text-color);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.info-list .value {
|
| 46 |
+
font-weight: 700;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.nickname-form {
|
| 50 |
+
display: flex;
|
| 51 |
+
gap: 0.5rem;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.nickname-form input {
|
| 55 |
+
flex-grow: 1;
|
| 56 |
+
padding: 0.5rem;
|
| 57 |
+
border: 1px solid #000000;
|
| 58 |
+
border-radius: 4px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.nickname-form button {
|
| 62 |
+
padding: 0.5rem 1rem;
|
| 63 |
+
border: none;
|
| 64 |
+
background-color: var(--primary-color);
|
| 65 |
+
color: white;
|
| 66 |
+
border-radius: 4px;
|
| 67 |
+
cursor: pointer;
|
| 68 |
+
transition: background-color 0.2s;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.nickname-form button:hover {
|
| 72 |
+
background-color: var(--primary-hover-color);
|
| 73 |
+
}
|
src/templates/static/css/style.css
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://cdn.jsdelivr.net/gh/spoqa/spoqa-han-sans@latest/css/SpoqaHanSansNeo.css');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--primary-color: #6598e5;
|
| 5 |
+
--primary-hover-color: #5540a3;
|
| 6 |
+
--text-color: #333;
|
| 7 |
+
--light-text-color: #666;
|
| 8 |
+
--danger-color: #e74c3c;
|
| 9 |
+
--font-family: 'Spoqa Han Sans Neo', sans-serif;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
font-family: var(--font-family);
|
| 14 |
+
margin: 0;
|
| 15 |
+
background: linear-gradient(135deg, #ece9f7, #cacae0);
|
| 16 |
+
color: var(--text-color);
|
| 17 |
+
display: flex;
|
| 18 |
+
flex-direction: column;
|
| 19 |
+
min-height: 100vh;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.navbar {
|
| 23 |
+
background-color: white;
|
| 24 |
+
padding: 10px 2rem;
|
| 25 |
+
display: flex;
|
| 26 |
+
justify-content: space-between;
|
| 27 |
+
align-items: center;
|
| 28 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 29 |
+
flex-shrink: 0;
|
| 30 |
+
}
|
| 31 |
+
.navbar-brand {
|
| 32 |
+
color: var(--primary-color);
|
| 33 |
+
font-size: 1.5rem;
|
| 34 |
+
font-weight: 700;
|
| 35 |
+
text-decoration: none;
|
| 36 |
+
}
|
| 37 |
+
.navbar-menu a, .navbar-menu span {
|
| 38 |
+
color: #333;
|
| 39 |
+
text-decoration: none;
|
| 40 |
+
font-weight: 500;
|
| 41 |
+
margin-left: 1rem;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.container {
|
| 45 |
+
flex-grow: 1;
|
| 46 |
+
padding: 3rem;
|
| 47 |
+
width: 100%;
|
| 48 |
+
box-sizing: border-box;
|
| 49 |
+
}
|
src/templates/static/js/diary_logic.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// --- Emotion Map ---
|
| 3 |
+
const emotionMap = {
|
| 4 |
+
'๊ธฐ์จ': { emoji: '๐', bgClass: 'bg-๊ธฐ์จ', itemClass: 'item-๊ธฐ์จ' },
|
| 5 |
+
'์ฌํ': { emoji: '๐ข', bgClass: 'bg-์ฌํ', itemClass: 'item-์ฌํ' },
|
| 6 |
+
'๋ถ๋
ธ': { emoji: '๐ ', bgClass: 'bg-๋ถ๋
ธ', itemClass: 'item-๋ถ๋
ธ' },
|
| 7 |
+
'๋ถ์': { emoji: '๐', bgClass: 'bg-๋ถ์', itemClass: 'item-๋ถ์' },
|
| 8 |
+
'๋นํฉ': { emoji: '๐ฎ', bgClass: 'bg-๋นํฉ', itemClass: 'item-๋นํฉ' },
|
| 9 |
+
'์์ฒ': { emoji: '๐', bgClass: 'bg-์์ฒ', itemClass: 'item-์์ฒ' },
|
| 10 |
+
'default': { emoji: '๐ค', bgClass: 'bg-default', itemClass: 'item-default' }
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
// --- DOM Elements ---
|
| 14 |
+
const currentYearEl = document.getElementById('current-year');
|
| 15 |
+
const prevYearBtn = document.getElementById('prev-year');
|
| 16 |
+
const nextYearBtn = document.getElementById('next-year');
|
| 17 |
+
const monthList = document.querySelector('.month-list');
|
| 18 |
+
const calendarMonthTitle = document.getElementById('calendar-month-title');
|
| 19 |
+
const diaryListContainer = document.getElementById('diary-list-container');
|
| 20 |
+
console.log("diaryListContainer element:", diaryListContainer); // ์์ ํ์ธ ๋ก๊ทธ
|
| 21 |
+
const recModalOverlay = document.getElementById('rec-modal-overlay');
|
| 22 |
+
const recModalTitle = document.getElementById('rec-modal-title');
|
| 23 |
+
const recModalBody = document.getElementById('rec-modal-body');
|
| 24 |
+
const recModalCloseBtn = document.getElementById('rec-modal-close');
|
| 25 |
+
|
| 26 |
+
// --- State ---
|
| 27 |
+
let diaryDataByDate = {};
|
| 28 |
+
let currentYear, currentMonth;
|
| 29 |
+
let fp; // flatpickr instance
|
| 30 |
+
let lastFetchedYear = null; // ์๋ณ ์นด์ดํธ๋ฅผ ๋ง์ง๋ง์ผ๋ก ๊ฐ์ ธ์จ ์ฐ๋
|
| 31 |
+
|
| 32 |
+
// --- Functions ---
|
| 33 |
+
|
| 34 |
+
async function updateMonthlyCounts(year) {
|
| 35 |
+
if (year === lastFetchedYear) return; // ์ด๋ฏธ ํด๋น ์ฐ๋์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ๋ฉด ์คํ ์ํจ
|
| 36 |
+
|
| 37 |
+
try {
|
| 38 |
+
const response = await fetch(`/api/diaries/counts?year=${year}`);
|
| 39 |
+
if (!response.ok) throw new Error('Failed to load diary counts.');
|
| 40 |
+
const counts = await response.json();
|
| 41 |
+
|
| 42 |
+
document.querySelectorAll('.month-item').forEach(item => {
|
| 43 |
+
const month_key = (parseInt(item.dataset.month) + 1).toString(); // ์ ๋ฒํธ๋ฅผ ๋ฌธ์์ด๋ก ๋ณํ
|
| 44 |
+
const countSpan = item.querySelector('.diary-count');
|
| 45 |
+
const count = counts[month_key] || 0; // ๋ฌธ์์ด ํค๋ก ์ ๊ทผ
|
| 46 |
+
|
| 47 |
+
if (count > 0) {
|
| 48 |
+
countSpan.textContent = count;
|
| 49 |
+
} else {
|
| 50 |
+
countSpan.textContent = '';
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
lastFetchedYear = year; // ๋ง์ง๋ง์ผ๋ก ๊ฐ์ ธ์จ ์ฐ๋ ๊ธฐ๋ก
|
| 54 |
+
} catch (error) {
|
| 55 |
+
console.error("Error fetching diary counts:", error);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
async function fetchDiaries(year, month) {
|
| 60 |
+
try {
|
| 61 |
+
console.log(`Fetching diaries for year: ${year}, month: ${month}`);
|
| 62 |
+
const response = await fetch(`/api/diaries?year=${year}&month=${month}`);
|
| 63 |
+
if (!response.ok) {
|
| 64 |
+
const errorText = await response.text();
|
| 65 |
+
throw new Error(`Diary data failed to load. Status: ${response.status}, Message: ${errorText}`);
|
| 66 |
+
}
|
| 67 |
+
const diaries = await response.json();
|
| 68 |
+
console.log("Received diaries:", diaries);
|
| 69 |
+
|
| 70 |
+
diaryDataByDate = {};
|
| 71 |
+
diaries.forEach(diary => {
|
| 72 |
+
// Ensure diary.date is valid before assignment
|
| 73 |
+
if (diary.date) {
|
| 74 |
+
diaryDataByDate[diary.date] = diaryDataByDate[diary.date] || [];
|
| 75 |
+
diaryDataByDate[diary.date].push(diary);
|
| 76 |
+
} else {
|
| 77 |
+
console.warn("Diary item with missing date:", diary);
|
| 78 |
+
}
|
| 79 |
+
});
|
| 80 |
+
console.log("Processed diaryDataByDate:", diaryDataByDate);
|
| 81 |
+
return diaries;
|
| 82 |
+
} catch (error) {
|
| 83 |
+
console.error("Error in fetchDiaries:", error);
|
| 84 |
+
// display a user-friendly error message on the UI
|
| 85 |
+
diaryListContainer.innerHTML = `<div class="placeholder"><p>์ผ๊ธฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</p><p style="font-size: 0.8em; color: #666;">${error.message}</p></div>`;
|
| 86 |
+
return [];
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function renderTimeline(dateStr) {
|
| 91 |
+
const diaries = diaryDataByDate[dateStr] || [];
|
| 92 |
+
diaryListContainer.innerHTML = '';
|
| 93 |
+
if (diaries.length === 0) {
|
| 94 |
+
diaryListContainer.innerHTML = '<div class="placeholder"><p>์์ฑ๋ ์ผ๊ธฐ๊ฐ ์์ต๋๋ค.</p></div>';
|
| 95 |
+
return;
|
| 96 |
+
}
|
| 97 |
+
diaries.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
| 98 |
+
diaries.forEach(diary => {
|
| 99 |
+
const emotionInfo = emotionMap[diary.emotion] || emotionMap.default;
|
| 100 |
+
const item = document.createElement('div');
|
| 101 |
+
item.className = `timeline-item ${emotionInfo.itemClass}`;
|
| 102 |
+
item.dataset.diary = JSON.stringify(diary); // ์ ์ฒด diary ๊ฐ์ฒด ์ ์ฅ
|
| 103 |
+
const time = new Date(diary.createdAt).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
|
| 104 |
+
|
| 105 |
+
item.innerHTML = `
|
| 106 |
+
<div class="item-header">
|
| 107 |
+
<span class="item-time">${time}</span>
|
| 108 |
+
<div class="item-controls">
|
| 109 |
+
<span class="item-emotion">${emotionInfo.emoji}</span>
|
| 110 |
+
<button class="delete-diary-btn" data-diary-id="${diary.id}">์ญ์ </button>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="item-content">
|
| 114 |
+
<p>${diary.content.replace(/\n/g, '<br>')}</p>
|
| 115 |
+
</div>
|
| 116 |
+
`;
|
| 117 |
+
diaryListContainer.appendChild(item);
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function updateUI(year, month) { // month is 0-indexed
|
| 122 |
+
currentYear = year;
|
| 123 |
+
currentMonth = month;
|
| 124 |
+
currentYearEl.textContent = year;
|
| 125 |
+
calendarMonthTitle.textContent = new Date(year, month).toLocaleString('en-US', { month: 'long' });
|
| 126 |
+
document.querySelectorAll('.month-item').forEach(item => {
|
| 127 |
+
item.classList.toggle('active', parseInt(item.dataset.month) === month);
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
async function handleDateChange(year, month) { // month is 0-indexed
|
| 132 |
+
updateUI(year, month);
|
| 133 |
+
await updateMonthlyCounts(year); // ์ฐ๋๊ฐ ๋ฐ๋ ๋๋ง๋ค ์นด์ดํธ ์
๋ฐ์ดํธ
|
| 134 |
+
await fetchDiaries(year, month + 1);
|
| 135 |
+
if (fp) fp.redraw();
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const parseRecs = (text) => {
|
| 139 |
+
const contents = { ์์ฉ: '', ์ ํ: '' };
|
| 140 |
+
if (!text) return contents;
|
| 141 |
+
const regex = /#+\s*\[\s*(์์ฉ|๊ณต๊ฐ|์ ํ|ํ๊ธฐ)\s*\]([\s\S]*?)(?=(?:#+\s*\[\s*(?:์์ฉ|๊ณต๊ฐ|์ ํ|ํ๊ธฐ)\s*\])|$)/gi;
|
| 142 |
+
let match;
|
| 143 |
+
while ((match = regex.exec(text)) !== null) {
|
| 144 |
+
const type = match[1].trim();
|
| 145 |
+
let content = match[2].trim();
|
| 146 |
+
if (type === '์์ฉ' || type === '๊ณต๊ฐ') contents.์์ฉ = content;
|
| 147 |
+
else if (type === '์ ํ' || type === 'ํ๊ธฐ') contents.์ ํ = content;
|
| 148 |
+
}
|
| 149 |
+
return contents;
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
const parseAndClean = (markdown) => {
|
| 153 |
+
if (!markdown) return '<p class="empty-msg">์ถ์ฒ ํญ๋ชฉ์ด ์์ต๋๋ค.</p>';
|
| 154 |
+
|
| 155 |
+
const rawHtml = marked.parse(markdown);
|
| 156 |
+
const tempDiv = document.createElement('div');
|
| 157 |
+
tempDiv.innerHTML = rawHtml;
|
| 158 |
+
|
| 159 |
+
// ๋ฐฉ์ 1: "์ถ์ฒ ์ด์ "๊ฐ ์ด ํค๋์ธ ๊ฒฝ์ฐ ํด๋น ์ด ์ ์ฒด ์ ๊ฑฐ
|
| 160 |
+
const tables = tempDiv.querySelectorAll('table');
|
| 161 |
+
tables.forEach(table => {
|
| 162 |
+
let reasonColumnIndex = -1;
|
| 163 |
+
table.querySelectorAll('th').forEach((th, index) => {
|
| 164 |
+
if (th.textContent.trim() === '์ถ์ฒ ์ด์ ') {
|
| 165 |
+
reasonColumnIndex = index;
|
| 166 |
+
}
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
if (reasonColumnIndex !== -1) {
|
| 170 |
+
table.querySelectorAll('tr').forEach(row => {
|
| 171 |
+
if (row.cells[reasonColumnIndex]) {
|
| 172 |
+
row.deleteCell(reasonColumnIndex);
|
| 173 |
+
}
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
// ๋ฐฉ์ 2: "์ถ์ฒ ์ด์ :" ํ
์คํธ๊ฐ ํฌํจ๋ ํ ์ ๊ฑฐ
|
| 179 |
+
const rowsToRemove = [];
|
| 180 |
+
tempDiv.querySelectorAll('td').forEach(td => {
|
| 181 |
+
if (td.textContent.includes('์ถ์ฒ ์ด์ :')) {
|
| 182 |
+
const row = td.closest('tr');
|
| 183 |
+
if (row) rowsToRemove.push(row);
|
| 184 |
+
}
|
| 185 |
+
});
|
| 186 |
+
rowsToRemove.forEach(row => row.remove());
|
| 187 |
+
|
| 188 |
+
// ์นดํ
๊ณ ๋ฆฌ ํ
์คํธ("์ํ", "์์
", "๋์")๋ฅผ ์ด๋ชจ์ง๋ก ๋ณ๊ฒฝ (์ฒซ ๋ฒ์งธ ์ด๋ง)
|
| 189 |
+
const categoryEmojiMap = { '์ํ': '๐ฌ', '์์
': '๐ต', '๋์': '๐' };
|
| 190 |
+
tempDiv.querySelectorAll('tr').forEach(row => {
|
| 191 |
+
// ํค๋ ํ์ด ์๋๊ณ , ์
์ด ์กด์ฌํ ๊ฒฝ์ฐ
|
| 192 |
+
if (row.cells.length > 0 && row.cells[0].tagName === 'TD') {
|
| 193 |
+
const firstCell = row.cells[0];
|
| 194 |
+
let cellHtml = firstCell.innerHTML;
|
| 195 |
+
for (const category in categoryEmojiMap) {
|
| 196 |
+
const regex = new RegExp(`(<strong>)?${category}(</strong>)?`, "g");
|
| 197 |
+
cellHtml = cellHtml.replace(regex, categoryEmojiMap[category]);
|
| 198 |
+
}
|
| 199 |
+
firstCell.innerHTML = cellHtml;
|
| 200 |
+
}
|
| 201 |
+
});
|
| 202 |
+
|
| 203 |
+
return tempDiv.innerHTML;
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
// --- Event Listeners ---
|
| 207 |
+
const detailModalOverlay = document.getElementById('diary-detail-modal-overlay');
|
| 208 |
+
const detailModalCloseBtn = document.getElementById('diary-detail-modal-close');
|
| 209 |
+
|
| 210 |
+
monthList.addEventListener('click', (e) => {
|
| 211 |
+
if (e.target.classList.contains('month-item')) {
|
| 212 |
+
const month = parseInt(e.target.dataset.month);
|
| 213 |
+
if (month !== currentMonth) fp.changeMonth(month - currentMonth);
|
| 214 |
+
}
|
| 215 |
+
});
|
| 216 |
+
|
| 217 |
+
prevYearBtn.addEventListener('click', () => fp.changeYear(fp.currentYear - 1));
|
| 218 |
+
nextYearBtn.addEventListener('click', () => fp.changeYear(fp.currentYear + 1));
|
| 219 |
+
|
| 220 |
+
diaryListContainer.addEventListener('click', async (e) => {
|
| 221 |
+
// ์ญ์ ๋ฒํผ ๋ก์ง
|
| 222 |
+
if (e.target.classList.contains('delete-diary-btn')) {
|
| 223 |
+
e.stopPropagation(); // ์ด๋ฒคํธ ๋ฒ๋ธ๋ง ๋ฐฉ์ง
|
| 224 |
+
const diaryId = e.target.dataset.diaryId;
|
| 225 |
+
if (!diaryId || !confirm('์ ๋ง๋ก ์ด ์ผ๊ธฐ๋ฅผ ์ญ์ ํ์๊ฒ ์ต๋๊น?')) {
|
| 226 |
+
return;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
try {
|
| 230 |
+
const response = await fetch(`/diary/delete/${diaryId}`, {
|
| 231 |
+
method: 'DELETE',
|
| 232 |
+
});
|
| 233 |
+
|
| 234 |
+
if (!response.ok) {
|
| 235 |
+
const errorData = await response.json();
|
| 236 |
+
throw new Error(errorData.error || '์ญ์ ์ ์คํจํ์ต๋๋ค.');
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// ์ญ์ ์ฑ๊ณต ํ UI ์
๋ฐ์ดํธ
|
| 240 |
+
const selectedDate = fp.selectedDates[0];
|
| 241 |
+
await handleDateChange(selectedDate.getFullYear(), selectedDate.getMonth());
|
| 242 |
+
renderTimeline(flatpickr.formatDate(selectedDate, "Y-m-d"));
|
| 243 |
+
|
| 244 |
+
} catch (error) {
|
| 245 |
+
console.error('์ญ์ ์ค ์ค๋ฅ ๋ฐ์:', error);
|
| 246 |
+
alert(error.message);
|
| 247 |
+
}
|
| 248 |
+
return;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// ์์ธ ๋ชจ๋ฌ ๋ก์ง
|
| 252 |
+
const timelineItem = e.target.closest('.timeline-item');
|
| 253 |
+
if (timelineItem && timelineItem.dataset.diary) {
|
| 254 |
+
try {
|
| 255 |
+
const diary = JSON.parse(timelineItem.dataset.diary);
|
| 256 |
+
openDiaryDetailModal(diary);
|
| 257 |
+
} catch (jsonError) {
|
| 258 |
+
console.error("Failed to parse diary data from dataset:", jsonError);
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
+
function openDiaryDetailModal(diary) {
|
| 264 |
+
const modalTitle = document.getElementById('diary-detail-title');
|
| 265 |
+
const modalBody = document.getElementById('diary-detail-body');
|
| 266 |
+
|
| 267 |
+
modalTitle.innerHTML = ''; // ์ ๋ชฉ ์ ๊ฑฐ
|
| 268 |
+
|
| 269 |
+
let bodyHtml = `
|
| 270 |
+
<div class="diary-content-section">
|
| 271 |
+
<h3>๋์ ๊ธฐ๋ก</h3>
|
| 272 |
+
<p>${diary.content.replace(/\n/g, '<br>')}</p>
|
| 273 |
+
</div>
|
| 274 |
+
`;
|
| 275 |
+
|
| 276 |
+
if (diary.recommendation) {
|
| 277 |
+
const sections = parseRecs(diary.recommendation);
|
| 278 |
+
if (sections.์์ฉ) {
|
| 279 |
+
bodyHtml += `
|
| 280 |
+
<div class="diary-content-section">
|
| 281 |
+
<h3>์์ฉ</h3>
|
| 282 |
+
${parseAndClean(sections.์์ฉ)}
|
| 283 |
+
</div>
|
| 284 |
+
`;
|
| 285 |
+
}
|
| 286 |
+
if (sections.์ ํ) {
|
| 287 |
+
bodyHtml += `
|
| 288 |
+
<div class="diary-content-section">
|
| 289 |
+
<h3>์ ํ</h3>
|
| 290 |
+
${parseAndClean(sections.์ ํ)}
|
| 291 |
+
</div>
|
| 292 |
+
`;
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
modalBody.innerHTML = bodyHtml;
|
| 297 |
+
detailModalOverlay.style.display = 'flex';
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
function closeDiaryDetailModal() {
|
| 301 |
+
detailModalOverlay.style.display = 'none';
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
detailModalCloseBtn.addEventListener('click', closeDiaryDetailModal);
|
| 305 |
+
detailModalOverlay.addEventListener('click', (e) => {
|
| 306 |
+
if (e.target === detailModalOverlay) {
|
| 307 |
+
closeDiaryDetailModal();
|
| 308 |
+
}
|
| 309 |
+
});
|
| 310 |
+
|
| 311 |
+
function initializeCalendar() {
|
| 312 |
+
fp = flatpickr("#calendar", {
|
| 313 |
+
inline: true,
|
| 314 |
+
dateFormat: "Y-m-d",
|
| 315 |
+
locale: "en",
|
| 316 |
+
onReady: async (selectedDates, dateStr, instance) => {
|
| 317 |
+
const today = new Date();
|
| 318 |
+
await handleDateChange(today.getFullYear(), today.getMonth());
|
| 319 |
+
instance.setDate(today, true);
|
| 320 |
+
},
|
| 321 |
+
onChange: (selectedDates, dateStr, instance) => {
|
| 322 |
+
if (selectedDates.length > 0) renderTimeline(dateStr);
|
| 323 |
+
},
|
| 324 |
+
onMonthChange: async (selectedDates, dateStr, instance) => {
|
| 325 |
+
await handleDateChange(instance.currentYear, instance.currentMonth);
|
| 326 |
+
},
|
| 327 |
+
onYearChange: async (selectedDates, dateStr, instance) => {
|
| 328 |
+
await handleDateChange(instance.currentYear, instance.currentMonth);
|
| 329 |
+
},
|
| 330 |
+
onDayCreate: (dObj, dStr, fp, dayElem) => {
|
| 331 |
+
// ๋ ์ง ์ซ์๋ฅผ span์ผ๋ก ๊ฐ์ธ์ z-index ์ ์ด
|
| 332 |
+
dayElem.innerHTML = `<span class="flatpickr-day-num">${dayElem.innerHTML}</span>`;
|
| 333 |
+
|
| 334 |
+
const date = flatpickr.formatDate(dayElem.dateObj, "Y-m-d");
|
| 335 |
+
const diariesForDay = diaryDataByDate[date];
|
| 336 |
+
if (diariesForDay && diariesForDay.length > 0) {
|
| 337 |
+
const latestDiary = diariesForDay[diariesForDay.length - 1];
|
| 338 |
+
const emotionInfo = emotionMap[latestDiary.emotion] || emotionMap.default;
|
| 339 |
+
|
| 340 |
+
// ๋ ์ง ์
์ ์ง์ ๋ฐฐ๊ฒฝ์ ํด๋์ค๋ฅผ ์ถ๊ฐ (๊ฐ์์์ ::before๊ฐ ๏ฟฝ๏ฟฝ ํด๋์ค๋ฅผ ์ฌ์ฉ)
|
| 341 |
+
dayElem.classList.add('has-diary', emotionInfo.bgClass);
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
});
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
// --- Initial Load ---
|
| 348 |
+
initializeCalendar();
|
| 349 |
+
});
|
src/templates/static/js/macros_rec_tabs.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('click', function(event) {
|
| 2 |
+
if (event.target.classList.contains('rec-tab-btn')) {
|
| 3 |
+
const tabId = event.target.dataset.tab;
|
| 4 |
+
const tabsContainer = event.target.closest('.rec-tabs');
|
| 5 |
+
const contentContainer = tabsContainer.parentElement;
|
| 6 |
+
|
| 7 |
+
tabsContainer.querySelectorAll('.rec-tab-btn').forEach(btn => btn.classList.remove('active'));
|
| 8 |
+
event.target.classList.add('active');
|
| 9 |
+
|
| 10 |
+
contentContainer.querySelectorAll('.rec-content').forEach(content => content.classList.remove('active'));
|
| 11 |
+
document.getElementById(tabId).classList.add('active');
|
| 12 |
+
}
|
| 13 |
+
});
|
src/templates/static/js/main_logic.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 2 |
+
// --- DOM ์์ ๊ฐ์ ธ์ค๊ธฐ ---
|
| 3 |
+
const diaryTextarea = document.getElementById('diary');
|
| 4 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 5 |
+
const resultContainer = document.getElementById('result-container');
|
| 6 |
+
const resultDiv = document.getElementById('result');
|
| 7 |
+
const saveStatus = document.getElementById('save-status');
|
| 8 |
+
const saveBtnContainer = document.getElementById('save-action-container');
|
| 9 |
+
const saveDiaryBtn = document.getElementById('final-save-btn');
|
| 10 |
+
const diaryBook = document.querySelector('.diary-book');
|
| 11 |
+
const leftPage = diaryBook.querySelector('.left-page');
|
| 12 |
+
const rightPage = diaryBook.querySelector('.right-page');
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
// --- ์ํ ๋ณ์ ---
|
| 16 |
+
let currentEmotion = null;
|
| 17 |
+
let currentCandidates = [];
|
| 18 |
+
let progressInterval = null;
|
| 19 |
+
let diaryText = ''; // ์ผ๊ธฐ ๋ด์ฉ์ ์ ์ฅํ ๋ณ์
|
| 20 |
+
|
| 21 |
+
// --- [์ ํธ๋ฆฌํฐ] ์ถ์ฒ ๋ด์ฉ ํ์ฑ ํจ์ ---
|
| 22 |
+
function parseRecommendation(text) {
|
| 23 |
+
const contents = { acceptance: '', diversion: '' };
|
| 24 |
+
if (!text) return contents;
|
| 25 |
+
const regex = /#+\s*\[\s*(์์ฉ|์ ํ)\s*\]([\s\S]*?)(?=(?:#+\s*\[\s*(?:์์ฉ|์ ํ)\s*\])|$)/gi;
|
| 26 |
+
let match;
|
| 27 |
+
while ((match = regex.exec(text)) !== null) {
|
| 28 |
+
const type = match[1].trim();
|
| 29 |
+
const content = match[2].trim();
|
| 30 |
+
if (type === '์์ฉ') contents.acceptance = content;
|
| 31 |
+
else if (type === '์ ํ') contents.diversion = content;
|
| 32 |
+
}
|
| 33 |
+
return contents;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// --- [์ ํธ๋ฆฌํฐ] ๋ก๋ฉ ๋ฐ ํ์ ---
|
| 37 |
+
function showLoader(message) {
|
| 38 |
+
resultDiv.innerHTML = `
|
| 39 |
+
<div style="text-align: center; padding: 20px;">
|
| 40 |
+
<p class="loading-text" style="margin-bottom: 10px; color: #666;">${message}</p>
|
| 41 |
+
<div class="progress-bar-container" style="width: 100%; background-color: #f0f0f0; border-radius: 10px; overflow: hidden; height: 8px;">
|
| 42 |
+
<div class="progress-bar" style="width: 0%; height: 100%; background-color: var(--primary-color, #6598e5); transition: width 0.1s;"></div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
`;
|
| 46 |
+
if (progressInterval) clearInterval(progressInterval);
|
| 47 |
+
const bar = resultDiv.querySelector('.progress-bar');
|
| 48 |
+
let width = 0;
|
| 49 |
+
progressInterval = setInterval(() => {
|
| 50 |
+
if (width < 95) {
|
| 51 |
+
width += 1;
|
| 52 |
+
if(bar) bar.style.width = width + '%';
|
| 53 |
+
}
|
| 54 |
+
}, 50);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// --- [์ ํธ๋ฆฌํฐ] ๋ก๋ฉ ๋ฐ ์ค์ง ---
|
| 58 |
+
function stopLoader() {
|
| 59 |
+
if (progressInterval) {
|
| 60 |
+
clearInterval(progressInterval);
|
| 61 |
+
progressInterval = null;
|
| 62 |
+
}
|
| 63 |
+
const bar = resultDiv.querySelector('.progress-bar');
|
| 64 |
+
if (bar) bar.style.width = '100%';
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// --- [ํต์ฌ] ๊ฒฐ๊ณผ ํ๋ฉด ๋ ๋๋ง ํจ์ ---
|
| 68 |
+
function renderFullResult(data) {
|
| 69 |
+
stopLoader();
|
| 70 |
+
const recommendation = data.recommendation || '';
|
| 71 |
+
const candidates = data.candidates || [];
|
| 72 |
+
if (!currentEmotion && candidates.length > 0) {
|
| 73 |
+
currentEmotion = candidates[0].emotion;
|
| 74 |
+
}
|
| 75 |
+
const { acceptance, diversion } = parseRecommendation(recommendation);
|
| 76 |
+
let chipsHTML = '';
|
| 77 |
+
const showChips = (data.top_score < 0.8) || (candidates.length > 0);
|
| 78 |
+
if (showChips) {
|
| 79 |
+
chipsHTML = `<div class="emotion-chips" style="display: flex; gap: 10px; justify-content: center; margin-bottom: 20px;">`;
|
| 80 |
+
candidates.forEach(candidate => {
|
| 81 |
+
const isActive = candidate.emotion === currentEmotion;
|
| 82 |
+
chipsHTML += `
|
| 83 |
+
<button class="emotion-chip ${isActive ? 'active' : ''}" data-emotion="${candidate.emotion}">
|
| 84 |
+
${candidate.emoji} ${candidate.emotion}
|
| 85 |
+
<span class="score-badge">${Math.round(candidate.score*100)}%</span>
|
| 86 |
+
</button>
|
| 87 |
+
`;
|
| 88 |
+
});
|
| 89 |
+
chipsHTML += `</div>`;
|
| 90 |
+
}
|
| 91 |
+
const contentHTML = `
|
| 92 |
+
${chipsHTML}
|
| 93 |
+
<div class="rec-tabs">
|
| 94 |
+
<button class="rec-tab-btn active" data-tab="acceptance">์์ฉ</button>
|
| 95 |
+
<button class="rec-tab-btn" data-tab="diversion">์ ํ</button>
|
| 96 |
+
</div>
|
| 97 |
+
<div id="rec-acceptance" class="rec-content active">
|
| 98 |
+
${marked.parse(acceptance || '์ถ์ฒ ๋ด์ฉ์ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.')}
|
| 99 |
+
</div>
|
| 100 |
+
<div id="rec-diversion" class="rec-content">
|
| 101 |
+
${marked.parse(diversion || '์ถ์ฒ ๋ด์ฉ์ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค.')}
|
| 102 |
+
</div>
|
| 103 |
+
`;
|
| 104 |
+
resultDiv.innerHTML = contentHTML;
|
| 105 |
+
if (saveBtnContainer) {
|
| 106 |
+
saveBtnContainer.style.display = 'flex';
|
| 107 |
+
}
|
| 108 |
+
resultDiv.querySelectorAll('.emotion-chip').forEach(chip => {
|
| 109 |
+
chip.addEventListener('click', handleChipClick);
|
| 110 |
+
});
|
| 111 |
+
resultDiv.querySelectorAll('.rec-tab-btn').forEach(button => {
|
| 112 |
+
button.addEventListener('click', () => {
|
| 113 |
+
const tab = button.dataset.tab;
|
| 114 |
+
resultDiv.querySelectorAll('.rec-tab-btn').forEach(btn => btn.classList.remove('active'));
|
| 115 |
+
button.classList.add('active');
|
| 116 |
+
resultDiv.querySelectorAll('.rec-content').forEach(content => content.classList.remove('active'));
|
| 117 |
+
resultDiv.querySelector(`#rec-${tab}`).classList.add('active');
|
| 118 |
+
});
|
| 119 |
+
});
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// --- [์ด๋ฒคํธ ํธ๋ค๋ฌ] ---
|
| 123 |
+
function updateButtonState() {
|
| 124 |
+
if(diaryTextarea && submitBtn) {
|
| 125 |
+
submitBtn.disabled = diaryTextarea.value.trim() === '';
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
async function handleChipClick(event) {
|
| 130 |
+
const selectedChip = event.currentTarget;
|
| 131 |
+
const selectedEmotion = selectedChip.dataset.emotion;
|
| 132 |
+
if (currentEmotion === selectedEmotion) return;
|
| 133 |
+
currentEmotion = selectedEmotion;
|
| 134 |
+
resultDiv.querySelectorAll('.emotion-chip').forEach(chip => {
|
| 135 |
+
chip.classList.toggle('active', chip.dataset.emotion === selectedEmotion);
|
| 136 |
+
});
|
| 137 |
+
showLoader('์๋ก์ด ์ถ์ฒ์ ์์ฑํ๋ ์ค์
๋๋ค...');
|
| 138 |
+
try {
|
| 139 |
+
const response = await fetch('/api/recommend', {
|
| 140 |
+
method: 'POST',
|
| 141 |
+
headers: { 'Content-Type': 'application/json' },
|
| 142 |
+
body: JSON.stringify({
|
| 143 |
+
diary: diaryTextarea.value.trim(),
|
| 144 |
+
emotion: selectedEmotion
|
| 145 |
+
})
|
| 146 |
+
});
|
| 147 |
+
const data = await response.json();
|
| 148 |
+
if (data.error) {
|
| 149 |
+
stopLoader();
|
| 150 |
+
resultDiv.innerHTML = `<p style="color: red;">์ค๋ฅ: ${data.error}</p>`;
|
| 151 |
+
} else {
|
| 152 |
+
renderFullResult({
|
| 153 |
+
recommendation: data.recommendation,
|
| 154 |
+
candidates: currentCandidates,
|
| 155 |
+
top_score: 0
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
} catch (error) {
|
| 159 |
+
console.error('Error:', error);
|
| 160 |
+
stopLoader();
|
| 161 |
+
resultDiv.innerHTML = '<p style="color: red;">์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</p>';
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
async function handleDiarySubmission() {
|
| 166 |
+
diaryText = diaryTextarea.value.trim();
|
| 167 |
+
if (!diaryText) return;
|
| 168 |
+
submitBtn.disabled = true;
|
| 169 |
+
submitBtn.textContent = '๋ถ์ ์ค...';
|
| 170 |
+
if(saveBtnContainer) saveBtnContainer.style.display = 'none';
|
| 171 |
+
saveStatus.textContent = '';
|
| 172 |
+
showLoader('๊ฐ์ ์ ๋ถ์ํ๊ณ ์ถ์ฒ์ ์์ฑํ๋ ์ค์
๋๋ค...');
|
| 173 |
+
try {
|
| 174 |
+
const response = await fetch('/api/predict', {
|
| 175 |
+
method: 'POST',
|
| 176 |
+
headers: { 'Content-Type': 'application/json' },
|
| 177 |
+
body: JSON.stringify({ diary: diaryText })
|
| 178 |
+
});
|
| 179 |
+
const data = await response.json();
|
| 180 |
+
if (data.error) {
|
| 181 |
+
stopLoader();
|
| 182 |
+
resultDiv.innerHTML = `<p style="color: red;">์ค๋ฅ: ${data.error}</p>`;
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
currentEmotion = data.top_emotion;
|
| 186 |
+
currentCandidates = data.candidates;
|
| 187 |
+
renderFullResult(data);
|
| 188 |
+
} catch (error) {
|
| 189 |
+
console.error('Error:', error);
|
| 190 |
+
stopLoader();
|
| 191 |
+
resultDiv.innerHTML = '<p style="color: red;">์ฒ๋ฆฌ ์ค ์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</p>';
|
| 192 |
+
} finally {
|
| 193 |
+
submitBtn.disabled = false;
|
| 194 |
+
submitBtn.textContent = '๋ค์ ๋ถ์ํ๊ธฐ';
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
const emotionColors = {
|
| 199 |
+
'๊ธฐ์จ': '#FFD700', '์ฌํ': '#4682B4', '๋ถ๋
ธ': '#B22222',
|
| 200 |
+
'๋ถ์': '#8A2BE2', '๋นํฉ': '#FF8C00', '์์ฒ': '#2E8B57'
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
function playFullAnimation(element, orbColor, duration) {
|
| 204 |
+
const keyframes = [
|
| 205 |
+
{ top: 'calc(50vh - 25px)', left: 'calc(50vw - 25px)', transform: 'scale(1)', offset: 0 },
|
| 206 |
+
{ top: 'calc(100vh - 80px)', left: 'calc(50vw - 25px)', transform: 'scale(1.2, 0.8)', offset: 0.1 },
|
| 207 |
+
{ top: '60vh', left: '58vw', transform: 'scale(1, 1)', offset: 0.25 },
|
| 208 |
+
{ top: 'calc(100vh - 80px)', left: '68vw', transform: 'scale(1.15, 0.85)', offset: 0.40 },
|
| 209 |
+
{ top: '75vh', left: '76vw', transform: 'scale(1, 1)', offset: 0.55 },
|
| 210 |
+
{ top: 'calc(100vh - 80px)', left: '83vw', transform: 'scale(1.1, 0.9)', offset: 0.65 },
|
| 211 |
+
{ top: '85vh', left: '89vw', transform: 'scale(1, 1)', offset: 0.75 },
|
| 212 |
+
{ top: 'calc(100vh - 80px)', left: '94vw', transform: 'scale(1.05, 0.95)', offset: 0.85 },
|
| 213 |
+
{ top: '90vh', left: '96.5vw', transform: 'scale(1, 1)', offset: 0.92 },
|
| 214 |
+
{ top: 'calc(100vh - 80px)', left: '98vw', transform: 'scale(1.02, 0.98)', offset: 0.96 },
|
| 215 |
+
{ top: 'calc(100vh - 80px)', left: 'calc(100vw - 80px)', transform: 'scale(1, 1)', offset: 1 }
|
| 216 |
+
];
|
| 217 |
+
const options = { duration: duration, easing: 'ease-in-out', fill: 'forwards' };
|
| 218 |
+
return element.animate(keyframes, options);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
async function handleDiarySave() {
|
| 222 |
+
if (!diaryBook) return;
|
| 223 |
+
if (document.querySelector('.js-animating')) return;
|
| 224 |
+
|
| 225 |
+
if (saveDiaryBtn) saveDiaryBtn.disabled = true;
|
| 226 |
+
saveStatus.textContent = '๊ธฐ์ต์ ์ ์ฅํ๋ ์ค...';
|
| 227 |
+
|
| 228 |
+
const formData = new FormData();
|
| 229 |
+
formData.append('diary', diaryText);
|
| 230 |
+
formData.append('emotion', currentEmotion);
|
| 231 |
+
fetch('/diary/save', {
|
| 232 |
+
method: 'POST',
|
| 233 |
+
body: formData
|
| 234 |
+
}).then(response => response.json()).then(data => {
|
| 235 |
+
if (data.error) {
|
| 236 |
+
saveStatus.innerHTML = `<span style="color: red;">์ ์ฅ ์คํจ: ${data.error}</span>`;
|
| 237 |
+
}
|
| 238 |
+
}).catch(err => {
|
| 239 |
+
saveStatus.innerHTML = `<span style="color: red;">์ ์ฅ ์ค ์ค๋ฅ ๋ฐ์</span>`;
|
| 240 |
+
console.error(err);
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
const bookFoldDuration = 1500;
|
| 244 |
+
const bounceDuration = 5000;
|
| 245 |
+
const emotionKey = (currentEmotion || '๊ธฐ์จ').split(' ')[0];
|
| 246 |
+
const orbColor = emotionColors[emotionKey] || '#a1c4fd';
|
| 247 |
+
|
| 248 |
+
const animElement = document.createElement('div');
|
| 249 |
+
animElement.classList.add('js-animating-orb');
|
| 250 |
+
animElement.style.position = 'fixed';
|
| 251 |
+
animElement.style.zIndex = '9999';
|
| 252 |
+
animElement.style.width = '50px';
|
| 253 |
+
animElement.style.height = '50px';
|
| 254 |
+
animElement.style.borderRadius = '50%';
|
| 255 |
+
animElement.style.background = `radial-gradient(circle at 30% 30%, rgba(255,255,255,0.5), ${orbColor} 80%)`;
|
| 256 |
+
animElement.style.boxShadow = `0 0 35px ${orbColor}, inset 3px 3px 8px rgba(0,0,0,0.4), inset -3px -3px 8px rgba(255,255,255,0.7)`;
|
| 257 |
+
animElement.style.top = 'calc(50vh - 25px)';
|
| 258 |
+
animElement.style.left = 'calc(50vw - 25px)';
|
| 259 |
+
animElement.style.opacity = '0';
|
| 260 |
+
animElement.style.transform = 'scale(0)';
|
| 261 |
+
document.body.appendChild(animElement);
|
| 262 |
+
|
| 263 |
+
diaryBook.classList.add('js-animating');
|
| 264 |
+
const bookRect = diaryBook.getBoundingClientRect();
|
| 265 |
+
const translateX = (window.innerWidth / 2) - (bookRect.left + bookRect.width / 2);
|
| 266 |
+
const translateY = (window.innerHeight / 2) - (bookRect.top + bookRect.height / 2);
|
| 267 |
+
|
| 268 |
+
const bookToOrbKeyframes = [
|
| 269 |
+
{ transform: 'translate(0, 0) scale(1)', opacity: 1, backgroundColor: '#fdfbf7', borderRadius: '20px' },
|
| 270 |
+
{ backgroundColor: orbColor, borderRadius: '50%', offset: 0.7 },
|
| 271 |
+
{ transform: `translate(${translateX}px, ${translateY}px) scale(0)`, opacity: 0, backgroundColor: orbColor, borderRadius: '50%' }
|
| 272 |
+
];
|
| 273 |
+
const animOptions = { duration: bookFoldDuration, easing: 'ease-in-out', fill: 'forwards' };
|
| 274 |
+
const bookAnimation = diaryBook.animate(bookToOrbKeyframes, animOptions);
|
| 275 |
+
|
| 276 |
+
const orbAppearKeyframes = [
|
| 277 |
+
{ transform: 'scale(0)', opacity: 0 },
|
| 278 |
+
{ transform: 'scale(1)', opacity: 1, offset: 0.8 },
|
| 279 |
+
{ transform: 'scale(1)', opacity: 1 }
|
| 280 |
+
];
|
| 281 |
+
animElement.animate(orbAppearKeyframes, { duration: bookFoldDuration, easing: 'ease-out', fill: 'forwards' });
|
| 282 |
+
|
| 283 |
+
bookAnimation.onfinish = () => {
|
| 284 |
+
diaryBook.style.visibility = 'hidden';
|
| 285 |
+
const bounceAnimation = playFullAnimation(animElement, orbColor, bounceDuration);
|
| 286 |
+
bounceAnimation.onfinish = () => {
|
| 287 |
+
if (document.body.contains(animElement)) document.body.removeChild(animElement);
|
| 288 |
+
window.location.href = '/my_diary';
|
| 289 |
+
};
|
| 290 |
+
};
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// --- ๋ ์ง ํ์ ์
๋ฐ์ดํธ ---
|
| 294 |
+
function updateDateLabel() {
|
| 295 |
+
const dateLabel = document.querySelector('.date-label');
|
| 296 |
+
if (dateLabel) {
|
| 297 |
+
const today = new Date();
|
| 298 |
+
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
| 299 |
+
const formattedDate = today.toLocaleDateString('ko-KR', options);
|
| 300 |
+
dateLabel.textContent = formattedDate;
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// --- ์ด๊ธฐํ ์คํ ---
|
| 305 |
+
if(diaryTextarea) diaryTextarea.addEventListener('input', updateButtonState);
|
| 306 |
+
if(submitBtn) submitBtn.addEventListener('click', handleDiarySubmission);
|
| 307 |
+
if(saveDiaryBtn) saveDiaryBtn.addEventListener('click', handleDiarySave);
|
| 308 |
+
|
| 309 |
+
if(diaryTextarea) updateButtonState();
|
| 310 |
+
updateDateLabel();
|
| 311 |
+
});
|
src/templates/static/js/main_onboarding.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Onboarding script
|
| 2 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 3 |
+
const overlay = document.getElementById('onboarding-overlay');
|
| 4 |
+
const nextBtn = document.getElementById('onboarding-next');
|
| 5 |
+
const prevBtn = document.getElementById('onboarding-prev');
|
| 6 |
+
const closeBtn = document.getElementById('onboarding-close');
|
| 7 |
+
const dots = document.querySelectorAll('.onboarding-dot');
|
| 8 |
+
const slides = document.querySelectorAll('.onboarding-slide');
|
| 9 |
+
const nicknameInput = document.getElementById('onboarding-nickname-input');
|
| 10 |
+
let currentSlide = 1;
|
| 11 |
+
|
| 12 |
+
const onboardingComplete = localStorage.getItem('onboardingComplete');
|
| 13 |
+
if (onboardingComplete !== 'true') {
|
| 14 |
+
overlay.classList.add('active');
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
function showSlide(slideNumber) {
|
| 18 |
+
slides.forEach(slide => slide.classList.remove('active'));
|
| 19 |
+
dots.forEach(dot => dot.classList.remove('active'));
|
| 20 |
+
|
| 21 |
+
document.getElementById(`slide${slideNumber}`).classList.add('active');
|
| 22 |
+
document.querySelector(`.onboarding-dot[data-slide='${slideNumber}']`).classList.add('active');
|
| 23 |
+
currentSlide = slideNumber;
|
| 24 |
+
|
| 25 |
+
// Update button visibility and text
|
| 26 |
+
if (currentSlide === 1) {
|
| 27 |
+
prevBtn.style.display = 'none';
|
| 28 |
+
} else {
|
| 29 |
+
prevBtn.style.display = 'inline-block';
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
if (currentSlide === slides.length) {
|
| 33 |
+
nextBtn.textContent = '์์ํ๊ธฐ';
|
| 34 |
+
} else {
|
| 35 |
+
nextBtn.textContent = '๋ค์';
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
nextBtn.addEventListener('click', async () => {
|
| 40 |
+
if (currentSlide < slides.length) {
|
| 41 |
+
showSlide(currentSlide + 1);
|
| 42 |
+
} else { // Last slide (nickname setting)
|
| 43 |
+
const nickname = nicknameInput.value.trim();
|
| 44 |
+
if (nickname) {
|
| 45 |
+
try {
|
| 46 |
+
const response = await fetch('/update_nickname', {
|
| 47 |
+
method: 'POST',
|
| 48 |
+
headers: {
|
| 49 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
| 50 |
+
},
|
| 51 |
+
body: `nickname=${encodeURIComponent(nickname)}`,
|
| 52 |
+
});
|
| 53 |
+
if (!response.ok) {
|
| 54 |
+
throw new Error('Failed to update nickname');
|
| 55 |
+
}
|
| 56 |
+
// Optionally, update session storage or display a success message
|
| 57 |
+
console.log('Nickname updated successfully');
|
| 58 |
+
} catch (error) {
|
| 59 |
+
console.error('Error updating nickname:', error);
|
| 60 |
+
// Handle error, e.g., show a message to the user
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
localStorage.setItem('onboardingComplete', 'true');
|
| 64 |
+
closeOnboarding();
|
| 65 |
+
}
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
prevBtn.addEventListener('click', () => {
|
| 69 |
+
if (currentSlide > 1) {
|
| 70 |
+
showSlide(currentSlide - 1);
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
closeBtn.addEventListener('click', () => {
|
| 75 |
+
localStorage.setItem('onboardingComplete', 'true');
|
| 76 |
+
closeOnboarding();
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
dots.forEach(dot => {
|
| 80 |
+
dot.addEventListener('click', (e) => {
|
| 81 |
+
const slideNumber = parseInt(e.target.dataset.slide);
|
| 82 |
+
showSlide(slideNumber);
|
| 83 |
+
});
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
function closeOnboarding() {
|
| 87 |
+
overlay.classList.remove('active');
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
showSlide(currentSlide); // Initialize first slide
|
| 91 |
+
});
|
src/templates/static/js/main_theme.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ํ
๋ง ๋ณ๊ฒฝ ์คํฌ๋ฆฝํธ
|
| 2 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 3 |
+
const body = document.body;
|
| 4 |
+
const root = document.documentElement;
|
| 5 |
+
const themePalette = document.querySelector('.theme-palette');
|
| 6 |
+
const themeWrappers = document.querySelectorAll('.theme-option-wrapper');
|
| 7 |
+
|
| 8 |
+
// --- ํ
๋ง ์ค์ ---
|
| 9 |
+
const themes = {
|
| 10 |
+
// ๊ทธ๋ผ๋์ธํธ ํ
๋ง
|
| 11 |
+
default: { bg: 'linear-gradient(135deg, #ece9f7, #cacae0)', primary: '#6598e5' },
|
| 12 |
+
sunset: { bg: 'linear-gradient(135deg, #ff7e5f, #feb47b)', primary: '#e5533b' },
|
| 13 |
+
forest: { bg: 'linear-gradient(135deg, #5a3f37, #2c7744)', primary: '#92b57a' },
|
| 14 |
+
sky: { bg: 'linear-gradient(135deg, #a1c4fd, #c2e9fb)', primary: '#6a89cc' },
|
| 15 |
+
night: { bg: 'linear-gradient(135deg, #0f2027, #203a43, #2c5364)', primary: '#a3b1c6' },
|
| 16 |
+
sea: { bg: 'linear-gradient(135deg, #2c7744, #6598e5)', primary: '#92b57a' },
|
| 17 |
+
|
| 18 |
+
// ๋จ์ ํ
๋ง
|
| 19 |
+
current: { bg: 'linear-gradient(135deg, #ece9f7, #cacae0)', primary: '#6598e5' },
|
| 20 |
+
blue: { bg: 'linear-gradient(135deg, #a1c4fd, #c2e9fb)', primary: '#6a89cc' }, // ํ๋ ํ
๋ง ์์์ผ๋ก ๋ณ๊ฒฝ
|
| 21 |
+
lightyellow: { bg: '#fff9e6', primary: '#d4a237' },
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
function applyTheme(theme) {
|
| 25 |
+
body.style.background = theme.bg;
|
| 26 |
+
root.style.setProperty('--primary-color', theme.primary);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function saveTheme(themeName, themeData) {
|
| 30 |
+
localStorage.setItem('themeName', themeName);
|
| 31 |
+
if (themeName.startsWith('image-')) {
|
| 32 |
+
localStorage.setItem('themeIsImage', 'true');
|
| 33 |
+
localStorage.setItem('themeValue', themeData.bg);
|
| 34 |
+
}
|
| 35 |
+
else {
|
| 36 |
+
localStorage.setItem('themeIsImage', 'false');
|
| 37 |
+
localStorage.setItem('themeValue', themeName);
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// --- ์ด๋ฒคํธ ๋ฆฌ์ค๋ ---
|
| 42 |
+
themePalette.addEventListener('click', (e) => {
|
| 43 |
+
const target = e.target;
|
| 44 |
+
const wrapper = target.closest('.theme-option-wrapper');
|
| 45 |
+
|
| 46 |
+
// ๋ฉ์ธ ๋ฒํผ(.theme-btn) ํด๋ฆญ ์ ๋ฉ๋ด ํ ๊ธ
|
| 47 |
+
if (target.classList.contains('theme-btn')) {
|
| 48 |
+
e.stopPropagation();
|
| 49 |
+
themeWrappers.forEach(w => {
|
| 50 |
+
if (w !== wrapper) w.classList.remove('active');
|
| 51 |
+
});
|
| 52 |
+
wrapper.classList.toggle('active');
|
| 53 |
+
return; // ๋ฉ์ธ ๋ฒํผ ํด๋ฆญ ์ ํ
๋ง ์ ์ฉ ๋ก์ง์ ์คํํ์ง ์์
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const subOptions = target.closest('.sub-options');
|
| 57 |
+
if (subOptions) {
|
| 58 |
+
// --- ํ
๋ง ์ ์ฉ ๋ก์ง ---
|
| 59 |
+
let themeName, themeData;
|
| 60 |
+
|
| 61 |
+
// ์ด๋ฏธ์ง ๋ฒํผ ํด๋ฆญ ์
|
| 62 |
+
if (target.tagName === 'IMG' && target.dataset.themeBg) {
|
| 63 |
+
themeName = 'image-' + target.dataset.themeBg;
|
| 64 |
+
themeData = { bg: target.dataset.themeBg };
|
| 65 |
+
body.style.background = `url(${themeData.bg}) no-repeat center center / cover`;
|
| 66 |
+
saveTheme(themeName, themeData);
|
| 67 |
+
}
|
| 68 |
+
// ์์ ๋ฒํผ (์๋ธ) ํด๋ฆญ ์
|
| 69 |
+
else if (target.classList.contains('sub-option-btn') && target.dataset.color) {
|
| 70 |
+
themeName = target.dataset.color;
|
| 71 |
+
if (themes[themeName]) {
|
| 72 |
+
themeData = themes[themeName];
|
| 73 |
+
applyTheme(themeData);
|
| 74 |
+
saveTheme(themeName, themeData);
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
// ํ
๋ง ์ ์ฉ ํ ๋ชจ๋ ๋ฉ๋ด ๋ซ๊ธฐ
|
| 78 |
+
themeWrappers.forEach(w => w.classList.remove('active'));
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
// ํ๋ ํธ ๋ฐ๊นฅ ์์ญ ํด๋ฆญ ์ ๋ชจ๋ ๋ฉ๋ด ๋ซ๊ธฐ
|
| 83 |
+
document.addEventListener('click', (e) => {
|
| 84 |
+
if (!themePalette.contains(e.target)) {
|
| 85 |
+
themeWrappers.forEach(w => w.classList.remove('active'));
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
});
|
src/templates/static/js/script.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 2 |
+
// --- ๋งค์ง ๋ด๋น๊ฒ์ด์
๋ฐ ๋์ ---
|
| 3 |
+
const marker = document.querySelector('.nav-marker');
|
| 4 |
+
const navItems = document.querySelectorAll('.nav-item a');
|
| 5 |
+
const navList = document.querySelector('.navbar-menu');
|
| 6 |
+
|
| 7 |
+
function moveMarker(e) {
|
| 8 |
+
const item = e.target.closest('li'); // li ์์ ๊ธฐ์ค
|
| 9 |
+
if (item && marker) {
|
| 10 |
+
marker.style.width = (item.offsetWidth - 10) + 'px';
|
| 11 |
+
marker.style.left = (item.offsetLeft + 12) + 'px';
|
| 12 |
+
marker.style.opacity = '1';
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// ๊ฐ ๋ฉ๋ด์ ๋ง์ฐ์ค ์ฌ๋ฆฌ๋ฉด ๋ง์ปค ์ด๋
|
| 17 |
+
navItems.forEach(link => {
|
| 18 |
+
link.addEventListener('mouseenter', moveMarker);
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
// ๋ฉ๋ด ๋ฐ์ผ๋ก ๋๊ฐ๋ฉด ๋ง์ปค ์จ๊ธฐ๊ธฐ (์ ํ์ฌํญ)
|
| 22 |
+
if (navList) {
|
| 23 |
+
navList.addEventListener('mouseleave', () => {
|
| 24 |
+
if(marker) marker.style.opacity = '0';
|
| 25 |
+
});
|
| 26 |
+
}
|
| 27 |
+
});
|