[BUILD] v0.4.4.0 - 통합 로드맵 업데이트 및 모듈 시스템 개선

This commit is contained in:
choibk 2026-01-27 01:46:17 +09:00
parent bce16b176c
commit 763217acaf
10 changed files with 117 additions and 24 deletions

View File

@ -3,7 +3,7 @@
**프로젝트명:** SOKUREE Platform - Smart Integrated Management System
**최초 작성일:** 2026-01-25
**최근 업데이트:** 2026-01-26
**버전:** v0.4.3.1
**버전:** v0.4.4.0
---
@ -84,6 +84,24 @@
- [x] **라이선스 발급 표준 정의**
- [x] 발급 관리 프로그램 연동을 위한 모듈별 고유 코드(`asset`, `production`, `material`, `quality`, `cctv`) 및 필수 규격 확정
#### 🏷️ Tag: `v0.4.3.5` (완료)
- [x] **버전 관리 표준 확립**
- MAJOR.MINOR.PATCH.BUILD 4단계 체계 도입 및 문서화 (`version manage.md`)
- 배포 표준 절차(Standard Deployment Procedure) 정의
- [x] **시스템 안정성 및 빌드 최적화**
- 빌드 시 정적 자산 강제 업데이트 스크립트 보강
- 미사용 변수 및 린트 에러 수정을 통한 빌드 실패 방지
- 버전 정보 동기화 로직 최적화 (package.json 기반)
#### 🏷️ Tag: `v0.4.4.0` (진행중)
- [x] **모듈 카테고리 동적 생성 기능**
- 모듈 정의(`IModuleDefinition`) 시 카테고리를 하드코딩하지 않고 직접 문자열로 정의 가능하도록 개선
- 사이드바 그룹화 로직을 동적으로 처리하여 신규 카테고리 추가 시 코드 수정 최소화
- [x] **UI 시각화 및 사용성 개선**
- 사이드바 메뉴 선택 시 하이라이트 색상을 브랜드 테마(`var(--sokuree-brand-primary)`)와 일치시켜 시각적 일관성 확보
- [x] **통합 로드맵 및 문서 현행화**
- `INTEGRATED_ROADMAP.md` 업데이트 및 버전 관리 규정 반영
#### 🏷️ Tag: `v0.5.0.x`
- [ ] 모듈 간 연동을 위한 API 인터페이스 정의
- [ ] 자재/재고 관리 모듈 추가

View File

@ -1,7 +1,7 @@
{
"name": "smartims",
"private": true,
"version": "0.4.3.5",
"version": "0.4.4.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -1,6 +1,6 @@
{
"name": "server",
"version": "0.4.3.5",
"version": "0.4.4.0",
"description": "",
"main": "index.js",
"scripts": {

View File

@ -310,7 +310,7 @@ router.get('/modules', isAuthenticated, async (req, res) => {
const [rows] = await db.query('SELECT * FROM system_modules');
const modules = {};
const defaults = ['asset', 'production', 'cctv'];
const defaults = ['asset', 'production', 'cctv', 'material', 'quality'];
// Get stored subscriber ID
const [subRows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'");
@ -415,17 +415,21 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async
const names = {
'asset': '자산 관리',
'production': '생산 관리',
'cctv': 'CCTV'
'cctv': 'CCTV',
'material': '자재/재고 관리',
'quality': '품질 관리'
};
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';
const licenseManagerUrl = process.env.LICENSE_MANAGER_URL || 'http://sokuree.com:3006/api';
try {
console.log(`📡 Syncing with License Manager: ${licenseManagerUrl}/licenses/activate`);
await axios.post(`${licenseManagerUrl}/licenses/activate`, { licenseKey });
console.log(`✅ Synced activation status for key: ${licenseKey.substring(0, 20)}...`);
} catch (syncErr) {
console.error(`❌ Sync Error Object:`, syncErr);
const errorDetail = syncErr.response ?
`Status: ${syncErr.response.status}, Data: ${JSON.stringify(syncErr.response.data)}` :
syncErr.message;

19
server/test_sync.js Normal file
View File

@ -0,0 +1,19 @@
const axios = require('axios');
async function test() {
const licenseManagerUrl = 'http://localhost:3006/api';
const licenseKey = 'test-key';
try {
console.log(`📡 Testing sync with ${licenseManagerUrl}/licenses/activate`);
const response = await axios.post(`${licenseManagerUrl}/licenses/activate`, { licenseKey });
console.log('✅ Success:', response.data);
} catch (err) {
if (err.response) {
console.error('❌ Error Response:', err.response.status, err.response.data);
} else {
console.error('❌ Error Message:', err.message);
}
}
}
test();

View File

@ -4,7 +4,7 @@ import { AssetListPage } from './pages/AssetListPage';
import { AssetRegisterPage } from './pages/AssetRegisterPage';
import { AssetSettingsPage } from './pages/AssetSettingsPage';
import { AssetDetailPage } from './pages/AssetDetailPage';
import { BasicSettingsPage } from '../../platform/pages/BasicSettingsPage';
import { PlaceholderPage } from '../../shared/ui/PlaceholderPage';
export const assetModule: IModuleDefinition = {
moduleName: 'asset-management',
@ -14,10 +14,10 @@ export const assetModule: IModuleDefinition = {
{ path: '/list', element: <AssetListPage />, label: '자산 목록', group: '기준정보' },
{ path: '/register', element: <AssetRegisterPage />, label: '자산 등록', group: '기준정보' },
{ path: '/settings/modules', element: <BasicSettingsPage />, label: '모듈 관리', group: '설정' },
{ path: '/settings/modules', element: <PlaceholderPage title="모듈 관리" description="준비 중인 페이지입니다. 라이선스 관리와는 별도로 각 모듈의 세부 작동 방식을 설정하는 공간입니다." />, label: '모듈 관리', group: '설정' },
{ path: '/settings', element: <AssetSettingsPage />, label: '자산 설정', group: '설정' },
{ path: '/settings/iot', element: <AssetSettingsPage />, label: 'IOT 센서 관리', group: '설정' }, // Using AssetSettingsPage as placeholder
{ path: '/settings/facilities', element: <AssetListPage />, label: '설비 관리', group: '설정' },
{ path: '/settings/iot', element: <PlaceholderPage title="IOT 센서 관리" description="자산에 부착된 IOT 센서의 상태를 모니터링하고 임계치를 설정할 수 있는 기능이 준비 중입니다." />, label: 'IOT 센서 관리', group: '설정' },
{ path: '/settings/facilities', element: <PlaceholderPage title="설비 관리" description="생산 설비의 효율 및 정비 주기를 전문적으로 관리하기 위한 전용 메뉴를 개발 중입니다." />, label: '설비 관리', group: '설정' },
{ path: '/detail/:assetId', element: <AssetDetailPage /> },

View File

@ -14,5 +14,6 @@ export const qualityModule: IModuleDefinition = {
label: '품질 현황',
icon: <ClipboardCheck size={16} />
}
]
],
requiredRoles: ['admin', 'manager', 'user']
};

View File

@ -25,11 +25,10 @@ export function SystemProvider({ children }: { children: ReactNode }) {
const refreshModules = async () => {
try {
const response = await apiClient.get('/system/modules');
console.log('[SystemContext] Modules from server:', response.data.modules);
if (response.data.modules) {
setModules(response.data.modules);
} else {
// Fallback or assume old format (unsafe with new backend)
// Better to assume new format based on my changes
setModules(response.data.modules || response.data);
}
} catch (error) {

View File

@ -0,0 +1,49 @@
import { Construction, ArrowRight } from 'lucide-react';
interface PlaceholderPageProps {
title: string;
description: string;
}
export function PlaceholderPage({ title, description }: PlaceholderPageProps) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] p-8 text-center animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="w-20 h-20 bg-blue-50 rounded-3xl flex items-center justify-center mb-6 shadow-sm border border-blue-100">
<Construction className="text-blue-600 w-10 h-10" />
</div>
<h1 className="text-3xl font-extrabold text-slate-800 mb-4 tracking-tight">
{title}
</h1>
<p className="text-slate-500 max-w-md text-lg leading-relaxed mb-8">
{description}
</p>
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 max-w-lg w-full">
<h3 className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-4"> </h3>
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm text-slate-600">
<div className="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]"></div>
<span> </span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-400">
<div className="w-2 h-2 rounded-full bg-slate-300"></div>
<span> </span>
</div>
<div className="flex items-center gap-3 text-sm text-slate-400">
<div className="w-2 h-2 rounded-full bg-slate-300"></div>
<span> </span>
</div>
</div>
</div>
<button
onClick={() => window.history.back()}
className="mt-10 flex items-center gap-2 text-blue-600 font-bold hover:gap-3 transition-all group"
>
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</button>
</div>
);
}

View File

@ -21,9 +21,9 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
const isSupervisor = user?.role === 'supervisor';
const checkModulePermission = (moduleKey: string) => {
// Supervisor always has access
if (isSupervisor) return true;
// Check allowed_modules list
// Supervisor and Admin always have access to see active modules
if (isSupervisor || isAdmin) return true;
// Check allowed_modules list for other roles (manager, user, etc.)
return user?.allowed_modules?.includes(moduleKey);
};
@ -60,7 +60,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
{isAdmin && (
<div className="module-group">
<button
className={`module-header ${expandedModules.includes('sys_mgmt') ? 'active' : ''}`}
className="module-header" // Removed active class based on expanded state
onClick={() => toggleModule('sys_mgmt')}
>
<div className="module-title">
@ -116,7 +116,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
return (
<div key={mod.moduleName} className="module-group">
<button
className={`module-header ${isExpanded ? 'active' : ''}`}
className={`module-header ${isExpanded ? 'bg-white/5' : ''}`}
onClick={() => toggleModule(mod.moduleName)}
>
<div className="module-title">
@ -133,7 +133,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
<Link
key={`${mod.moduleName}-${route.path}`}
to={`${mod.basePath}${route.path}`}
className={`nav-item ${location.pathname.startsWith(`${mod.basePath}${route.path}`) ? 'active' : ''}`}
className={`nav-item ${location.pathname === `${mod.basePath}${route.path}` ? 'active' : ''}`}
>
<span>{route.label}</span>
</Link>
@ -147,7 +147,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
return (
<div key={groupName} className="nested-module-group pl-2 mt-1">
<button
className={`nav-item w-full flex justify-between items-center ${isGroupExpanded ? 'active bg-slate-800' : ''}`}
className={`nav-item w-full flex justify-between items-center ${isGroupExpanded ? 'bg-slate-800/40' : ''}`}
onClick={() => toggleModule(groupKey)}
>
<div className="flex items-center gap-2">
@ -167,7 +167,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
<Link
key={`${mod.moduleName}-${route.path}`}
to={`${mod.basePath}${route.path}`}
className={`nav-item py-1.5 text-xs ${location.pathname.startsWith(`${mod.basePath}${route.path}`) ? 'active' : ''}`}
className={`nav-item py-1.5 text-xs ${location.pathname === `${mod.basePath}${route.path}` ? 'active' : ''}`}
>
<span>{route.label}</span>
</Link>
@ -200,7 +200,10 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
})
.map((mod) => {
const moduleKey = mod.moduleName.split('-')[0];
if (!isModuleActive(moduleKey) || !checkModulePermission(moduleKey)) return null;
const active = isModuleActive(moduleKey);
const permitted = checkModulePermission(moduleKey);
console.log(`[Sidebar] Module: ${mod.moduleName}, Key: ${moduleKey}, Active: ${active}, Permitted: ${permitted}`);
if (!active || !permitted) return null;
if (mod.requiredRoles && !checkRole(mod.requiredRoles)) return null;
const isExpanded = expandedModules.includes(mod.moduleName);
@ -220,7 +223,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
return (
<div key={mod.moduleName} className="module-group">
<button
className={`module-header ${isExpanded ? 'active' : ''}`}
className={`module-header ${isExpanded ? 'bg-white/5' : ''}`}
onClick={() => toggleModule(mod.moduleName)}
>
<div className="module-title">
@ -236,7 +239,7 @@ export function MainLayout({ modulesList }: MainLayoutProps) {
<Link
key={`${mod.moduleName}-${route.path}`}
to={`${mod.basePath}${route.path}`}
className={`nav-item ${location.pathname.startsWith(`${mod.basePath}${route.path}`) ? 'active' : ''}`}
className={`nav-item ${location.pathname === `${mod.basePath}${route.path}` ? 'active' : ''}`}
>
<span>{route.label}</span>
</Link>