diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..8bcd0f7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,33 @@ +{ + "permissions": { + "allow": [ + "Bash(python apply_cors_fix.py:*)", + "Bash(python -c:*)", + "Bash(python quick_fix.py:*)", + "Bash(python complete_fix.py:*)", + "Bash(python app_updated.py:*)", + "Bash(python start_server.py:*)", + "Bash(python improve_error_display.py:*)", + "Bash(python fix_gemini_model.py:*)", + "Bash(taskkill:*)", + "Bash(git init:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git remote add:*)", + "Bash(git push:*)", + "Bash(python init_gitea.py:*)", + "Bash(curl:*)", + "Bash(python add_csv_buttons.py:*)", + "Bash(python add_org_fields.py:*)", + "Bash(python add_position_list_and_admin.py:*)", + "Bash(python add_dept_function.py:*)", + "Bash(python add_dept_relation.py:*)", + "Bash(cat:*)", + "Bash(python add_random_positions.py:*)", + "Bash(timeout /t 3 /nobreak)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6a01f91 --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +# HR Position Management System - Environment Variables Example +# 複製此檔案為 .env 並填入實際值 +# IMPORTANT: 請勿將 .env 檔案提交到版本控制系統! + +# ==================== MySQL Database Configuration ==================== +DB_HOST=your_database_host +DB_PORT=3306 +DB_NAME=your_database_name +DB_USER=your_database_user +DB_PASSWORD=your_secure_password_here + +# ==================== Gitea Version Control Configuration ==================== +GITEA_URL=https://your-gitea-server.com/ +GITEA_USER=your_gitea_username +GITEA_PASSWORD=your_gitea_password +GITEA_TOKEN=your_gitea_access_token + +# ==================== LLM API Keys ==================== +# Google Gemini API +GEMINI_API_KEY=your_gemini_api_key_here +GEMINI_MODEL=gemini-1.5-flash + +# DeepSeek API +DEEPSEEK_API_KEY=your_deepseek_api_key_here +DEEPSEEK_API_URL=https://api.deepseek.com/v1 + +# OpenAI API +OPENAI_API_KEY=your_openai_api_key_here +OPENAI_API_URL=https://api.openai.com/v1 + +# ==================== Flask Configuration ==================== +FLASK_APP=start_server.py +FLASK_ENV=development +FLASK_HOST=127.0.0.1 +FLASK_PORT=5000 +FLASK_DEBUG=false + +# 重要:請產生一個隨機的安全密鑰 +# 可使用: python -c "import secrets; print(secrets.token_hex(32))" +SECRET_KEY=generate_a_secure_random_key_here + +# ==================== CORS Configuration ==================== +# 允許的來源,以逗號分隔 +CORS_ORIGINS=http://localhost:5000,http://127.0.0.1:5000 + +# ==================== Application Configuration ==================== +HR_DB_SCHEMA=hr_position_system diff --git a/.gitignore b/.gitignore index a701fe2..88f6a44 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,8 @@ htmlcov/ # Backup files *.backup backup/ + +# 禁止 push 到 Gitea 的檔案 +.gitignore +USER_COMMANDS_LOG.md +nul diff --git a/Check.md b/Check.md new file mode 100644 index 0000000..6867845 --- /dev/null +++ b/Check.md @@ -0,0 +1,247 @@ +# HR Position Management System - 資安檢核報告 + +**專案名稱**: HR Position Management System +**檢核日期**: 2024-12-04 +**版本**: v2.1 + +--- + +## 一、專案結構與依賴檢查 + +### 1. 入口檔案 +- ✅ 有 `start_server.py` 作為主要入口檔案 +- ✅ 有 `index.html` 作為前端入口 + +### 2. 專案結構 +- ⚠️ 無明確的資料夾結構(app、routes、static、templates 等) +- ❌ 所有檔案皆放在根目錄,缺乏模組化架構 +- **建議**: 建立標準專案結構如 `/static`、`/templates`、`/routes`、`/config` + +### 3. 依賴檔案 +- ✅ 有 `requirements.txt` +- ❌ 無 `package.json`(前端無使用 Node.js 打包工具) + +### 4. 使用框架 +- ✅ 後端使用 **Flask** 框架 +- ✅ 使用 **flask_cors** 處理跨域 +- ✅ 使用 **python-dotenv** 讀取環境變數 + +### 5. README.md +- ✅ 有 `README.md` 檔案 +- ✅ 包含安裝與啟動說明 +- ✅ 包含 API 端點說明 + +### 6. 依賴套件安全性 +- ⚠️ 未指定套件版本號碼,可能造成相容性問題 +- **建議**: 在 requirements.txt 中指定明確版本號 + +### 7. 監聽位址與端口 +- ⚠️ **start_server.py:286** - `app.run(host='0.0.0.0', port=5000, debug=True)` +- ❌ **host='0.0.0.0'** 會監聽所有網路介面,存在安全風險 +- ❌ **debug=True** 在生產環境中不應開啟 +- **建議**: + - 開發環境使用 `host='127.0.0.1'` + - 生產環境從環境變數讀取 host/port + - 移除或設定 `debug=False` + +--- + +## 二、安全性與環境變數檢核 + +### 1. 環境變數檔案 +- ✅ 存在 `.env` 檔案 +- ❌ 無 `.env.example` 範本檔案供其他開發者參考 +- **建議**: 建立 `.env.example`,內含變數名稱但無實際值 + +### 2. .gitignore 設定 +- ✅ 有 `.gitignore` 檔案 +- ✅ 排除 `.env` +- ✅ 排除 `__pycache__` +- ✅ 排除 `*.log` +- ✅ 排除 `node_modules` +- ✅ 排除備份檔案 `*.backup` +- ✅ 設定排除 `.gitignore` 本身 + +### 3. 資料庫連線設定 +- ✅ 有 MySQL 資料庫設定(DB_HOST、DB_PORT、DB_NAME、DB_USER、DB_PASSWORD) +- ⚠️ 目前使用記憶體模擬資料庫,未實際連接 MySQL + +### 4. 敏感資訊硬編碼檢查 +- ❌ **嚴重問題** - `.env` 檔案中包含實際敏感資訊: + - `DB_PASSWORD=Bb123456` - 資料庫密碼 + - `GITEA_PASSWORD=!QAZ2wsx` - Gitea 密碼 + - `GITEA_TOKEN=9e0a888d1a25bde9cf2ad5dff2bb7ee6d68d6ff0` - Gitea Token + - `GEMINI_API_KEY=AIzaSy...` - Google API Key +- ❌ `SECRET_KEY=your_secret_key_here_change_in_production` - Flask 密鑰使用預設值 +- **建議**: + 1. 立即更換所有已洩露的密碼和 Token + 2. 確保 `.env` 永不提交到版本控制 + 3. 使用環境變數或密鑰管理服務 + +### 5. SQL Injection / XSS 防護 +- ⚠️ **SQL Injection**: 目前使用記憶體字典,未使用 ORM 或參數化查詢 +- ⚠️ **XSS 防護**: 前端直接將 API 回傳資料插入 DOM,存在潛在 XSS 風險 +- **建議**: + 1. 使用 SQLAlchemy ORM 進行資料庫操作 + 2. 前端使用 `textContent` 替代 `innerHTML` + 3. 對使用者輸入進行驗證和消毒 + +### 6. 其他安全疑慮 +- ❌ **CORS 設定過於寬鬆**: `CORS(app)` 允許所有來源 +- ❌ **無身分驗證機制**: API 端點無需認證即可存取 +- ❌ **無速率限制**: 未防範暴力攻擊或 DDoS +- ❌ **無 HTTPS**: 敏感資料可能以明文傳輸 +- ❌ **無 CSRF 保護**: 表單未使用 CSRF Token +- **建議**: + 1. 設定 CORS 允許的特定來源 + 2. 實作 JWT 或 Session 認證 + 3. 加入 rate limiting + 4. 生產環境強制使用 HTTPS + 5. 實作 CSRF 保護 + +--- + +## 三、程式品質與可維護性 + +### 1. 錯誤處理 +- ✅ **start_server.py** 有 try/except 錯誤處理 +- ✅ 有全域錯誤處理器 (`@app.errorhandler(404)`, `@app.errorhandler(500)`) +- ✅ **llm_config.py** 有完整的錯誤處理 +- ⚠️ 前端錯誤處理可改進,部分情況直接使用 console.log + +### 2. 程式碼品質 +- ⚠️ 程式碼未遵循 PEP8 完整規範 +- ⚠️ 缺少單元測試 +- **建議**: 加入 pytest 單元測試 + +--- + +## 四、檢核結果總覽 + +| 類別 | 項目 | 狀態 | +|------|------|------| +| **專案結構** | 入口檔案 | ✅ | +| | 專案結構 | ⚠️ | +| | requirements.txt | ✅ | +| | 框架識別 | ✅ | +| | README.md | ✅ | +| | 依賴安全性 | ⚠️ | +| | 監聽位址 | ❌ | +| **安全性** | .env 檔案 | ✅ | +| | .gitignore | ✅ | +| | DB 連線設定 | ⚠️ | +| | 敏感資訊硬編碼 | ❌ | +| | SQL Injection 防護 | ⚠️ | +| | XSS 防護 | ⚠️ | +| | CORS 設定 | ❌ | +| | 身分驗證 | ❌ | +| | CSRF 保護 | ❌ | +| **程式品質** | 錯誤處理 | ✅ | +| | 程式碼品質 | ⚠️ | + +--- + +## 五、評分 + +| 評分項目 | 滿分 | 得分 | 說明 | +|----------|------|------|------| +| 專案結構與依賴 | 20 | 14 | 缺乏模組化架構、監聽設定不安全 | +| 環境變數管理 | 15 | 10 | 有 .env 但包含實際敏感資訊 | +| .gitignore 設定 | 10 | 10 | 設定完整 | +| 資料庫安全 | 15 | 8 | 未使用 ORM、無參數化查詢 | +| API 安全性 | 20 | 5 | 無認證、CORS 過寬、無速率限制 | +| XSS/CSRF 防護 | 10 | 4 | 缺乏完整防護 | +| 錯誤處理 | 10 | 8 | 大致完整,前端可改進 | + +### **總分: 59 / 100** + +--- + +## 六、修改建議優先順序 + +### 🔴 高優先(立即處理) + +1. **更換所有已洩露的憑證** + - 更換 DB_PASSWORD + - 更換 GITEA_PASSWORD 和 GITEA_TOKEN + - 重新產生 GEMINI_API_KEY + - 設定強度足夠的 SECRET_KEY + +2. **修改伺服器監聽設定** + ```python + # start_server.py + if __name__ == '__main__': + host = os.getenv('FLASK_HOST', '127.0.0.1') + port = int(os.getenv('FLASK_PORT', 5000)) + debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true' + app.run(host=host, port=port, debug=debug) + ``` + +3. **建立 .env.example** + ```env + DB_HOST=your_db_host + DB_PORT=3306 + DB_NAME=your_db_name + DB_USER=your_db_user + DB_PASSWORD=your_db_password + GEMINI_API_KEY=your_api_key + SECRET_KEY=generate_a_secure_random_key + ``` + +### 🟡 中優先(儘快處理) + +4. **限制 CORS 來源** + ```python + CORS(app, origins=['http://localhost:5000', 'https://your-domain.com']) + ``` + +5. **實作基本身分驗證** + - 使用 Flask-Login 或 JWT + - 保護敏感 API 端點 + +6. **使用 ORM 防止 SQL Injection** + ```python + from flask_sqlalchemy import SQLAlchemy + db = SQLAlchemy(app) + ``` + +### 🟢 低優先(計畫性改進) + +7. **建立標準專案結構** + ``` + hr-position-system/ + ├── app/ + │ ├── __init__.py + │ ├── routes/ + │ ├── models/ + │ └── utils/ + ├── static/ + ├── templates/ + ├── config.py + └── run.py + ``` + +8. **加入單元測試** + +9. **實作 CSRF 保護** + ```python + from flask_wtf.csrf import CSRFProtect + csrf = CSRFProtect(app) + ``` + +10. **加入速率限制** + ```python + from flask_limiter import Limiter + limiter = Limiter(app, key_func=get_remote_address) + ``` + +--- + +## 七、結論 + +此專案目前處於**開發階段**,具備基本功能但存在多項安全隱患。**最緊急的問題是敏感資訊外洩風險**,需立即處理。建議在部署到生產環境前,完成所有高優先和中優先的修改項目。 + +--- + +**檢核人員**: Claude Code +**檢核日期**: 2024-12-04 diff --git a/GEMINI_API_FIX.md b/GEMINI_API_FIX.md deleted file mode 100644 index 3b9e5b9..0000000 --- a/GEMINI_API_FIX.md +++ /dev/null @@ -1,236 +0,0 @@ -# Gemini API Referrer 錯誤解決方案 - -## 🔴 錯誤訊息 - -```json -{ - "error": { - "code": 403, - "message": "Requests from referer \u003cempty\u003e are blocked.", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "API_KEY_HTTP_REFERRER_BLOCKED" - } - ] - } -} -``` - -## 📋 問題原因 - -Gemini API Key 有 **HTTP Referrer 限制**,這是 Google 的安全機制。當從服務器端調用時,HTTP Referrer 為空,導致請求被阻擋。 - ---- - -## ✅ 解決方案 - -### 方案 1: 修改 Gemini API Key 設定(推薦) - -1. **訪問 Google AI Studio** - https://makersuite.google.com/app/apikey - -2. **找到您的 API Key** - -3. **點擊「Edit API Key」或創建新的 API Key** - -4. **設定 Application restrictions** - - 選擇 **"None"** 或 **"IP addresses"** - - **不要**選擇 "HTTP referrers (websites)" - -5. **保存設定** - -6. **更新 .env 文件** - ```env - GEMINI_API_KEY=your_new_unrestricted_api_key - ``` - -7. **重啟 Flask 服務器** - ```bash - # 按 Ctrl+C 停止服務器 - python start_server.py - ``` - ---- - -### 方案 2: 使用其他 LLM API(臨時解決) - -如果您有其他 LLM API Key,可以暫時使用: - -#### 選項 A: 使用 DeepSeek - -1. **獲取 DeepSeek API Key** - https://www.deepseek.com/ - -2. **添加到 .env** - ```env - DEEPSEEK_API_KEY=your_deepseek_api_key - ``` - -3. **修改默認 API** - 編輯 `index.html`,找到 `callClaudeAPI` 調用,改為: - ```javascript - const result = await callClaudeAPI(prompt, 'deepseek'); - ``` - -#### 選項 B: 使用 OpenAI - -1. **獲取 OpenAI API Key** - https://platform.openai.com/api-keys - -2. **添加到 .env** - ```env - OPENAI_API_KEY=your_openai_api_key - ``` - -3. **修改默認 API** - 編輯 `index.html`,找到 `callClaudeAPI` 調用,改為: - ```javascript - const result = await callClaudeAPI(prompt, 'openai'); - ``` - ---- - -### 方案 3: 創建 API 選擇器(最佳長期方案) - -讓用戶在前端選擇要使用的 LLM API。 - -我可以幫您添加一個下拉選單,讓用戶可以在 Gemini、DeepSeek 和 OpenAI 之間切換。 - ---- - -## 🔍 驗證修正 - -### 測試 API 連線 - -訪問測試頁面:http://127.0.0.1:5000/api-test - -點擊每個 API 的「🧪 測試連線」按鈕,查看哪些 API 可用。 - -### 成功的表現 - -- ✅ API 測試顯示「✓ 連線成功」 -- ✅ 瀏覽器不再顯示 403 錯誤 -- ✅ AI 自動填充功能正常工作 - ---- - -## 📝 詳細錯誤訊息說明 - -您在截圖中看到的錯誤: - -``` -"reason": "API_KEY_HTTP_REFERRER_BLOCKED" -``` - -這表示: -- Gemini API Key 設定了 HTTP Referrer 限制 -- 服務器端請求沒有 Referrer,被 Google 阻擋 -- 需要移除 Referrer 限制或使用其他 API - ---- - -## 🎯 推薦操作順序 - -### 立即操作(5 分鐘) - -1. **使用 DeepSeek 或 OpenAI**(臨時解決) - ```bash - # 編輯 .env 添加其他 API Key - # 重啟服務器 - python start_server.py - ``` - -2. **測試頁面重新載入** - - 按 Ctrl+F5 刷新 - - 測試 AI 功能 - -### 長期解決(10 分鐘) - -1. **修改 Gemini API Key 設定** - - 訪問 Google AI Studio - - 移除 HTTP Referrer 限制 - - 創建新的無限制 API Key - -2. **更新配置** - - 更新 .env 文件 - - 重啟服務器 - -3. **全面測試** - - 測試所有 API - - 確保都能正常工作 - ---- - -## 💡 補充說明 - -### 為什麼服務器端請求沒有 Referrer? - -當從 Python Flask 後端調用 API 時: -- HTTP 請求是由服務器發出的 -- 沒有瀏覽器上下文 -- Referer header 為空或不存在 -- Google 的安全機制會阻擋這類請求 - -### 如何避免這個問題? - -1. **使用無限制的 API Key**(推薦) -2. **使用 IP 地址限制**而非 Referrer 限制 -3. **使用服務帳戶**(企業方案) - ---- - -## 🆘 仍然有問題? - -### 如果修改 API Key 後還是不行 - -1. **檢查 API Key 是否生效** - - 等待 1-2 分鐘 - - Google 的設定更新需要時間 - -2. **確認 .env 文件正確** - ```bash - # 查看 .env 內容 - type .env - ``` - -3. **重啟服務器** - ```bash - # 完全停止後重新啟動 - python start_server.py - ``` - -4. **清除瀏覽器緩存** - - 按 Ctrl+Shift+Delete - - 清除緩存和 Cookie - - 重新載入頁面 - ---- - -## 📊 API 對比 - -| API | 優點 | 缺點 | 推薦度 | -|-----|------|------|--------| -| **Gemini** | 免費額度高,速度快 | Referrer 限制問題 | ⭐⭐⭐ | -| **DeepSeek** | 便宜,中文支持好 | 需要付費 | ⭐⭐⭐⭐ | -| **OpenAI** | 穩定,功能強大 | 價格較高 | ⭐⭐⭐⭐⭐ | - ---- - -## ✅ 完成檢查清單 - -修正完成後,請檢查: - -- [ ] 至少一個 LLM API 測試成功 -- [ ] AI 自動填充功能正常 -- [ ] 沒有 403 錯誤 -- [ ] 錯誤訊息可以完整顯示和複製 -- [ ] 瀏覽器控制台沒有錯誤 - ---- - -**文件版本**: 1.0 -**最後更新**: 2024-12-04 -**問題類型**: Gemini API HTTP Referrer 限制 -**解決狀態**: ✅ 已提供完整解決方案 diff --git a/README.md b/README.md index b578c02..e531c2c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HR Position Management System -人力資源崗位管理系統 v2.0 +人力資源崗位管理系統 v2.1 ## 功能特色 @@ -27,19 +27,24 @@ - 技能要求 - 工作環境描述 - 職涯發展路徑 +- **保存功能**:點擊「保存並退出」或「保存並新增」將資料寫入資料庫 -### 4. 崗位清單(新功能) +### 4. 崗位清單 - 顯示所有崗位資料(表格形式) +- 自動刷新:切換到崗位清單時自動載入最新資料 +- 點擊「檢視」按鈕開啟對應的崗位描述 - 點擊欄位標題排序(升序/降序切換) - 支援匯出 CSV -### 5. 管理者頁面(新功能) -- 使用者管理(新增/編輯/刪除) -- 三種權限等級: +### 5. 管理者頁面 +- **使用者管理**(新增/編輯/刪除) - 一般使用者(綠色標籤) - 管理者(橘色標籤) - 最高權限管理者(紅色標籤) -- 匯出使用者清單 CSV +- **崗位資料管理** + - 匯出完整崗位資料為 CSV + - 即時統計資訊(總數、已描述、未描述) + - 自動更新統計資料 ### 6. 通用功能 - **CSV 匯入/匯出**:所有頁籤皆支援 @@ -115,6 +120,29 @@ python start_server.py | GET | `/api/jobs` | 獲取所有職務 | | POST | `/api/jobs` | 新增職務 | +### 崗位描述 API +| 方法 | 路徑 | 說明 | +|------|------|------| +| GET | `/api/position-descriptions` | 獲取所有崗位描述 | +| GET | `/api/position-descriptions/` | 獲取單一崗位描述 | +| POST | `/api/position-descriptions` | 新增或更新崗位描述 | +| PUT | `/api/position-descriptions/` | 更新崗位描述 | +| DELETE | `/api/position-descriptions/` | 刪除崗位描述 | + +### 崗位清單 API +| 方法 | 路徑 | 說明 | +|------|------|------| +| GET | `/api/position-list` | 獲取崗位清單 | +| GET | `/api/position-list/export` | 匯出完整崗位清單 CSV | + +### CSV 匯入匯出 API +| 方法 | 路徑 | 說明 | +|------|------|------| +| GET | `/api/positions/csv-template` | 下載崗位資料 CSV 範本 | +| POST | `/api/positions/import-csv` | 批次匯入崗位資料 | +| GET | `/api/jobs/csv-template` | 下載職務資料 CSV 範本 | +| POST | `/api/jobs/import-csv` | 批次匯入職務資料 | + ### LLM API | 方法 | 路徑 | 說明 | |------|------|------| @@ -157,6 +185,23 @@ hr-position-system/ ## 版本歷史 +### v2.1 (2024-12-04) +- **新增崗位描述保存功能** + - 保存並退出:資料寫入資料庫後切換至崗位清單 + - 保存並新增:資料寫入資料庫後清空表單 +- **崗位清單功能增強** + - 切換至崗位清單時自動刷新資料 + - 點擊「檢視」按鈕載入對應崗位描述 + - 更新表頭欄位(移除事業體/部門,新增崗位性質/等級) +- **管理者頁面新增崗位資料管理** + - 匯出完整崗位資料為 CSV + - 顯示即時統計(總數、已描述、未描述) + - 自動更新統計資料 +- **後端 API 擴充** + - 崗位描述 CRUD API + - 崗位清單查詢與匯出 API + - CSV 批次匯入 API + ### v2.0 (2024-12-04) - 新增 CSV 匯入匯出功能(所有頁籤) - 新增崗位清單頁籤(含欄位排序) @@ -167,7 +212,7 @@ hr-position-system/ - 改善錯誤訊息顯示(可複製) - 修正 Windows 編碼問題 -### v1.0 (2024-12-04) +### v1.0 (2024-12-03) - 初始版本 - 崗位基礎資料維護 - 職務基礎資料維護 diff --git a/SDD.md b/SDD.md index 962504c..560ed24 100644 --- a/SDD.md +++ b/SDD.md @@ -1,8 +1,8 @@ # HR 基礎資料維護系統 - 軟體設計文件 (SDD) -**文件版本**:2.0 -**建立日期**:2024-12-03 -**最後更新**:2024-12-04 +**文件版本**:2.1 +**建立日期**:2024-12-03 +**最後更新**:2024-12-04 **文件狀態**:Released --- @@ -22,6 +22,8 @@ | 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 | | 職務基礎資料 | 職務類別與屬性設定維護 | | 崗位描述 | 職責描述、崗位要求與任職條件維護 | +| 崗位清單 | 顯示所有崗位資料,支援查看描述與匯出 | +| 管理者頁面 | 使用者管理與完整崗位資料匯出 | ### 1.3 參考文件 @@ -320,7 +322,33 @@ | DELETE | `/api/jobs/{id}` | 刪除職務 | | POST | `/api/jobs/{id}/change-code` | 更改職務編號 | -#### 4.1.3 參照資料 API +#### 4.1.3 崗位描述 API + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/api/position-descriptions` | 獲取所有崗位描述 | +| GET | `/api/position-descriptions/{position_code}` | 獲取單一崗位描述 | +| POST | `/api/position-descriptions` | 新增或更新崗位描述 | +| PUT | `/api/position-descriptions/{position_code}` | 更新崗位描述 | +| DELETE | `/api/position-descriptions/{position_code}` | 刪除崗位描述 | + +#### 4.1.4 崗位清單 API + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/api/position-list` | 獲取崗位清單(支援分頁、搜尋) | +| GET | `/api/position-list/export` | 匯出完整崗位清單為 CSV | + +#### 4.1.5 CSV 匯入匯出 API + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/api/positions/csv-template` | 下載崗位資料 CSV 範本 | +| POST | `/api/positions/import-csv` | 批次匯入崗位資料 | +| GET | `/api/jobs/csv-template` | 下載職務資料 CSV 範本 | +| POST | `/api/jobs/import-csv` | 批次匯入職務資料 | + +#### 4.1.6 參照資料 API | 方法 | 端點 | 說明 | |------|------|------| @@ -674,39 +702,40 @@ interface Job { } ``` -### 7.3 崗位描述資料 (JobDescription) +### 7.3 崗位描述資料 (PositionDescription) ```typescript -interface JobDescription { - id: string; // ID (PK) - basicInfo: { - empNo: string; // 工號 - empName: string; // 姓名 - positionCode: string; // 崗位代碼 - versionDate: string; // 版本更新日期 - }; - positionInfo: { - positionName: string; // 崗位名稱 - department: string; // 所屬部門 - positionEffectiveDate: string; // 崗位生效日期 - directSupervisor: string; // 直接領導職務 - positionGradeJob: string; // 崗位職等&職務 - reportTo: string; // 匯報對象職務 - directReports: string; // 直接下級 - workLocation: string; // 任職地點 - empAttribute: string; // 員工屬性 - }; - responsibilities: { - positionPurpose: string; // 崗位設置目的 - mainResponsibilities: string;// 主要崗位職責 - }; - requirements: { - education: string; // 教育程度 - basicSkills: string; // 基本技能 - professionalKnowledge: string; // 專業知識 - workExperienceReq: string; // 工作經驗 - otherRequirements: string; // 其他要求 - }; +interface PositionDescription { + id: string; // 崗位編號 (PK) + positionCode: string; // 崗位編號 + positionName: string; // 崗位名稱 + effectiveDate: string; // 生效日期 + jobDuties: string; // 工作職責 + requiredSkills: string; // 所需技能 + workEnvironment: string; // 工作環境 + careerPath: string; // 職涯發展路徑 + createdAt: string; // 建立時間 + updatedAt: string; // 更新時間 +} +``` + +### 7.4 崗位清單資料 (PositionListItem) + +```typescript +interface PositionListItem { + positionCode: string; // 崗位編號 + positionName: string; // 崗位名稱 + positionCategory: string; // 崗位類別 + positionNature: string; // 崗位性質 + headcount: string; // 編制人數 + positionLevel: string; // 崗位等級 + effectiveDate: string; // 生效日期 + minEducation: string; // 最低學歷 + salaryRange: string; // 薪資範圍 + hasDescription: boolean; // 是否有描述 + jobDuties: string; // 工作職責 + requiredSkills: string; // 所需技能 + workEnvironment: string; // 工作環境 createdAt: string; // 建立時間 updatedAt: string; // 更新時間 } @@ -807,6 +836,7 @@ const i18n = { |------|------|------|----------| | 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 | | 2.0 | 2024-12-04 | System | 新增 MySQL 資料庫整合、多 LLM API 支援、全局錯誤處理、Gitea 版本控制 | +| 2.1 | 2024-12-04 | System | 新增崗位描述保存功能、崗位清單模組、管理者頁面匯出功能、CSV 批次匯入 | --- diff --git a/USER_COMMANDS_LOG.md b/USER_COMMANDS_LOG.md index c267be2..1dd6c4d 100644 --- a/USER_COMMANDS_LOG.md +++ b/USER_COMMANDS_LOG.md @@ -2,7 +2,7 @@ **專案**: HR Position Management System **日期**: 2024-12-04 -**版本**: 1.0 +**版本**: 2.1 --- @@ -243,12 +243,152 @@ email信箱,使用者權限設定(一般使用者/管理者/最高權限管理 --- +#### 15. 建立隨機崗位資料 +``` +指令: "隨機建立10筆資料到崗位清單中" +選擇: excel_table copy.md 的組織資料(12-71行) +``` + +**執行結果**: ✅ 完成 +- 建立 add_random_positions.py 腳本 +- 從 77 筆組織崗位資料中隨機選取 10 筆 +- 透過 API 批次建立崗位資料 +- 自動對應崗位類別、等級、學歷、薪資範圍 + +--- + +#### 16. CSV 批次匯入功能 +``` +指令: "建立一個的按鈕,此按鈕可以批次匯入崗位清單的資料" +指令: "建立一個的按紐,此按鈕可下載一個可以被系統讀取的崗位清單範本,具有欄位的表頭" +``` + +**執行結果**: ✅ 完成 + +**後端實作** (app_updated.py): +- ✅ GET `/api/positions/csv-template` - 下載崗位資料 CSV 範本 +- ✅ POST `/api/positions/import-csv` - 批次匯入崗位資料 +- ✅ GET `/api/jobs/csv-template` - 下載職務資料 CSV 範本 +- ✅ POST `/api/jobs/import-csv` - 批次匯入職務資料 +- 支援 UTF-8 BOM 編碼 +- 完整錯誤驗證與回報 + +**前端實作** (index.html): +- ✅ 新增「下載範本」按鈕 +- ✅ 更新 CSV 匯入函數使用 FormData API +- ✅ 顯示匯入成功/失敗統計 + +**重要修正**: +- 修正 Flask 路由順序:CSV 路由必須在動態路由 `` 之前 +- 修正 UTF-8 編碼問題(Windows) + +--- + +#### 17. 崗位描述與清單整合 +``` +指令: "#在<崗位描述>頁籤,每次當我按下<保存並退出>,或<保存並新增>,資料都會自動新增到<崗位清單>中" +指令: "#崗位清單每次click都會自動更新" +指令: "#click崗位清單的<檢視>,會開啟<崗位描述>的對應資料" + +進一步clarify: +指令: "#<崗位描述>按下<保存>按鈕後,資料會寫入資料庫" +指令: "#<崗位清單>會顯示已經建在資料庫中的資料,顯示表頭以及表身" +指令: "#<崗位清單>未顯示的表頭可先隱藏" +指令: "#<管理者頁面>中,新增一功能,可以匯出完整崗位資料的table" +``` + +**執行結果**: ✅ 全部完成 + +**後端 API 實作** (app_updated.py): +1. **崗位描述 API**: + - ✅ GET `/api/position-descriptions` - 獲取所有崗位描述 + - ✅ GET `/api/position-descriptions/` - 獲取單一崗位描述 + - ✅ POST `/api/position-descriptions` - 新增或更新崗位描述 + - ✅ PUT `/api/position-descriptions/` - 更新崗位描述 + - ✅ DELETE `/api/position-descriptions/` - 刪除崗位描述 + +2. **崗位清單 API**: + - ✅ GET `/api/position-list` - 獲取崗位清單(結合基礎資料與描述) + - ✅ GET `/api/position-list/export` - 匯出完整崗位資料為 CSV + - 支援分頁和搜尋 + - 自動合併崗位基礎資料與描述資料 + +**前端功能實作** (index.html): +1. **崗位描述保存**: + - ✅ 更新 `saveJobDescAndExit()` - 保存後切換到崗位清單 + - ✅ 更新 `saveJobDescAndNew()` - 保存後清空表單 + - 驗證必填欄位 + - 顯示成功/失敗訊息 + +2. **崗位清單顯示**: + - ✅ 實作 `loadPositionList()` - 從 API 載入資料 + - ✅ 實作 `renderPositionList()` - 渲染表格 + - ✅ 實作 `viewPositionDesc()` - 檢視崗位描述 + - ✅ 實作 `switchModule()` - 模組切換函數 + - ✅ 更新表頭欄位(移除事業體/部門,新增崗位性質/等級) + - 自動刷新:切換到崗位清單時自動載入資料 + +3. **管理者頁面擴充**: + - ✅ 新增「崗位資料管理」區塊 + - ✅ 實作 `exportCompletePositionData()` - 匯出完整資料 + - ✅ 實作 `refreshPositionStats()` - 更新統計資料 + - ✅ 顯示即時統計(總數、已描述、未描述) + - 切換到管理者頁面時自動更新統計 + +**資料結構**: +```typescript +interface PositionDescription { + id: string; + positionCode: string; + positionName: string; + effectiveDate: string; + jobDuties: string; + requiredSkills: string; + workEnvironment: string; + careerPath: string; + createdAt: string; + updatedAt: string; +} + +interface PositionListItem { + positionCode: string; + positionName: string; + positionCategory: string; + positionNature: string; + headcount: string; + positionLevel: string; + effectiveDate: string; + hasDescription: boolean; + jobDuties: string; + requiredSkills: string; + workEnvironment: string; + createdAt: string; + updatedAt: string; +} +``` + +--- + +#### 18. 更新文件並推送 Gitea +``` +指令: "更新SDD文件 and readme. push to gitea" +指令: "更新所有相關文件後push to gitea" +指令: "更新@/d:/00001_Vibe_coding/1204剛為/USER_COMMANDS_LOG.md , 但排除這個檔案上傳gitea" +``` + +**執行結果**: ⏳ 進行中 +- ✅ 更新 SDD.md 至版本 2.1 +- ✅ 更新 README.md 至版本 2.1 +- ✅ 更新 USER_COMMANDS_LOG.md(本文件) +- ⏳ 準備推送至 Gitea(排除 USER_COMMANDS_LOG.md) + +--- + ## 📊 指令統計 -**總計**: 15 個指令 -**已完成**: 13 個 -**進行中**: 1 個(測試) -**待執行**: 1 個(推送到 Gitea) +**總計**: 18 個指令 +**已完成**: 17 個 +**進行中**: 1 個(推送到 Gitea) --- diff --git a/add_dept_function.py b/add_dept_function.py new file mode 100644 index 0000000..fae0f5e --- /dev/null +++ b/add_dept_function.py @@ -0,0 +1,487 @@ +# -*- coding: utf-8 -*- +""" +添加部門職責頁籤和修正檢視按鈕功能 +""" +import sys +import codecs + +if sys.platform == 'win32': + sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict') + sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict') + +# 讀取 index.html +with open('index.html', 'r', encoding='utf-8') as f: + content = f.read() + +# ==================== 1. 修正檢視按鈕功能 ==================== +old_view_function = ''' // 檢視崗位 + function viewPosition(code) { + const position = positionListData.find(p => p.positionCode === code); + if (position) { + showToast('檢視崗位: ' + position.positionName); + } + }''' + +new_view_function = ''' // 檢視崗位 - 切換到崗位基礎資料頁籤並載入資料 + function viewPosition(code) { + const position = positionListData.find(p => p.positionCode === code); + if (position) { + // 切換到崗位基礎資料模組 + document.querySelectorAll('.module-btn').forEach(b => { + b.classList.remove('active', 'job-active', 'desc-active'); + }); + document.querySelector('.module-btn[data-module="position"]').classList.add('active'); + + document.querySelectorAll('.module-content').forEach(m => m.classList.remove('active')); + document.getElementById('module-position').classList.add('active'); + + // 填入崗位資料 + document.getElementById('positionCode').value = position.positionCode || ''; + document.getElementById('positionName').value = position.positionName || ''; + + // 根據崗位類別設定下拉選單 + const categoryMap = {'技術職': '01', '管理職': '02', '業務職': '03', '行政職': '04', '專業職': '05'}; + const categoryCode = categoryMap[position.positionCategory] || ''; + document.getElementById('positionCategory').value = categoryCode; + if (typeof updateCategoryName === 'function') updateCategoryName(); + + document.getElementById('headcount').value = position.headcount || ''; + document.getElementById('effectiveDate').value = position.effectiveDate || ''; + + // 填入組織欄位 + if (document.getElementById('businessUnit')) { + document.getElementById('businessUnit').value = position.businessUnit || ''; + } + if (document.getElementById('department')) { + document.getElementById('department').value = position.department || ''; + } + + showToast('已載入崗位: ' + position.positionName); + } + }''' + +if old_view_function in content: + content = content.replace(old_view_function, new_view_function) + print("[OK] Fixed viewPosition function") +else: + print("[INFO] viewPosition function pattern not found or already updated") + +# ==================== 2. 添加部門職責頁籤按鈕 ==================== +# 在崗位描述按鈕後面添加部門職責按鈕 +old_module_buttons = ''' + + + + +
+ + + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ +
+ + + + +
+
+ + +''' + +# 在崗位描述模組之前插入 +jobdesc_module_start = ' + + + ''' + +if jobdesc_section_pattern in content and 'deptFunctionInfoSection' not in content: + content = content.replace(old_section_end + ''' + + + + + ''', new_section_end + '''-->''') + print("[OK] Added Department Function info section to Job Description") +else: + print("[INFO] Dept Function info section already exists or pattern not found - trying alternative approach") + +# ==================== 3. 添加相關 JavaScript 函數 ==================== +dept_relation_js = ''' + // ==================== 部門職責關聯功能 ==================== + + // 重新載入部門職責下拉選單 + function refreshDeptFunctionList() { + const select = document.getElementById('jd_deptFunction'); + if (!select) return; + + // 清空現有選項 + select.innerHTML = ''; + + // 從 deptFunctionData 載入選項 + if (typeof deptFunctionData !== 'undefined' && deptFunctionData.length > 0) { + deptFunctionData.forEach(df => { + const option = document.createElement('option'); + option.value = df.deptFunctionCode; + option.textContent = `${df.deptFunctionCode} - ${df.deptFunctionName} (${df.deptFunctionDept})`; + select.appendChild(option); + }); + showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料'); + } else { + showToast('尚無部門職責資料,請先建立部門職責'); + } + } + + // 載入選中的部門職責資訊 + function loadDeptFunctionInfo() { + const select = document.getElementById('jd_deptFunction'); + const infoSection = document.getElementById('deptFunctionInfoSection'); + + if (!select || !infoSection) return; + + const selectedCode = select.value; + + if (!selectedCode) { + infoSection.style.display = 'none'; + return; + } + + // 從 deptFunctionData 找到對應的資料 + if (typeof deptFunctionData !== 'undefined') { + const deptFunc = deptFunctionData.find(d => d.deptFunctionCode === selectedCode); + + if (deptFunc) { + // 填入部門職責資訊 + document.getElementById('jd_deptFunctionCode').value = deptFunc.deptFunctionCode || ''; + document.getElementById('jd_deptFunctionBU').value = deptFunc.deptFunctionBU || ''; + document.getElementById('jd_deptMission').value = deptFunc.deptMission || ''; + document.getElementById('jd_deptCoreFunctions').value = deptFunc.deptCoreFunctions || ''; + document.getElementById('jd_deptKPIs').value = deptFunc.deptKPIs || ''; + + // 自動填入所屬部門 + const deptInput = document.getElementById('jd_department'); + if (deptInput && !deptInput.value) { + deptInput.value = deptFunc.deptFunctionDept; + } + + // 顯示部門職責資訊區塊 + infoSection.style.display = 'block'; + + showToast('已載入部門職責: ' + deptFunc.deptFunctionName); + } + } + } + + // 在頁面載入時初始化部門職責下拉選單 + document.addEventListener('DOMContentLoaded', function() { + // 延遲載入,確保 deptFunctionData 已初始化 + setTimeout(refreshDeptFunctionList, 500); + }); + +''' + +# 在部門職責模組功能之後插入 +dept_module_js_end = ' // ==================== 管理者頁面功能 ====================' + +if dept_module_js_end in content and 'refreshDeptFunctionList' not in content: + content = content.replace(dept_module_js_end, dept_relation_js + dept_module_js_end) + print("[OK] Added Department Function relation JavaScript functions") +else: + print("[INFO] Dept Function relation JS already exists or pattern not found") + +# 寫回檔案 +with open('index.html', 'w', encoding='utf-8') as f: + f.write(content) + +print("\n[DONE] Department Function relation added!") +print("- Added Department Function dropdown to Job Description form") +print("- Added Department Function info display section") +print("- Added JavaScript functions for loading dept function data") diff --git a/add_random_positions.py b/add_random_positions.py new file mode 100644 index 0000000..f8db85d --- /dev/null +++ b/add_random_positions.py @@ -0,0 +1,258 @@ +""" +隨機建立 10 筆崗位資料到系統 +從 excel_table copy.md 中隨機選取資料並透過 API 建立 +""" + +import requests +import random +from datetime import datetime +import sys +import io + +# 設定 UTF-8 輸出 +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +# 從 excel_table copy.md 中讀取的組織及崗位資料 +org_positions = [ + {"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "組長"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "班長"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "副班長"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "作業員"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "專員"}, + {"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "組長"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "班長"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "副班長"}, + {"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "作業員"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "", "position": "處長"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "", "position": "專員"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程一部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程二部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程二部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程二部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備一部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備二部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備二部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備二部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "副理"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "", "position": "處長"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "", "position": "專員"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "測試工程部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "測試工程部", "position": "課長"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "測試工程部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "新產品導入部", "position": "經副理"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "新產品導入部", "position": "專員"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "新產品導入部", "position": "工程師"}, + {"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "研發部", "position": "經副理"}, + {"business": "產品事業體", "division": "先進產品事業處", "department": "產品管理部(APD)", "position": "經副理"}, + {"business": "產品事業體", "division": "先進產品事業處", "department": "產品管理部(APD)", "position": "工程師"}, + {"business": "產品事業體", "division": "成熟產品事業處", "department": "產品管理部(MPD)", "position": "經副理"}, + {"business": "產品事業體", "division": "成熟產品事業處", "department": "產品管理部(MPD)", "position": "專案經副理"}, + {"business": "產品事業體", "division": "成熟產品事業處", "department": "產品管理部(MPD)", "position": "工程師"}, + {"business": "晶圓三廠", "division": "晶圓三廠", "department": "品質部", "position": "經副理"}, + {"business": "晶圓三廠", "division": "晶圓三廠", "department": "品質部", "position": "工程師"}, + {"business": "晶圓三廠", "division": "晶圓三廠", "department": "製造部", "position": "經副理"}, + {"business": "晶圓三廠", "division": "晶圓三廠", "department": "製造部", "position": "課長"}, + {"business": "晶圓三廠", "division": "晶圓三廠", "department": "製造部", "position": "班長"}, + {"business": "晶圓三廠", "division": "製程工程處", "department": "工程一部", "position": "經副理"}, + {"business": "晶圓三廠", "division": "製程工程處", "department": "工程一部", "position": "工程師"}, + {"business": "晶圓三廠", "division": "製程工程處", "department": "工程二部", "position": "經副理"}, + {"business": "晶圓三廠", "division": "製程工程處", "department": "工程二部", "position": "工程師"}, + {"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "招募任用部", "position": "經副理"}, + {"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "招募任用部", "position": "專員"}, + {"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "訓練發展部", "position": "經副理"}, + {"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "訓練發展部", "position": "專員"}, + {"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "薪酬管理部", "position": "經副理"}, + {"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "薪酬管理部", "position": "專員"}, +] + +# 崗位類別對應 +position_category_map = { + "處長": "02", # 管理職 + "經副理": "02", + "副理": "02", + "課長": "02", + "組長": "02", + "班長": "02", + "副班長": "02", + "工程師": "01", # 技術職 + "專員": "04", # 行政職 + "作業員": "06", # 生產職 +} + +# 崗位等級對應 +position_level_map = { + "處長": "L7", + "經副理": "L5", + "副理": "L5", + "課長": "L4", + "組長": "L3", + "班長": "L3", + "副班長": "L2", + "工程師": "L3", + "專員": "L3", + "作業員": "L1", +} + +# 學歷要求對應 +education_map = { + "處長": "MA", # 碩士 + "經副理": "BA", # 大學 + "副理": "BA", + "課長": "BA", + "組長": "JC", # 專科 + "班長": "JC", + "副班長": "HS", # 高中職 + "工程師": "BA", + "專員": "BA", + "作業員": "HS", +} + +# 薪資範圍對應 +salary_range_map = { + "處長": "E", + "經副理": "D", + "副理": "D", + "課長": "C", + "組長": "C", + "班長": "B", + "副班長": "B", + "工程師": "C", + "專員": "C", + "作業員": "A", +} + +def generate_position_code(business, division, department, position, index): + """生成崗位編號""" + # 事業體代碼 + business_code_map = { + "岡山製造事業體": "KS", + "產品事業體": "PD", + "晶圓三廠": "F3", + "集團人資行政事業體": "HR", + } + + # 崗位代碼 + position_code_map = { + "處長": "DIR", + "經副理": "MGR", + "副理": "AMG", + "課長": "SUP", + "組長": "LDR", + "班長": "CHF", + "副班長": "ACF", + "工程師": "ENG", + "專員": "SPC", + "作業員": "OPR", + } + + biz_code = business_code_map.get(business, "XX") + pos_code = position_code_map.get(position, "XXX") + + return f"{biz_code}-{pos_code}-{index:03d}" + +def create_position_data(org_data, index): + """創建崗位資料""" + business = org_data["business"] + division = org_data["division"] + department = org_data["department"] + position = org_data["position"] + + position_code = generate_position_code(business, division, department, position, index) + + # 組合崗位名稱 + dept_str = f"{department}-" if department else "" + position_name = f"{dept_str}{position}" + + return { + "basicInfo": { + "positionCode": position_code, + "positionName": position_name, + "positionCategory": position_category_map.get(position, "04"), + "positionCategoryName": "管理職" if position_category_map.get(position, "04") == "02" else "技術職", + "positionNature": "FT", + "positionNatureName": "全職", + "headcount": str(random.randint(1, 5)), + "positionLevel": position_level_map.get(position, "L3"), + "effectiveDate": "2024-01-01", + "positionDesc": f"{business} {division} {position_name}", + "positionRemark": f"組織架構: {business} > {division} > {department if department else '(處級)'}" + }, + "recruitInfo": { + "minEducation": education_map.get(position, "BA"), + "requiredGender": "", + "salaryRange": salary_range_map.get(position, "C"), + "workExperience": str(random.randint(0, 5)), + "minAge": "22", + "maxAge": "50", + "jobType": "FT", + "recruitPosition": position, + "jobTitle": position, + "jobDesc": "", + "positionReq": "", + "titleReq": "", + "majorReq": "", + "skillReq": "", + "langReq": "", + "otherReq": "", + "superiorPosition": "", + "recruitRemark": "" + } + } + +def main(): + """主程式""" + api_url = "http://localhost:5000/api/positions" + + # 隨機選取 10 筆資料 + selected = random.sample(org_positions, min(10, len(org_positions))) + + print("=" * 60) + print("隨機建立 10 筆崗位資料") + print("=" * 60) + print() + + success_count = 0 + fail_count = 0 + + for i, org_data in enumerate(selected, 1): + position_data = create_position_data(org_data, i) + + print(f"[{i}/10] 建立崗位: {position_data['basicInfo']['positionCode']} - {position_data['basicInfo']['positionName']}") + + try: + response = requests.post(api_url, json=position_data, timeout=5) + + if response.status_code == 201: + print(f" [OK] 成功") + success_count += 1 + else: + error_msg = response.json().get('error', '未知錯誤') + print(f" [ERROR] 失敗: {error_msg}") + fail_count += 1 + + except requests.exceptions.ConnectionError: + print(f" [ERROR] 失敗: 無法連接到伺服器 (請確認伺服器是否已啟動)") + fail_count += 1 + except Exception as e: + print(f" [ERROR] 失敗: {str(e)}") + fail_count += 1 + + print() + + print("=" * 60) + print(f"建立完成: 成功 {success_count} 筆, 失敗 {fail_count} 筆") + print("=" * 60) + +if __name__ == "__main__": + main() diff --git a/app.py b/app.py new file mode 100644 index 0000000..6c3a3c3 --- /dev/null +++ b/app.py @@ -0,0 +1,1504 @@ +""" +HR 崗位基礎資料維護系統 - Flask Backend API +============================================= +提供崗位資料的 CRUD 操作 API 和 LLM API 整合 +""" + +from flask import Flask, request, jsonify, send_from_directory, make_response +from flask_cors import CORS +from datetime import datetime +import json +import os +import uuid +import csv +import io +import sys + +# 設定 UTF-8 輸出 +if sys.stdout.encoding != 'utf-8': + sys.stdout.reconfigure(encoding='utf-8') + +# Import LLM configuration +try: + from llm_config import LLMConfig + llm_config = LLMConfig() + LLM_ENABLED = True +except ImportError: + print("Warning: llm_config not found. LLM features will be disabled.") + LLM_ENABLED = False + +app = Flask(__name__, static_folder='.') +CORS(app) + +# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL) +positions_db = {} + +# 預設崗位資料 +default_positions = { + "MGR-001": { + "id": "MGR-001", + "basicInfo": { + "positionCode": "MGR-001", + "positionName": "管理職-資深班長", + "positionCategory": "02", + "positionCategoryName": "管理職", + "positionNature": "FT", + "positionNatureName": "全職", + "headcount": "5", + "positionLevel": "L3", + "effectiveDate": "2001-01-01", + "positionDesc": "負責生產線的日常管理與人員調度", + "positionRemark": "" + }, + "recruitInfo": { + "minEducation": "JC", + "requiredGender": "", + "salaryRange": "C", + "workExperience": "3", + "minAge": "25", + "maxAge": "45", + "jobType": "FT", + "recruitPosition": "MGR", + "jobTitle": "資深班長", + "jobDesc": "", + "positionReq": "", + "titleReq": "", + "majorReq": "", + "skillReq": "", + "langReq": "", + "otherReq": "", + "superiorPosition": "", + "recruitRemark": "" + }, + "createdAt": "2024-01-01T00:00:00", + "updatedAt": "2024-01-01T00:00:00" + } +} + +positions_db.update(default_positions) + +# 職務資料庫 +jobs_db = {} + +# 預設職務資料 +default_jobs = { + "VP-001": { + "id": "VP-001", + "jobCategoryCode": "MGR", + "jobCategoryName": "管理職", + "jobCode": "VP-001", + "jobName": "副總", + "jobNameEn": "Vice President", + "jobEffectiveDate": "2001-01-01", + "jobHeadcount": 2, + "jobSortOrder": 10, + "jobRemark": "", + "jobLevel": "*保密*", + "hasAttendanceBonus": False, + "hasHousingAllowance": True, + "createdAt": "2024-01-01T00:00:00", + "updatedAt": "2024-01-01T00:00:00" + } +} + +jobs_db.update(default_jobs) + +# 崗位描述資料庫 +position_descriptions_db = {} + +# 預設崗位描述資料 +default_descriptions = { + "MGR-001": { + "id": "MGR-001", + "positionCode": "MGR-001", + "positionName": "管理職-資深班長", + "effectiveDate": "2024-01-01", + "jobDuties": "1. 生產線管理\n2. 人員調度\n3. 品質監控", + "requiredSkills": "領導能力、溝通協調、問題解決", + "workEnvironment": "生產現場", + "careerPath": "班長 → 組長 → 課長", + "createdAt": "2024-01-01T00:00:00", + "updatedAt": "2024-01-01T00:00:00" + } +} + +position_descriptions_db.update(default_descriptions) + + +# ==================== 靜態頁面 ==================== + +@app.route('/') +def index(): + """返回主頁面""" + return send_from_directory('.', 'index.html') + + +@app.route('/api-test') +def api_test_page(): + """返回 API 測試頁面""" + return send_from_directory('.', 'api_test.html') + + +# ==================== 崗位資料 API ==================== + +# CSV 路由必須在 路由之前定義 +@app.route('/api/positions/csv-template', methods=['GET']) +def download_position_csv_template(): + """下載崗位資料 CSV 範本""" + # CSV 欄位定義 + headers = [ + '崗位編號*', '崗位名稱*', '崗位類別代碼*', '崗位類別名稱', + '崗位性質代碼', '崗位性質名稱', '人數', '崗位等級', + '生效日期', '崗位說明', '崗位備註', + '最低學歷', '性別要求', '薪資範圍', '工作經驗年數', + '最低年齡', '最高年齡', '職缺性質', '招募崗位', + '職稱', '工作說明', '崗位要求', '職稱要求', + '專業要求', '技能要求', '語言要求', '其他要求', + '上級崗位', '招募備註' + ] + + # 範例資料 + example_row = [ + 'MGR-001', '管理職-資深班長', '02', '管理職', + 'FT', '全職', '5', 'L3', + '2024-01-01', '負責生產線的日常管理與人員調度', '', + 'JC', '', 'C', '3', + '25', '45', 'FT', 'MGR', + '資深班長', '', '', '', + '', '', '', '', + '', '' + ] + + # 建立 CSV 內容 + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(headers) + writer.writerow(example_row) + + # 建立回應 + response = make_response(output.getvalue()) + response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig' + response.headers['Content-Disposition'] = 'attachment; filename=position_template.csv' + + return response + + +@app.route('/api/positions/import-csv', methods=['POST']) +def import_positions_csv(): + """批次匯入崗位資料 CSV""" + try: + if 'file' not in request.files: + return jsonify({ + 'success': False, + 'error': '未提供 CSV 檔案' + }), 400 + + file = request.files['file'] + + if file.filename == '': + return jsonify({ + 'success': False, + 'error': '未選擇檔案' + }), 400 + + if not file.filename.endswith('.csv'): + return jsonify({ + 'success': False, + 'error': '檔案格式錯誤,請上傳 CSV 檔案' + }), 400 + + # 讀取 CSV 內容 + stream = io.StringIO(file.stream.read().decode('utf-8-sig')) + csv_reader = csv.DictReader(stream) + + success_count = 0 + error_count = 0 + errors = [] + + for row_num, row in enumerate(csv_reader, start=2): + try: + # 驗證必填欄位 + position_code = row.get('崗位編號*', '').strip() + position_name = row.get('崗位名稱*', '').strip() + position_category = row.get('崗位類別代碼*', '').strip() + + if not position_code: + errors.append(f"第 {row_num} 列: 崗位編號為必填") + error_count += 1 + continue + + if not position_name: + errors.append(f"第 {row_num} 列: 崗位名稱為必填") + error_count += 1 + continue + + if not position_category: + errors.append(f"第 {row_num} 列: 崗位類別代碼為必填") + error_count += 1 + continue + + # 檢查是否已存在 + if position_code in positions_db: + errors.append(f"第 {row_num} 列: 崗位編號 {position_code} 已存在") + error_count += 1 + continue + + # 建立崗位資料 + now = datetime.now().isoformat() + new_position = { + 'id': position_code, + 'basicInfo': { + 'positionCode': position_code, + 'positionName': position_name, + 'positionCategory': position_category, + 'positionCategoryName': row.get('崗位類別名稱', '').strip(), + 'positionNature': row.get('崗位性質代碼', 'FT').strip(), + 'positionNatureName': row.get('崗位性質名稱', '全職').strip(), + 'headcount': row.get('人數', '1').strip(), + 'positionLevel': row.get('崗位等級', 'L3').strip(), + 'effectiveDate': row.get('生效日期', '2024-01-01').strip(), + 'positionDesc': row.get('崗位說明', '').strip(), + 'positionRemark': row.get('崗位備註', '').strip() + }, + 'recruitInfo': { + 'minEducation': row.get('最低學歷', 'BA').strip(), + 'requiredGender': row.get('性別要求', '').strip(), + 'salaryRange': row.get('薪資範圍', 'C').strip(), + 'workExperience': row.get('工作經驗年數', '0').strip(), + 'minAge': row.get('最低年齡', '22').strip(), + 'maxAge': row.get('最高年齡', '50').strip(), + 'jobType': row.get('職缺性質', 'FT').strip(), + 'recruitPosition': row.get('招募崗位', '').strip(), + 'jobTitle': row.get('職稱', '').strip(), + 'jobDesc': row.get('工作說明', '').strip(), + 'positionReq': row.get('崗位要求', '').strip(), + 'titleReq': row.get('職稱要求', '').strip(), + 'majorReq': row.get('專業要求', '').strip(), + 'skillReq': row.get('技能要求', '').strip(), + 'langReq': row.get('語言要求', '').strip(), + 'otherReq': row.get('其他要求', '').strip(), + 'superiorPosition': row.get('上級崗位', '').strip(), + 'recruitRemark': row.get('招募備註', '').strip() + }, + 'createdAt': now, + 'updatedAt': now + } + + positions_db[position_code] = new_position + success_count += 1 + + except Exception as e: + errors.append(f"第 {row_num} 列: {str(e)}") + error_count += 1 + + return jsonify({ + 'success': True, + 'message': f'匯入完成: 成功 {success_count} 筆, 失敗 {error_count} 筆', + 'successCount': success_count, + 'errorCount': error_count, + 'errors': errors[:10] # 只返回前 10 個錯誤 + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'匯入失敗: {str(e)}' + }), 500 + + +@app.route('/api/positions', methods=['GET']) +def get_positions(): + """ + 獲取所有崗位資料 + Query params: + - page: 頁碼 (default: 1) + - size: 每頁筆數 (default: 20) + - search: 搜尋關鍵字 + """ + page = request.args.get('page', 1, type=int) + size = request.args.get('size', 20, type=int) + search = request.args.get('search', '', type=str) + + # 過濾搜尋 + filtered = list(positions_db.values()) + if search: + filtered = [ + p for p in filtered + if search.lower() in p['basicInfo'].get('positionCode', '').lower() + or search.lower() in p['basicInfo'].get('positionName', '').lower() + ] + + # 分頁 + total = len(filtered) + start = (page - 1) * size + end = start + size + paginated = filtered[start:end] + + return jsonify({ + 'success': True, + 'data': paginated, + 'pagination': { + 'page': page, + 'size': size, + 'total': total, + 'totalPages': (total + size - 1) // size + } + }) + + +@app.route('/api/positions/', methods=['GET']) +def get_position(position_id): + """獲取單一崗位資料""" + if position_id not in positions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位資料' + }), 404 + + return jsonify({ + 'success': True, + 'data': positions_db[position_id] + }) + + +@app.route('/api/positions', methods=['POST']) +def create_position(): + """ + 新增崗位資料 + Request body: { + basicInfo: {...}, + recruitInfo: {...} + } + """ + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + # 驗證必填欄位 + basic_info = data.get('basicInfo', {}) + if not basic_info.get('positionCode'): + return jsonify({ + 'success': False, + 'error': '崗位編號為必填欄位' + }), 400 + + if not basic_info.get('positionName'): + return jsonify({ + 'success': False, + 'error': '崗位名稱為必填欄位' + }), 400 + + position_code = basic_info['positionCode'] + + # 檢查是否已存在 + if position_code in positions_db: + return jsonify({ + 'success': False, + 'error': f'崗位編號 {position_code} 已存在' + }), 409 + + # 建立新記錄 + now = datetime.now().isoformat() + new_position = { + 'id': position_code, + 'basicInfo': basic_info, + 'recruitInfo': data.get('recruitInfo', {}), + 'createdAt': now, + 'updatedAt': now + } + + positions_db[position_code] = new_position + + return jsonify({ + 'success': True, + 'message': '崗位資料新增成功', + 'data': new_position + }), 201 + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'新增失敗: {str(e)}' + }), 500 + + +@app.route('/api/positions/', methods=['PUT']) +def update_position(position_id): + """ + 更新崗位資料 + Request body: { + basicInfo: {...}, + recruitInfo: {...} + } + """ + try: + if position_id not in positions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位資料' + }), 404 + + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + # 更新資料 + existing = positions_db[position_id] + + if 'basicInfo' in data: + existing['basicInfo'].update(data['basicInfo']) + + if 'recruitInfo' in data: + existing['recruitInfo'].update(data['recruitInfo']) + + existing['updatedAt'] = datetime.now().isoformat() + + return jsonify({ + 'success': True, + 'message': '崗位資料更新成功', + 'data': existing + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'更新失敗: {str(e)}' + }), 500 + + +@app.route('/api/positions/', methods=['DELETE']) +def delete_position(position_id): + """刪除崗位資料""" + try: + if position_id not in positions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位資料' + }), 404 + + deleted = positions_db.pop(position_id) + + return jsonify({ + 'success': True, + 'message': '崗位資料已刪除', + 'data': deleted + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'刪除失敗: {str(e)}' + }), 500 + + +@app.route('/api/positions//change-code', methods=['POST']) +def change_position_code(position_id): + """ + 更改崗位編號 + Request body: { newCode: "新編號" } + """ + try: + if position_id not in positions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位資料' + }), 404 + + data = request.get_json() + new_code = data.get('newCode') + + if not new_code: + return jsonify({ + 'success': False, + 'error': '請提供新的崗位編號' + }), 400 + + if new_code in positions_db: + return jsonify({ + 'success': False, + 'error': f'崗位編號 {new_code} 已存在' + }), 409 + + # 更新編號 + position = positions_db.pop(position_id) + position['id'] = new_code + position['basicInfo']['positionCode'] = new_code + position['updatedAt'] = datetime.now().isoformat() + + positions_db[new_code] = position + + return jsonify({ + 'success': True, + 'message': f'崗位編號已從 {position_id} 更改為 {new_code}', + 'data': position + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'更改失敗: {str(e)}' + }), 500 + + +# ==================== 職務基礎資料 API ==================== + +# CSV 路由必須在 路由之前定義 +@app.route('/api/jobs/csv-template', methods=['GET']) +def download_job_csv_template(): + """下載職務資料 CSV 範本""" + # CSV 欄位定義 + headers = [ + '職務編號*', '職務類別代碼*', '職務類別名稱', '職務名稱*', + '職務英文名稱', '生效日期', '人數', '排序', + '備註', '職級', '全勤獎金', '住宿津貼' + ] + + # 範例資料 + example_row = [ + 'VP-001', 'MGR', '管理職', '副總', + 'Vice President', '2024-01-01', '2', '10', + '', '*保密*', '否', '是' + ] + + # 建立 CSV 內容 + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(headers) + writer.writerow(example_row) + + # 建立回應 + response = make_response(output.getvalue()) + response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig' + response.headers['Content-Disposition'] = 'attachment; filename=job_template.csv' + + return response + + +@app.route('/api/jobs/import-csv', methods=['POST']) +def import_jobs_csv(): + """批次匯入職務資料 CSV""" + try: + if 'file' not in request.files: + return jsonify({ + 'success': False, + 'error': '未提供 CSV 檔案' + }), 400 + + file = request.files['file'] + + if file.filename == '': + return jsonify({ + 'success': False, + 'error': '未選擇檔案' + }), 400 + + if not file.filename.endswith('.csv'): + return jsonify({ + 'success': False, + 'error': '檔案格式錯誤,請上傳 CSV 檔案' + }), 400 + + # 讀取 CSV 內容 + stream = io.StringIO(file.stream.read().decode('utf-8-sig')) + csv_reader = csv.DictReader(stream) + + success_count = 0 + error_count = 0 + errors = [] + + for row_num, row in enumerate(csv_reader, start=2): + try: + # 驗證必填欄位 + job_code = row.get('職務編號*', '').strip() + job_name = row.get('職務名稱*', '').strip() + job_category_code = row.get('職務類別代碼*', '').strip() + + if not job_code: + errors.append(f"第 {row_num} 列: 職務編號為必填") + error_count += 1 + continue + + if not job_name: + errors.append(f"第 {row_num} 列: 職務名稱為必填") + error_count += 1 + continue + + if not job_category_code: + errors.append(f"第 {row_num} 列: 職務類別代碼為必填") + error_count += 1 + continue + + # 檢查是否已存在 + if job_code in jobs_db: + errors.append(f"第 {row_num} 列: 職務編號 {job_code} 已存在") + error_count += 1 + continue + + # 建立職務資料 + now = datetime.now().isoformat() + new_job = { + 'id': job_code, + 'jobCategoryCode': job_category_code, + 'jobCategoryName': row.get('職務類別名稱', '').strip(), + 'jobCode': job_code, + 'jobName': job_name, + 'jobNameEn': row.get('職務英文名稱', '').strip(), + 'jobEffectiveDate': row.get('生效日期', '2024-01-01').strip(), + 'jobHeadcount': int(row.get('人數', '1').strip() or '1'), + 'jobSortOrder': int(row.get('排序', '0').strip() or '0'), + 'jobRemark': row.get('備註', '').strip(), + 'jobLevel': row.get('職級', '').strip(), + 'hasAttendanceBonus': row.get('全勤獎金', '否').strip() in ['是', 'True', 'true', '1'], + 'hasHousingAllowance': row.get('住宿津貼', '否').strip() in ['是', 'True', 'true', '1'], + 'createdAt': now, + 'updatedAt': now + } + + jobs_db[job_code] = new_job + success_count += 1 + + except Exception as e: + errors.append(f"第 {row_num} 列: {str(e)}") + error_count += 1 + + return jsonify({ + 'success': True, + 'message': f'匯入完成: 成功 {success_count} 筆, 失敗 {error_count} 筆', + 'successCount': success_count, + 'errorCount': error_count, + 'errors': errors[:10] # 只返回前 10 個錯誤 + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'匯入失敗: {str(e)}' + }), 500 + + +@app.route('/api/jobs', methods=['GET']) +def get_jobs(): + """ + 獲取所有職務資料 + Query params: + - page: 頁碼 (default: 1) + - size: 每頁筆數 (default: 20) + - search: 搜尋關鍵字 + - category: 職務類別篩選 + """ + page = request.args.get('page', 1, type=int) + size = request.args.get('size', 20, type=int) + search = request.args.get('search', '', type=str) + category = request.args.get('category', '', type=str) + + # 過濾搜尋 + filtered = list(jobs_db.values()) + if search: + filtered = [ + j for j in filtered + if search.lower() in j.get('jobCode', '').lower() + or search.lower() in j.get('jobName', '').lower() + ] + if category: + filtered = [j for j in filtered if j.get('jobCategoryCode') == category] + + # 排序 + filtered.sort(key=lambda x: x.get('jobSortOrder', 0)) + + # 分頁 + total = len(filtered) + start = (page - 1) * size + end = start + size + paginated = filtered[start:end] + + return jsonify({ + 'success': True, + 'data': paginated, + 'pagination': { + 'page': page, + 'size': size, + 'total': total, + 'totalPages': (total + size - 1) // size + } + }) + + +@app.route('/api/jobs/', methods=['GET']) +def get_job(job_id): + """獲取單一職務資料""" + if job_id not in jobs_db: + return jsonify({ + 'success': False, + 'error': '找不到該職務資料' + }), 404 + + return jsonify({ + 'success': True, + 'data': jobs_db[job_id] + }) + + +@app.route('/api/jobs', methods=['POST']) +def create_job(): + """ + 新增職務資料 + Request body: { + jobCategoryCode: "MGR", + jobCategoryName: "管理職", + jobCode: "VP-001", + jobName: "副總", + ... + } + """ + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + # 驗證必填欄位 + if not data.get('jobCode'): + return jsonify({ + 'success': False, + 'error': '職務編號為必填欄位' + }), 400 + + if not data.get('jobName'): + return jsonify({ + 'success': False, + 'error': '職務名稱為必填欄位' + }), 400 + + if not data.get('jobCategoryCode'): + return jsonify({ + 'success': False, + 'error': '職務類別為必填欄位' + }), 400 + + job_code = data['jobCode'] + + # 檢查是否已存在 + if job_code in jobs_db: + return jsonify({ + 'success': False, + 'error': f'職務編號 {job_code} 已存在' + }), 409 + + # 建立新記錄 + now = datetime.now().isoformat() + new_job = { + 'id': job_code, + **data, + 'createdAt': now, + 'updatedAt': now + } + + jobs_db[job_code] = new_job + + return jsonify({ + 'success': True, + 'message': '職務資料新增成功', + 'data': new_job + }), 201 + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'新增失敗: {str(e)}' + }), 500 + + +@app.route('/api/jobs/', methods=['PUT']) +def update_job(job_id): + """更新職務資料""" + try: + if job_id not in jobs_db: + return jsonify({ + 'success': False, + 'error': '找不到該職務資料' + }), 404 + + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + # 更新資料 + existing = jobs_db[job_id] + existing.update(data) + existing['updatedAt'] = datetime.now().isoformat() + + return jsonify({ + 'success': True, + 'message': '職務資料更新成功', + 'data': existing + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'更新失敗: {str(e)}' + }), 500 + + +@app.route('/api/jobs/', methods=['DELETE']) +def delete_job(job_id): + """刪除職務資料""" + try: + if job_id not in jobs_db: + return jsonify({ + 'success': False, + 'error': '找不到該職務資料' + }), 404 + + deleted = jobs_db.pop(job_id) + + return jsonify({ + 'success': True, + 'message': '職務資料已刪除', + 'data': deleted + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'刪除失敗: {str(e)}' + }), 500 + + +@app.route('/api/jobs//change-code', methods=['POST']) +def change_job_code(job_id): + """ + 更改職務編號 + Request body: { newCode: "新編號" } + """ + try: + if job_id not in jobs_db: + return jsonify({ + 'success': False, + 'error': '找不到該職務資料' + }), 404 + + data = request.get_json() + new_code = data.get('newCode') + + if not new_code: + return jsonify({ + 'success': False, + 'error': '請提供新的職務編號' + }), 400 + + if new_code in jobs_db: + return jsonify({ + 'success': False, + 'error': f'職務編號 {new_code} 已存在' + }), 409 + + # 更新編號 + job = jobs_db.pop(job_id) + job['id'] = new_code + job['jobCode'] = new_code + job['updatedAt'] = datetime.now().isoformat() + + jobs_db[new_code] = job + + return jsonify({ + 'success': True, + 'message': f'職務編號已從 {job_id} 更改為 {new_code}', + 'data': job + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'更改失敗: {str(e)}' + }), 500 + + +# ==================== 參照資料 API ==================== + +@app.route('/api/reference/categories', methods=['GET']) +def get_categories(): + """獲取崗位類別選項""" + return jsonify({ + 'success': True, + 'data': [ + {'code': '01', 'name': '技術職'}, + {'code': '02', 'name': '管理職'}, + {'code': '03', 'name': '業務職'}, + {'code': '04', 'name': '行政職'}, + {'code': '05', 'name': '研發職'}, + {'code': '06', 'name': '生產職'} + ] + }) + + +@app.route('/api/reference/job-categories', methods=['GET']) +def get_job_categories(): + """獲取職務類別選項""" + return jsonify({ + 'success': True, + 'data': [ + {'code': 'MGR', 'name': '管理職'}, + {'code': 'TECH', 'name': '技術職'}, + {'code': 'SALE', 'name': '業務職'}, + {'code': 'ADMIN', 'name': '行政職'}, + {'code': 'RD', 'name': '研發職'}, + {'code': 'PROD', 'name': '生產職'} + ] + }) + + +@app.route('/api/reference/natures', methods=['GET']) +def get_natures(): + """獲取崗位性質選項""" + return jsonify({ + 'success': True, + 'data': [ + {'code': 'FT', 'name': '全職'}, + {'code': 'PT', 'name': '兼職'}, + {'code': 'CT', 'name': '約聘'}, + {'code': 'IN', 'name': '實習'}, + {'code': 'DP', 'name': '派遣'} + ] + }) + + +@app.route('/api/reference/education', methods=['GET']) +def get_education(): + """獲取學歷選項""" + return jsonify({ + 'success': True, + 'data': [ + {'code': 'HS', 'name': '高中/職'}, + {'code': 'JC', 'name': '專科'}, + {'code': 'BA', 'name': '大學'}, + {'code': 'MA', 'name': '碩士'}, + {'code': 'PHD', 'name': '博士'} + ] + }) + + +@app.route('/api/reference/majors', methods=['GET']) +def get_majors(): + """獲取專業選項""" + return jsonify({ + 'success': True, + 'data': [ + {'code': 'CS', 'name': '資訊工程'}, + {'code': 'EE', 'name': '電機電子'}, + {'code': 'ME', 'name': '機械工程'}, + {'code': 'BA', 'name': '企業管理'}, + {'code': 'ACC', 'name': '會計財務'}, + {'code': 'HR', 'name': '人力資源'}, + {'code': 'MKT', 'name': '行銷企劃'}, + {'code': 'LAW', 'name': '法律'}, + {'code': 'CHE', 'name': '化學工程'}, + {'code': 'IE', 'name': '工業工程'} + ] + }) + + +# ==================== 崗位描述 API ==================== + +@app.route('/api/position-descriptions', methods=['GET']) +def get_position_descriptions(): + """獲取所有崗位描述""" + return jsonify({ + 'success': True, + 'data': list(position_descriptions_db.values()) + }) + + +@app.route('/api/position-descriptions/', methods=['GET']) +def get_position_description(position_code): + """獲取單一崗位描述""" + if position_code not in position_descriptions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位描述' + }), 404 + + return jsonify({ + 'success': True, + 'data': position_descriptions_db[position_code] + }) + + +@app.route('/api/position-descriptions', methods=['POST']) +def create_position_description(): + """新增或更新崗位描述""" + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + position_code = data.get('positionCode') + if not position_code: + return jsonify({ + 'success': False, + 'error': '崗位編號為必填欄位' + }), 400 + + # 檢查崗位是否存在 + if position_code not in positions_db: + return jsonify({ + 'success': False, + 'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料' + }), 404 + + now = datetime.now().isoformat() + + # 如果已存在則更新,否則新增 + if position_code in position_descriptions_db: + position_descriptions_db[position_code].update({ + **data, + 'updatedAt': now + }) + message = '崗位描述更新成功' + else: + position_descriptions_db[position_code] = { + 'id': position_code, + **data, + 'createdAt': now, + 'updatedAt': now + } + message = '崗位描述新增成功' + + return jsonify({ + 'success': True, + 'message': message, + 'data': position_descriptions_db[position_code] + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'操作失敗: {str(e)}' + }), 500 + + +@app.route('/api/position-descriptions/', methods=['PUT']) +def update_position_description(position_code): + """更新崗位描述""" + try: + if position_code not in position_descriptions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位描述' + }), 404 + + data = request.get_json() + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + position_descriptions_db[position_code].update({ + **data, + 'updatedAt': datetime.now().isoformat() + }) + + return jsonify({ + 'success': True, + 'message': '崗位描述更新成功', + 'data': position_descriptions_db[position_code] + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'更新失敗: {str(e)}' + }), 500 + + +@app.route('/api/position-descriptions/', methods=['DELETE']) +def delete_position_description(position_code): + """刪除崗位描述""" + try: + if position_code not in position_descriptions_db: + return jsonify({ + 'success': False, + 'error': '找不到該崗位描述' + }), 404 + + deleted = position_descriptions_db.pop(position_code) + + return jsonify({ + 'success': True, + 'message': '崗位描述已刪除', + 'data': deleted + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'刪除失敗: {str(e)}' + }), 500 + + +# ==================== 崗位清單 API ==================== + +@app.route('/api/position-list', methods=['GET']) +def get_position_list(): + """ + 獲取崗位清單 (結合崗位基礎資料和崗位描述) + Query params: + - page: 頁碼 (default: 1) + - size: 每頁筆數 (default: 20) + - search: 搜尋關鍵字 + """ + page = request.args.get('page', 1, type=int) + size = request.args.get('size', 20, type=int) + search = request.args.get('search', '', type=str) + + # 組合崗位資料和描述 + combined_list = [] + for position_code, position in positions_db.items(): + description = position_descriptions_db.get(position_code, {}) + + combined = { + 'positionCode': position_code, + 'positionName': position['basicInfo'].get('positionName', ''), + 'positionCategory': position['basicInfo'].get('positionCategoryName', ''), + 'positionNature': position['basicInfo'].get('positionNatureName', ''), + 'headcount': position['basicInfo'].get('headcount', ''), + 'positionLevel': position['basicInfo'].get('positionLevel', ''), + 'effectiveDate': position['basicInfo'].get('effectiveDate', ''), + 'minEducation': position['recruitInfo'].get('minEducation', ''), + 'salaryRange': position['recruitInfo'].get('salaryRange', ''), + 'hasDescription': position_code in position_descriptions_db, + 'jobDuties': description.get('jobDuties', ''), + 'requiredSkills': description.get('requiredSkills', ''), + 'workEnvironment': description.get('workEnvironment', ''), + 'createdAt': position.get('createdAt', ''), + 'updatedAt': position.get('updatedAt', '') + } + combined_list.append(combined) + + # 過濾搜尋 + if search: + combined_list = [ + item for item in combined_list + if search.lower() in item['positionCode'].lower() + or search.lower() in item['positionName'].lower() + ] + + # 分頁 + total = len(combined_list) + start = (page - 1) * size + end = start + size + paginated = combined_list[start:end] + + return jsonify({ + 'success': True, + 'data': paginated, + 'pagination': { + 'page': page, + 'size': size, + 'total': total, + 'totalPages': (total + size - 1) // size + } + }) + + +@app.route('/api/position-list/export', methods=['GET']) +def export_position_list(): + """匯出完整崗位清單為 CSV""" + try: + # CSV 欄位定義 + headers = [ + '崗位編號', '崗位名稱', '崗位類別', '崗位性質', '人數', '崗位等級', + '生效日期', '最低學歷', '薪資範圍', '工作職責', '所需技能', '工作環境', + '建立時間', '更新時間' + ] + + # 組合所有崗位資料 + rows = [] + for position_code, position in positions_db.items(): + description = position_descriptions_db.get(position_code, {}) + + row = [ + position_code, + position['basicInfo'].get('positionName', ''), + position['basicInfo'].get('positionCategoryName', ''), + position['basicInfo'].get('positionNatureName', ''), + position['basicInfo'].get('headcount', ''), + position['basicInfo'].get('positionLevel', ''), + position['basicInfo'].get('effectiveDate', ''), + position['recruitInfo'].get('minEducation', ''), + position['recruitInfo'].get('salaryRange', ''), + description.get('jobDuties', '').replace('\n', ' | '), + description.get('requiredSkills', ''), + description.get('workEnvironment', ''), + position.get('createdAt', ''), + position.get('updatedAt', '') + ] + rows.append(row) + + # 建立 CSV 內容 + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(headers) + writer.writerows(rows) + + # 建立回應 + response = make_response(output.getvalue()) + response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig' + response.headers['Content-Disposition'] = f'attachment; filename=position_list_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' + + return response + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'匯出失敗: {str(e)}' + }), 500 + + +# ==================== LLM API ==================== + +@app.route('/api/llm/config', methods=['GET']) +def get_llm_config(): + """獲取 LLM API 配置狀態""" + if not LLM_ENABLED: + return jsonify({ + 'success': False, + 'error': 'LLM 功能未啟用' + }), 503 + + try: + config_data = {} + for api_name, api_config in llm_config.apis.items(): + config_data[api_name] = { + 'name': api_config['name'], + 'enabled': api_config['enabled'], + 'endpoint': api_config['endpoint'], + 'api_key': api_config['api_key'][:8] + '...' if api_config['api_key'] else '' + } + + return jsonify(config_data) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'獲取配置失敗: {str(e)}' + }), 500 + + +@app.route('/api/llm/test/', methods=['GET']) +def test_llm_api(api_name): + """測試單個 LLM API 連線""" + if not LLM_ENABLED: + return jsonify({ + 'success': False, + 'message': 'LLM 功能未啟用' + }), 503 + + try: + if api_name not in llm_config.apis: + return jsonify({ + 'success': False, + 'message': f'不支援的 API: {api_name}' + }), 400 + + # 執行連線測試 + if api_name == 'gemini': + success, message = llm_config.test_gemini_connection() + elif api_name == 'deepseek': + success, message = llm_config.test_deepseek_connection() + elif api_name == 'openai': + success, message = llm_config.test_openai_connection() + else: + return jsonify({ + 'success': False, + 'message': f'未實作的 API: {api_name}' + }), 400 + + return jsonify({ + 'success': success, + 'message': message + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'message': f'測試失敗: {str(e)}' + }), 500 + + +@app.route('/api/llm/test-all', methods=['GET']) +def test_all_llm_apis(): + """測試所有已配置的 LLM API""" + if not LLM_ENABLED: + return jsonify({ + 'success': False, + 'error': 'LLM 功能未啟用' + }), 503 + + try: + results = llm_config.test_all_connections() + + response = {} + for api_name, (success, message) in results.items(): + response[api_name] = { + 'success': success, + 'message': message + } + + return jsonify({ + 'success': True, + 'results': response + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'測試失敗: {str(e)}' + }), 500 + + +@app.route('/api/llm/generate', methods=['POST']) +def generate_llm_text(): + """ + 使用 LLM API 生成文字 + Request body: { + "api": "gemini" | "deepseek" | "openai", + "prompt": "提示詞", + "max_tokens": 2000 + } + """ + if not LLM_ENABLED: + return jsonify({ + 'success': False, + 'error': 'LLM 功能未啟用' + }), 503 + + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + api_name = data.get('api', 'gemini') + prompt = data.get('prompt', '') + max_tokens = data.get('max_tokens', 2000) + + if not prompt: + return jsonify({ + 'success': False, + 'error': '請提供提示詞' + }), 400 + + # 執行文字生成 + if api_name == 'gemini': + success, result = llm_config.generate_text_gemini(prompt, max_tokens) + elif api_name == 'deepseek': + success, result = llm_config.generate_text_deepseek(prompt, max_tokens) + elif api_name == 'openai': + model = data.get('model', 'gpt-3.5-turbo') + success, result = llm_config.generate_text_openai(prompt, model, max_tokens) + else: + return jsonify({ + 'success': False, + 'error': f'不支援的 API: {api_name}' + }), 400 + + if success: + return jsonify({ + 'success': True, + 'text': result + }) + else: + return jsonify({ + 'success': False, + 'error': result + }), 500 + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'生成失敗: {str(e)}' + }), 500 + + +# ==================== 錯誤處理 ==================== + +@app.errorhandler(404) +def not_found(e): + return jsonify({ + 'success': False, + 'error': '找不到請求的資源' + }), 404 + + +@app.errorhandler(500) +def server_error(e): + return jsonify({ + 'success': False, + 'error': '伺服器內部錯誤' + }), 500 + + +# ==================== 主程式 ==================== + +if __name__ == '__main__': + print(""" +╔══════════════════════════════════════════════════════════════╗ +║ HR 基礎資料維護系統 - Flask Backend ║ +╠══════════════════════════════════════════════════════════════╣ +║ 伺服器啟動中... ║ +║ 訪問網址: http://localhost:5000 ║ +║ ║ +║ 主要頁面: ║ +║ / - 主頁面 ║ +║ /api-test - LLM API 測試頁面 ║ +║ ║ +║ 崗位資料 API: ║ +║ GET /api/positions - 獲取所有崗位 ║ +║ GET /api/positions/ - 獲取單一崗位 ║ +║ POST /api/positions - 新增崗位 ║ +║ PUT /api/positions/ - 更新崗位 ║ +║ DELETE /api/positions/ - 刪除崗位 ║ +║ ║ +║ 職務資料 API: ║ +║ GET /api/jobs - 獲取所有職務 ║ +║ GET /api/jobs/ - 獲取單一職務 ║ +║ POST /api/jobs - 新增職務 ║ +║ PUT /api/jobs/ - 更新職務 ║ +║ DELETE /api/jobs/ - 刪除職務 ║ +║ ║ +║ LLM API: ║ +║ GET /api/llm/config - 獲取 LLM 配置 ║ +║ GET /api/llm/test/ - 測試單個 API ║ +║ GET /api/llm/test-all - 測試所有 API ║ +║ POST /api/llm/generate - 生成文字 ║ +║ ║ +║ 按 Ctrl+C 停止伺服器 ║ +╚══════════════════════════════════════════════════════════════╝ + """) + + if LLM_ENABLED: + print("[OK] LLM 功能已啟用") + enabled_apis = llm_config.get_enabled_apis() + if enabled_apis: + print(f" 已配置的 API: {', '.join(enabled_apis)}") + else: + print(" 警告: 尚未配置任何 LLM API Key") + else: + print("[!] LLM 功能未啟用 (llm_config.py 未找到)") + + print() + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/app_llm_endpoints.py b/app_llm_endpoints.py new file mode 100644 index 0000000..95f3e8f --- /dev/null +++ b/app_llm_endpoints.py @@ -0,0 +1,159 @@ +""" +LLM API endpoints to be added to app.py +Add these imports at the top and routes before the error handlers +""" + +# ==================== ADD THIS IMPORT AT THE TOP ==================== +# from llm_config import LLMConfig +# llm_config = LLMConfig() + +# ==================== ADD THESE ROUTES BEFORE ERROR HANDLERS ==================== + +@app.route('/api/llm/config', methods=['GET']) +def get_llm_config(): + """獲取 LLM API 配置狀態""" + try: + config_data = {} + for api_name, api_config in llm_config.apis.items(): + config_data[api_name] = { + 'name': api_config['name'], + 'enabled': api_config['enabled'], + 'endpoint': api_config['endpoint'], + 'api_key': api_config['api_key'][:8] + '...' if api_config['api_key'] else '' + } + + return jsonify(config_data) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'獲取配置失敗: {str(e)}' + }), 500 + + +@app.route('/api/llm/test/', methods=['GET']) +def test_llm_api(api_name): + """測試單個 LLM API 連線""" + try: + if api_name not in llm_config.apis: + return jsonify({ + 'success': False, + 'message': f'不支援的 API: {api_name}' + }), 400 + + # 執行連線測試 + if api_name == 'gemini': + success, message = llm_config.test_gemini_connection() + elif api_name == 'deepseek': + success, message = llm_config.test_deepseek_connection() + elif api_name == 'openai': + success, message = llm_config.test_openai_connection() + else: + return jsonify({ + 'success': False, + 'message': f'未實作的 API: {api_name}' + }), 400 + + return jsonify({ + 'success': success, + 'message': message + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'message': f'測試失敗: {str(e)}' + }), 500 + + +@app.route('/api/llm/test-all', methods=['GET']) +def test_all_llm_apis(): + """測試所有已配置的 LLM API""" + try: + results = llm_config.test_all_connections() + + response = {} + for api_name, (success, message) in results.items(): + response[api_name] = { + 'success': success, + 'message': message + } + + return jsonify({ + 'success': True, + 'results': response + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'測試失敗: {str(e)}' + }), 500 + + +@app.route('/api/llm/generate', methods=['POST']) +def generate_llm_text(): + """ + 使用 LLM API 生成文字 + Request body: { + "api": "gemini" | "deepseek" | "openai", + "prompt": "提示詞", + "max_tokens": 2000 + } + """ + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': '請提供有效的 JSON 資料' + }), 400 + + api_name = data.get('api', 'gemini') + prompt = data.get('prompt', '') + max_tokens = data.get('max_tokens', 2000) + + if not prompt: + return jsonify({ + 'success': False, + 'error': '請提供提示詞' + }), 400 + + # 執行文字生成 + if api_name == 'gemini': + success, result = llm_config.generate_text_gemini(prompt, max_tokens) + elif api_name == 'deepseek': + success, result = llm_config.generate_text_deepseek(prompt, max_tokens) + elif api_name == 'openai': + model = data.get('model', 'gpt-3.5-turbo') + success, result = llm_config.generate_text_openai(prompt, model, max_tokens) + else: + return jsonify({ + 'success': False, + 'error': f'不支援的 API: {api_name}' + }), 400 + + if success: + return jsonify({ + 'success': True, + 'text': result + }) + else: + return jsonify({ + 'success': False, + 'error': result + }), 500 + + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'生成失敗: {str(e)}' + }), 500 + + +# ==================== ADD ROUTE FOR API TEST PAGE ==================== + +@app.route('/api-test') +def api_test_page(): + """返回 API 測試頁面""" + return send_from_directory('.', 'api_test.html') diff --git a/apply_cors_fix.py b/apply_cors_fix.py new file mode 100644 index 0000000..46486ef --- /dev/null +++ b/apply_cors_fix.py @@ -0,0 +1,236 @@ +""" +自動修正 index.html 中的 CORS 錯誤 +將直接調用 Claude API 改為通過 Flask 後端調用 +""" + +import os +import re +from pathlib import Path + +def fix_cors_in_index_html(): + """修正 index.html 中的 CORS 問題""" + + # 文件路徑 + index_path = Path(__file__).parent / 'index.html' + backup_path = Path(__file__).parent / 'index.html.backup' + + if not index_path.exists(): + print(f"❌ 錯誤: 找不到 {index_path}") + return False + + print(f"📂 讀取文件: {index_path}") + + # 讀取文件內容 + with open(index_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 檢查是否已經修正過 + if '/api/llm/generate' in content: + print("✓ 文件已經修正過,無需重複修正") + return True + + # 備份原文件 + print(f"💾 創建備份: {backup_path}") + with open(backup_path, 'w', encoding='utf-8') as f: + f.write(content) + + # 原始代碼模式 + old_pattern = r'''async function callClaudeAPI\(prompt\) \{ + try \{ + const response = await fetch\("https://api\.anthropic\.com/v1/messages", \{ + method: "POST", + headers: \{ + "Content-Type": "application/json", + \}, + body: JSON\.stringify\(\{ + model: "claude-sonnet-4-20250514", + max_tokens: 2000, + messages: \[ + \{ role: "user", content: prompt \} + \] + \}\) + \}\); + + if \(!response\.ok\) \{ + throw new Error\(`API request failed: \$\{response\.status\}`\); + \} + + const data = await response\.json\(\); + let responseText = data\.content\[0\]\.text; + responseText = responseText\.replace\(/```json\\n\?/g, ""\)\.replace\(/```\\n\?/g, ""\)\.trim\(\); + return JSON\.parse\(responseText\); + \} catch \(error\) \{ + console\.error\("Error calling Claude API:", error\); + throw error; + \} + \}''' + + # 新代碼 + new_code = '''async function callClaudeAPI(prompt, api = 'gemini') { + try { + // 調用後端 Flask API,避免 CORS 錯誤 + const response = await fetch("/api/llm/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api: api, // 使用指定的 LLM API (gemini, deepseek, openai) + prompt: prompt, + max_tokens: 2000 + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `API 請求失敗: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'API 調用失敗'); + } + + // 解析返回的文字為 JSON + let responseText = data.text; + responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim(); + return JSON.parse(responseText); + + } catch (error) { + console.error("Error calling LLM API:", error); + + // 顯示友好的錯誤訊息 + alert(`AI 生成錯誤: ${error.message}\\n\\n請確保:\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`); + + throw error; + } + }''' + + # 簡單替換(如果正則表達式匹配失敗) + old_simple = '''async function callClaudeAPI(prompt) { + try { + const response = await fetch("https://api.anthropic.com/v1/messages", {''' + + new_simple = '''async function callClaudeAPI(prompt, api = 'gemini') { + try { + // 調用後端 Flask API,避免 CORS 錯誤 + const response = await fetch("/api/llm/generate", {''' + + print("🔧 應用修正...") + + # 嘗試正則表達式替換 + new_content = re.sub(old_pattern, new_code, content, flags=re.MULTILINE) + + # 如果正則表達式沒有匹配,使用簡單替換 + if new_content == content: + print("⚠️ 正則表達式未匹配,使用簡單替換...") + + # 找到函數的開始和結束 + start_marker = 'async function callClaudeAPI(prompt) {' + end_marker = ' }' + + start_idx = content.find(start_marker) + if start_idx == -1: + print("❌ 錯誤: 找不到 callClaudeAPI 函數") + return False + + # 找到函數結束(找到第一個與縮進匹配的 }) + end_idx = start_idx + brace_count = 0 + in_function = False + + for i in range(start_idx, len(content)): + if content[i] == '{': + brace_count += 1 + in_function = True + elif content[i] == '}': + brace_count -= 1 + if in_function and brace_count == 0: + end_idx = i + 1 + break + + if end_idx == start_idx: + print("❌ 錯誤: 無法找到函數結束位置") + return False + + # 替換函數 + new_content = content[:start_idx] + new_code + content[end_idx:] + + # 寫回文件 + print(f"💾 保存修正後的文件: {index_path}") + with open(index_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + print("\n✅ CORS 修正完成!") + print("\n📋 接下來的步驟:") + print("1. 確保 .env 文件中已配置至少一個 LLM API Key") + print("2. 啟動 Flask 後端: python app_updated.py") + print("3. 在瀏覽器中重新載入頁面 (Ctrl+F5)") + print("4. 測試 AI 自動填充功能") + print(f"\n💡 原文件已備份至: {backup_path}") + + return True + + +def verify_flask_backend(): + """檢查是否有正確的 Flask 後端文件""" + app_updated = Path(__file__).parent / 'app_updated.py' + + if not app_updated.exists(): + print("\n⚠️ 警告: 找不到 app_updated.py") + print("請確保使用包含 LLM API 端點的 Flask 後端") + return False + + print(f"\n✓ 找到更新版 Flask 後端: {app_updated}") + return True + + +def check_env_file(): + """檢查 .env 文件配置""" + env_path = Path(__file__).parent / '.env' + + if not env_path.exists(): + print("\n⚠️ 警告: 找不到 .env 文件") + return False + + with open(env_path, 'r', encoding='utf-8') as f: + env_content = f.read() + + has_gemini = 'GEMINI_API_KEY=' in env_content and 'your_gemini_api_key_here' not in env_content + has_deepseek = 'DEEPSEEK_API_KEY=' in env_content and 'your_deepseek_api_key_here' not in env_content + has_openai = 'OPENAI_API_KEY=' in env_content and 'your_openai_api_key_here' not in env_content + + print("\n📋 LLM API Key 配置狀態:") + print(f" {'✓' if has_gemini else '✗'} Gemini API Key") + print(f" {'✓' if has_deepseek else '✗'} DeepSeek API Key") + print(f" {'✓' if has_openai else '✗'} OpenAI API Key") + + if not (has_gemini or has_deepseek or has_openai): + print("\n⚠️ 警告: 沒有配置任何 LLM API Key") + print("請編輯 .env 文件,添加至少一個有效的 API Key") + return False + + return True + + +if __name__ == '__main__': + print("=" * 60) + print("HR Position System - CORS 錯誤自動修正工具") + print("=" * 60) + print() + + # 修正 CORS 問題 + if fix_cors_in_index_html(): + # 驗證其他配置 + verify_flask_backend() + check_env_file() + + print("\n" + "=" * 60) + print("✅ 修正完成!") + print("=" * 60) + else: + print("\n" + "=" * 60) + print("❌ 修正失敗,請查看上述錯誤訊息") + print("=" * 60) + print("\n您也可以手動修正,請參考: CORS_FIX_GUIDE.md") diff --git a/complete_fix.py b/complete_fix.py new file mode 100644 index 0000000..170b9f3 --- /dev/null +++ b/complete_fix.py @@ -0,0 +1,99 @@ +with open('index.html', 'r', encoding='utf-8') as f: + lines = f.readlines() + +# 找到函數的起始行 +start_line = None +for i, line in enumerate(lines): + if 'async function callClaudeAPI' in line: + start_line = i + break + +if start_line is None: + print("ERROR: Could not find callClaudeAPI function") + exit(1) + +# 找到函數的結束行 +end_line = None +brace_count = 0 +for i in range(start_line, len(lines)): + for char in lines[i]: + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + if brace_count == 0: + end_line = i + break + if end_line: + break + +if end_line is None: + print("ERROR: Could not find end of function") + exit(1) + +print(f"Found function from line {start_line+1} to {end_line+1}") + +# 新函數 +new_function = ''' async function callClaudeAPI(prompt, api = 'gemini') { + try { + // 調用後端 Flask API,避免 CORS 錯誤 + const response = await fetch("/api/llm/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api: api, + prompt: prompt, + max_tokens: 2000 + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `API 請求失敗: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'API 調用失敗'); + } + + let responseText = data.text; + responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim(); + return JSON.parse(responseText); + } catch (error) { + console.error("Error calling LLM API:", error); + alert(`AI 生成錯誤: ${error.message}\\n\\n請確保:\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`); + throw error; + } + } +''' + +# 替換 +new_lines = lines[:start_line] + [new_function] + lines[end_line+1:] + +# 寫回 +with open('index.html', 'w', encoding='utf-8') as f: + f.writelines(new_lines) + +print("SUCCESS: Function completely replaced") +print("\nVerifying...") + +# 驗證 +with open('index.html', 'r', encoding='utf-8') as f: + content = f.read() + +if 'api.anthropic.com' in content: + print("WARNING: Still contains references to api.anthropic.com") +else: + print("VERIFIED: No more direct API calls") + +if '/api/llm/generate' in content: + print("VERIFIED: Now using Flask backend") + +print("\nDone! Please:") +print("1. Restart Flask: python app_updated.py") +print("2. Reload browser: Ctrl+F5") +print("3. Test AI generation") diff --git a/convert_to_table.py b/convert_to_table.py new file mode 100644 index 0000000..1e0d43b --- /dev/null +++ b/convert_to_table.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# 讀取原始文件 +with open('excel.md', 'r', encoding='utf-8') as f: + lines = f.readlines() + +# 跳過表頭,處理數據 +data_lines = lines[1:] # 跳過第一行表頭 + +result = [] +current_values = { + '事業體': '', + '處級單位': '', + '部級單位': '', +} + +for line in data_lines: + line = line.rstrip('\n\r') + if not line.strip(): + continue + + # 分割 Tab + parts = line.split('\t') + + # 確保至少有4個元素 + while len(parts) < 4: + parts.append('') + + # 處理每一列 + # 第1列:事業體 + if parts[0].strip(): + current_values['事業體'] = parts[0].strip() + + # 第2列:處別(處級單位) + if parts[1].strip(): + current_values['處級單位'] = parts[1].strip() + + # 第3列:單位名稱(可能是處級、部級或其他) + if parts[2].strip(): + unit_name = parts[2].strip() + # 判斷單位級別 + if unit_name.endswith('處'): + # 如果是處級單位,更新處級單位,清空部級單位 + current_values['處級單位'] = unit_name + current_values['部級單位'] = '' + elif unit_name.endswith('部'): + # 如果是部級單位,更新部級單位 + current_values['部級單位'] = unit_name + elif '部' in unit_name: + # 如果包含"部",視為部級單位 + current_values['部級單位'] = unit_name + elif '處' in unit_name: + # 如果包含"處",視為處級單位 + current_values['處級單位'] = unit_name + current_values['部級單位'] = '' + + # 第4列:崗位名稱 + position_name = parts[3].strip() if len(parts) > 3 else '' + + # 如果沒有崗位名稱,嘗試從其他列找(可能崗位名稱在其他位置) + if not position_name: + # 從後往前找第一個非空值作為崗位名稱 + for i in range(len(parts) - 1, -1, -1): + if parts[i].strip() and i != 0 and i != 1 and i != 2: + position_name = parts[i].strip() + break + + # 只有當有崗位名稱時才加入結果 + if position_name: + result.append([ + current_values['事業體'], + current_values['處級單位'], + current_values['部級單位'], + position_name + ]) + +# 生成 Markdown 表格 +output = [] +output.append('| 事業體 | 處級單位 | 部級單位 | 崗位名稱 |') +output.append('|--------|----------|----------|----------|') + +for row in result: + # 轉義管道符號 + row_escaped = [cell.replace('|', '\\|') if cell else '' for cell in row] + output.append(f"| {row_escaped[0]} | {row_escaped[1]} | {row_escaped[2]} | {row_escaped[3]} |") + +# 寫入新文件 +with open('excel_table.md', 'w', encoding='utf-8') as f: + f.write('\n'.join(output)) + +print(f"轉換完成!共生成 {len(result)} 行數據。") +print("輸出文件:excel_table.md") diff --git a/excel.md b/excel.md new file mode 100644 index 0000000..ddbc9b6 --- /dev/null +++ b/excel.md @@ -0,0 +1,314 @@ +事業體 處別 單位名稱 崗位名稱 +半導體事業群 半導體事業群 半導體事業群 營運長 + 營運長助理 +汽車事業體 汽車事業體 汽車事業體 副總經理 + 專案經理 +法務室 法務室 法務室 經副理 + 法務專員 + 專利工程師 +岡山製造事業體 生產處 生產處 處長 + 專員 + 生產部 經副理 + 生產課 課長 + 組長 + 班長 + 副班長 + 作業員 + 生產企劃部 經副理 + 課長 + 專員 + 工程師 + 岡山製造事業體 岡山品質管制部 經副理 + 封裝品質管制課 課長 + 工程師 + 組長 + 班長 + 副班長 + 作業員 + 品質管制整合課 課長 + 工程師 + 岡山製造事業體 副總經理 + 副總經理助理 + 封裝工程處 封裝工程處 處長 + 專員 + 工程師 + 製程工程一部 經副理 + 製程工程二部 經副理 + 上片銲線工程課 課長 + 工程師 + 切割點膠工程課 課長 + 工程師 + 正印~彎腳工程課 課長 + 工程師 + 模壓電鍍工程課 課長 + 工程師 + 設備一部 經副理 + 設備二部 經副理 + 設備一課 課長 + 工程師 + 設備二課 課長 + 工程師 + 設備三課 課長 + 工程師 + 副總辦公室 工業工程部 經副理 + 工程師 + 工業工程課 課長 + 工程師 + 專案管理 副理 + 工程師 + 測試工程與研發處 測試工程與研發處 處長 + 專員 + 測試工程部 經副理 + 設備課 課長 + 工程師 + 測試課 課長 + 工程師 + 新產品導入部 經副理 + 專員 + 工程師 + 研發部 經副理 + 封裝技術課 課長 + 工程師 + 設計模擬課 課長 + 工程師 + 專員 + 資材處 資材處 處長 + 採購部 經副理 + 採購一課 課長 + 專員 + 採購二課 課長 + 專員 + 外部資源部 專員 + 生管部 經副理 + 生產排程課 課長 + 專員 + 成品倉 課長 + 班長 + 副班長 + 作業員 + 原物料控制部 經副理 + 物料控制課 課長 + 專員 + 原物料倉 班長 + 副班長 + 作業員 + 廠務與環安衛管理處 廠務與環安衛管理處 處長 + 工程師 + 廠務部 經副理 + 廠務課 課長 + 工程師 + 安衛中心 課長 + 工程師 + 專員 + 智動化課 課長 + 工程師 +產品事業體 產品事業體 產品事業體 處長 + 先進產品事業處 先進產品事業處 處長 + 產品管理部(APD) 經副理 + 產品工程(APD) 經副理 + 工程師 + 產品管理(APD) 經副理 + 工程師 + 成熟產品事業處 成熟產品事業處 處長 + 產品管理部(MPD) 經副理 + 產品工程(MPD) 經副理 + 專案經副理 + 工程師 + 產品管理(MPD) 經副理 + 專案經副理 + 工程師 +晶圓三廠 晶圓三廠 晶圓三廠 顧問 + 專員 + 品質部 經副理 + 工程師 + 作業員 + 製造部 經副理 + 課長 + 班長 + 副班長 + 作業員 + 廠務部(Fab3) 經副理 + 工程師 + 製程工程處 工程一部 經副理 + 工程師 + 工程二部 經副理 + 工程師 + 工程三部 經副理 + 工程師 + 製程整合部(Fab3) 經副理 + 工程師 +集團人資行政事業體 集團人資行政事業體 集團人資行政事業體 人資長 + 行政總務管理部 經副理 + 專員 + 助理 + 招募任用部 經副理 + 專員 + 訓練發展部 經副理 + 專員 + 薪酬管理部 經副理 + 專員 +集團財務事業體 集團財務事業體 集團財務事業體 財務長 + 岡山強茂財務處 岡山強茂財務處 處長 + 岡山強茂財務部 經副理 + 岡山強茂財務課 課長 + 專員 + 集團財務事業體 集團投資人關係 專案副理 +集團會計事業體 集團會計事業體 集團會計事業體 會計長 + 岡山會計處 岡山會計處 處長 + 岡山會計處 會計部 經副理 + 岡山會計處 會計課 課長 + 岡山會計處 專員 + 岡山會計處 稅務課 課長 + 岡山會計處 專員 + 岡山會計處 管理會計部 經副理 + 岡山會計處 封裝管理會計課 課長 + 岡山會計處 專員 + 岡山會計處 晶圓管理會計課 課長 + 岡山會計處 專員 + 集團會計處 集團會計處 處長 + 集團會計處 集團合併報表部 經副理 + 集團會計處 專員 +集團資訊事業體 集團資訊事業體 集團資訊事業體 資訊長 + 資安行動小組 資安行動課 課長 + 資訊一處 應用系統部 經副理 + 資訊一處 工程師 + 資訊一處 電腦整合製造部 經副理 + 資訊一處 工程師 + 資訊一處 系統網路服務部 經副理 + 資訊一處 工程師 + 資訊二處 資訊二處 處長 +新創事業體 新創事業體 新創事業體 處長 + 新創事業體 資源管理部 經副理 + 新創事業體 專員 + 研發中心 中低壓產品研發處 經副理 + 研發中心 工程師 + 研發中心 高壓產品研發處 經副理 + 研發中心 工程師 +稽核室 稽核室 稽核室 主任 + 專員 +總經理室 總經理室 總經理室 總裁 + 總經理 + ESG專案辦公室 ESG專案辦公室 經副理 + 環境永續小組 課長 + 專員/工程師 + 社會永續小組 課長 + 專員/工程師 + 公司治理永續小組 課長 + 專員/工程師 + 專案管理室 專案管理室 副總經理 + 專案管理 經副理 + 專員/工程師 + PVS專案課 專員/工程師 +總品質事業體 總品質事業體 總品質事業體 處長 + 客戶品質管理部 經副理 + 客戶品質工程課 課長 + 工程師 + 專員 + 品質工程課 課長 + 工程師 + 產品品質管理部 經副理 + 新產品品質管理課 課長 + 工程師 + 變更管理課 課長 + 工程師 + 客戶支援工程課 課長 + 工程師 + 班長 + 作業員 + 品質系統及客戶工程整合部 經副理 + 客戶工程整合課 課長 + 工程師 + 品質系統課 課長 + 工程師 + 封測外包品質管理部 經副理 + 課長 + 工程師 + 品質保證部 經副理 + 失效分析課 課長 + 工程師 + 供應商管理課 課長 + 工程師 + 班長 + 副班長 + 作業員 + 信賴性保證課 課長 + 工程師 + 班長 + 副班長 + 作業員 + 晶圓供應商管理課 課長 + 工程師 +營業事業體 營業事業體 營業事業體 副總經理 + 副總經理助理 + 商業開發暨市場應用處 商業開發暨市場應用處 處長 + 經理 + 工程師 + 海外銷售事業處 海外銷售事業處 處長 + 日本區暨代工業務部 經副理 + 日本區 課長 + 專員 + 助理 + 代工 課長 + 專員 + 助理 + 歐亞區業務部 經副理 + 助理 + 歐洲 課長 + 專員 + 助理 + 南亞 課長 + 專員 + 助理 + 東協 課長 + 專員 + 助理 + 韓國區業務部-韓國區 經副理 + 課長 + 專員 + 助理 + 全球跨區客戶管理 專案經理 + 經副理 + 美洲區業務部 經副理 + 課長 + 專員 + 助理 + 全球技術服務處 全球技術服務處 處長 + 工程師 + 助理 + 應用工程部(GTS) 經副理 + 專案經副理 + 技術經副理 + 工程師 + 系統工程部 經副理 + 工程師 + 特性測試部 經副理 + 特性測試課 課長 + 工程師 + 全球行銷暨業務支援處 全球行銷暨業務支援處 副總經理 + 業務生管部 經副理 + 業務生管課 課長 + 專員 + 物流&船務 課長 + 專員 + 市場行銷企劃部 處長 + 經理 + 專員 + 行銷推廣 專員 + 市場戰略 專員 + MOSFET晶圓採購部 經副理 + 課長 + 專員 + 大中華區銷售事業處 大中華區銷售事業處 處長 + 台灣區業務部 專員 + 助理 + 業務一部 處長/資深經理 + 經副理 + 專員 + 助理 + 業務二部 處長/資深經理 + 經副理 + 專員 + 助理 + HH專案組 經副理 + 專員 + 助理 diff --git a/excel_table copy.md b/excel_table copy.md new file mode 100644 index 0000000..e35b786 --- /dev/null +++ b/excel_table copy.md @@ -0,0 +1,315 @@ +| 事業體 | 處級單位 | 部級單位 | 崗位名稱 | +|--------|----------|----------|----------| +| 半導體事業群 | 半導體事業群 | | 營運長 | +| 半導體事業群 | 半導體事業群 | | 營運長助理 | +| 汽車事業體 | 汽車事業體 | | 副總經理 | +| 汽車事業體 | 汽車事業體 | | 專案經理 | +| 法務室 | 法務室 | | 經副理 | +| 法務室 | 法務室 | | 法務專員 | +| 法務室 | 法務室 | | 專利工程師 | +| 岡山製造事業體 | 生產處 | | 處長 | +| 岡山製造事業體 | 生產處 | | 專員 | +| 岡山製造事業體 | 生產處 | 生產部 | 經副理 | +| 岡山製造事業體 | 生產處 | 生產部 | 課長 | +| 岡山製造事業體 | 生產處 | 生產部 | 組長 | +| 岡山製造事業體 | 生產處 | 生產部 | 班長 | +| 岡山製造事業體 | 生產處 | 生產部 | 副班長 | +| 岡山製造事業體 | 生產處 | 生產部 | 作業員 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 經副理 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 課長 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 專員 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 工程師 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 經副理 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 課長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 工程師 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 組長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 班長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 副班長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 作業員 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 課長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 工程師 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 副總經理 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 副總經理助理 | +| 岡山製造事業體 | 封裝工程處 | | 處長 | +| 岡山製造事業體 | 封裝工程處 | | 專員 | +| 岡山製造事業體 | 封裝工程處 | | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程一部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 設備一部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 工程師 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 經副理 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 工程師 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 課長 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 工程師 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 副理 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | | 處長 | +| 岡山製造事業體 | 測試工程與研發處 | | 專員 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 經副理 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 新產品導入部 | 經副理 | +| 岡山製造事業體 | 測試工程與研發處 | 新產品導入部 | 專員 | +| 岡山製造事業體 | 測試工程與研發處 | 新產品導入部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 經副理 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 專員 | +| 岡山製造事業體 | 資材處 | | 處長 | +| 岡山製造事業體 | 資材處 | 採購部 | 經副理 | +| 岡山製造事業體 | 資材處 | 採購部 | 課長 | +| 岡山製造事業體 | 資材處 | 採購部 | 專員 | +| 岡山製造事業體 | 資材處 | 採購部 | 課長 | +| 岡山製造事業體 | 資材處 | 採購部 | 專員 | +| 岡山製造事業體 | 資材處 | 外部資源部 | 專員 | +| 岡山製造事業體 | 資材處 | 生管部 | 經副理 | +| 岡山製造事業體 | 資材處 | 生管部 | 課長 | +| 岡山製造事業體 | 資材處 | 生管部 | 專員 | +| 岡山製造事業體 | 資材處 | 生管部 | 課長 | +| 岡山製造事業體 | 資材處 | 生管部 | 班長 | +| 岡山製造事業體 | 資材處 | 生管部 | 副班長 | +| 岡山製造事業體 | 資材處 | 生管部 | 作業員 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 經副理 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 課長 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 專員 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 班長 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 副班長 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 作業員 | +| 岡山製造事業體 | 廠務與環安衛管理處 | | 處長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | | 工程師 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 經副理 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 課長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 工程師 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 課長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 工程師 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 專員 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 課長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 工程師 | +| 產品事業體 | 產品事業體 | 廠務部 | 處長 | +| 產品事業體 | 先進產品事業處 | | 處長 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 經副理 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 經副理 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 工程師 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 經副理 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 工程師 | +| 產品事業體 | 成熟產品事業處 | | 處長 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 專案經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 工程師 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 專案經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 工程師 | +| 晶圓三廠 | 晶圓三廠 | 產品管理部(MPD) | 顧問 | +| 晶圓三廠 | 晶圓三廠 | 產品管理部(MPD) | 專員 | +| 晶圓三廠 | 晶圓三廠 | 品質部 | 經副理 | +| 晶圓三廠 | 晶圓三廠 | 品質部 | 工程師 | +| 晶圓三廠 | 晶圓三廠 | 品質部 | 作業員 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 經副理 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 課長 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 班長 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 副班長 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 作業員 | +| 晶圓三廠 | 晶圓三廠 | 廠務部(Fab3) | 經副理 | +| 晶圓三廠 | 晶圓三廠 | 廠務部(Fab3) | 工程師 | +| 晶圓三廠 | 製程工程處 | 工程一部 | 經副理 | +| 晶圓三廠 | 製程工程處 | 工程一部 | 工程師 | +| 晶圓三廠 | 製程工程處 | 工程二部 | 經副理 | +| 晶圓三廠 | 製程工程處 | 工程二部 | 工程師 | +| 晶圓三廠 | 製程工程處 | 工程三部 | 經副理 | +| 晶圓三廠 | 製程工程處 | 工程三部 | 工程師 | +| 晶圓三廠 | 製程工程處 | 製程整合部(Fab3) | 經副理 | +| 晶圓三廠 | 製程工程處 | 製程整合部(Fab3) | 工程師 | +| 集團人資行政事業體 | 集團人資行政事業體 | 製程整合部(Fab3) | 人資長 | +| 集團人資行政事業體 | 集團人資行政事業體 | 行政總務管理部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 行政總務管理部 | 專員 | +| 集團人資行政事業體 | 集團人資行政事業體 | 行政總務管理部 | 助理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 招募任用部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 招募任用部 | 專員 | +| 集團人資行政事業體 | 集團人資行政事業體 | 訓練發展部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 訓練發展部 | 專員 | +| 集團人資行政事業體 | 集團人資行政事業體 | 薪酬管理部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 薪酬管理部 | 專員 | +| 集團財務事業體 | 集團財務事業體 | 薪酬管理部 | 財務長 | +| 集團財務事業體 | 岡山強茂財務處 | | 處長 | +| 集團財務事業體 | 岡山強茂財務處 | 岡山強茂財務部 | 經副理 | +| 集團財務事業體 | 岡山強茂財務處 | 岡山強茂財務部 | 課長 | +| 集團財務事業體 | 岡山強茂財務處 | 岡山強茂財務部 | 專員 | +| 集團財務事業體 | 集團財務事業體 | 岡山強茂財務部 | 專案副理 | +| 集團會計事業體 | 集團會計事業體 | 岡山強茂財務部 | 會計長 | +| 集團會計事業體 | 岡山會計處 | | 處長 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 經副理 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 專員 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 專員 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 經副理 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 專員 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 專員 | +| 集團會計事業體 | 集團會計處 | | 處長 | +| 集團會計事業體 | 集團會計處 | 集團合併報表部 | 經副理 | +| 集團會計事業體 | 集團會計處 | 集團合併報表部 | 專員 | +| 集團資訊事業體 | 集團資訊事業體 | 集團合併報表部 | 資訊長 | +| 集團資訊事業體 | 資安行動小組 | 集團合併報表部 | 課長 | +| 集團資訊事業體 | 資訊一處 | 應用系統部 | 經副理 | +| 集團資訊事業體 | 資訊一處 | 應用系統部 | 工程師 | +| 集團資訊事業體 | 資訊一處 | 電腦整合製造部 | 經副理 | +| 集團資訊事業體 | 資訊一處 | 電腦整合製造部 | 工程師 | +| 集團資訊事業體 | 資訊一處 | 系統網路服務部 | 經副理 | +| 集團資訊事業體 | 資訊一處 | 系統網路服務部 | 工程師 | +| 集團資訊事業體 | 資訊二處 | | 處長 | +| 新創事業體 | 新創事業體 | | 處長 | +| 新創事業體 | 新創事業體 | 資源管理部 | 經副理 | +| 新創事業體 | 新創事業體 | 資源管理部 | 專員 | +| 新創事業體 | 中低壓產品研發處 | | 經副理 | +| 新創事業體 | 研發中心 | | 工程師 | +| 新創事業體 | 高壓產品研發處 | | 經副理 | +| 新創事業體 | 研發中心 | | 工程師 | +| 稽核室 | 稽核室 | | 主任 | +| 稽核室 | 稽核室 | | 專員 | +| 總經理室 | 總經理室 | | 總裁 | +| 總經理室 | 總經理室 | | 總經理 | +| 總經理室 | ESG專案辦公室 | | 經副理 | +| 總經理室 | ESG專案辦公室 | | 課長 | +| 總經理室 | ESG專案辦公室 | | 專員/工程師 | +| 總經理室 | ESG專案辦公室 | | 課長 | +| 總經理室 | ESG專案辦公室 | | 專員/工程師 | +| 總經理室 | ESG專案辦公室 | | 課長 | +| 總經理室 | ESG專案辦公室 | | 專員/工程師 | +| 總經理室 | 專案管理室 | | 副總經理 | +| 總經理室 | 專案管理室 | | 經副理 | +| 總經理室 | 專案管理室 | | 專員/工程師 | +| 總經理室 | 專案管理室 | | 專員/工程師 | +| 總品質事業體 | 總品質事業體 | | 處長 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 專員 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 班長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 作業員 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 封測外包品質管理部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 封測外包品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 封測外包品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 副班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 作業員 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 副班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 作業員 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 營業事業體 | 營業事業體 | 品質保證部 | 副總經理 | +| 營業事業體 | 營業事業體 | 品質保證部 | 副總經理助理 | +| 營業事業體 | 商業開發暨市場應用處 | | 處長 | +| 營業事業體 | 商業開發暨市場應用處 | | 經理 | +| 營業事業體 | 商業開發暨市場應用處 | | 工程師 | +| 營業事業體 | 海外銷售事業處 | | 處長 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 課長 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 專員 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 助理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 專案經理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 助理 | +| 營業事業體 | 全球技術服務處 | | 處長 | +| 營業事業體 | 全球技術服務處 | | 工程師 | +| 營業事業體 | 全球技術服務處 | | 助理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 經副理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 專案經副理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 技術經副理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 工程師 | +| 營業事業體 | 全球技術服務處 | 系統工程部 | 經副理 | +| 營業事業體 | 全球技術服務處 | 系統工程部 | 工程師 | +| 營業事業體 | 全球技術服務處 | 特性測試部 | 經副理 | +| 營業事業體 | 全球技術服務處 | 特性測試部 | 課長 | +| 營業事業體 | 全球技術服務處 | 特性測試部 | 工程師 | +| 營業事業體 | 全球行銷暨業務支援處 | | 副總經理 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 經副理 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 課長 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 課長 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 處長 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 經理 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | MOSFET晶圓採購部 | 經副理 | +| 營業事業體 | 全球行銷暨業務支援處 | MOSFET晶圓採購部 | 課長 | +| 營業事業體 | 全球行銷暨業務支援處 | MOSFET晶圓採購部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | | 處長 | +| 營業事業體 | 大中華區銷售事業處 | 台灣區業務部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 台灣區業務部 | 助理 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 處長/資深經理 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 經副理 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 助理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 處長/資深經理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 經副理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 助理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 經副理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 助理 | \ No newline at end of file diff --git a/excel_table.md b/excel_table.md new file mode 100644 index 0000000..e35b786 --- /dev/null +++ b/excel_table.md @@ -0,0 +1,315 @@ +| 事業體 | 處級單位 | 部級單位 | 崗位名稱 | +|--------|----------|----------|----------| +| 半導體事業群 | 半導體事業群 | | 營運長 | +| 半導體事業群 | 半導體事業群 | | 營運長助理 | +| 汽車事業體 | 汽車事業體 | | 副總經理 | +| 汽車事業體 | 汽車事業體 | | 專案經理 | +| 法務室 | 法務室 | | 經副理 | +| 法務室 | 法務室 | | 法務專員 | +| 法務室 | 法務室 | | 專利工程師 | +| 岡山製造事業體 | 生產處 | | 處長 | +| 岡山製造事業體 | 生產處 | | 專員 | +| 岡山製造事業體 | 生產處 | 生產部 | 經副理 | +| 岡山製造事業體 | 生產處 | 生產部 | 課長 | +| 岡山製造事業體 | 生產處 | 生產部 | 組長 | +| 岡山製造事業體 | 生產處 | 生產部 | 班長 | +| 岡山製造事業體 | 生產處 | 生產部 | 副班長 | +| 岡山製造事業體 | 生產處 | 生產部 | 作業員 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 經副理 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 課長 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 專員 | +| 岡山製造事業體 | 生產處 | 生產企劃部 | 工程師 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 經副理 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 課長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 工程師 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 組長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 班長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 副班長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 作業員 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 課長 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 工程師 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 副總經理 | +| 岡山製造事業體 | 岡山製造事業體 | 岡山品質管制部 | 副總經理助理 | +| 岡山製造事業體 | 封裝工程處 | | 處長 | +| 岡山製造事業體 | 封裝工程處 | | 專員 | +| 岡山製造事業體 | 封裝工程處 | | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程一部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 製程工程二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 設備一部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 經副理 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 工程師 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 課長 | +| 岡山製造事業體 | 封裝工程處 | 設備二部 | 工程師 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 經副理 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 工程師 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 課長 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 工程師 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 副理 | +| 岡山製造事業體 | 副總辦公室 | 工業工程部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | | 處長 | +| 岡山製造事業體 | 測試工程與研發處 | | 專員 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 經副理 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 測試工程部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 新產品導入部 | 經副理 | +| 岡山製造事業體 | 測試工程與研發處 | 新產品導入部 | 專員 | +| 岡山製造事業體 | 測試工程與研發處 | 新產品導入部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 經副理 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 課長 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 工程師 | +| 岡山製造事業體 | 測試工程與研發處 | 研發部 | 專員 | +| 岡山製造事業體 | 資材處 | | 處長 | +| 岡山製造事業體 | 資材處 | 採購部 | 經副理 | +| 岡山製造事業體 | 資材處 | 採購部 | 課長 | +| 岡山製造事業體 | 資材處 | 採購部 | 專員 | +| 岡山製造事業體 | 資材處 | 採購部 | 課長 | +| 岡山製造事業體 | 資材處 | 採購部 | 專員 | +| 岡山製造事業體 | 資材處 | 外部資源部 | 專員 | +| 岡山製造事業體 | 資材處 | 生管部 | 經副理 | +| 岡山製造事業體 | 資材處 | 生管部 | 課長 | +| 岡山製造事業體 | 資材處 | 生管部 | 專員 | +| 岡山製造事業體 | 資材處 | 生管部 | 課長 | +| 岡山製造事業體 | 資材處 | 生管部 | 班長 | +| 岡山製造事業體 | 資材處 | 生管部 | 副班長 | +| 岡山製造事業體 | 資材處 | 生管部 | 作業員 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 經副理 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 課長 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 專員 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 班長 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 副班長 | +| 岡山製造事業體 | 資材處 | 原物料控制部 | 作業員 | +| 岡山製造事業體 | 廠務與環安衛管理處 | | 處長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | | 工程師 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 經副理 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 課長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 工程師 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 課長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 工程師 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 專員 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 課長 | +| 岡山製造事業體 | 廠務與環安衛管理處 | 廠務部 | 工程師 | +| 產品事業體 | 產品事業體 | 廠務部 | 處長 | +| 產品事業體 | 先進產品事業處 | | 處長 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 經副理 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 經副理 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 工程師 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 經副理 | +| 產品事業體 | 先進產品事業處 | 產品管理部(APD) | 工程師 | +| 產品事業體 | 成熟產品事業處 | | 處長 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 專案經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 工程師 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 專案經副理 | +| 產品事業體 | 成熟產品事業處 | 產品管理部(MPD) | 工程師 | +| 晶圓三廠 | 晶圓三廠 | 產品管理部(MPD) | 顧問 | +| 晶圓三廠 | 晶圓三廠 | 產品管理部(MPD) | 專員 | +| 晶圓三廠 | 晶圓三廠 | 品質部 | 經副理 | +| 晶圓三廠 | 晶圓三廠 | 品質部 | 工程師 | +| 晶圓三廠 | 晶圓三廠 | 品質部 | 作業員 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 經副理 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 課長 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 班長 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 副班長 | +| 晶圓三廠 | 晶圓三廠 | 製造部 | 作業員 | +| 晶圓三廠 | 晶圓三廠 | 廠務部(Fab3) | 經副理 | +| 晶圓三廠 | 晶圓三廠 | 廠務部(Fab3) | 工程師 | +| 晶圓三廠 | 製程工程處 | 工程一部 | 經副理 | +| 晶圓三廠 | 製程工程處 | 工程一部 | 工程師 | +| 晶圓三廠 | 製程工程處 | 工程二部 | 經副理 | +| 晶圓三廠 | 製程工程處 | 工程二部 | 工程師 | +| 晶圓三廠 | 製程工程處 | 工程三部 | 經副理 | +| 晶圓三廠 | 製程工程處 | 工程三部 | 工程師 | +| 晶圓三廠 | 製程工程處 | 製程整合部(Fab3) | 經副理 | +| 晶圓三廠 | 製程工程處 | 製程整合部(Fab3) | 工程師 | +| 集團人資行政事業體 | 集團人資行政事業體 | 製程整合部(Fab3) | 人資長 | +| 集團人資行政事業體 | 集團人資行政事業體 | 行政總務管理部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 行政總務管理部 | 專員 | +| 集團人資行政事業體 | 集團人資行政事業體 | 行政總務管理部 | 助理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 招募任用部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 招募任用部 | 專員 | +| 集團人資行政事業體 | 集團人資行政事業體 | 訓練發展部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 訓練發展部 | 專員 | +| 集團人資行政事業體 | 集團人資行政事業體 | 薪酬管理部 | 經副理 | +| 集團人資行政事業體 | 集團人資行政事業體 | 薪酬管理部 | 專員 | +| 集團財務事業體 | 集團財務事業體 | 薪酬管理部 | 財務長 | +| 集團財務事業體 | 岡山強茂財務處 | | 處長 | +| 集團財務事業體 | 岡山強茂財務處 | 岡山強茂財務部 | 經副理 | +| 集團財務事業體 | 岡山強茂財務處 | 岡山強茂財務部 | 課長 | +| 集團財務事業體 | 岡山強茂財務處 | 岡山強茂財務部 | 專員 | +| 集團財務事業體 | 集團財務事業體 | 岡山強茂財務部 | 專案副理 | +| 集團會計事業體 | 集團會計事業體 | 岡山強茂財務部 | 會計長 | +| 集團會計事業體 | 岡山會計處 | | 處長 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 經副理 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 專員 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 會計部 | 專員 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 經副理 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 專員 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 課長 | +| 集團會計事業體 | 岡山會計處 | 管理會計部 | 專員 | +| 集團會計事業體 | 集團會計處 | | 處長 | +| 集團會計事業體 | 集團會計處 | 集團合併報表部 | 經副理 | +| 集團會計事業體 | 集團會計處 | 集團合併報表部 | 專員 | +| 集團資訊事業體 | 集團資訊事業體 | 集團合併報表部 | 資訊長 | +| 集團資訊事業體 | 資安行動小組 | 集團合併報表部 | 課長 | +| 集團資訊事業體 | 資訊一處 | 應用系統部 | 經副理 | +| 集團資訊事業體 | 資訊一處 | 應用系統部 | 工程師 | +| 集團資訊事業體 | 資訊一處 | 電腦整合製造部 | 經副理 | +| 集團資訊事業體 | 資訊一處 | 電腦整合製造部 | 工程師 | +| 集團資訊事業體 | 資訊一處 | 系統網路服務部 | 經副理 | +| 集團資訊事業體 | 資訊一處 | 系統網路服務部 | 工程師 | +| 集團資訊事業體 | 資訊二處 | | 處長 | +| 新創事業體 | 新創事業體 | | 處長 | +| 新創事業體 | 新創事業體 | 資源管理部 | 經副理 | +| 新創事業體 | 新創事業體 | 資源管理部 | 專員 | +| 新創事業體 | 中低壓產品研發處 | | 經副理 | +| 新創事業體 | 研發中心 | | 工程師 | +| 新創事業體 | 高壓產品研發處 | | 經副理 | +| 新創事業體 | 研發中心 | | 工程師 | +| 稽核室 | 稽核室 | | 主任 | +| 稽核室 | 稽核室 | | 專員 | +| 總經理室 | 總經理室 | | 總裁 | +| 總經理室 | 總經理室 | | 總經理 | +| 總經理室 | ESG專案辦公室 | | 經副理 | +| 總經理室 | ESG專案辦公室 | | 課長 | +| 總經理室 | ESG專案辦公室 | | 專員/工程師 | +| 總經理室 | ESG專案辦公室 | | 課長 | +| 總經理室 | ESG專案辦公室 | | 專員/工程師 | +| 總經理室 | ESG專案辦公室 | | 課長 | +| 總經理室 | ESG專案辦公室 | | 專員/工程師 | +| 總經理室 | 專案管理室 | | 副總經理 | +| 總經理室 | 專案管理室 | | 經副理 | +| 總經理室 | 專案管理室 | | 專員/工程師 | +| 總經理室 | 專案管理室 | | 專員/工程師 | +| 總品質事業體 | 總品質事業體 | | 處長 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 專員 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 客戶品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 班長 | +| 總品質事業體 | 總品質事業體 | 產品品質管理部 | 作業員 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質系統及客戶工程整合部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 封測外包品質管理部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 封測外包品質管理部 | 課長 | +| 總品質事業體 | 總品質事業體 | 封測外包品質管理部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 經副理 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 副班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 作業員 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 副班長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 作業員 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 課長 | +| 總品質事業體 | 總品質事業體 | 品質保證部 | 工程師 | +| 營業事業體 | 營業事業體 | 品質保證部 | 副總經理 | +| 營業事業體 | 營業事業體 | 品質保證部 | 副總經理助理 | +| 營業事業體 | 商業開發暨市場應用處 | | 處長 | +| 營業事業體 | 商業開發暨市場應用處 | | 經理 | +| 營業事業體 | 商業開發暨市場應用處 | | 工程師 | +| 營業事業體 | 海外銷售事業處 | | 處長 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 日本區暨代工業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 歐亞區業務部 | 助理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 課長 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 專員 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 助理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 專案經理 | +| 營業事業體 | 海外銷售事業處 | 韓國區業務部-韓國區 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 經副理 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 課長 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 專員 | +| 營業事業體 | 海外銷售事業處 | 美洲區業務部 | 助理 | +| 營業事業體 | 全球技術服務處 | | 處長 | +| 營業事業體 | 全球技術服務處 | | 工程師 | +| 營業事業體 | 全球技術服務處 | | 助理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 經副理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 專案經副理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 技術經副理 | +| 營業事業體 | 全球技術服務處 | 應用工程部(GTS) | 工程師 | +| 營業事業體 | 全球技術服務處 | 系統工程部 | 經副理 | +| 營業事業體 | 全球技術服務處 | 系統工程部 | 工程師 | +| 營業事業體 | 全球技術服務處 | 特性測試部 | 經副理 | +| 營業事業體 | 全球技術服務處 | 特性測試部 | 課長 | +| 營業事業體 | 全球技術服務處 | 特性測試部 | 工程師 | +| 營業事業體 | 全球行銷暨業務支援處 | | 副總經理 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 經副理 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 課長 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 課長 | +| 營業事業體 | 全球行銷暨業務支援處 | 業務生管部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 處長 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 經理 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | 市場行銷企劃部 | 專員 | +| 營業事業體 | 全球行銷暨業務支援處 | MOSFET晶圓採購部 | 經副理 | +| 營業事業體 | 全球行銷暨業務支援處 | MOSFET晶圓採購部 | 課長 | +| 營業事業體 | 全球行銷暨業務支援處 | MOSFET晶圓採購部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | | 處長 | +| 營業事業體 | 大中華區銷售事業處 | 台灣區業務部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 台灣區業務部 | 助理 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 處長/資深經理 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 經副理 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 業務一部 | 助理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 處長/資深經理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 經副理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 助理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 經副理 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 專員 | +| 營業事業體 | 大中華區銷售事業處 | 業務二部 | 助理 | \ No newline at end of file diff --git a/fix_cors.js b/fix_cors.js new file mode 100644 index 0000000..66a8426 --- /dev/null +++ b/fix_cors.js @@ -0,0 +1,155 @@ +/** + * CORS 錯誤修正 + * 將直接調用 Claude API 改為通過後端 Flask API 調用 + * + * 使用方法: + * 1. 在 index.html 中找到 callClaudeAPI 函數 + * 2. 將其替換為下面的新版本 + */ + +// ==================== 修正後的 AI Generation Functions ==================== + +/** + * 調用後端 LLM API 生成文字 + * @param {string} prompt - 提示詞 + * @param {string} api - API 名稱 (gemini, deepseek, openai) + * @returns {Promise} - 生成的 JSON 數據 + */ +async function callClaudeAPI(prompt, api = 'gemini') { + try { + // 調用後端 Flask API,而不是直接調用 Claude API + const response = await fetch("/api/llm/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api: api, // 使用 Gemini 作為默認 + prompt: prompt, + max_tokens: 2000 + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `API 請求失敗: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'API 調用失敗'); + } + + // 解析返回的文字為 JSON + let responseText = data.text; + + // 移除可能的 markdown 代碼塊標記 + responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim(); + + // 解析 JSON + return JSON.parse(responseText); + + } catch (error) { + console.error("Error calling LLM API:", error); + + // 使用全局錯誤處理器顯示錯誤 + if (window.errorHandler) { + window.errorHandler.showError({ + title: 'AI 生成錯誤', + message: error.message || '調用 AI API 時發生錯誤', + type: 'error', + details: error.stack + }); + } else { + alert(`AI 生成錯誤: ${error.message}`); + } + + throw error; + } +} + +/** + * 設置按鈕載入狀態 + * @param {HTMLElement} btn - 按鈕元素 + * @param {boolean} loading - 是否載入中 + */ +function setButtonLoading(btn, loading) { + if (loading) { + btn.disabled = true; + btn.innerHTML = '
AI 生成中...'; + } else { + btn.disabled = false; + btn.innerHTML = '✨ I\'m feeling lucky'; + } +} + +// ==================== 選擇 LLM API 提供者 ==================== + +/** + * 讓用戶選擇使用哪個 LLM API + * @returns {Promise} - 選擇的 API 名稱 + */ +async function selectLLMProvider() { + // 獲取可用的 API 列表 + try { + const response = await fetch('/api/llm/config'); + const config = await response.json(); + + const enabledAPIs = []; + for (const [key, value] of Object.entries(config)) { + if (value.enabled) { + enabledAPIs.push({ + key: key, + name: value.name + }); + } + } + + if (enabledAPIs.length === 0) { + throw new Error('沒有可用的 LLM API,請先配置 API Key'); + } + + // 如果只有一個 API,直接使用 + if (enabledAPIs.length === 1) { + return enabledAPIs[0].key; + } + + // 多個 API 時,使用第一個(默認 Gemini) + return enabledAPIs[0].key; + + } catch (error) { + console.error('無法獲取 LLM 配置:', error); + // 默認使用 Gemini + return 'gemini'; + } +} + +/** + * 增強版的 callClaudeAPI - 自動選擇最佳 API + * @param {string} prompt - 提示詞 + * @returns {Promise} - 生成的 JSON 數據 + */ +async function callAIAPI(prompt) { + const api = await selectLLMProvider(); + return callClaudeAPI(prompt, api); +} + +// ==================== 使用示例 ==================== + +/* +// 原來的調用方式(會導致 CORS 錯誤): +const result = await callClaudeAPI(prompt); + +// 修正後的調用方式 1(使用默認 Gemini): +const result = await callClaudeAPI(prompt, 'gemini'); + +// 修正後的調用方式 2(使用 DeepSeek): +const result = await callClaudeAPI(prompt, 'deepseek'); + +// 修正後的調用方式 3(使用 OpenAI): +const result = await callClaudeAPI(prompt, 'openai'); + +// 修正後的調用方式 4(自動選擇最佳 API): +const result = await callAIAPI(prompt); +*/ diff --git a/fix_csv_routes.py b/fix_csv_routes.py new file mode 100644 index 0000000..b311265 --- /dev/null +++ b/fix_csv_routes.py @@ -0,0 +1,53 @@ +""" +修復 app_updated.py 中重複的 CSV 路由 +""" + +import re + +# 讀取檔案 +with open('app_updated.py', 'r', encoding='utf-8') as f: + content = f.read() + +# 找到並刪除重複的 CSV 匯入/匯出 API 區塊 (第 852 行開始) +# 保留第一次定義(已經移到正確位置的),刪除後面的重複定義 + +# 找到 "# ==================== CSV 匯入/匯出 API ====================" 的位置 +csv_section_pattern = r'# ====================CSV匯入/匯出 API ====================.*?(?=# ====================)' + +# 刪除重複的 CSV 區塊 (保留第一次定義) +lines = content.split('\n') +new_lines = [] +skip_until_next_section = False +first_csv_section_found = False + +i = 0 +while i < len(lines): + line = lines[i] + + # 檢查是否是 CSV 匯入/匯出 API 區段 + if '# ==================== CSV 匯入/匯出 API ====================' in line: + if not first_csv_section_found: + # 第一次遇到,跳過這個區塊(因為我們已經在前面定義了) + first_csv_section_found = True + skip_until_next_section = True + else: + # 第二次遇到重複區塊,跳過 + skip_until_next_section = True + + # 檢查是否遇到下一個區段 + if skip_until_next_section and '# ====================' in line and 'CSV 匯入/匯出' not in line: + skip_until_next_section = False + new_lines.append(line) + i += 1 + continue + + if not skip_until_next_section: + new_lines.append(line) + + i += 1 + +# 寫回檔案 +with open('app_updated.py', 'w', encoding='utf-8') as f: + f.write('\n'.join(new_lines)) + +print("已修復 CSV 路由重複問題") diff --git a/fix_gemini_model.py b/fix_gemini_model.py new file mode 100644 index 0000000..4f26a0e --- /dev/null +++ b/fix_gemini_model.py @@ -0,0 +1,90 @@ +""" +修正 Gemini 模型名稱和關閉按鈕 +""" + +# 1. 修正 llm_config.py 中的 Gemini 模型 +with open('llm_config.py', 'r', encoding='utf-8') as f: + content = f.read() + +# 備份 +with open('llm_config.py.backup', 'w', encoding='utf-8') as f: + f.write(content) + +# 替換模型名稱:gemini-pro -> gemini-2.0-flash-exp +old_model = 'gemini-pro' +new_model = 'gemini-2.0-flash-exp' # 使用最新的 Gemini 2.0 Flash + +content = content.replace( + f'models/{old_model}:generateContent', + f'models/{new_model}:generateContent' +) + +if 'gemini-2.0-flash-exp' in content: + print(f"SUCCESS: Updated Gemini model to {new_model}") +else: + print("ERROR: Could not update model") + +with open('llm_config.py', 'w', encoding='utf-8') as f: + f.write(content) + + +# 2. 修正 index.html 中的關閉按鈕 +with open('index.html', 'r', encoding='utf-8') as f: + html_content = f.read() + +# 備份 +with open('index.html.backup3', 'w', encoding='utf-8') as f: + f.write(html_content) + +# 找到並修正關閉按鈕 +# 問題:onclick 使用了複雜的選擇器,可能失效 +# 解決:使用更簡單可靠的方式 + +old_close_button = ''' + + + +
+
${message}
+ + ${suggestions && suggestions.length > 0 ? ` +
+ 💡 請確保: +
    + ${suggestions.map(s => `
  • ${s}
  • `).join('')} +
+
+ ` : ''} + + ${details ? ` +
+ 🔍 詳細錯誤訊息(點擊展開) +
${details}
+ +
+ ` : ''} +
+ + +
+ +
+ + `; + + document.body.appendChild(modal); + + // 點擊背景關閉 + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); + } + }); + } + + // 複製錯誤訊息到剪貼板 + function copyErrorDetails() { + const text = document.getElementById('errorDetailsText').textContent; + navigator.clipboard.writeText(text).then(() => { + alert('錯誤訊息已複製到剪貼板!'); + }).catch(err => { + // Fallback: 選取文字 + const range = document.createRange(); + range.selectNode(document.getElementById('errorDetailsText')); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + try { + document.execCommand('copy'); + alert('錯誤訊息已複製到剪貼板!'); + } catch (e) { + alert('複製失敗,請手動選取並複製'); + } + }); + } + ''' + + new_content = new_content.replace(' ', error_display_function + '\n ') + print("Added showCopyableError function") + +# 寫回 +with open('index.html', 'w', encoding='utf-8') as f: + f.write(new_content) + +print("\nDone! Improvements:") +print("1. Error messages now show in a modal dialog") +print("2. Full error details are expandable") +print("3. Error details can be copied to clipboard") +print("4. Better formatting and readability") +print("\nPlease reload the page (Ctrl+F5) to see the changes") diff --git a/index.html b/index.html index 5bd6d14..a1499ff 100644 --- a/index.html +++ b/index.html @@ -620,6 +620,10 @@ 職務基礎資料 + + +
+ + + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ +
+ + + + +
+
+ +
@@ -1130,6 +1279,17 @@
+
+ +
+ + +
+
@@ -1188,6 +1348,35 @@
+ + +
@@ -1364,18 +1553,18 @@ 崗位名稱 - - 事業體 - - - 部門 - 崗位類別 + + 崗位性質 + 編制人數 + + 崗位等級 + 生效日期 @@ -1476,6 +1665,56 @@
+ + +
+
+

崗位資料管理

+

管理和匯出完整的崗位資料表

+
+ +
+ +
+
+ + + +

匯出完整崗位資料

+
+

+ 匯出所有崗位的完整資料,包含基本資料、描述、要求等所有欄位。 +

+ +
+ + +
+
+ + + +

資料統計

+
+
+

崗位總數: -

+

已描述: -

+

未描述: -

+
+ +
+
+
@@ -1514,6 +1753,30 @@ diff --git a/init_gitea.py b/init_gitea.py new file mode 100644 index 0000000..1378159 --- /dev/null +++ b/init_gitea.py @@ -0,0 +1,244 @@ +""" +Gitea repository initialization script +Creates a new repository on Gitea server and sets up git remote +""" + +import os +import requests +import subprocess +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +def create_gitea_repo(): + """Create a new repository on Gitea""" + + gitea_url = os.getenv('GITEA_URL').rstrip('/') + gitea_token = os.getenv('GITEA_TOKEN') + gitea_user = os.getenv('GITEA_USER') + + # Repository details + repo_name = 'hr-position-system' + repo_description = 'HR基礎資料維護系統 - 崗位與職務管理系統' + + # API endpoint + api_url = f"{gitea_url}/api/v1/user/repos" + + # Request headers + headers = { + 'Authorization': f'token {gitea_token}', + 'Content-Type': 'application/json' + } + + # Repository data + data = { + 'name': repo_name, + 'description': repo_description, + 'private': False, + 'auto_init': False, + 'default_branch': 'main' + } + + print(f"Creating repository on Gitea: {gitea_url}") + print(f"Repository name: {repo_name}") + + try: + # Create repository + response = requests.post(api_url, json=data, headers=headers) + + if response.status_code == 201: + repo_info = response.json() + print(f"✓ Repository created successfully!") + print(f" Repository URL: {repo_info.get('html_url')}") + print(f" Clone URL (HTTPS): {repo_info.get('clone_url')}") + print(f" Clone URL (SSH): {repo_info.get('ssh_url')}") + return repo_info + elif response.status_code == 409: + print(f"✓ Repository '{repo_name}' already exists") + # Get existing repository info + repo_url = f"{gitea_url}/api/v1/repos/{gitea_user}/{repo_name}" + response = requests.get(repo_url, headers=headers) + if response.status_code == 200: + return response.json() + return None + else: + print(f"✗ Failed to create repository: {response.status_code}") + print(f" Error: {response.text}") + return None + + except Exception as e: + print(f"✗ Error creating repository: {str(e)}") + return None + +def init_git_local(): + """Initialize local git repository""" + + repo_path = os.path.dirname(os.path.abspath(__file__)) + + try: + # Check if git is already initialized + git_dir = os.path.join(repo_path, '.git') + if os.path.exists(git_dir): + print("✓ Git repository already initialized") + return True + + # Initialize git + subprocess.run(['git', 'init'], cwd=repo_path, check=True) + subprocess.run(['git', 'checkout', '-b', 'main'], cwd=repo_path, check=True) + print("✓ Git repository initialized") + + return True + + except Exception as e: + print(f"✗ Error initializing git: {str(e)}") + return False + +def add_git_remote(repo_info): + """Add Gitea remote to local repository""" + + if not repo_info: + return False + + repo_path = os.path.dirname(os.path.abspath(__file__)) + clone_url = repo_info.get('clone_url') + + try: + # Check if remote already exists + result = subprocess.run( + ['git', 'remote', 'get-url', 'origin'], + cwd=repo_path, + capture_output=True, + text=True + ) + + if result.returncode == 0: + current_remote = result.stdout.strip() + if current_remote == clone_url: + print(f"✓ Remote 'origin' already configured: {clone_url}") + return True + else: + # Update remote URL + subprocess.run( + ['git', 'remote', 'set-url', 'origin', clone_url], + cwd=repo_path, + check=True + ) + print(f"✓ Remote 'origin' updated: {clone_url}") + return True + else: + # Add new remote + subprocess.run( + ['git', 'remote', 'add', 'origin', clone_url], + cwd=repo_path, + check=True + ) + print(f"✓ Remote 'origin' added: {clone_url}") + return True + + except Exception as e: + print(f"✗ Error adding remote: {str(e)}") + return False + +def create_initial_commit(): + """Create initial commit with project files""" + + repo_path = os.path.dirname(os.path.abspath(__file__)) + + try: + # Check if there are already commits + result = subprocess.run( + ['git', 'rev-parse', 'HEAD'], + cwd=repo_path, + capture_output=True, + text=True + ) + + if result.returncode == 0: + print("✓ Repository already has commits") + return True + + # Add files + subprocess.run(['git', 'add', '.gitignore'], cwd=repo_path, check=True) + subprocess.run(['git', 'add', 'database_schema.sql'], cwd=repo_path, check=True) + subprocess.run(['git', 'add', 'init_database.py'], cwd=repo_path, check=True) + subprocess.run(['git', 'add', 'init_gitea.py'], cwd=repo_path, check=True) + subprocess.run(['git', 'add', 'SDD.md'], cwd=repo_path, check=True) + + # Create initial commit + subprocess.run( + ['git', 'commit', '-m', 'Initial commit: Project setup and database schema'], + cwd=repo_path, + check=True + ) + print("✓ Initial commit created") + + return True + + except Exception as e: + print(f"✗ Error creating initial commit: {str(e)}") + return False + +def test_gitea_connection(): + """Test connection to Gitea server""" + + gitea_url = os.getenv('GITEA_URL').rstrip('/') + gitea_token = os.getenv('GITEA_TOKEN') + + headers = { + 'Authorization': f'token {gitea_token}' + } + + try: + response = requests.get(f"{gitea_url}/api/v1/user", headers=headers) + if response.status_code == 200: + user_info = response.json() + print(f"✓ Gitea connection test successful") + print(f" User: {user_info.get('login')}") + print(f" Email: {user_info.get('email')}") + return True + else: + print(f"✗ Gitea connection test failed: {response.status_code}") + return False + except Exception as e: + print(f"✗ Gitea connection test failed: {str(e)}") + return False + +if __name__ == '__main__': + print("=" * 60) + print("HR Position System - Gitea Repository Initialization") + print("=" * 60) + print() + + # Test connection + print("Step 1: Testing Gitea connection...") + if not test_gitea_connection(): + print("\nPlease check your Gitea configuration in .env file") + exit(1) + + # Initialize local git + print("\nStep 2: Initializing local git repository...") + if not init_git_local(): + exit(1) + + # Create Gitea repository + print("\nStep 3: Creating Gitea repository...") + repo_info = create_gitea_repo() + if not repo_info: + exit(1) + + # Add remote + print("\nStep 4: Configuring git remote...") + if not add_git_remote(repo_info): + exit(1) + + # Create initial commit + print("\nStep 5: Creating initial commit...") + create_initial_commit() + + print("\n" + "=" * 60) + print("✓ Gitea repository setup completed!") + print("=" * 60) + print("\nNext steps:") + print(" 1. Run: git push -u origin main") + print(" 2. Visit:", repo_info.get('html_url')) diff --git a/position_template.csv b/position_template.csv new file mode 100644 index 0000000..da8fac0 --- /dev/null +++ b/position_template.csv @@ -0,0 +1 @@ +{"error":"\u627e\u4e0d\u5230\u8a72\u5d17\u4f4d\u8cc7\u6599","success":false} diff --git a/quick_fix.py b/quick_fix.py new file mode 100644 index 0000000..0988ba1 --- /dev/null +++ b/quick_fix.py @@ -0,0 +1,110 @@ +import re + +with open('index.html', 'r', encoding='utf-8') as f: + content = f.read() + +# 備份 +with open('index.html.backup', 'w', encoding='utf-8') as f: + f.write(content) +print("Backup created: index.html.backup") + +# 舊代碼 +old = ''' async function callClaudeAPI(prompt) { + try { + const response = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "claude-sonnet-4-20250514", + max_tokens: 2000, + messages: [ + { role: "user", content: prompt } + ] + }) + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.status}`); + } + + const data = await response.json(); + let responseText = data.content[0].text; + responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim(); + return JSON.parse(responseText); + } catch (error) { + console.error("Error calling Claude API:", error); + throw error; + } + }''' + +# 新代碼 +new = ''' async function callClaudeAPI(prompt, api = 'gemini') { + try { + // 調用後端 Flask API,避免 CORS 錯誤 + const response = await fetch("/api/llm/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + api: api, + prompt: prompt, + max_tokens: 2000 + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `API 請求失敗: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'API 調用失敗'); + } + + let responseText = data.text; + responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim(); + return JSON.parse(responseText); + } catch (error) { + console.error("Error calling LLM API:", error); + alert(`AI 生成錯誤: ${error.message}\\n\\n請確保:\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`); + throw error; + } + }''' + +# 替換 +new_content = content.replace(old, new) + +if new_content == content: + print("ERROR: Pattern not found, trying alternative method...") + # 使用更簡單的替換 + new_content = content.replace( + 'const response = await fetch("https://api.anthropic.com/v1/messages", {', + 'const response = await fetch("/api/llm/generate", {' + ) + new_content = new_content.replace( + 'async function callClaudeAPI(prompt) {', + 'async function callClaudeAPI(prompt, api = \'gemini\') {' + ) + + if new_content != content: + print("SUCCESS: Applied simple replacement") + else: + print("ERROR: Could not fix the file") + exit(1) +else: + print("SUCCESS: Pattern replaced") + +# 寫回 +with open('index.html', 'w', encoding='utf-8') as f: + f.write(new_content) + +print("File updated: index.html") +print("\nNext steps:") +print("1. Start Flask backend: python app_updated.py") +print("2. Reload browser page (Ctrl+F5)") +print("3. Test AI generation") diff --git a/start_server.py b/start_server.py index 2152abf..d636178 100644 --- a/start_server.py +++ b/start_server.py @@ -26,7 +26,10 @@ except ImportError: LLM_ENABLED = False app = Flask(__name__, static_folder='.') -CORS(app) + +# CORS 設定 - 限制允許的來源 +cors_origins = os.getenv('CORS_ORIGINS', 'http://localhost:5000,http://127.0.0.1:5000').split(',') +CORS(app, origins=cors_origins) # 模擬資料庫 positions_db = {} @@ -262,11 +265,19 @@ def server_error(e): # ==================== 主程式 ==================== if __name__ == '__main__': + # 從環境變數讀取設定,預設為安全值 + host = os.getenv('FLASK_HOST', '127.0.0.1') + port = int(os.getenv('FLASK_PORT', 5000)) + debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true' + print("=" * 60) print("HR Position System - Flask Backend") print("=" * 60) - print("\nServer starting...") - print("URL: http://localhost:5000") + print(f"\nServer starting...") + print(f"Host: {host}") + print(f"Port: {port}") + print(f"Debug: {debug}") + print(f"URL: http://{host}:{port}") print() if LLM_ENABLED: @@ -283,4 +294,4 @@ if __name__ == '__main__': print("=" * 60) print() - app.run(host='0.0.0.0', port=5000, debug=True) + app.run(host=host, port=port, debug=debug) diff --git a/公司現行組織及職位表 的複本.csv b/公司現行組織及職位表 的複本.csv new file mode 100644 index 0000000..3117b2b --- /dev/null +++ b/公司現行組織及職位表 的複本.csv @@ -0,0 +1,314 @@ +Ʒ~,BO,W,^W +bƷ~s,bƷ~s,bƷ~s,B +,,,BUz +TƷ~,TƷ~,TƷ~,`gz +,,,M׸gz +kȫ,kȫ,kȫ,gƲz +,,,kȱM +,,,MQu{v +ssyƷ~,ͲB,ͲB,B +,,,M +,,Ͳ,gƲz +,,Ͳ,Ҫ +,,,ժ +,,,Z +,,,ƯZ +,,,@~ +,,Ͳ,gƲz +,,,Ҫ +,,,M +,,,u{v +,ssyƷ~,s~ި,gƲz +,,ʸ˫~ި,Ҫ +,,,u{v +,,,ժ +,,,Z +,,,ƯZ +,,,@~ +,,~ިX,Ҫ +,,,u{v +,,ssyƷ~,`gz +,,,`gzUz +,ʸˤu{B,ʸˤu{B,B +,,,M +,,,u{v +,,s{u{@,gƲz +,,s{u{G,gƲz +,,WZuu{,Ҫ +,,,u{v +,,Iu{,Ҫ +,,,u{v +,,L~s}u{,Ҫ +,,,u{v +,,qu{,Ҫ +,,,u{v +,,]Ƥ@,gƲz +,,]ƤG,gƲz +,,]Ƥ@,Ҫ +,,,u{v +,,]ƤG,Ҫ +,,,u{v +,,]ƤT,Ҫ +,,,u{v +,`줽,u~u{,gƲz +,,,u{v +,,u~u{,Ҫ +,,,u{v +,,M׺޲z,Ʋz +,,,u{v +,դu{PoB,դu{PoB,B +,,,M +,,դu{,gƲz +,,]ƽ,Ҫ +,,,u{v +,,ս,Ҫ +,,,u{v +,,s~ɤJ,gƲz +,,,M +,,,u{v +,,o,gƲz +,,ʸ˧޳N,Ҫ +,,,u{v +,,]p,Ҫ +,,,u{v +,,,M +,B,B,B +,,ʳ,gƲz +,,ʤ@,Ҫ +,,,M +,,ʤG,Ҫ +,,,M +,,~귽,M +,,ͺ޳,gƲz +,,ͲƵ{,Ҫ +,,,M +,,~,Ҫ +,,,Z +,,,ƯZ +,,,@~ +,,쪫Ʊ,gƲz +,,Ʊ,Ҫ +,,,M +,,쪫ƭ,Z +,,,ƯZ +,,,@~ +,tȻPwú޲zB,tȻPwú޲zB,B +,,,u{v +,,tȳ,gƲz +,,tȽ,Ҫ +,,,u{v +,,wä,Ҫ +,,,u{v +,,,M +,,ʤƽ,Ҫ +,,,u{v +~Ʒ~,~Ʒ~,~Ʒ~,B +,i~Ʒ~B,i~Ʒ~B,B +,,~޲z(APD),gƲz +,,~u{(APD),gƲz +,,,u{v +,,~޲z(APD),gƲz +,,,u{v +,~Ʒ~B,~Ʒ~B,B +,,~޲z(MPD),gƲz +,,~u{(MPD),gƲz +,,,M׸gƲz +,,,u{v +,,~޲z(MPD),gƲz +,,,M׸gƲz +,,,u{v +Tt,Tt,Tt,U +,,,M +,,~賡,gƲz +,,,u{v +,,,@~ +,,sy,gƲz +,,,Ҫ +,,,Z +,,,ƯZ +,,,@~ +,,tȳ(Fab3),gƲz +,,,u{v +,s{u{B,u{@,gƲz +,,,u{v +,,u{G,gƲz +,,,u{v +,,u{T,gƲz +,,,u{v +,,s{X(Fab3),gƲz +,,,u{v +ΤHFƷ~,ΤHFƷ~,ΤHFƷ~,H +,,F`Ⱥ޲z,gƲz +,,,M +,,,Uz +,,۶ҥγ,gƲz +,,,M +,,Vmoi,gƲz +,,,M +,,~S޲z,gƲz +,,,M +ΰ]ȨƷ~,ΰ]ȨƷ~,ΰ]ȨƷ~,]Ȫ +,sjZ]ȳB,sjZ]ȳB,B +,,sjZ]ȳ,gƲz +,,sjZ]Ƚ,Ҫ +,,,M +,ΰ]ȨƷ~,ΧHY,MװƲz +η|pƷ~,η|pƷ~,η|pƷ~,|p +,s|pB,s|pB,B +,s|pB,|p,gƲz +,s|pB,|p,Ҫ +,s|pB,,M +,s|pB,|Ƚ,Ҫ +,s|pB,,M +,s|pB,޲z|p,gƲz +,s|pB,ʸ˺޲z|p,Ҫ +,s|pB,,M +,s|pB,޲z|p,Ҫ +,s|pB,,M +,η|pB,η|pB,B +,η|pB,ΦXֳ,gƲz +,η|pB,,M +θTƷ~,θTƷ~,θTƷ~,T +,wʤp,wʽ,Ҫ +,T@B,Ψtγ,gƲz +,T@B,,u{v +,T@B,qXsy,gƲz +,T@B,,u{v +,T@B,tκAȳ,gƲz +,T@B,,u{v +,TGB,TGB,B +sШƷ~,sШƷ~,sШƷ~,B +,sШƷ~,귽޲z,gƲz +,sШƷ~,,M +,o,C~oB,gƲz +,o,,u{v +,o,~oB,gƲz +,o,,u{v +]֫,]֫,]֫,D +,,,M +`gz,`gz,`gz,` +,,,`gz +,ESGM׿줽,ESGM׿줽,gƲz +,,ҥp,Ҫ +,,,M/u{v +,,|p,Ҫ +,,,M/u{v +,,qvzp,Ҫ +,,,M/u{v +,M׺޲z,M׺޲z,`gz +,,M׺޲z,gƲz +,,,M/u{v +,,PVSM׽,M/u{v +`~Ʒ~,`~Ʒ~,`~Ʒ~,B +,,Ȥ~޲z,gƲz +,,Ȥ~u{,Ҫ +,,,u{v +,,,M +,,~u{,Ҫ +,,,u{v +,,~~޲z,gƲz +,,s~~޲z,Ҫ +,,,u{v +,,ܧ޲z,Ҫ +,,,u{v +,,Ȥ䴩u{,Ҫ +,,,u{v +,,,Z +,,,@~ +,,~tΤΫȤu{X,gƲz +,,Ȥu{X,Ҫ +,,,u{v +,,~tν,Ҫ +,,,u{v +,,ʴ~]~޲z,gƲz +,,,Ҫ +,,,u{v +,,~Oҳ,gƲz +,,ĤR,Ҫ +,,,u{v +,,Ӻ޲z,Ҫ +,,,u{v +,,,Z +,,,ƯZ +,,,@~ +,,HʫOҽ,Ҫ +,,,u{v +,,,Z +,,,ƯZ +,,,@~ +,,Ӻ޲z,Ҫ +,,,u{v +~Ʒ~,~Ʒ~,~Ʒ~,`gz +,,,`gzUz +,ӷ~}o[γB,ӷ~}o[γB,B +,,,gz +,,,u{v +,~PƷ~B,~PƷ~B,B +,,饻Ϻ[Nu~ȳ,gƲz +,,饻,Ҫ +,,,M +,,,Uz +,,Nu,Ҫ +,,,M +,,,Uz +,,ڨȰϷ~ȳ,gƲz +,,,Uz +,,ڬw,Ҫ +,,,M +,,,Uz +,,n,Ҫ +,,,M +,,,Uz +,,F,Ҫ +,,,M +,,,Uz +,,Ϸ~ȳ-,gƲz +,,,Ҫ +,,,M +,,,Uz +,,yϫȤ޲z,M׸gz +,,,gƲz +,,wϷ~ȳ,gƲz +,,,Ҫ +,,,M +,,,Uz +,y޳NAȳB,y޳NAȳB,B +,,,u{v +,,,Uz +,,Τu{(GTS),gƲz +,,,M׸gƲz +,,,޳NgƲz +,,,u{v +,,tΤu{,gƲz +,,,u{v +,,Sʴճ,gƲz +,,Sʴս,Ҫ +,,,u{v +,yP[~Ȥ䴩B,yP[~Ȥ䴩B,`gz +,,~ȥͺ޳,gƲz +,,~ȥͺ޽,Ҫ +,,,M +,,y&,Ҫ +,,,M +,,P,B +,,,gz +,,,M +,,Ps,M +,,Բ,M +,,MOSFETʳ,gƲz +,,,Ҫ +,,,M +,jذϾPƷ~B,jذϾPƷ~B,B +,,xWϷ~ȳ,M +,,,Uz +,,~Ȥ@,B/`gz +,,,gƲz +,,,M +,,,Uz +,,~ȤG,B/`gz +,,,gƲz +,,,M +,,,Uz +,,HHMײ,gƲz +,,,M +,,,Uz diff --git a/公司現行組織及職位表 的複本.xlsx b/公司現行組織及職位表 的複本.xlsx new file mode 100644 index 0000000..31a0b85 Binary files /dev/null and b/公司現行組織及職位表 的複本.xlsx differ diff --git a/若瑄資安規則.md b/若瑄資安規則.md new file mode 100644 index 0000000..3d26ee9 --- /dev/null +++ b/若瑄資安規則.md @@ -0,0 +1,34 @@ +你是一位資深全端工程師,請根據目前專案的檔案結構與程式內容,簡述此專案的整體狀態。 +重點請對照以下檢核項目,逐項說明是否存在與其狀況: + +- 專案結構與依賴檢查 + 1. 是否有入口檔案(如 app.py、main.js、server.js) + 2. 是否有明確的專案結構(app、routes、static、templates、src 等) + 3. 是否有 requirements.txt 或 package.json + 4. 是否可看出使用框架(Flask、FastAPI、Express、Next.js…) + 5. 是否包含 README.md 且有安裝與啟動說明 + 6. 無多餘或不安全的依賴套件 + 7. 監聽的 port 號碼、主機位址並列出在哪個檔案出現(例如 127.0.0.1:3000、localhost:5000、0.0.0.0:8000…,從環境變數讀取) + + +- 安全性與環境變數檢核 + 1. 是否存在 .env 或 .env.example + 2. 是否有 .gitignore 且內容正確(排除 .env、__pycache__、node_modules、logs 等) + 3. 是否有資料庫連線設定(DB_HOST、SQLAlchemy、Prisma 等) + 4. DB 連線字串來自 `.env`,無硬編碼敏感資訊(API_KEY、DB 密碼等) + 5. 使用者輸入有防 SQL Injection / XSS 機制 + 6. 其他明顯缺漏或安全疑慮 + +- 程式品質與可維護性 + 1. 錯誤處理(try/except / middleware)完善 + + +- 請用條列方式輸出,例如: + - 專案結構與依賴檢查: + - ✅ 1. 有 app.py 作為入口 + - ❌ 7. 無 README.md + - 安全性與環境變數檢核: + - ❌ 1. 無 .env 檔案 +- 依據上述的檢核結果給予分數,總分100分 +- 先列出即可,不要修改程式碼 +- 將以上檢核項目列出後,產生 Check.md 檔案,不要再產生其他測試文檔 \ No newline at end of file