From 04299542b5d2641161656ee66d178dbbd2e221a8 Mon Sep 17 00:00:00 2001 From: hyungi Date: Wed, 10 Sep 2025 08:49:19 +0900 Subject: [PATCH] =?UTF-8?q?[TEST]=20Cloudflare=20Tunnel=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91=20=EB=B0=8F=20=EB=A6=AC=EB=B9=84=EC=A0=84=20=EC=A6=9D?= =?UTF-8?q?=EB=B6=84=20=EA=B3=84=EC=82=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŒ Nginx ํ”„๋ก์‹œ ์„ค์ • (ํ…Œ์ŠคํŠธ์šฉ): - nginx-proxy.conf: /api ์š”์ฒญ์„ ๋ฐฑ์—”๋“œ๋กœ ํ”„๋ก์‹œ - docker-compose.proxy.yml: ํ”„๋ก์‹œ ์„œ๋ฒ„ ์„ค์ • - VITE_API_URL=/api ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •์œผ๋กœ ๋‹จ์ผ ๋„๋ฉ”์ธ ์ ‘์† ๐ŸŽจ UI ํ…์ŠคํŠธ ๋ณ€๊ฒฝ (ํ…Œ์ŠคํŠธ์šฉ): - LoginPage: TK-MP System โ†’ BOM ํ…Œ์ŠคํŠธ ์„œ๋ฒ„ - LoginPage: ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ โ†’ BOM ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ v1.0 - LogMonitoringPage: ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ถ”๊ฐ€ (๋กœ๊ทธ์ธ/ํ™œ๋™/์‹œ์Šคํ…œ ๋กœ๊ทธ) - SystemSettingsPage: ํ™œ๋™ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ธฐ๋Šฅ ๊ฐœ์„  ๐Ÿ”ง ๋ฐฑ์—”๋“œ ์ˆ˜์ • (ํ…Œ์ŠคํŠธ์šฉ): - files.py: ๋ฆฌ๋น„์ „ ์ฆ๋ถ„ ๊ณ„์‚ฐ ๋กœ์ง ์ˆ˜์ • (์ „์ฒด ์žฌ๋ถ„๋ฅ˜ โ†’ ์ฐจ์ด๋ถ„๋งŒ ๋ถ„๋ฅ˜) - create_system_admin.py: database_url โ†’ get_database_url() ์ˆ˜์ • โš ๏ธ ์ฃผ์˜: ์ด ์ปค๋ฐ‹์€ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. --- backend/app/routers/files.py | 210 ++++++++++++++++++++- backend/scripts/create_system_admin.py | 2 +- docker-compose.proxy.yml | 15 ++ frontend/src/pages/LogMonitoringPage.jsx | 75 +++++++- frontend/src/pages/LoginPage.jsx | 6 +- frontend/src/pages/SystemSettingsPage.jsx | 218 +++++++++++++++++++++- nginx-proxy.conf | 36 ++++ 7 files changed, 548 insertions(+), 14 deletions(-) create mode 100644 docker-compose.proxy.yml create mode 100644 nginx-proxy.conf diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index cbf6cf5..9d16cfc 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -380,10 +380,12 @@ async def upload_file( # ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ์ธ ๊ฒฝ์šฐ ์ฐจ์ด๋ถ„ ๊ณ„์‚ฐ materials_diff = [] + original_materials_to_classify = materials_to_classify.copy() # ์›๋ณธ ๋ณด์กด + if parent_file_id is not None: # ์ƒˆ ํŒŒ์ผ์˜ ์ž์žฌ๋“ค์„ ์ˆ˜๋Ÿ‰๋ณ„๋กœ ๊ทธ๋ฃนํ™” new_materials_grouped = {} - for material_data in materials_to_classify: + for material_data in original_materials_to_classify: description = material_data["original_description"] size_spec = material_data["size_spec"] quantity = float(material_data.get("quantity", 0)) @@ -434,6 +436,9 @@ async def upload_file( # ์ฐจ์ด๋ถ„๋งŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก materials_to_classify ๊ต์ฒด materials_to_classify = materials_diff print(f"์ฐจ์ด๋ถ„ ์ž์žฌ ๊ฐœ์ˆ˜: {len(materials_to_classify)}") + print(f"๐Ÿ”„ ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ: ์ฐจ์ด๋ถ„ {len(materials_diff)}๊ฐœ๋งŒ ๋ถ„๋ฅ˜ ์ฒ˜๋ฆฌ") + else: + print(f"๐Ÿ†• ์‹ ๊ทœ ์—…๋กœ๋“œ: ์ „์ฒด {len(materials_to_classify)}๊ฐœ ๋ถ„๋ฅ˜ ์ฒ˜๋ฆฌ") # ๋ถ„๋ฅ˜๊ฐ€ ํ•„์š”ํ•œ ์ž์žฌ ์ฒ˜๋ฆฌ print(f"๋ถ„๋ฅ˜ํ•  ์ž์žฌ ์ด ๊ฐœ์ˆ˜: {len(materials_to_classify)}") @@ -2701,4 +2706,205 @@ async def confirm_material_purchase_api( except Exception as e: db.rollback() print(f"๊ตฌ๋งค์ˆ˜๋Ÿ‰ ํ™•์ • ์‹คํŒจ: {str(e)}") - raise HTTPException(status_code=500, detail=f"๊ตฌ๋งค์ˆ˜๋Ÿ‰ ํ™•์ • ์‹คํŒจ: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"๊ตฌ๋งค์ˆ˜๋Ÿ‰ ํ™•์ • ์‹คํŒจ: {str(e)}") + + +@router.put("/{file_id}") +async def update_file_info( + file_id: int, + bom_name: Optional[str] = Body(None), + description: Optional[str] = Body(None), + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """ํŒŒ์ผ ์ •๋ณด ์ˆ˜์ •""" + try: + # ํŒŒ์ผ ์กด์žฌ ํ™•์ธ ๋ฐ ๊ด€๋ จ ์ •๋ณด ์กฐํšŒ + file_query = text(""" + SELECT id, bom_name, description, job_no, original_filename + FROM files + WHERE id = :file_id + """) + file_result = db.execute(file_query, {"file_id": file_id}).fetchone() + + if not file_result: + raise HTTPException(status_code=404, detail="ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + + # ์—…๋ฐ์ดํŠธํ•  ํ•„๋“œ ์ค€๋น„ + update_fields = [] + params = {} + + if bom_name is not None: + update_fields.append("bom_name = :bom_name") + params["bom_name"] = bom_name + + if description is not None: + update_fields.append("description = :description") + params["description"] = description + + if not update_fields: + raise HTTPException(status_code=400, detail="์ˆ˜์ •ํ•  ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค") + + # BOM ์ด๋ฆ„ ์ˆ˜์ •์ธ ๊ฒฝ์šฐ, ๊ฐ™์€ job_no์˜ ๋ชจ๋“  ๋ฆฌ๋น„์ „์„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ + if bom_name is not None and file_result.job_no: + # ๊ฐ™์€ job_no์˜ ๋ชจ๋“  ํŒŒ์ผ ์—…๋ฐ์ดํŠธ + update_query = text(f""" + UPDATE files + SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP + WHERE job_no = :job_no + """) + params["job_no"] = file_result.job_no + + else: + # ๋‹จ์ผ ํŒŒ์ผ๋งŒ ์—…๋ฐ์ดํŠธ + update_query = text(f""" + UPDATE files + SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP + WHERE id = :file_id + """) + params["file_id"] = file_id + + db.execute(update_query, params) + db.commit() + + # ํ™œ๋™ ๋กœ๊ทธ ๊ธฐ๋ก - ๊ฐ„๋‹จํ•˜๊ฒŒ ์ฒ˜๋ฆฌ + logger.info(f"ํŒŒ์ผ ์ •๋ณด ์ˆ˜์ • ์™„๋ฃŒ: ์‚ฌ์šฉ์ž={current_user['username']}, ํŒŒ์ผID={file_id}, BOM๋ช…={bom_name or 'N/A'}") + + return {"message": "ํŒŒ์ผ ์ •๋ณด๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค"} + + except Exception as e: + logger.error(f"ํŒŒ์ผ ์ •๋ณด ์ˆ˜์ • ์‹คํŒจ: {str(e)}") + db.rollback() + raise HTTPException(status_code=500, detail=f"ํŒŒ์ผ ์ •๋ณด ์ˆ˜์ • ์‹คํŒจ: {str(e)}") + + +@router.get("/{file_id}/export-excel") +async def export_materials_to_excel( + file_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """์ž์žฌ ๋ชฉ๋ก์„ ์—‘์…€๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ""" + try: + # ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ + file_query = text(""" + SELECT f.id, f.original_filename, f.bom_name, f.job_no, f.revision, + p.project_name, p.official_project_code + FROM files f + LEFT JOIN projects p ON f.project_id = p.id + WHERE f.id = :file_id + """) + file_result = db.execute(file_query, {"file_id": file_id}).fetchone() + + if not file_result: + raise HTTPException(status_code=404, detail="ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + + # ์ž์žฌ ๋ชฉ๋ก ์กฐํšŒ + materials_query = text(""" + SELECT + m.line_number, + m.original_description, + m.quantity, + m.unit, + m.size_spec, + m.main_nom, + m.red_nom, + m.material_grade, + m.classified_category, + m.classification_confidence, + m.is_verified, + m.verified_by, + m.created_at + FROM materials m + WHERE m.file_id = :file_id + ORDER BY m.line_number ASC + """) + + materials_result = db.execute(materials_query, {"file_id": file_id}).fetchall() + + # ์—‘์…€ ๋ฐ์ดํ„ฐ ์ค€๋น„ + excel_data = [] + for material in materials_result: + excel_data.append({ + "๋ผ์ธ๋ฒˆํ˜ธ": material.line_number, + "ํ’ˆ๋ช…": material.original_description, + "์ˆ˜๋Ÿ‰": material.quantity, + "๋‹จ์œ„": material.unit, + "์‚ฌ์ด์ฆˆ": material.size_spec, + "์ฃผ์š”NOM": material.main_nom, + "์ถ•์†ŒNOM": material.red_nom, + "์žฌ์งˆ๋“ฑ๊ธ‰": material.material_grade, + "๋ถ„๋ฅ˜": material.classified_category, + "์‹ ๋ขฐ๋„": material.classification_confidence, + "๊ฒ€์ฆ์—ฌ๋ถ€": "๊ฒ€์ฆ์™„๋ฃŒ" if material.is_verified else "๋ฏธ๊ฒ€์ฆ", + "๊ฒ€์ฆ์ž": material.verified_by or "", + "๋“ฑ๋ก์ผ": material.created_at.strftime("%Y-%m-%d %H:%M:%S") if material.created_at else "" + }) + + # ํ™œ๋™ ๋กœ๊ทธ ๊ธฐ๋ก + activity_logger = ActivityLogger(db) + activity_logger.log_activity( + username=current_user["username"], + activity_type="์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ", + activity_description=f"์ž์žฌ ๋ชฉ๋ก ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ: {file_result.original_filename}", + target_type="file", + target_id=file_id, + metadata={ + "file_name": file_result.original_filename, + "bom_name": file_result.bom_name, + "job_no": file_result.job_no, + "revision": file_result.revision, + "materials_count": len(excel_data) + } + ) + + return { + "message": "์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ์ค€๋น„ ์™„๋ฃŒ", + "file_info": { + "filename": file_result.original_filename, + "bom_name": file_result.bom_name, + "job_no": file_result.job_no, + "revision": file_result.revision, + "project_name": file_result.project_name + }, + "materials": excel_data, + "total_count": len(excel_data) + } + + except Exception as e: + logger.error(f"์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ: {str(e)}") + raise HTTPException(status_code=500, detail=f"์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ: {str(e)}") + + +@router.get("/{file_id}/materials/view-log") +async def log_materials_view( + file_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """์ž์žฌ ๋ชฉ๋ก ์กฐํšŒ ๋กœ๊ทธ ๊ธฐ๋ก""" + try: + # ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ + file_query = text("SELECT original_filename, bom_name FROM files WHERE id = :file_id") + file_result = db.execute(file_query, {"file_id": file_id}).fetchone() + + if file_result: + # ํ™œ๋™ ๋กœ๊ทธ ๊ธฐ๋ก + activity_logger = ActivityLogger(db) + activity_logger.log_activity( + username=current_user["username"], + activity_type="์ž์žฌ ๋ชฉ๋ก ์กฐํšŒ", + activity_description=f"์ž์žฌ ๋ชฉ๋ก ์กฐํšŒ: {file_result.original_filename}", + target_type="file", + target_id=file_id, + metadata={ + "file_name": file_result.original_filename, + "bom_name": file_result.bom_name + } + ) + + return {"message": "์กฐํšŒ ๋กœ๊ทธ ๊ธฐ๋ก ์™„๋ฃŒ"} + + except Exception as e: + logger.error(f"์กฐํšŒ ๋กœ๊ทธ ๊ธฐ๋ก ์‹คํŒจ: {str(e)}") + return {"message": "์กฐํšŒ ๋กœ๊ทธ ๊ธฐ๋ก ์‹คํŒจ"} \ No newline at end of file diff --git a/backend/scripts/create_system_admin.py b/backend/scripts/create_system_admin.py index e3feb54..c495348 100755 --- a/backend/scripts/create_system_admin.py +++ b/backend/scripts/create_system_admin.py @@ -30,7 +30,7 @@ def create_system_admin(): # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ try: - engine = create_engine(settings.database_url) + engine = create_engine(settings.get_database_url()) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) db = SessionLocal() diff --git a/docker-compose.proxy.yml b/docker-compose.proxy.yml new file mode 100644 index 0000000..b46bbf5 --- /dev/null +++ b/docker-compose.proxy.yml @@ -0,0 +1,15 @@ +services: + nginx-proxy: + image: nginx:alpine + container_name: tk-mp-nginx-proxy + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./nginx-proxy.conf:/etc/nginx/conf.d/default.conf + networks: + - tk-mp-project_tk-mp-network + +networks: + tk-mp-project_tk-mp-network: + external: true \ No newline at end of file diff --git a/frontend/src/pages/LogMonitoringPage.jsx b/frontend/src/pages/LogMonitoringPage.jsx index 367bfaf..191c2dd 100644 --- a/frontend/src/pages/LogMonitoringPage.jsx +++ b/frontend/src/pages/LogMonitoringPage.jsx @@ -3,6 +3,7 @@ import api from '../api'; import { reportError, logUserActionError } from '../utils/errorLogger'; const LogMonitoringPage = ({ onNavigate, user }) => { + const [activeTab, setActiveTab] = useState('login-logs'); // 'login-logs', 'activity-logs', 'system-logs' const [stats, setStats] = useState({ totalUsers: 0, activeUsers: 0, @@ -11,6 +12,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => { recentErrors: 0 }); const [recentActivity, setRecentActivity] = useState([]); + const [activityLogs, setActivityLogs] = useState([]); const [frontendErrors, setFrontendErrors] = useState([]); const [isLoading, setIsLoading] = useState(true); const [message, setMessage] = useState({ type: '', text: '' }); @@ -22,10 +24,26 @@ const LogMonitoringPage = ({ onNavigate, user }) => { return () => clearInterval(interval); }, []); + const loadActivityLogs = async () => { + try { + const response = await api.get('/auth/logs/system?limit=50'); + if (response.data.success) { + setActivityLogs(response.data.logs); + } + } catch (error) { + console.error('ํ™œ๋™ ๋กœ๊ทธ ๋กœ๋”ฉ ์‹คํŒจ:', error); + } + }; + const loadDashboardData = async () => { try { setIsLoading(true); + // ํ™œ๋™ ๋กœ๊ทธ๋„ ํ•จ๊ป˜ ๋กœ๋“œ + if (activeTab === 'activity-logs') { + await loadActivityLogs(); + } + // ๋ณ‘๋ ฌ๋กœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ const [usersResponse, loginLogsResponse] = await Promise.all([ api.get('/auth/users'), @@ -158,9 +176,60 @@ const LogMonitoringPage = ({ onNavigate, user }) => {

๐Ÿ“ˆ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง

-

- ์‹ค์‹œ๊ฐ„ ์‹œ์Šคํ…œ ํ™œ๋™ ๋ฐ ์˜ค๋ฅ˜ ๋ชจ๋‹ˆํ„ฐ๋ง -

+ + + + {/* ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ */} +
+
+ + +
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index 715f96b..a22362c 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -46,8 +46,8 @@ const LoginPage = () => {
-

๐Ÿš€ TK-MP System

-

ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ

+

BOM ํ…Œ์ŠคํŠธ ์„œ๋ฒ„

+

BOM ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ v1.0

@@ -105,7 +105,7 @@ const LoginPage = () => {

๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.

- TK-MP Project Management System v2.0 + BOM ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ v1.0
diff --git a/frontend/src/pages/SystemSettingsPage.jsx b/frontend/src/pages/SystemSettingsPage.jsx index a6bec6b..b437edd 100644 --- a/frontend/src/pages/SystemSettingsPage.jsx +++ b/frontend/src/pages/SystemSettingsPage.jsx @@ -6,6 +6,9 @@ const SystemSettingsPage = ({ onNavigate, user }) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [showCreateForm, setShowCreateForm] = useState(false); + const [activeTab, setActiveTab] = useState('users'); // 'users', 'login-logs', 'activity-logs' + const [loginLogs, setLoginLogs] = useState([]); + const [activityLogs, setActivityLogs] = useState([]); const [newUser, setNewUser] = useState({ username: '', email: '', @@ -16,7 +19,12 @@ const SystemSettingsPage = ({ onNavigate, user }) => { useEffect(() => { loadUsers(); - }, []); + if (activeTab === 'login-logs') { + loadLoginLogs(); + } else if (activeTab === 'activity-logs') { + loadActivityLogs(); + } + }, [activeTab]); const loadUsers = async () => { try { @@ -33,6 +41,36 @@ const SystemSettingsPage = ({ onNavigate, user }) => { } }; + const loadLoginLogs = async () => { + try { + setLoading(true); + const response = await api.get('/auth/logs/login?limit=50'); + if (response.data.success) { + setLoginLogs(response.data.logs); + } + } catch (err) { + console.error('๋กœ๊ทธ์ธ ๋กœ๊ทธ ๋กœ๋”ฉ ์‹คํŒจ:', err); + setError('๋กœ๊ทธ์ธ ๋กœ๊ทธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setLoading(false); + } + }; + + const loadActivityLogs = async () => { + try { + setLoading(true); + const response = await api.get('/auth/logs/system?limit=50'); + if (response.data.success) { + setActivityLogs(response.data.logs); + } + } catch (err) { + console.error('ํ™œ๋™ ๋กœ๊ทธ ๋กœ๋”ฉ ์‹คํŒจ:', err); + setError('ํ™œ๋™ ๋กœ๊ทธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setLoading(false); + } + }; + const handleCreateUser = async (e) => { e.preventDefault(); @@ -104,6 +142,23 @@ const SystemSettingsPage = ({ onNavigate, user }) => { } }; + const getActivityTypeColor = (activityType) => { + switch (activityType) { + case 'FILE_UPLOAD': + return '#10b981'; // ์ดˆ๋ก์ƒ‰ + case 'ํŒŒ์ผ ์ •๋ณด ์ˆ˜์ •': + return '#f59e0b'; // ์ฃผํ™ฉ์ƒ‰ + case '์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ': + return '#3b82f6'; // ํŒŒ๋ž€์ƒ‰ + case '์ž์žฌ ๋ชฉ๋ก ์กฐํšŒ': + return '#8b5cf6'; // ๋ณด๋ผ์ƒ‰ + case 'LOGIN': + return '#6b7280'; // ํšŒ์ƒ‰ + default: + return '#6b7280'; + } + }; + // ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ํ™•์ธ if (user?.role !== 'admin') { return ( @@ -143,9 +198,63 @@ const SystemSettingsPage = ({ onNavigate, user }) => { โš™๏ธ ์‹œ์Šคํ…œ ์„ค์ •

- ์‚ฌ์šฉ์ž ๊ณ„์ • ๊ด€๋ฆฌ ๋ฐ ์‹œ์Šคํ…œ ์„ค์ • + ์‚ฌ์šฉ์ž ๊ณ„์ • ๊ด€๋ฆฌ ๋ฐ ์‹œ์Šคํ…œ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง

+ + + {/* ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ */} +
+
+ + + +
)} - {/* ์‚ฌ์šฉ์ž ๋ชฉ๋ก */} + {/* ํƒญ๋ณ„ ์ฝ˜ํ…์ธ  */} {loading ? (
๋กœ๋”ฉ ์ค‘...
- ) : ( + ) : activeTab === 'users' ? ( + // ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ํƒญ
@@ -446,7 +556,105 @@ const SystemSettingsPage = ({ onNavigate, user }) => {
- )} + ) : activeTab === 'login-logs' ? ( + // ๋กœ๊ทธ์ธ ๋กœ๊ทธ ํƒญ +
+

๐Ÿ” ๋กœ๊ทธ์ธ ๋กœ๊ทธ

+ + + + + + + + + + + {loginLogs.map((log, index) => ( + + + + + + + ))} + +
+ ์‚ฌ์šฉ์ž๋ช… + + IP ์ฃผ์†Œ + + ์ƒํƒœ + + ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ +
+ {log.username} + + {log.ip_address} + + + {log.status === 'success' ? '์„ฑ๊ณต' : '์‹คํŒจ'} + + + {new Date(log.login_time).toLocaleString('ko-KR')} +
+
+ ) : activeTab === 'activity-logs' ? ( + // ํ™œ๋™ ๋กœ๊ทธ ํƒญ +
+

๐Ÿ“Š ํ™œ๋™ ๋กœ๊ทธ

+ + + + + + + + + + + {activityLogs.map((log, index) => ( + + + + + + + ))} + +
+ ์‚ฌ์šฉ์ž๋ช… + + ํ™œ๋™ ์œ ํ˜• + + ์ƒ์„ธ ๋‚ด์šฉ + + ์‹œ๊ฐ„ +
+ {log.username} + + + {log.activity_type} + + + {log.activity_description} + + {new Date(log.created_at).toLocaleString('ko-KR')} +
+
+ ) : null} ); diff --git a/nginx-proxy.conf b/nginx-proxy.conf new file mode 100644 index 0000000..683ff43 --- /dev/null +++ b/nginx-proxy.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name localhost; + + # ํด๋ผ์ด์–ธํŠธ ์ตœ๋Œ€ ์—…๋กœ๋“œ ํฌ๊ธฐ + client_max_body_size 100M; + + # ํ”„๋ก ํŠธ์—”๋“œ ์ •์  ํŒŒ์ผ + location / { + proxy_pass http://frontend:3000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API ์š”์ฒญ์„ ๋ฐฑ์—”๋“œ๋กœ ํ”„๋ก์‹œ + location /api/ { + proxy_pass http://backend:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_redirect off; + + proxy_request_buffering off; + client_max_body_size 100M; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } +} \ No newline at end of file