chore: save point before architectural refactoring for modularity
This commit is contained in:
parent
7ee6446f0f
commit
59e876c838
26
server/check_sub.js
Normal file
26
server/check_sub.js
Normal file
@ -0,0 +1,26 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const db = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
port: process.env.DB_PORT || 3307
|
||||
});
|
||||
const [rows] = await db.query("SELECT * FROM system_settings WHERE setting_key = 'subscriber_id'");
|
||||
if (rows.length > 0) {
|
||||
console.log('Subscriber ID Value: [' + rows[0].setting_value + ']');
|
||||
console.log('Length:', rows[0].setting_value.length);
|
||||
} else {
|
||||
console.log('Subscriber ID NOT SET');
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
check();
|
||||
27
server/debug_db.js
Normal file
27
server/debug_db.js
Normal file
@ -0,0 +1,27 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const db = await mysql.createConnection({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'smartims_db',
|
||||
port: process.env.DB_PORT || 3307
|
||||
});
|
||||
const [rows] = await db.query("SELECT * FROM system_settings");
|
||||
console.log('--- System Settings ---');
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
|
||||
const [modules] = await db.query("SELECT * FROM system_modules");
|
||||
console.log('--- System Modules ---');
|
||||
console.log(JSON.stringify(modules, null, 2));
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
check();
|
||||
@ -54,8 +54,9 @@ const sessionStore = new MySQLStore(sessionStoreOptions);
|
||||
|
||||
// Middleware
|
||||
app.use(cors({
|
||||
origin: true, // Allow all origins (or specific one)
|
||||
credentials: true // Important for cookies
|
||||
origin: true,
|
||||
credentials: true,
|
||||
allowedHeaders: ['Content-Type', 'X-CSRF-Token', 'Authorization']
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use('/uploads', express.static(uploadDir));
|
||||
@ -65,7 +66,7 @@ app.use(session({
|
||||
key: 'smartims_sid',
|
||||
secret: process.env.SESSION_SECRET || 'smartims_session_secret_key',
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
resave: true, // Force save to avoid session loss in some environments
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
@ -82,6 +83,9 @@ app.use(async (req, res, next) => {
|
||||
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;
|
||||
|
||||
// Explicitly save session to ensure store sync
|
||||
req.session.save();
|
||||
} catch (err) {
|
||||
console.error('Session timeout fetch error:', err);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ const csrfProtection = (req, res, next) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Skip for Login endpoint (initial session creation)
|
||||
// Skip for Login endpoint
|
||||
if (req.path === '/api/login' || req.path === '/api/auth/login') {
|
||||
return next();
|
||||
}
|
||||
@ -24,8 +24,18 @@ const csrfProtection = (req, res, next) => {
|
||||
const tokenFromSession = req.session.csrfToken;
|
||||
|
||||
if (!tokenFromSession || !tokenFromHeader || tokenFromSession !== tokenFromHeader) {
|
||||
console.error('CSRF mismatch:', { session: tokenFromSession, header: tokenFromHeader });
|
||||
return res.status(403).json({ success: false, message: 'Invalid CSRF Token' });
|
||||
console.error('🔒 [CSRF Security] Token Mismatch Detected');
|
||||
console.error(`- Path: ${req.path}`);
|
||||
console.error(`- Session ID: ${req.sessionID ? req.sessionID.substring(0, 8) + '...' : 'NONE'}`);
|
||||
console.error(`- Session User: ${req.session?.user?.id || 'GUEST'}`);
|
||||
console.error(`- Token in Session: ${tokenFromSession ? 'EXISTS (' + tokenFromSession.substring(0, 5) + '...)' : 'MISSING'}`);
|
||||
console.error(`- Token in Header: ${tokenFromHeader ? 'EXISTS (' + tokenFromHeader.substring(0, 5) + '...)' : 'MISSING'}`);
|
||||
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Invalid CSRF Token',
|
||||
error: '세션 보안 토큰(CSRF)이 일치하지 않습니다. 보안 정책에 따라 요청이 거부되었습니다. 다시 로그인하거나 새로고침 후 시도해 주세요.'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
132
server/package-lock.json
generated
132
server/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
@ -95,6 +96,12 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
@ -104,6 +111,17 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -258,6 +276,18 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -350,6 +380,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -448,6 +487,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -674,6 +728,63 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -809,6 +920,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@ -1341,6 +1467,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
@ -27,4 +28,4 @@
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
server/reset_admin.js
Normal file
35
server/reset_admin.js
Normal file
@ -0,0 +1,35 @@
|
||||
const db = require('./db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
async function resetAdmin() {
|
||||
try {
|
||||
const adminId = 'admin';
|
||||
const password = 'admin123';
|
||||
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
|
||||
|
||||
console.log(`Resetting password for ${adminId}...`);
|
||||
console.log(`New Hash: ${hashedPassword}`);
|
||||
|
||||
const [result] = await db.query(
|
||||
'UPDATE users SET password = ? WHERE id = ?',
|
||||
[hashedPassword, adminId]
|
||||
);
|
||||
|
||||
if (result.affectedRows > 0) {
|
||||
console.log('✅ Admin password reset successfully to "admin123"');
|
||||
} else {
|
||||
console.log('❌ Admin user not found. Creating new admin...');
|
||||
await db.query(
|
||||
'INSERT INTO users (id, password, name, role, department, position) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[adminId, hashedPassword, '시스템 관리자', 'admin', 'IT팀', '관리자']
|
||||
);
|
||||
console.log('✅ Default Admin Created (admin / admin123)');
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('❌ Reset Failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
resetAdmin();
|
||||
@ -13,11 +13,12 @@ let publicKey = null;
|
||||
try {
|
||||
if (fs.existsSync(publicKeyPath)) {
|
||||
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
||||
console.log('✅ Public Key loaded successfully for license verification');
|
||||
} else {
|
||||
console.error('WARNING: public_key.pem not found in server root. License verification will fail.');
|
||||
console.error('❌ WARNING: public_key.pem not found at:', publicKeyPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading public key:', e);
|
||||
console.error('❌ Error loading public key:', e);
|
||||
}
|
||||
|
||||
// 0. Server Configuration (Subscriber ID & Session Timeout)
|
||||
@ -87,6 +88,8 @@ router.get('/modules', isAuthenticated, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
// 2. Activate Module (Apply License)
|
||||
// Only admin can manage system modules
|
||||
router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async (req, res) => {
|
||||
@ -133,8 +136,6 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async
|
||||
INSERT INTO license_history (module_code, license_key, license_type, subscriber_id, activated_at)
|
||||
VALUES (?, ?, ?, ?, NOW())
|
||||
`;
|
||||
// Store "activated_at" as roughly the time it was active until now (or simply log the event time)
|
||||
// Actually, let's treat "activated_at" in history as "when it was archived/replaced".
|
||||
await db.query(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]);
|
||||
}
|
||||
|
||||
@ -159,11 +160,25 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async
|
||||
|
||||
await db.query(sql, [code, names[code] || code, licenseKey, result.type, result.expiryDate, result.subscriberId]);
|
||||
|
||||
// 5. Sync status with License Manager
|
||||
const licenseManagerUrl = process.env.LICENSE_MANAGER_URL || 'http://localhost:3006/api';
|
||||
try {
|
||||
await axios.post(`${licenseManagerUrl}/licenses/activate`, { licenseKey });
|
||||
console.log(`✅ Synced activation status for key: ${licenseKey.substring(0, 20)}...`);
|
||||
} catch (syncErr) {
|
||||
console.error('⚠️ Failed to sync status with License Manager:', syncErr.message);
|
||||
// We don't fail the whole activation if sync fails, but we log it
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Module activated', type: result.type, expiry: result.expiryDate });
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Database error' });
|
||||
console.error('❌ License Activation Error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message || 'Database error',
|
||||
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ import { Save, Clock, Info } from 'lucide-react';
|
||||
|
||||
export function BasicSettingsPage() {
|
||||
const [settings, setSettings] = useState({
|
||||
subscriber_id: '',
|
||||
session_timeout: 60
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -51,23 +50,7 @@ export function BasicSettingsPage() {
|
||||
</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">
|
||||
|
||||
@ -28,7 +28,7 @@ export function LicensePage() {
|
||||
|
||||
async function fetchSubscriberId() {
|
||||
try {
|
||||
const res = await apiClient.get('/system/config');
|
||||
const res = await apiClient.get('/system/settings');
|
||||
setServerSubscriberId(res.data.subscriber_id || '');
|
||||
setInputSubscriberId(res.data.subscriber_id || '');
|
||||
} catch (err) {
|
||||
@ -40,7 +40,7 @@ export function LicensePage() {
|
||||
if (!inputSubscriberId.trim()) return alert('구독자 ID를 입력해주세요.');
|
||||
setIsConfigSaving(true);
|
||||
try {
|
||||
await apiClient.post('/system/config', { subscriber_id: inputSubscriberId });
|
||||
await apiClient.post('/system/settings', { subscriber_id: inputSubscriberId });
|
||||
setServerSubscriberId(inputSubscriberId);
|
||||
alert('구독자 ID가 저장되었습니다.');
|
||||
} catch (err) {
|
||||
@ -88,19 +88,7 @@ export function LicensePage() {
|
||||
|
||||
// ... Generator logic omitted for brevity as it's dev-only ...
|
||||
|
||||
// History State
|
||||
const [historyModule, setHistoryModule] = useState<string | null>(null);
|
||||
const [historyData, setHistoryData] = useState<any[]>([]);
|
||||
|
||||
const fetchHistory = async (code: string) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/system/modules/${code}/history`);
|
||||
setHistoryData(res.data);
|
||||
setHistoryModule(code);
|
||||
} catch (err) {
|
||||
alert('이력을 불러오는데 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -109,35 +97,36 @@ export function LicensePage() {
|
||||
라이선스 및 모듈 관리
|
||||
</h2>
|
||||
|
||||
{/* Server Configuration Section */}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-6 mb-8">
|
||||
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
||||
<Terminal size={20} className="text-slate-600" />
|
||||
서버 환경 설정
|
||||
</h3>
|
||||
<div className="flex items-end gap-4 max-w-xl">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
서버 구독자 ID (Server Subscriber ID)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border border-slate-300 rounded px-3 py-2"
|
||||
placeholder="예: SAMSUNG, DEMO_USER_01"
|
||||
value={inputSubscriberId}
|
||||
onChange={(e) => setInputSubscriberId(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
* 라이선스 키에 포함된 ID와 일치해야 활성화됩니다.
|
||||
</p>
|
||||
<div className="max-w-xl">
|
||||
<div className="flex items-end gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
서버 구독자 ID (Server Subscriber ID)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border border-slate-300 rounded px-3 py-2"
|
||||
placeholder="예: SAMSUNG, DEMO_USER_01"
|
||||
value={inputSubscriberId}
|
||||
onChange={(e) => setInputSubscriberId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSaveConfig}
|
||||
disabled={isConfigSaving}
|
||||
className="bg-slate-800 text-white px-6 py-2 rounded hover:bg-slate-900 transition disabled:opacity-50 h-[42px]"
|
||||
>
|
||||
{isConfigSaving ? '저장 중...' : '설정 저장'}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSaveConfig}
|
||||
disabled={isConfigSaving}
|
||||
className="bg-slate-800 text-white px-4 py-2 rounded hover:bg-slate-900 transition disabled:opacity-50"
|
||||
>
|
||||
{isConfigSaving ? '저장 중...' : '설정 저장'}
|
||||
</button>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
* 라이선스 키에 포함된 ID와 일치해야 활성화됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -193,10 +182,10 @@ export function LicensePage() {
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => fetchHistory(code)}
|
||||
className="flex-1 bg-gray-100 text-gray-700 border border-gray-200 py-2 rounded hover:bg-gray-200 transition text-sm"
|
||||
onClick={() => setSelectedModule(code)}
|
||||
className="flex-1 bg-blue-50 text-blue-600 border border-blue-100 py-2 rounded hover:bg-blue-100 transition text-sm flex items-center justify-center gap-1"
|
||||
>
|
||||
변경 이력
|
||||
<Key size={14} /> 활성화
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeactivate(code)}
|
||||
@ -260,54 +249,7 @@ export function LicensePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History Modal */}
|
||||
{historyModule && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl">
|
||||
<h3 className="text-lg font-bold mb-4">라이선스 변경 이력 ({moduleInfo[historyModule]?.title})</h3>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left border-collapse">
|
||||
<thead className="bg-gray-50 text-gray-700">
|
||||
<tr>
|
||||
<th className="p-2 border-b">교체 일시</th>
|
||||
<th className="p-2 border-b">구독자</th>
|
||||
<th className="p-2 border-b">유형</th>
|
||||
<th className="p-2 border-b">라이선스 키 (일부)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{historyData.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-4 text-center text-gray-500">이력이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
historyData.map((h: any) => (
|
||||
<tr key={h.id} className="border-b">
|
||||
<td className="p-2">{new Date(h.activated_at).toLocaleString()}</td>
|
||||
<td className="p-2">{h.subscriber_id || 'N/A'}</td>
|
||||
<td className="p-2 uppercase">{h.license_type || 'N/A'}</td>
|
||||
<td className="p-2 font-mono text-gray-500 text-xs">
|
||||
{h.license_key ? h.license_key.substring(0, 20) + '...' : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<button
|
||||
onClick={() => setHistoryModule(null)}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user