라이선스 관리 DB 분리
This commit is contained in:
parent
a771fc0561
commit
7ee6446f0f
@ -1,6 +1,6 @@
|
|||||||
# CCTV 모바일 영상 및 카메라 설정 가이드
|
# CCTV 모바일 영상 및 카메라 설정 가이드
|
||||||
|
|
||||||
본 문서는 SmartAsset CCTV 모니터링 모듈의 고급 설정 기능을 설명합니다.
|
본 문서는 Smart IMS CCTV 모니터링 모듈의 고급 설정 기능을 설명합니다.
|
||||||
|
|
||||||
## 1. 전송 방식 (Transport Mode)
|
## 1. 전송 방식 (Transport Mode)
|
||||||
오래된 카메라나 네트워크 환경에 따라 RTSP 데이터 전송 방식을 선택할 수 있습니다.
|
오래된 카메라나 네트워크 환경에 따라 RTSP 데이터 전송 방식을 선택할 수 있습니다.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# 시스템 배포 가이드 (Deployment Guide)
|
# 시스템 배포 가이드 (Deployment Guide)
|
||||||
|
|
||||||
본 문서는 빌드된 스마트어셋(SmartAsset) 솔루션을 실서버(Synology NAS 등)에 배포할 때 필요한 절차와 주의사항을 설명합니다.
|
본 문서는 빌드된 스마트 IMS(Smart IMS) 솔루션을 실서버(Synology NAS 등)에 배포할 때 필요한 절차와 주의사항을 설명합니다.
|
||||||
|
|
||||||
## 1. 배포 대상 폴더 및 파일
|
## 1. 배포 대상 폴더 및 파일
|
||||||
서버에 업로드해야 하는 핵심 구성 요소는 다음과 같습니다.
|
서버에 업로드해야 하는 핵심 구성 요소는 다음과 같습니다.
|
||||||
@ -52,7 +52,7 @@
|
|||||||
업로드가 완료된 후 서버 터미널(SSH)에서 다음 명령을 실행합니다.
|
업로드가 완료된 후 서버 터미널(SSH)에서 다음 명령을 실행합니다.
|
||||||
|
|
||||||
1. **의존성 설치**: `cd server` 이동 후 `npm install`
|
1. **의존성 설치**: `cd server` 이동 후 `npm install`
|
||||||
2. **서비스 시작**: `pm2 start index.js --name "smartasset"`
|
2. **서비스 시작**: `pm2 start index.js --name "smartims"`
|
||||||
3. **상태 확인**: `pm2 status`를 통해 서버가 `online` 상태인지 확인합니다.
|
3. **상태 확인**: `pm2 status`를 통해 서버가 `online` 상태인지 확인합니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# 라이선스 관리자 매뉴얼 (최종본)
|
# 라이선스 관리자 매뉴얼 (최종본)
|
||||||
|
|
||||||
본 문서는 스마트어셋(SmartAsset) 시스템의 라이선스 관리 기능을 사용하는 방법을 설명합니다. 모든 라이선스 관리는 **RSA 비대칭 암호화** 방식을 따르며, `tools/license_manager.cjs` CLI 도구를 통해 수행됩니다.
|
본 문서는 스마트 IMS(Smart IMS) 시스템의 라이선스 관리 기능을 사용하는 방법을 설명합니다. 모든 라이선스 관리는 **RSA 비대칭 암호화** 방식을 따르며, `tools/license_manager.cjs` CLI 도구를 통해 수행됩니다.
|
||||||
|
|
||||||
## 1. 사전 준비 및 구독자 설정 (중요)
|
## 1. 사전 준비 및 구독자 설정 (중요)
|
||||||
라이선스 키를 활성화하기 전에 서버가 어떤 고객사(구독자)에게 속해 있는지 식별하기 위해 **구독자 ID**를 설정해야 합니다.
|
라이선스 키를 활성화하기 전에 서버가 어떤 고객사(구독자)에게 속해 있는지 식별하기 위해 **구독자 ID**를 설정해야 합니다.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SmartAsset Project - Task & Feature Roadmap
|
# Smart IMS Project - Task & Feature Roadmap
|
||||||
|
|
||||||
## 1. Core Platform & Security
|
## 1. Core Platform & Security
|
||||||
- [x] **Project Initialization**: Created Vite + React + TypeScript base.
|
- [x] **Project Initialization**: Created Vite + React + TypeScript base.
|
||||||
|
|||||||
@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="google" content="notranslate" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>SmartAsset - 통합 자산관리 플랫폼</title>
|
<title>Smart IMS - 통합 자산/공정 관리 플랫폼</title>
|
||||||
<script src="https://cdn.jsdelivr.net/gh/phoboslab/jsmpeg@master/jsmpeg.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/gh/phoboslab/jsmpeg@master/jsmpeg.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "temp_app",
|
"name": "smartims",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "temp_app",
|
"name": "smartims",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "temp_app",
|
"name": "smartims",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ async function addColumn() {
|
|||||||
process.env.DB_HOST = process.env.DB_HOST || 'localhost';
|
process.env.DB_HOST = process.env.DB_HOST || 'localhost';
|
||||||
process.env.DB_USER = process.env.DB_USER || 'root';
|
process.env.DB_USER = process.env.DB_USER || 'root';
|
||||||
process.env.DB_PASSWORD = process.env.DB_PASSWORD || 'password'; // Fallback or assume env is loaded
|
process.env.DB_PASSWORD = process.env.DB_PASSWORD || 'password'; // Fallback or assume env is loaded
|
||||||
process.env.DB_NAME = process.env.DB_NAME || 'smartasset_db';
|
process.env.DB_NAME = process.env.DB_NAME || 'sokuree_platform_dev';
|
||||||
|
|
||||||
console.log(`Connecting to database: ${process.env.DB_NAME} at ${process.env.DB_HOST}`);
|
console.log(`Connecting to database: ${process.env.DB_NAME} at ${process.env.DB_HOST}`);
|
||||||
|
|
||||||
|
|||||||
8
server/config/public_key.pem
Normal file
8
server/config/public_key.pem
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN RSA PUBLIC KEY-----
|
||||||
|
MIIBCgKCAQEAjwHtoUUGe+Ib7KA9a2KV00c3B+4a0UTeDu9+jCg+fzyo6qD1ii0p
|
||||||
|
hJoASYkEVpHm5tMGJNMp/fJGyi3glPvfI8OFzs3JDgXWB6f4ao38fgAPtGE/x4Q2
|
||||||
|
wsIHNvIY4scdFgokWYA1+k9//c4kszavOL8fRc85jOlkzUqzQLm3R1x34AVJEFdu
|
||||||
|
oo29vuHlFSzOqJ3c36cNUDJsya+h6Um96zPfM3UA2LLYsYVclr7Bem0jvSNv/sKt
|
||||||
|
hVcdG1QtlHTPAjQI4HZZf/51uIY/K2uXrVq3w4dplqMlLwIqDNb97Ire+Q+VaIe1
|
||||||
|
n+GJBR5Jfa5soYsVjTwRId8VFvjcG0EJCQIDAQAB
|
||||||
|
-----END RSA PUBLIC KEY-----
|
||||||
@ -62,19 +62,33 @@ app.use('/uploads', express.static(uploadDir));
|
|||||||
|
|
||||||
// Session Middleware
|
// Session Middleware
|
||||||
app.use(session({
|
app.use(session({
|
||||||
key: 'smartasset_sid',
|
key: 'smartims_sid',
|
||||||
secret: process.env.SESSION_SECRET || 'smartasset_session_secret_key',
|
secret: process.env.SESSION_SECRET || 'smartims_session_secret_key',
|
||||||
store: sessionStore,
|
store: sessionStore,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: true, // Save new sessions even if empty (helps with some client handshake issues)
|
saveUninitialized: false,
|
||||||
cookie: {
|
cookie: {
|
||||||
httpOnly: true, // Prevent JS access
|
httpOnly: true,
|
||||||
secure: false, // Set true if using HTTPS
|
secure: false, // Set true if using HTTPS
|
||||||
maxAge: 1000 * 60 * 60 * 24, // 1 day
|
maxAge: null, // Browser session by default
|
||||||
sameSite: 'lax' // Recommended for better CSRF protection and reliability
|
sameSite: 'lax'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Dynamic Session Timeout Middleware
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
if (req.session && req.session.user) {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'session_timeout'");
|
||||||
|
const timeoutMinutes = rows.length > 0 ? parseInt(rows[0].setting_value) : 60;
|
||||||
|
req.session.cookie.maxAge = timeoutMinutes * 60 * 1000;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Session timeout fetch error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Apply CSRF Protection
|
// Apply CSRF Protection
|
||||||
app.use(csrfProtection);
|
app.use(csrfProtection);
|
||||||
|
|
||||||
|
|||||||
@ -127,7 +127,7 @@ class StreamRelay {
|
|||||||
const transportMode = process.env.CCTV_TRANSPORT_OVERRIDE || camera.transport_mode || 'tcp';
|
const transportMode = process.env.CCTV_TRANSPORT_OVERRIDE || camera.transport_mode || 'tcp';
|
||||||
|
|
||||||
// 3. Unique User-Agent to prevent session conflicts
|
// 3. Unique User-Agent to prevent session conflicts
|
||||||
const userAgent = `SmartAsset-Relay-v1.0.8-${transportMode}`;
|
const userAgent = `SmartIMS-Relay-v1.0.8-${transportMode}`;
|
||||||
|
|
||||||
// Quality Scaling
|
// Quality Scaling
|
||||||
let scaleFilter = [];
|
let scaleFilter = [];
|
||||||
|
|||||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const { generateToken } = require('../middleware/csrfMiddleware');
|
|||||||
// Key must be 32 bytes for aes-256-cbc
|
// Key must be 32 bytes for aes-256-cbc
|
||||||
// 'my_super_secret_key_manage_asset' is 32 chars?
|
// 'my_super_secret_key_manage_asset' is 32 chars?
|
||||||
// let's use a simpler approach to ensure length on startup or fallback
|
// let's use a simpler approach to ensure length on startup or fallback
|
||||||
const SECRET_KEY = process.env.ENCRYPTION_KEY || 'smartasset_secret_key_0123456789'; // 32 chars needed
|
const SECRET_KEY = process.env.ENCRYPTION_KEY || 'smartims_secret_key_0123456789'; // 32 chars needed
|
||||||
// Ideally use a buffer from hex, but string is okay if 32 chars.
|
// Ideally use a buffer from hex, but string is okay if 32 chars.
|
||||||
// Let's pad it to ensure stability if env is missing.
|
// Let's pad it to ensure stability if env is missing.
|
||||||
const keyBuffer = crypto.scryptSync(SECRET_KEY, 'salt', 32);
|
const keyBuffer = crypto.scryptSync(SECRET_KEY, 'salt', 32);
|
||||||
@ -127,7 +127,7 @@ router.post('/logout', (req, res) => {
|
|||||||
console.error('Logout error:', err);
|
console.error('Logout error:', err);
|
||||||
return res.status(500).json({ success: false, message: 'Logout failed' });
|
return res.status(500).json({ success: false, message: 'Logout failed' });
|
||||||
}
|
}
|
||||||
res.clearCookie('smartasset_sid'); // matching key in index.js
|
res.clearCookie('smartims_sid'); // matching key in index.js
|
||||||
res.json({ success: true, message: 'Logged out' });
|
res.json({ success: true, message: 'Logged out' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const { generateLicense, verifyLicense } = require('../utils/licenseManager');
|
|||||||
const { checkRemoteKey } = require('../utils/remoteLicense');
|
const { checkRemoteKey } = require('../utils/remoteLicense');
|
||||||
|
|
||||||
// Load Public Key for Verification
|
// Load Public Key for Verification
|
||||||
const publicKeyPath = path.join(__dirname, '../public_key.pem');
|
const publicKeyPath = path.join(__dirname, '../config/public_key.pem');
|
||||||
let publicKey = null;
|
let publicKey = null;
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(publicKeyPath)) {
|
if (fs.existsSync(publicKeyPath)) {
|
||||||
@ -20,25 +20,34 @@ try {
|
|||||||
console.error('Error loading public key:', e);
|
console.error('Error loading public key:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0. Server Configuration (Subscriber ID)
|
// 0. Server Configuration (Subscriber ID & Session Timeout)
|
||||||
router.get('/config', isAuthenticated, hasRole('admin'), async (req, res) => {
|
router.get('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'");
|
const [rows] = await db.query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key IN ('subscriber_id', 'session_timeout')");
|
||||||
res.json({ subscriber_id: rows.length > 0 ? rows[0].setting_value : '' });
|
const settings = {};
|
||||||
|
rows.forEach(r => settings[r.setting_key] = r.setting_value);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
subscriber_id: settings.subscriber_id || '',
|
||||||
|
session_timeout: parseInt(settings.session_timeout) || 60 // Default 60 min
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: 'Database error' });
|
res.status(500).json({ error: 'Database error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/config', isAuthenticated, hasRole('admin'), async (req, res) => {
|
router.post('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
||||||
const { subscriber_id } = req.body;
|
const { subscriber_id, session_timeout } = req.body;
|
||||||
if (!subscriber_id) return res.status(400).json({ error: 'Subscriber ID is required' });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sql = `INSERT INTO system_settings (setting_key, setting_value) VALUES ('subscriber_id', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`;
|
if (subscriber_id !== undefined) {
|
||||||
await db.query(sql, [subscriber_id]);
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('subscriber_id', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [subscriber_id]);
|
||||||
res.json({ message: 'Subscriber ID saved' });
|
}
|
||||||
|
if (session_timeout !== undefined) {
|
||||||
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('session_timeout', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [session_timeout.toString()]);
|
||||||
|
}
|
||||||
|
res.json({ message: 'Settings saved' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: 'Database error' });
|
res.status(500).json({ error: 'Database error' });
|
||||||
@ -150,10 +159,6 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async
|
|||||||
|
|
||||||
await db.query(sql, [code, names[code] || code, licenseKey, result.type, result.expiryDate, result.subscriberId]);
|
await db.query(sql, [code, names[code] || code, licenseKey, result.type, result.expiryDate, result.subscriberId]);
|
||||||
|
|
||||||
// 5. Update Issued License Status (if exists in issued_licenses table)
|
|
||||||
// This ensures CLI 'list' command reflects activation even if done via UI
|
|
||||||
await db.query("UPDATE issued_licenses SET status = 'ACTIVE' WHERE license_key = ?", [licenseKey]);
|
|
||||||
|
|
||||||
res.json({ success: true, message: 'Module activated', type: result.type, expiry: result.expiryDate });
|
res.json({ success: true, message: 'Module activated', type: result.type, expiry: result.expiryDate });
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { AssetRegisterPage } from '../modules/asset/pages/AssetRegisterPage';
|
|||||||
import { AssetSettingsPage } from '../modules/asset/pages/AssetSettingsPage';
|
import { AssetSettingsPage } from '../modules/asset/pages/AssetSettingsPage';
|
||||||
import { AssetDetailPage } from '../modules/asset/pages/AssetDetailPage';
|
import { AssetDetailPage } from '../modules/asset/pages/AssetDetailPage';
|
||||||
import { UserManagementPage } from '../platform/pages/UserManagementPage';
|
import { UserManagementPage } from '../platform/pages/UserManagementPage';
|
||||||
|
import { BasicSettingsPage } from '../platform/pages/BasicSettingsPage';
|
||||||
import { ProductionPage } from '../production/pages/ProductionPage';
|
import { ProductionPage } from '../production/pages/ProductionPage';
|
||||||
import { MonitoringPage } from '../modules/cctv/pages/MonitoringPage';
|
import { MonitoringPage } from '../modules/cctv/pages/MonitoringPage';
|
||||||
import { AuthProvider } from '../shared/auth/AuthContext';
|
import { AuthProvider } from '../shared/auth/AuthContext';
|
||||||
@ -59,8 +60,9 @@ function App() {
|
|||||||
|
|
||||||
{/* Admin Routes */}
|
{/* Admin Routes */}
|
||||||
<Route path="/admin/users" element={<UserManagementPage />} />
|
<Route path="/admin/users" element={<UserManagementPage />} />
|
||||||
|
<Route path="/admin/settings" element={<BasicSettingsPage />} />
|
||||||
<Route path="/admin/license" element={<LicensePage />} />
|
<Route path="/admin/license" element={<LicensePage />} />
|
||||||
<Route path="/admin" element={<div>Admin (Coming Soon)</div>} />
|
<Route path="/admin" element={<Navigate to="/admin/settings" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Fallback */}
|
{/* Fallback */}
|
||||||
|
|||||||
@ -37,11 +37,11 @@ export function LoginPage() {
|
|||||||
<div className="brand-logo">
|
<div className="brand-logo">
|
||||||
<Box size={32} />
|
<Box size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h1>SmartAsset</h1>
|
<h1>Smart IMS</h1>
|
||||||
<p>통합 자산관리 시스템</p>
|
<p>통합 자산/공정 관리 플랫폼</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="login-form">
|
<form onSubmit={handleSubmit} className="login-form" autoComplete="off">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="id">아이디</label>
|
<label htmlFor="id">아이디</label>
|
||||||
<div className="input-wrapper">
|
<div className="input-wrapper">
|
||||||
@ -53,6 +53,7 @@ export function LoginPage() {
|
|||||||
onChange={(e) => setId(e.target.value)}
|
onChange={(e) => setId(e.target.value)}
|
||||||
placeholder="아이디를 입력하세요"
|
placeholder="아이디를 입력하세요"
|
||||||
required
|
required
|
||||||
|
autoComplete="one-time-code"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,6 +69,7 @@ export function LoginPage() {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="비밀번호를 입력하세요"
|
placeholder="비밀번호를 입력하세요"
|
||||||
required
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +82,7 @@ export function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="login-footer">
|
<div className="login-footer">
|
||||||
<p>© 2026 SmartAsset System. All rights reserved.</p>
|
<p>© 2026 Smart IMS System. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
108
src/platform/pages/BasicSettingsPage.tsx
Normal file
108
src/platform/pages/BasicSettingsPage.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card } from '../../shared/ui/Card';
|
||||||
|
import { Button } from '../../shared/ui/Button';
|
||||||
|
import { Input } from '../../shared/ui/Input';
|
||||||
|
import { apiClient } from '../../shared/api/client';
|
||||||
|
import { Save, Clock, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
export function BasicSettingsPage() {
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
subscriber_id: '',
|
||||||
|
session_timeout: 60
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get('/system/settings');
|
||||||
|
setSettings(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch settings', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await apiClient.post('/system/settings', settings);
|
||||||
|
alert('설정이 저장되었습니다.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save failed', error);
|
||||||
|
alert('저장 중 오류가 발생했습니다.');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <div className="p-6">로딩 중...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container p-6 max-w-4xl mx-auto">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900">시스템 관리 - 기본 설정</h1>
|
||||||
|
<p className="text-slate-500 mt-1">플랫폼의 기본 운영 환경을 설정합니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Info size={20} className="text-indigo-500" />
|
||||||
|
식별 정보
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">구독자 ID (서버 식별자)</label>
|
||||||
|
<Input
|
||||||
|
value={settings.subscriber_id}
|
||||||
|
onChange={e => setSettings({ ...settings, subscriber_id: e.target.value })}
|
||||||
|
placeholder="예: SOKR-2024-001"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">라이선스 활성화 시 사용되는 서버 고유 식별자입니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Clock size={20} className="text-amber-500" />
|
||||||
|
보안 설정
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">세션 타임아웃 (분)</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="5"
|
||||||
|
max="1440"
|
||||||
|
className="w-32"
|
||||||
|
value={settings.session_timeout}
|
||||||
|
onChange={e => setSettings({ ...settings, session_timeout: parseInt(e.target.value) })}
|
||||||
|
/>
|
||||||
|
<span className="text-slate-500 text-sm">분 동안 활동이 없으면 자동으로 로그아웃됩니다.</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">(최소 5분 ~ 최대 24시간)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
icon={<Save size={18} />}
|
||||||
|
>
|
||||||
|
{saving ? '저장 중...' : '설정 저장'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -28,7 +28,7 @@ export function MainLayout() {
|
|||||||
<div className="sidebar-header">
|
<div className="sidebar-header">
|
||||||
<div className="brand">
|
<div className="brand">
|
||||||
<Box className="brand-icon" size={24} />
|
<Box className="brand-icon" size={24} />
|
||||||
<span className="brand-text">Platform</span>
|
<span className="brand-text">Smart IMS</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -49,6 +49,10 @@ export function MainLayout() {
|
|||||||
|
|
||||||
{expandedModules.includes('sys_mgmt') && (
|
{expandedModules.includes('sys_mgmt') && (
|
||||||
<div className="module-items">
|
<div className="module-items">
|
||||||
|
<Link to="/admin/settings" className={`nav-item ${location.pathname.includes('/admin/settings') ? 'active' : ''}`}>
|
||||||
|
<Settings size={18} />
|
||||||
|
<span>기본 설정</span>
|
||||||
|
</Link>
|
||||||
<Link to="/admin/users" className={`nav-item ${location.pathname.includes('/admin/users') ? 'active' : ''}`}>
|
<Link to="/admin/users" className={`nav-item ${location.pathname.includes('/admin/users') ? 'active' : ''}`}>
|
||||||
<UserIcon size={18} />
|
<UserIcon size={18} />
|
||||||
<span>사용자 관리</span>
|
<span>사용자 관리</span>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* SmartAsset License Management CLI
|
* SmartIMS License Management CLI
|
||||||
*
|
*
|
||||||
* Usage: node tools/license_manager.cjs <command> [args]
|
* Usage: node tools/license_manager.cjs <command> [args]
|
||||||
*
|
*
|
||||||
@ -33,7 +33,7 @@ const dbConfig = {
|
|||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
user: process.env.DB_USER || 'root',
|
user: process.env.DB_USER || 'root',
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME || 'smartasset_db',
|
database: process.env.DB_NAME || 'sokuree_platform_dev',
|
||||||
port: process.env.DB_PORT || 3306
|
port: process.env.DB_PORT || 3306
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ async function main() {
|
|||||||
|
|
||||||
function printUsage() {
|
function printUsage() {
|
||||||
console.log(`
|
console.log(`
|
||||||
SmartAsset License Manager
|
SmartIMS License Manager
|
||||||
Usage: node tools/license_manager.cjs <command> [args]
|
Usage: node tools/license_manager.cjs <command> [args]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user