[BUILD] v0.4.4.0 - 통합 로드맵 업데이트 및 모듈 시스템 개선
This commit is contained in:
parent
bce16b176c
commit
763217acaf
@ -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 인터페이스 정의
|
||||
- [ ] 자재/재고 관리 모듈 추가
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "smartims",
|
||||
"private": true,
|
||||
"version": "0.4.3.5",
|
||||
"version": "0.4.4.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.4.3.5",
|
||||
"version": "0.4.4.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -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
19
server/test_sync.js
Normal 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();
|
||||
@ -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 /> },
|
||||
|
||||
|
||||
@ -14,5 +14,6 @@ export const qualityModule: IModuleDefinition = {
|
||||
label: '품질 현황',
|
||||
icon: <ClipboardCheck size={16} />
|
||||
}
|
||||
]
|
||||
],
|
||||
requiredRoles: ['admin', 'manager', 'user']
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
49
src/shared/ui/PlaceholderPage.tsx
Normal file
49
src/shared/ui/PlaceholderPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user