hfexample commited on
Commit
e221c83
ยท
0 Parent(s):

Deploy clean snapshot of the repository

Browse files
This view is limited to 50 files because it contains too many changes. ย  See raw diff
Files changed (50) hide show
  1. .gitattributes +6 -0
  2. .github/workflows/sync-to-hub.yml +38 -0
  3. .gitignore +114 -0
  4. DEVELOPMENT.md +42 -0
  5. DEVLOG.md +138 -0
  6. Dockerfile +41 -0
  7. README.md +171 -0
  8. app_deps.txt +59 -0
  9. core_ml_deps.txt +26 -0
  10. cursor/mcp.json +12 -0
  11. notebooks/explore_data.py +160 -0
  12. notebooks/tabel.py +104 -0
  13. notebooks/text_cleaner.py +50 -0
  14. output.txt +22 -0
  15. package-lock.json +345 -0
  16. package.json +5 -0
  17. requirements.txt +76 -0
  18. run.py +13 -0
  19. scripts/evaluate_model.py +168 -0
  20. scripts/evaluate_step1.py +228 -0
  21. scripts/migrate_recommendations.py +85 -0
  22. scripts/newtrain.py +282 -0
  23. scripts/train_final.py +331 -0
  24. server.log +256 -0
  25. src/__init__.py +67 -0
  26. src/auth.py +78 -0
  27. src/emotion_engine.py +56 -0
  28. src/main.py +373 -0
  29. src/models.py +31 -0
  30. src/recommender.py +31 -0
  31. src/templates/_macros.html +30 -0
  32. src/templates/auth_combined.html +73 -0
  33. src/templates/base.html +43 -0
  34. src/templates/base_auth.html +19 -0
  35. src/templates/diary.html +81 -0
  36. src/templates/login.html +8 -0
  37. src/templates/main.html +159 -0
  38. src/templates/page.html +31 -0
  39. src/templates/signup.html +8 -0
  40. src/templates/static/css/base_auth.css +121 -0
  41. src/templates/static/css/diary.css +488 -0
  42. src/templates/static/css/main.css +651 -0
  43. src/templates/static/css/page.css +73 -0
  44. src/templates/static/css/style.css +49 -0
  45. src/templates/static/js/diary_logic.js +349 -0
  46. src/templates/static/js/macros_rec_tabs.js +13 -0
  47. src/templates/static/js/main_logic.js +311 -0
  48. src/templates/static/js/main_onboarding.js +91 -0
  49. src/templates/static/js/main_theme.js +88 -0
  50. 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
+ ![Main Page](https://via.placeholder.com/400x250.png?text=Main+Page)
30
+ ![Result Page](https://via.placeholder.com/400x250.png?text=Result+Page)
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] "GET /auth/logout HTTP/1.1" 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] "POST /auth/signup HTTP/1.1" 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] "POST /auth/login HTTP/1.1" 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] "GET /auth/logout HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "POST /auth/signup HTTP/1.1" 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] "POST /auth/login HTTP/1.1" 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] "GET /auth/logout HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "POST /auth/signup HTTP/1.1" 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] "POST /auth/login HTTP/1.1" 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] "GET /auth/logout HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET / HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET /favicon.ico HTTP/1.1" 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] "GET / HTTP/1.1" 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] "POST /auth/login HTTP/1.1" 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] "GET / HTTP/1.1" 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] "POST /auth/login HTTP/1.1" 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] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 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] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 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] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 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] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 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] "GET /auth/logout HTTP/1.1" 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] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 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] "GET / HTTP/1.1" 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">&times;</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">&times;</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="์˜ค๋Š˜ ๋‹น์‹ ์˜ ํ•˜๋ฃจ๋Š” ์–ด๋• ๋‚˜์š”?&#13;&#10;์†”์งํ•œ ๋งˆ์Œ์„ ์ ์–ด์ฃผ์„ธ์š”."></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
+ });