diff --git a/docs/roadmap/INTEGRATED_ROADMAP.md b/docs/roadmap/INTEGRATED_ROADMAP.md index 11aba7d..7b70d61 100644 --- a/docs/roadmap/INTEGRATED_ROADMAP.md +++ b/docs/roadmap/INTEGRATED_ROADMAP.md @@ -35,8 +35,13 @@ - [x] CCTV 설정 파일 DB 저장 시 username/password 필드 암호화 처리 - [x] CCTV 설정 파일 조회 시 username/password 필드 복호화 처리 - **(수정사항)**: `CameraManagementPage` 신설 및 `cryptoUtil`을 통한 RTSP 계정 정보의 DB 암호화 저장 로직 완성 (AES-256) - #### 🏷️ Tag: `v0.4.2.0` +- [ ] **라이선스 관리 오류 수정** +- [ ] **시스템 관리 기본설정 오류 수정** +- [ ] **시스템 업데이트 오류 수정** + + +#### 🏷️ Tag: `v0.4.3.0` - [ ] **소모품 관리** - [ ] 이 기능은 자재/재고 관리 모듈 생성하여 메뉴가 아니라 속성(카테고리)으로 추가할 예정 diff --git a/server/index.js b/server/index.js index 6398f81..16d26b4 100644 --- a/server/index.js +++ b/server/index.js @@ -438,14 +438,12 @@ const initTables = async () => { const [existingModules] = await db.query('SELECT 1 FROM system_modules LIMIT 1'); if (existingModules.length === 0) { const insert = `INSERT INTO system_modules (code, name, is_active, license_type) VALUES (?, ?, ?, ?)`; - await db.query(insert, ['asset', '자산 관리', true, 'dev']); + await db.query(insert, ['asset', '자산 관리', false, null]); await db.query(insert, ['production', '생산 관리', false, null]); - await db.query(insert, ['cctv', 'CCTV', true, 'dev']); + await db.query(insert, ['cctv', 'CCTV', false, null]); } else { - // One-time update: Rename 'monitoring' code to 'cctv' and ensure it's active - await db.query("UPDATE system_modules SET code = 'cctv', is_active = 1 WHERE code = 'monitoring'"); - // Also ensure 'cctv' is active if it already exists but was inactive due to renaming glitches - await db.query("UPDATE system_modules SET is_active = 1 WHERE code = 'cctv'"); + // One-time update: Rename 'monitoring' code to 'cctv' (migration) + await db.query("UPDATE system_modules SET code = 'cctv' WHERE code = 'monitoring'"); } console.log('✅ Tables Initialized'); diff --git a/server/routes/system.js b/server/routes/system.js index 14efba1..576aa87 100644 --- a/server/routes/system.js +++ b/server/routes/system.js @@ -278,7 +278,8 @@ router.get('/modules', isAuthenticated, async (req, res) => { // Get stored subscriber ID const [subRows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'"); - const serverSubscriberId = subRows.length > 0 ? subRows[0].setting_value : null; + // Ensure we return null or empty string if not found, DO NOT use any hardcoded fallback + const serverSubscriberId = (subRows.length > 0 && subRows[0].setting_value) ? subRows[0].setting_value : ''; defaults.forEach(code => { const found = rows.find(r => r.code === code); @@ -322,7 +323,10 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async } // 2. Check Module match - if (result.module !== code) { + // Allow legacy 'monitoring' licenses to activate 'cctv' module + const isMatch = result.module === code || (code === 'cctv' && result.module === 'monitoring'); + + if (!isMatch) { return res.status(400).json({ error: `This license is for '${result.module}' module, not '${code}'` }); } @@ -342,7 +346,7 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async } } else { if (result.subscriberId !== serverSubscriberId) { - return res.status(403).json({ + return res.status(400).json({ error: `구독자 ID 불일치: 라이선스 키는 [${result.subscriberId}] 전용이지만, 현재 서버는 [${serverSubscriberId}]로 설정되어 있습니다.` }); } diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index dc75932..050b61a 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -61,6 +61,8 @@ export function LoginPage() { placeholder="아이디를 입력하세요" required autoComplete="one-time-code" + readOnly + onFocus={(e) => e.target.readOnly = false} /> @@ -77,6 +79,8 @@ export function LoginPage() { placeholder="비밀번호를 입력하세요" required autoComplete="new-password" + readOnly + onFocus={(e) => e.target.readOnly = false} /> diff --git a/src/platform/pages/BasicSettingsPage.tsx b/src/platform/pages/BasicSettingsPage.tsx index 2056404..6e0013e 100644 --- a/src/platform/pages/BasicSettingsPage.tsx +++ b/src/platform/pages/BasicSettingsPage.tsx @@ -452,7 +452,14 @@ export function BasicSettingsPage() {
- { setSettings({ ...settings, db_config: { ...settings.db_config, password: e.target.value } }); setIsDbVerified(false); }} /> + e.target.readOnly = false} + value={settings.db_config.password} + onChange={e => { setSettings({ ...settings, db_config: { ...settings.db_config, password: e.target.value } }); setIsDbVerified(false); }} + />
@@ -528,6 +535,9 @@ export function BasicSettingsPage() { e.target.readOnly = false} value={verifyPassword} onChange={e => setVerifyPassword(e.target.value)} placeholder="비밀번호를 입력하세요" diff --git a/src/platform/pages/UserManagementPage.tsx b/src/platform/pages/UserManagementPage.tsx index 5bb57c2..3ac111e 100644 --- a/src/platform/pages/UserManagementPage.tsx +++ b/src/platform/pages/UserManagementPage.tsx @@ -242,6 +242,10 @@ export function UserManagementPage() {
e.target.readOnly = false} value={formData.id} onChange={(e) => setFormData({ ...formData, id: e.target.value })} disabled={isEditing} @@ -255,6 +259,10 @@ export function UserManagementPage() { 비밀번호 {!isEditing && '*'} e.target.readOnly = false} type="password" value={formData.password} onChange={(e) => setFormData({ ...formData, password: e.target.value })} diff --git a/src/system/pages/LicensePage.tsx b/src/system/pages/LicensePage.tsx index 9b68183..6c67fcf 100644 --- a/src/system/pages/LicensePage.tsx +++ b/src/system/pages/LicensePage.tsx @@ -13,7 +13,7 @@ export function LicensePage() { const moduleInfo: Record = { 'asset': { title: '자산 관리 모듈', desc: '자산 등록, 조회, 수정 및 유지보수 이력 관리' }, 'production': { title: '생산 관리 모듈', desc: '생산 계획, 실적 및 공정 관리' }, - 'monitoring': { title: 'CCTV 모듈', desc: '실시간 영상 모니터링 및 녹화 관리' } + 'cctv': { title: 'CCTV 모듈', desc: '실시간 영상 모니터링 및 녹화 관리' } }; // Subscriber Configuration State @@ -21,6 +21,13 @@ export function LicensePage() { const [inputSubscriberId, setInputSubscriberId] = useState(''); const [isConfigSaving, setIsConfigSaving] = useState(false); + // Supervisor Verification State + const [isConfigUnlocked, setIsConfigUnlocked] = useState(false); + const [showVerifyModal, setShowVerifyModal] = useState(false); + const [verifyPassword, setVerifyPassword] = useState(''); + const [verifying, setVerifying] = useState(false); + const [verifyError, setVerifyError] = useState(''); + // Initial Load for Subscriber ID useState(() => { fetchSubscriberId(); @@ -86,7 +93,30 @@ export function LicensePage() { } }; - // ... Generator logic omitted for brevity as it's dev-only ... + + const handleOpenVerify = () => { + setVerifyError(''); + setVerifyPassword(''); + setShowVerifyModal(true); + }; + + const handleVerifySupervisor = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + setVerifying(true); + setVerifyError(''); + try { + const res = await apiClient.post('/verify-supervisor', { password: verifyPassword }); + if (res.data.success) { + setIsConfigUnlocked(true); + setShowVerifyModal(false); + setVerifyPassword(''); + } + } catch (error: any) { + setVerifyError(error.response?.data?.message || '인증 실패'); + } finally { + setVerifying(false); + } + }; @@ -106,37 +136,61 @@ export function LicensePage() {
-
-

- - 서버 환경 설정 -

-
-
-
- - setInputSubscriberId(e.target.value)} - /> -
+
+
+

+ + 서버 환경 설정 +

+ {!isConfigUnlocked && ( -
-

- * 라이선스 키에 포함된 ID와 일치해야 활성화됩니다. -

+ )} + {isConfigUnlocked && ( + + 최고관리자 권한 + + )}
+ + {isConfigUnlocked ? ( +
+
+
+ + setInputSubscriberId(e.target.value)} + /> +
+ +
+

+ * 주의: 구독자 ID를 변경하면 기존에 활성화된 모든 모듈의 라이선스 효력이 중지될 수 있습니다.
+ * 반드시 라이선스 키에 포함된 ID와 일치하게 설정하십시오. +

+
+ ) : ( +
+

서버 환경 설정은 시스템의 핵심 식별자입니다.

+

보안을 위해 최고관리자(Supervisor) 인증 후 변경할 수 있습니다.

+
+ )}
@@ -260,6 +314,50 @@ export function LicensePage() { + {/* Supervisor Verification Modal */} + {showVerifyModal && ( +
+
+
+

+ 최고관리자 인증 +

+

서버 핵심 설정을 변경하려면 인증이 필요합니다.

+
+
+
+ + setVerifyPassword(e.target.value)} + /> + {verifyError &&

{verifyError}

} +
+
+ + +
+
+
+
+ )}
); }