최초커밋
This commit is contained in:
parent
1556bc2845
commit
349c1bc833
@ -32,12 +32,12 @@ REALTIME_BACKEND_API_URL=http://localhost:3004
|
||||
ADMIN_API_KEY=your_admin_api_key
|
||||
|
||||
# Database
|
||||
POSTGRES_HOST=fems-postgres
|
||||
POSTGRES_HOST=61.251.18.72
|
||||
POSTGRES_PORT=5432
|
||||
# POSTGRES_DB=wacefems-database
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=wacefems_database_user
|
||||
POSTGRES_PASSWORD=wacefems-pg-password-PPw09!keep
|
||||
POSTGRES_DB=duckil
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=gplm
|
||||
|
||||
# Optional Database Config
|
||||
POSTGRES_MAX_CONNECTIONS=100
|
||||
|
@ -44,12 +44,13 @@ const initializeServer = async () => {
|
||||
// 데이터베이스 연결 대기
|
||||
await waitForDatabase();
|
||||
|
||||
// 개발 환경에서만 데이터베이스 동기화
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
await sequelize.sync({ force: true });
|
||||
logger.info("Database synchronized.");
|
||||
await require("./utils/createInitialAdmin")();
|
||||
}
|
||||
// // 개발 환경에서만 데이터베이스 동기화
|
||||
// if (process.env.NODE_ENV !== "production") {
|
||||
// await sequelize.sync({ force: true });
|
||||
// // await sequelize.sync({ alter: true });
|
||||
// logger.info("Database synchronized.");
|
||||
// await require("./utils/createInitialAdmin")();
|
||||
// }
|
||||
|
||||
// 서버 시작
|
||||
const port = config.port;
|
||||
|
59
fems-api/src/controllers/app/common/common.controller.js
Normal file
59
fems-api/src/controllers/app/common/common.controller.js
Normal file
@ -0,0 +1,59 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const CommonService = require("../../../services/common.service");
|
||||
|
||||
//12.11 공통메뉴 (메뉴바용)
|
||||
router.get("/menu", async (req, res, next) => {
|
||||
try {
|
||||
const menuList = await CommonService.getMenu();
|
||||
res.json({
|
||||
success: true,
|
||||
data: menuList
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
//12.12 관리자용 전체메뉴 조회
|
||||
router.get("/admin/menu", async (req, res, next) => {
|
||||
try {
|
||||
const menuList = await CommonService.getAdminMenu();
|
||||
res.json({
|
||||
success: true,
|
||||
data: menuList
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
//12.12 메뉴 생성/수정
|
||||
router.post("/createmenu", async (req, res, next) => {
|
||||
try {
|
||||
const menuData = req.body;
|
||||
const result = await CommonService.saveMenu(menuData);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
//12.12 메뉴 삭제
|
||||
router.delete("/menu/:id", async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const result = await CommonService.deleteMenu(id);
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
76
fems-api/src/models/MenuInfo.js
Normal file
76
fems-api/src/models/MenuInfo.js
Normal file
@ -0,0 +1,76 @@
|
||||
// models/MenuInfo.js
|
||||
|
||||
const { Model, DataTypes } = require("sequelize");
|
||||
|
||||
class MenuInfo extends Model {
|
||||
static init(sequelize) {
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
menu_type: {
|
||||
type: DataTypes.NUMERIC,
|
||||
allowNull: true,
|
||||
},
|
||||
parent_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
},
|
||||
menu_name_kor: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: true,
|
||||
},
|
||||
menu_name_eng: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: true,
|
||||
},
|
||||
seq: {
|
||||
type: DataTypes.NUMERIC,
|
||||
allowNull: true,
|
||||
},
|
||||
menu_url: {
|
||||
type: DataTypes.STRING(256),
|
||||
allowNull: true,
|
||||
},
|
||||
menu_desc: {
|
||||
type: DataTypes.STRING(1024),
|
||||
allowNull: true,
|
||||
},
|
||||
writer: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
},
|
||||
regdate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
system_name: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'MenuInfo',
|
||||
tableName: 'menu_info_2',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['parent_id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MenuInfo;
|
70
fems-api/src/models/Userinfo.js
Normal file
70
fems-api/src/models/Userinfo.js
Normal file
@ -0,0 +1,70 @@
|
||||
// src/models/Userinfo.js
|
||||
const { Model, DataTypes } = require("sequelize");
|
||||
const bcrypt = require("bcryptjs");
|
||||
|
||||
class UserInfo extends Model {
|
||||
static init(sequelize) {
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true,
|
||||
},
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM(
|
||||
"super_admin",
|
||||
"company_admin",
|
||||
"branch_admin",
|
||||
"user"
|
||||
),
|
||||
defaultValue: "user",
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'UserInfo',
|
||||
tableName: 'user_info_2',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
}
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserInfo;
|
@ -16,6 +16,7 @@ const departmentController = require("../controllers/app/department/department.c
|
||||
const healthController = require("../controllers/app/health/health.controller");
|
||||
const companiesController = require("../controllers/admin/companies/companies.controller");
|
||||
const deviceController = require("../controllers/app/device/device.controller");
|
||||
const commonController = require("../controllers/app/common/common.controller");
|
||||
|
||||
router.use("/health", healthController);
|
||||
router.use("/auth", authController);
|
||||
@ -31,5 +32,6 @@ router.use("/equipment-parts", equipmentPartsController);
|
||||
router.use("/department", departmentController);
|
||||
router.use("/companies", companiesController);
|
||||
router.use("/devices", deviceController);
|
||||
router.use("/common", commonController);
|
||||
|
||||
module.exports = router;
|
||||
|
132
fems-api/src/services/common.service.js
Normal file
132
fems-api/src/services/common.service.js
Normal file
@ -0,0 +1,132 @@
|
||||
const {
|
||||
MenuInfo,
|
||||
} = require("../models");
|
||||
const { Op } = require("sequelize");
|
||||
|
||||
// 메뉴바용 메뉴 조회 (활성화된 것만)
|
||||
const getMenu = async () => {
|
||||
try {
|
||||
const allMenus = await MenuInfo.findAll({
|
||||
order: [['seq', 'ASC']],
|
||||
raw: true
|
||||
});
|
||||
const menuTree = buildMenuTree(allMenus);
|
||||
return { success: true, data: menuTree };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 메뉴바용 트리 빌더 (활성화된 것만)
|
||||
const buildMenuTree = (menus, parentId = null) => {
|
||||
return menus
|
||||
.filter(menu => menu.parent_id === parentId && menu.isActive === true)
|
||||
.map(menu => ({
|
||||
id: menu.id,
|
||||
menu_type: menu.menu_type,
|
||||
parent_id: menu.parent_id,
|
||||
menu_name_kor: menu.menu_name_kor,
|
||||
menu_name_eng: menu.menu_name_eng,
|
||||
seq: menu.seq,
|
||||
menu_url: menu.menu_url,
|
||||
menu_desc: menu.menu_desc,
|
||||
isActive: menu.isActive,
|
||||
children: buildMenuTree(menus, menu.id)
|
||||
}));
|
||||
};
|
||||
|
||||
// 관리자용 전체 메뉴 조회
|
||||
const getAdminMenu = async () => {
|
||||
try {
|
||||
const allMenus = await MenuInfo.findAll({
|
||||
order: [['seq', 'ASC']],
|
||||
raw: true
|
||||
});
|
||||
const menuTree = buildAdminMenuTree(allMenus);
|
||||
return { success: true, data: menuTree };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 관리자용 트리 빌더 (전체 메뉴)
|
||||
const buildAdminMenuTree = (menus, parentId = null) => {
|
||||
return menus
|
||||
.filter(menu => menu.parent_id === parentId)
|
||||
.map(menu => ({
|
||||
id: menu.id,
|
||||
menu_type: menu.menu_type,
|
||||
parent_id: menu.parent_id,
|
||||
menu_name_kor: menu.menu_name_kor,
|
||||
menu_name_eng: menu.menu_name_eng,
|
||||
seq: menu.seq,
|
||||
menu_url: menu.menu_url,
|
||||
menu_desc: menu.menu_desc,
|
||||
isActive: menu.isActive,
|
||||
children: buildAdminMenuTree(menus, menu.id)
|
||||
}));
|
||||
};
|
||||
|
||||
const saveMenu = async (menuData) => {
|
||||
try {
|
||||
if (menuData.id) {
|
||||
// 수정
|
||||
const menu = await MenuInfo.update({
|
||||
menu_type: menuData.menu_type,
|
||||
parent_id: menuData.parent_id,
|
||||
menu_name_kor: menuData.menu_name_kor,
|
||||
menu_name_eng: menuData.menu_name_eng,
|
||||
seq: menuData.seq,
|
||||
menu_url: menuData.menu_url,
|
||||
isActive: menuData.isActive,
|
||||
menu_desc: menuData.menu_desc || null,
|
||||
writer: 'system',
|
||||
}, {
|
||||
where: { id: menuData.id }
|
||||
});
|
||||
} else {
|
||||
// 생성
|
||||
const menu = await MenuInfo.create({
|
||||
menu_type: menuData.menu_type,
|
||||
parent_id: menuData.parent_id,
|
||||
menu_name_kor: menuData.menu_name_kor,
|
||||
menu_name_eng: menuData.menu_name_eng,
|
||||
seq: menuData.seq,
|
||||
menu_url: menuData.menu_url,
|
||||
isActive: menuData.isActive,
|
||||
menu_desc: menuData.menu_desc || null,
|
||||
writer: 'system',
|
||||
system_name: 'web',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: menuData
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMenu = async (id) => {
|
||||
try {
|
||||
const result = await MenuInfo.destroy({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getMenu,
|
||||
getAdminMenu,
|
||||
saveMenu,
|
||||
deleteMenu
|
||||
};
|
@ -7,93 +7,93 @@ const {
|
||||
userDefinitions,
|
||||
} = require("./setupData");
|
||||
|
||||
async function createDepartments(companyId, branchId) {
|
||||
const departments = {};
|
||||
// async function createDepartments(companyId, branchId) {
|
||||
// const departments = {};
|
||||
|
||||
for (const division of departmentStructure) {
|
||||
const parentDept = await Department.create({
|
||||
name: division.name,
|
||||
companyId,
|
||||
branchId,
|
||||
isActive: true,
|
||||
});
|
||||
departments[division.name] = parentDept.id;
|
||||
// for (const division of departmentStructure) {
|
||||
// const parentDept = await Department.create({
|
||||
// name: division.name,
|
||||
// companyId,
|
||||
// branchId,
|
||||
// isActive: true,
|
||||
// });
|
||||
// departments[division.name] = parentDept.id;
|
||||
|
||||
for (const team of division.children) {
|
||||
const childDept = await Department.create({
|
||||
name: team.name,
|
||||
companyId,
|
||||
branchId,
|
||||
parentId: parentDept.id,
|
||||
isActive: true,
|
||||
});
|
||||
departments[team.name] = childDept.id;
|
||||
}
|
||||
}
|
||||
// for (const team of division.children) {
|
||||
// const childDept = await Department.create({
|
||||
// name: team.name,
|
||||
// companyId,
|
||||
// branchId,
|
||||
// parentId: parentDept.id,
|
||||
// isActive: true,
|
||||
// });
|
||||
// departments[team.name] = childDept.id;
|
||||
// }
|
||||
// }
|
||||
|
||||
logger.info("Departments created successfully");
|
||||
return departments;
|
||||
}
|
||||
// logger.info("Departments created successfully");
|
||||
// return departments;
|
||||
// }
|
||||
|
||||
async function createRoles(companyId) {
|
||||
const roles = {};
|
||||
// async function createRoles(companyId) {
|
||||
// const roles = {};
|
||||
|
||||
for (const roleDef of roleDefinitions) {
|
||||
const role = await Role.create({
|
||||
...roleDef,
|
||||
companyId,
|
||||
isActive: true,
|
||||
});
|
||||
roles[roleDef.name] = role.id;
|
||||
}
|
||||
// for (const roleDef of roleDefinitions) {
|
||||
// const role = await Role.create({
|
||||
// ...roleDef,
|
||||
// companyId,
|
||||
// isActive: true,
|
||||
// });
|
||||
// roles[roleDef.name] = role.id;
|
||||
// }
|
||||
|
||||
logger.info("Roles created successfully");
|
||||
return roles;
|
||||
}
|
||||
// logger.info("Roles created successfully");
|
||||
// return roles;
|
||||
// }
|
||||
|
||||
async function createAllUsers(companyId, branchId, departments, roles) {
|
||||
// 기본 관리자 계정 생성
|
||||
for (const adminData of userDefinitions.basicAdmins) {
|
||||
const existingUser = await User.findOne({
|
||||
where: { username: adminData.username },
|
||||
});
|
||||
// async function createAllUsers(companyId, branchId, departments, roles) {
|
||||
// // 기본 관리자 계정 생성
|
||||
// for (const adminData of userDefinitions.basicAdmins) {
|
||||
// const existingUser = await User.findOne({
|
||||
// where: { username: adminData.username },
|
||||
// });
|
||||
|
||||
if (!existingUser) {
|
||||
await User.create({
|
||||
...adminData,
|
||||
isActive: true,
|
||||
companyId,
|
||||
branchId,
|
||||
});
|
||||
logger.info(`Created ${adminData.role} user successfully`);
|
||||
}
|
||||
}
|
||||
// if (!existingUser) {
|
||||
// await User.create({
|
||||
// ...adminData,
|
||||
// isActive: true,
|
||||
// companyId,
|
||||
// branchId,
|
||||
// });
|
||||
// logger.info(`Created ${adminData.role} user successfully`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 부서별 테스트 사용자 생성
|
||||
for (const userDef of userDefinitions.departmentUsers) {
|
||||
const existingUser = await User.findOne({
|
||||
where: { username: userDef.username },
|
||||
});
|
||||
// // 부서별 테스트 사용자 생성
|
||||
// for (const userDef of userDefinitions.departmentUsers) {
|
||||
// const existingUser = await User.findOne({
|
||||
// where: { username: userDef.username },
|
||||
// });
|
||||
|
||||
if (!existingUser) {
|
||||
const user = await User.create({
|
||||
...userDef,
|
||||
isActive: true,
|
||||
companyId,
|
||||
branchId,
|
||||
departmentId: departments[userDef.department],
|
||||
});
|
||||
// if (!existingUser) {
|
||||
// const user = await User.create({
|
||||
// ...userDef,
|
||||
// isActive: true,
|
||||
// companyId,
|
||||
// branchId,
|
||||
// departmentId: departments[userDef.department],
|
||||
// });
|
||||
|
||||
for (const roleName of userDef.roles) {
|
||||
await UserRole.create({
|
||||
userId: user.id,
|
||||
roleId: roles[roleName],
|
||||
});
|
||||
}
|
||||
logger.info(`Created department user ${userDef.name} successfully`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (const roleName of userDef.roles) {
|
||||
// await UserRole.create({
|
||||
// userId: user.id,
|
||||
// roleId: roles[roleName],
|
||||
// });
|
||||
// }
|
||||
// logger.info(`Created department user ${userDef.name} successfully`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
module.exports = {
|
||||
createDepartments,
|
||||
|
@ -78,6 +78,14 @@ const userDefinitions = {
|
||||
phone: "010-0000-0000",
|
||||
role: "super_admin",
|
||||
},
|
||||
{
|
||||
username: "plm_admin",
|
||||
password: "1",
|
||||
name: "System Administrator",
|
||||
email: "chpark@gdnsil.com",
|
||||
phone: "010-6300-1473",
|
||||
role: "super_admin",
|
||||
},
|
||||
{
|
||||
username: "company_admin",
|
||||
password: "Admin123!@#",
|
||||
|
369
fems-app/src/app/(admin)/common/menu/components/UserForm.tsx
Normal file
369
fems-app/src/app/(admin)/common/menu/components/UserForm.tsx
Normal file
@ -0,0 +1,369 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
// import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { User, Role } from "@/types/user";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { AxiosError } from "axios";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
// Define the form schema with Zod
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(3, "아이디는 3자 이상이어야 합니다"),
|
||||
password: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((val) => !val || val.length >= 8, {
|
||||
message: "비밀번호는 8자 이상이어야 합니다",
|
||||
}),
|
||||
name: z.string().min(2, "이름은 2자 이상이어야 합니다"),
|
||||
email: z.string().email("올바른 이메일 형식이 아닙니다"),
|
||||
phone: z.string().regex(/^[0-9-]+$/, "올바른 전화번호 형식이 아닙니다"),
|
||||
role: z.enum(["company_admin", "branch_admin", "user"], {
|
||||
required_error: "역할을 선택해주세요",
|
||||
}),
|
||||
roleId: z.string().uuid("올바른 권한 그룹을 선택해주세요"),
|
||||
isActive: z.boolean(),
|
||||
branchId: z.string().uuid("올바른 지점을 선택해주세요"),
|
||||
departmentId: z.string().uuid("올바른 부서를 선택해주세요"),
|
||||
});
|
||||
|
||||
type FormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
interface UserFormProps {
|
||||
initialData?: Partial<User>;
|
||||
onSubmit: (data: Partial<User>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const UserForm: React.FC<UserFormProps> = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { token, user } = useAuthStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
// Initialize the form with react-hook-form and zod resolver
|
||||
const form = useForm<FormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: initialData?.username || "",
|
||||
password: initialData?.password || "",
|
||||
name: initialData?.name || "",
|
||||
email: initialData?.email || "",
|
||||
phone: initialData?.phone || "",
|
||||
role:
|
||||
(initialData?.role as "company_admin" | "branch_admin" | "user") ||
|
||||
"user",
|
||||
roleId: initialData?.Roles?.[0]?.id || "",
|
||||
isActive: initialData?.isActive || false,
|
||||
branchId: initialData?.branchId || "",
|
||||
departmentId: initialData?.departmentId || "",
|
||||
},
|
||||
});
|
||||
|
||||
// Reset form when initialData changes
|
||||
React.useEffect(() => {
|
||||
if (initialData) {
|
||||
form.reset({
|
||||
username: initialData.username || "",
|
||||
password: initialData.password || "",
|
||||
name: initialData.name || "",
|
||||
email: initialData.email || "",
|
||||
phone: initialData.phone || "",
|
||||
role:
|
||||
(initialData.role as "company_admin" | "branch_admin" | "user") ||
|
||||
"user",
|
||||
roleId: initialData.Roles?.[0]?.id || "",
|
||||
isActive: initialData.isActive || false,
|
||||
branchId: initialData.branchId || "",
|
||||
departmentId: initialData.departmentId || "",
|
||||
});
|
||||
}
|
||||
}, [initialData, form]);
|
||||
|
||||
// Fetch available roles
|
||||
const userRoles = [
|
||||
{ value: "company_admin", label: "기업 관리자" },
|
||||
{ value: "branch_admin", label: "지점 관리자" },
|
||||
{ value: "user", label: "일반 유저" },
|
||||
];
|
||||
|
||||
// Fetch branches
|
||||
const { data: branches } = useQuery({
|
||||
queryKey: ["branches"],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<{ id: string; name: string }[]>(
|
||||
"/api/v1/admin/branches"
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
// Fetch departments
|
||||
const { data: departments } = useQuery({
|
||||
queryKey: ["departments", user?.companyId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<{ id: string; name: string }[]>(
|
||||
`/api/v1/admin/departments/${user?.companyId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: !!token && !!user?.companyId,
|
||||
});
|
||||
|
||||
// Fetch roles
|
||||
const { data: roles } = useQuery<Role[]>({
|
||||
queryKey: ["roles", user?.companyId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<Role[]>(
|
||||
`/api/v1/admin/roles/${user?.companyId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: !!token && !!user?.companyId,
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: FormSchema) => {
|
||||
try {
|
||||
await onSubmit(data);
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
toast({
|
||||
title: "에러",
|
||||
description:
|
||||
(err.response?.data as { message: string })?.message ||
|
||||
"에러가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>아이디</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!initialData && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>비밀번호</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>이름</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>이메일</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>전화번호</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>역할</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || "user"}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="역할을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{userRoles.map((role) => (
|
||||
<SelectItem key={role.value} value={role.value}>
|
||||
{role.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>권한 그룹</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="권한 그룹을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{roles?.map((role) => (
|
||||
<SelectItem key={role.id} value={role.id}>
|
||||
{role.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isActive"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel>활성화 여부</FormLabel>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branchId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>지점</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="지점을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{branches?.map((branch) => (
|
||||
<SelectItem key={branch.id} value={branch.id}>
|
||||
{branch.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="departmentId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>부서</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="부서를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{departments?.map((dept) => (
|
||||
<SelectItem key={dept.id} value={dept.id}>
|
||||
{dept.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit">저장</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
687
fems-app/src/app/(admin)/common/menu/page.tsx
Normal file
687
fems-app/src/app/(admin)/common/menu/page.tsx
Normal file
@ -0,0 +1,687 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { api } from "@/lib/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Loader2,
|
||||
Plus,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Trash2,
|
||||
AlertCircle
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface DBMenuItem {
|
||||
id: string;
|
||||
menu_type: string;
|
||||
parent_id: string | null;
|
||||
menu_name_kor: string;
|
||||
menu_name_eng: string;
|
||||
seq: string;
|
||||
menu_url: string;
|
||||
menu_desc?: string;
|
||||
isActive: boolean;
|
||||
children?: DBMenuItem[];
|
||||
order?: string;
|
||||
}
|
||||
|
||||
interface DBApiResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
success: boolean;
|
||||
data: DBMenuItem[];
|
||||
};
|
||||
}
|
||||
|
||||
interface MenuFormDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
menuData?: DBMenuItem;
|
||||
onSave: (data: DBMenuItem) => void;
|
||||
menus: DBMenuItem[];
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
menuNameKor: string;
|
||||
menuNameEng: string;
|
||||
menuUrl: string;
|
||||
menuSeq: string;
|
||||
menuType: string;
|
||||
isActive: boolean;
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
const getFlatMenuList = (menus: DBMenuItem[], depth = 0): { id: string; label: string; isChild: boolean }[] => {
|
||||
return menus.flatMap((menu) => [
|
||||
{
|
||||
id: menu.id,
|
||||
label: `${' '.repeat(depth)}${menu.menu_name_kor}`,
|
||||
isChild: depth > 0, // 깊이에 따라 자식 여부 판단
|
||||
},
|
||||
...(menu.children ? getFlatMenuList(menu.children, depth + 1) : []),
|
||||
]);
|
||||
};
|
||||
const MenuFormDialog = ({ isOpen, onClose, menuData, onSave, menus }: MenuFormDialogProps) => {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const form = useForm<FormData>({
|
||||
defaultValues: {
|
||||
menuNameKor: "",
|
||||
menuNameEng: "",
|
||||
menuUrl: "",
|
||||
menuSeq: "",
|
||||
menuType: "0",
|
||||
isActive: true, // 기본 활성화 상태를 true로 설정
|
||||
parentId: "root",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (menuData) {
|
||||
form.reset({
|
||||
menuNameKor: menuData.menu_name_kor || "",
|
||||
menuNameEng: menuData.menu_name_eng || "",
|
||||
menuUrl: menuData.menu_url || "",
|
||||
menuSeq: menuData.seq || "",
|
||||
menuType: menuData.menu_type || "0",
|
||||
isActive: menuData.isActive || false,
|
||||
parentId: menuData.parent_id || "root",
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
menuNameKor: "",
|
||||
menuNameEng: "",
|
||||
menuUrl: "",
|
||||
menuSeq: "",
|
||||
menuType: "0",
|
||||
isActive: true, // 새로 추가할 때 기본 활성화
|
||||
parentId: "root",
|
||||
});
|
||||
}
|
||||
}, [menuData, form]);
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
onSave({
|
||||
id: menuData?.id || "",
|
||||
menu_type: data.menuType,
|
||||
parent_id: data.parentId === "root" ? null : data.parentId,
|
||||
menu_name_kor: data.menuNameKor,
|
||||
menu_name_eng: data.menuNameEng,
|
||||
seq: data.menuSeq,
|
||||
menu_url: data.menuUrl,
|
||||
isActive: data.isActive,
|
||||
children: menuData?.children || [],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{menuData ? "메뉴 수정" : "메뉴 추가"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{menuData ? "기존 메뉴의 정보를 수정합니다." : "새로운 메뉴를 시스템에 추가합니다."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parentId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>상위메뉴 선택</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="메뉴 선택" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="메뉴 검색..."
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="mb-2"
|
||||
/>
|
||||
</div>
|
||||
<SelectItem value="root">기본메뉴</SelectItem>
|
||||
<Separator className="my-2" />
|
||||
{getFlatMenuList(menus)
|
||||
.filter((menu) =>
|
||||
menu.label.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
.map((menu, index, array) => (
|
||||
<React.Fragment key={menu.id}>
|
||||
<SelectItem
|
||||
value={menu.id}
|
||||
className={cn(
|
||||
menu.isChild ? "pl-6" : "font-bold", // 자식 메뉴는 들여쓰기 적용
|
||||
"relative"
|
||||
)}
|
||||
>
|
||||
{menu.isChild ? `└ ${menu.label.trim()}` : menu.label}
|
||||
</SelectItem>
|
||||
{array[index + 1]?.isChild === false && (
|
||||
<Separator className="my-2" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuNameKor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>메뉴 이름 (한글)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="메뉴 이름을 입력하세요" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuNameEng"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>메뉴 이름 (영문)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Menu Name" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="/example" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>순서</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" placeholder="순서를 입력하세요" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>타입</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="메뉴 타입 선택" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">관리자 메뉴</SelectItem>
|
||||
<SelectItem value="1">사용자 메뉴</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isActive"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FormLabel>활성화</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{menuData ? "수정" : "추가"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
interface MenusTableProps {
|
||||
menus: DBMenuItem[];
|
||||
toggleActive: (id: string, value: boolean) => void;
|
||||
openSections: { [key: string]: boolean };
|
||||
toggleSection: (id: string) => void;
|
||||
onMenuClick: (menu: DBMenuItem) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
const MenusTable = ({
|
||||
menus,
|
||||
toggleActive,
|
||||
openSections,
|
||||
toggleSection,
|
||||
onMenuClick,
|
||||
onDelete
|
||||
}: MenusTableProps) => {
|
||||
const renderMenuRow = (menu: DBMenuItem, level: number = 0) => (
|
||||
<React.Fragment key={menu.id}>
|
||||
<TableRow
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleSection(menu.id);
|
||||
}}
|
||||
>
|
||||
<TableCell
|
||||
style={{ paddingLeft: `${level * 20}px` }}
|
||||
className="font-medium relative group"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{menu.children && menu.children.length > 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // 부모 클릭 이벤트 방지
|
||||
toggleSection(menu.id);
|
||||
}}
|
||||
>
|
||||
{openSections[menu.id] ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="w-8" />
|
||||
)}
|
||||
<span
|
||||
className="font-medium cursor-pointer hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // 부모 클릭 이벤트 방지
|
||||
onMenuClick(menu);
|
||||
}}
|
||||
>
|
||||
{menu.menu_name_kor}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{menu.menu_name_eng}</TableCell>
|
||||
<TableCell className="font-mono">{menu.menu_url}</TableCell>
|
||||
<TableCell className="text-center">{menu.seq}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
menu.menu_type === "0"
|
||||
? "bg-blue-50 text-blue-700 ring-blue-700/10"
|
||||
: "bg-green-50 text-green-700 ring-green-600/20"
|
||||
)}
|
||||
>
|
||||
{menu.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Switch
|
||||
checked={menu.isActive}
|
||||
onCheckedChange={(value) => toggleActive(menu.id, value)}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 p-0 hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // 부모 클릭 이벤트 방지
|
||||
onDelete(menu.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{menu.children &&
|
||||
menu.children.length > 0 &&
|
||||
openSections[menu.id] &&
|
||||
menu.children.map((child) => renderMenuRow(child, level + 1))}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>메뉴 목록</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[300px]">메뉴 이름</TableHead>
|
||||
<TableHead>영문 이름</TableHead>
|
||||
<TableHead>URL</TableHead>
|
||||
<TableHead className="w-[100px] text-center">순서</TableHead>
|
||||
<TableHead className="w-[120px] text-center">타입</TableHead>
|
||||
<TableHead className="w-[140px] text-center">활성화/삭제</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{menus.map((menu) => renderMenuRow(menu))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{menus.length === 0 && (
|
||||
<div className="flex items-center justify-center h-32 text-muted-foreground">
|
||||
등록된 메뉴가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const addMenuOrder = (menus: DBMenuItem[], parentOrder: string = ''): DBMenuItem[] => {
|
||||
return menus.map((menu, index) => {
|
||||
const order = parentOrder ? `${parentOrder}-${index + 1}` : `${index + 1}`;
|
||||
return {
|
||||
...menu,
|
||||
order,
|
||||
children: menu.children ? addMenuOrder(menu.children, order) : [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const MenusPage = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedMenu, setSelectedMenu] = useState<DBMenuItem | null>(null);
|
||||
const [openSections, setOpenSections] = useState<{ [key: string]: boolean }>({});
|
||||
const [deleteAlertOpen, setDeleteAlertOpen] = useState(false);
|
||||
const [menuToDelete, setMenuToDelete] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
const toggleSection = (id: string) => {
|
||||
setOpenSections((prev) => ({
|
||||
...prev,
|
||||
[id]: !prev[id],
|
||||
}));
|
||||
};
|
||||
|
||||
const { data: dbMenuData, isLoading } = useQuery<DBApiResponse>({
|
||||
queryKey: ["admin-menus"],
|
||||
queryFn: async () => {
|
||||
const response = await api.get("/api/v1/app/common/admin/menu");
|
||||
return response.data;
|
||||
},
|
||||
// 추가 옵션
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
|
||||
const saveMenuMutation = useMutation({
|
||||
mutationFn: async (menuData: Omit<DBMenuItem, 'children' | 'order'>) => {
|
||||
const response = await api.post("/api/v1/app/common/createmenu", menuData);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 더 구체적인 refetch 처리
|
||||
queryClient.invalidateQueries({ queryKey: ["admin-menus"] });
|
||||
queryClient.refetchQueries({ queryKey: ["admin-menus"] });
|
||||
toast({
|
||||
title: "메뉴 저장 성공",
|
||||
description: "메뉴가 저장되었습니다.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "메뉴 저장 실패",
|
||||
description: "메뉴 저장 중 오류가 발생했습니다. 다시 시도해주세요.",
|
||||
variant: "destructive",
|
||||
});
|
||||
console.error("Menu save error:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const toggleActiveMutation = useMutation({
|
||||
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
|
||||
const response = await api.post("/api/v1/app/common/createmenu", {
|
||||
id,
|
||||
isActive,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 더 구체적인 refetch 처리
|
||||
queryClient.invalidateQueries({ queryKey: ["admin-menus"] });
|
||||
queryClient.refetchQueries({ queryKey: ["admin-menus"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "상태 변경 실패",
|
||||
description: "메뉴 상태 변경 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
console.error("Toggle active error:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMenuMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await api.delete(`/api/v1/app/common/menu/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// 더 구체적인 refetch 처리
|
||||
queryClient.invalidateQueries({ queryKey: ["admin-menus"] });
|
||||
queryClient.refetchQueries({ queryKey: ["admin-menus"] });
|
||||
toast({
|
||||
title: "메뉴 삭제 성공",
|
||||
description: "메뉴가 삭제되었습니다.",
|
||||
});
|
||||
setMenuToDelete(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "메뉴 삭제 실패",
|
||||
description: "메뉴 삭제 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
console.error("Delete menu error:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleMenuSave = (menu: DBMenuItem) => {
|
||||
const { children, order, ...saveData } = menu;
|
||||
saveMenuMutation.mutate(saveData);
|
||||
};
|
||||
|
||||
const handleMenuClick = (menu: DBMenuItem) => {
|
||||
setSelectedMenu(menu);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleAddClick = () => {
|
||||
setSelectedMenu(null);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: string) => {
|
||||
setMenuToDelete(id);
|
||||
setDeleteAlertOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (menuToDelete) {
|
||||
deleteMenuMutation.mutate(menuToDelete);
|
||||
}
|
||||
setDeleteAlertOpen(false);
|
||||
};
|
||||
|
||||
const toggleActive = (id: string, value: boolean) => {
|
||||
toggleActiveMutation.mutate({ id, isActive: value });
|
||||
};
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
const orderedMenus = addMenuOrder(dbMenuData?.data?.data || []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">메뉴 관리</h1>
|
||||
<p className="text-muted-foreground">
|
||||
시스템의 메뉴 구조를 관리하고 설정합니다.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleAddClick}
|
||||
disabled={saveMenuMutation.isPending}
|
||||
>
|
||||
{saveMenuMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
저장 중
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
메뉴 추가
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<MenusTable
|
||||
menus={orderedMenus}
|
||||
toggleActive={toggleActive}
|
||||
openSections={openSections}
|
||||
toggleSection={toggleSection}
|
||||
onMenuClick={handleMenuClick}
|
||||
onDelete={handleDeleteClick}
|
||||
/>
|
||||
|
||||
<MenuFormDialog
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
menuData={selectedMenu || undefined}
|
||||
onSave={handleMenuSave}
|
||||
menus={orderedMenus}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteAlertOpen} onOpenChange={setDeleteAlertOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>메뉴 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말로 이 메뉴를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenusPage;
|
@ -29,7 +29,7 @@ import { Loader2 } from "lucide-react"; // 로딩 아이콘
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(4, "아이디는 4자 이상이어야 합니다"),
|
||||
password: z.string().min(6, "비밀번호는 6자 이상이어야 합니다"),
|
||||
password: z.string().min(1, "비밀번호는 6자 이상이어야 합니다"),
|
||||
});
|
||||
|
||||
export default function LoginPage() {
|
||||
|
@ -1,4 +1,3 @@
|
||||
// src/components/layout/SideNav.tsx
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
@ -6,218 +5,67 @@ import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { api } from "@/lib/api";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Gauge,
|
||||
TrendingUp,
|
||||
DollarSign,
|
||||
Zap,
|
||||
Droplet,
|
||||
Wind,
|
||||
Flame,
|
||||
Building2,
|
||||
Box,
|
||||
Activity,
|
||||
Wrench,
|
||||
LineChart,
|
||||
BarChart,
|
||||
FileText,
|
||||
Bell,
|
||||
History,
|
||||
Settings as SettingsIcon,
|
||||
Target,
|
||||
Brain,
|
||||
Sliders,
|
||||
DollarSign,
|
||||
Users,
|
||||
HelpCircle,
|
||||
MessageSquare,
|
||||
Newspaper,
|
||||
Sliders,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Puzzle,
|
||||
Building2,
|
||||
Calendar,
|
||||
Gauge
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
// 관리자 메뉴에 필요한 권한 정의
|
||||
const ADMIN_PERMISSIONS = {
|
||||
COMPANY: "company:manage",
|
||||
BRANCH: "branches:manage",
|
||||
BILLING: "billing:manage",
|
||||
USERS: "users:manage",
|
||||
DEPARTMENTS: "departments:manage",
|
||||
ACCOUNTS: "accounts:manage",
|
||||
SYSTEM: "system:manage",
|
||||
};
|
||||
interface DBMenuItem {
|
||||
id: string;
|
||||
menu_type: string;
|
||||
parent_id: string | null;
|
||||
menu_name_kor: string;
|
||||
menu_name_eng: string;
|
||||
seq: string;
|
||||
menu_url: string;
|
||||
menu_desc?: string;
|
||||
isActive: boolean;
|
||||
children?: DBMenuItem[];
|
||||
}
|
||||
|
||||
const getMenuItems = (
|
||||
hasPermission: (permission: string) => boolean,
|
||||
role: string
|
||||
) => {
|
||||
// 기본 메뉴 아이템 (관리자 메뉴 제외)
|
||||
const baseMenuItems = [
|
||||
{
|
||||
title: "대시보드",
|
||||
items: [
|
||||
{
|
||||
title: "전체 현황",
|
||||
href: "/dashboard/overview",
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{ title: "KPI 지표", href: "/dashboard/kpi", icon: Gauge },
|
||||
{ title: "비용 현황", href: "/dashboard/costs", icon: DollarSign },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "에너지 모니터링",
|
||||
items: [
|
||||
{ title: "전력", href: "/electricity", icon: Zap },
|
||||
{ title: "가스", href: "/gas", icon: Flame },
|
||||
{ title: "용수", href: "/water", icon: Droplet },
|
||||
{ title: "스팀", href: "/steam", icon: Wind },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "설비 관리",
|
||||
items: [
|
||||
{ title: "설비 목록", href: "/inventory", icon: Box },
|
||||
{ title: "상태 모니터링", href: "/monitoring", icon: Activity },
|
||||
{ title: "정비 관리", href: "/maintenance", icon: Wrench },
|
||||
{ title: "부품 관리", href: "/parts", icon: Puzzle },
|
||||
{ title: "작업자 관리", href: "/personnel", icon: Users },
|
||||
{ title: "디바이스", href: "/devices", icon: Sliders },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "분석/리포트",
|
||||
items: [
|
||||
{ title: "에너지 분석", href: "/energy", icon: LineChart },
|
||||
{ title: "원단위 분석", href: "/efficiency", icon: BarChart },
|
||||
{ title: "보고서", href: "/reports", icon: FileText },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "알람/이벤트",
|
||||
items: [
|
||||
{ title: "실시간 알람", href: "/realtime", icon: Bell },
|
||||
{ title: "이력 관리", href: "/history", icon: History },
|
||||
{ title: "알람 설정", href: "/settings", icon: SettingsIcon },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "에너지 계획",
|
||||
items: [
|
||||
{ title: "절감 목표", href: "/targets", icon: Target },
|
||||
{ title: "수요 예측", href: "/forecast", icon: TrendingUp },
|
||||
{ title: "최적화", href: "/optimization", icon: Brain },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "지원/커뮤니티",
|
||||
items: [
|
||||
{ title: "도움말", href: "/faq", icon: HelpCircle },
|
||||
{ title: "게시판", href: "/community/forum", icon: MessageSquare },
|
||||
{ title: "뉴스", href: "/community/news", icon: Newspaper },
|
||||
],
|
||||
},
|
||||
];
|
||||
interface DBApiResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
success: boolean;
|
||||
data: DBMenuItem[];
|
||||
}
|
||||
}
|
||||
|
||||
// 관리자 권한 체크
|
||||
const isAdmin = ["super_admin", "company_admin"].includes(role);
|
||||
// 관리자가 아닌 경우에만 권한 체크
|
||||
const hasAnyAdminPermission =
|
||||
!isAdmin &&
|
||||
Object.values(ADMIN_PERMISSIONS).some((permission) =>
|
||||
hasPermission(permission)
|
||||
);
|
||||
|
||||
// 관리자 메뉴 아이템
|
||||
const adminMenuItem = {
|
||||
title: "관리",
|
||||
items: [
|
||||
{
|
||||
title: "회사 설정",
|
||||
href: "/company/profile",
|
||||
icon: Building2,
|
||||
permission: ADMIN_PERMISSIONS.COMPANY,
|
||||
},
|
||||
{
|
||||
title: "지점/공장 관리",
|
||||
href: "/company/branches",
|
||||
icon: Building2,
|
||||
permission: ADMIN_PERMISSIONS.BRANCH,
|
||||
},
|
||||
{
|
||||
title: "결재 관리",
|
||||
href: "/company/billing",
|
||||
icon: DollarSign,
|
||||
permission: ADMIN_PERMISSIONS.BILLING,
|
||||
},
|
||||
{
|
||||
title: "사용자 권한 관리",
|
||||
href: "/users/roles",
|
||||
icon: Users,
|
||||
permission: ADMIN_PERMISSIONS.USERS,
|
||||
},
|
||||
{
|
||||
title: "부서 관리",
|
||||
href: "/users/departments",
|
||||
icon: Users,
|
||||
permission: ADMIN_PERMISSIONS.DEPARTMENTS,
|
||||
},
|
||||
{
|
||||
title: "계정 관리",
|
||||
href: "/users/accounts",
|
||||
icon: Users,
|
||||
permission: ADMIN_PERMISSIONS.ACCOUNTS,
|
||||
},
|
||||
{
|
||||
title: "시스템 설정",
|
||||
href: "/system",
|
||||
icon: Sliders,
|
||||
permission: ADMIN_PERMISSIONS.SYSTEM,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 관리자면 모든 메뉴 표시, 아닌 경우 권한 체크
|
||||
const filteredAdminItems = {
|
||||
...adminMenuItem,
|
||||
items: isAdmin
|
||||
? adminMenuItem.items // 관리자는 모든 메뉴 표시
|
||||
: adminMenuItem.items.filter((item) => hasPermission(item.permission)), // 권한 체크
|
||||
};
|
||||
|
||||
// 관리자이거나 권한이 있는 메뉴가 있는 경우에만 관리자 메뉴 추가
|
||||
return isAdmin ||
|
||||
(hasAnyAdminPermission && filteredAdminItems.items.length > 0)
|
||||
? [...baseMenuItems, filteredAdminItems]
|
||||
: baseMenuItems;
|
||||
};
|
||||
interface MenuItem {
|
||||
id: string; // id 추가
|
||||
title: string;
|
||||
items: {
|
||||
id: string; // id 추가
|
||||
title: string;
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface MenuItemProps {
|
||||
item: {
|
||||
title: string;
|
||||
items: {
|
||||
title: string;
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
permission?: string;
|
||||
}[];
|
||||
};
|
||||
item: MenuItem;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const MenuItemComponent: React.FC<MenuItemProps> = ({
|
||||
item,
|
||||
isOpen,
|
||||
onToggle,
|
||||
pathname,
|
||||
}) => {
|
||||
const firstIcon = item.items[0]?.icon;
|
||||
const IconComponent = firstIcon || Box;
|
||||
const IconComponent = Box;
|
||||
|
||||
return (
|
||||
<div className="mb-1">
|
||||
@ -232,18 +80,14 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
>
|
||||
<IconComponent className="h-5 w-5 mr-2" />
|
||||
<span className="flex-1 text-left">{item.title}</span>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
{isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="ml-9 mt-1 space-y-1">
|
||||
{item.items.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.href}
|
||||
key={subItem.id}
|
||||
href={subItem.href}
|
||||
className={cn(
|
||||
"flex items-center px-3 py-2 text-sm rounded-md",
|
||||
@ -253,9 +97,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
: "text-gray-600 hover:bg-gray-50"
|
||||
)}
|
||||
>
|
||||
{subItem.icon && (
|
||||
<subItem.icon className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
)}
|
||||
<Box className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
{subItem.title}
|
||||
</Link>
|
||||
))}
|
||||
@ -264,18 +106,72 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
function processMenuItems(responseData: DBApiResponse, role: string): MenuItem[] {
|
||||
if (!responseData?.data?.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const menuData = responseData.data.data;
|
||||
|
||||
// 트리 구조로 된 메뉴 데이터로부터 MenuItem[] 생성
|
||||
const buildMenuItem = (menu: DBMenuItem): MenuItem => {
|
||||
const children = menu.children || [];
|
||||
|
||||
// 자식 메뉴들을 seq 기준으로 정렬
|
||||
const sortedChildren = [...children].sort((a, b) => {
|
||||
const seqA = parseInt(a.seq) || 0;
|
||||
const seqB = parseInt(b.seq) || 0;
|
||||
return seqA - seqB;
|
||||
});
|
||||
|
||||
// 메뉴 아이템 생성
|
||||
const result = {
|
||||
id: menu.id,
|
||||
title: menu.menu_name_kor,
|
||||
items: sortedChildren.map(child => ({
|
||||
id: child.id,
|
||||
title: child.menu_name_kor,
|
||||
href: child.menu_url || '#',
|
||||
icon: Box
|
||||
}))
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
// 최상위 메뉴들을 찾고 권한 체크 후 seq로 정렬
|
||||
return menuData
|
||||
.filter(menu =>
|
||||
(menu.menu_type !== "0" || ["super_admin", "company_admin"].includes(role)) &&
|
||||
!menu.parent_id
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const seqA = parseInt(a.seq) || 0;
|
||||
const seqB = parseInt(b.seq) || 0;
|
||||
return seqA - seqB;
|
||||
})
|
||||
.map(buildMenuItem);
|
||||
}
|
||||
export function SideNav() {
|
||||
const pathname = usePathname();
|
||||
const { user } = useAuth();
|
||||
const { hasPermission } = usePermissions();
|
||||
const { token } = useAuthStore();
|
||||
|
||||
const menuItems = getMenuItems(hasPermission, user?.role || "");
|
||||
const { data: dbMenuData } = useQuery<DBApiResponse>({
|
||||
queryKey: ["menus"],
|
||||
queryFn: async () => {
|
||||
const response = await api.get("/api/v1/app/common/menu");
|
||||
return response.data;
|
||||
},
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
const menuItems = dbMenuData ? processMenuItems(dbMenuData, user?.role || "") : [];
|
||||
|
||||
const [openSections, setOpenSections] = useState<{ [key: string]: boolean }>(
|
||||
() => {
|
||||
return menuItems.reduce((acc: { [key: string]: boolean }, item) => {
|
||||
if (item.items.some((subItem) => subItem.href === pathname)) {
|
||||
if (item.items?.some((subItem) => subItem.href === pathname)) {
|
||||
acc[item.title] = true;
|
||||
}
|
||||
return acc;
|
||||
@ -290,7 +186,6 @@ export function SideNav() {
|
||||
}));
|
||||
};
|
||||
|
||||
// 날짜 포맷팅 함수
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("ko-KR", {
|
||||
year: "numeric",
|
||||
@ -299,7 +194,6 @@ export function SideNav() {
|
||||
});
|
||||
};
|
||||
|
||||
// 사업자번호 포맷팅 함수
|
||||
const formatBusinessNumber = (number: string) => {
|
||||
if (!number) return "";
|
||||
const cleaned = number.replace(/[^0-9]/g, "");
|
||||
@ -308,26 +202,29 @@ export function SideNav() {
|
||||
|
||||
return (
|
||||
<nav className="w-64 bg-white border-r border-gray-200 h-screen flex flex-col">
|
||||
<div className="sticky top-0 z-10 bg-white p-4 border-b border-gray-200">
|
||||
<div
|
||||
className="sticky top-0 z-10 bg-white p-4 border-b border-gray-200 cursor-pointer"
|
||||
onClick={() => (window.location.href = "/dashboard/overview")}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Gauge className="h-8 w-6 text-blue-600" />
|
||||
<h1 className="text-xl font-semibold text-gray-900">FEMS</h1>
|
||||
<h1 className="text-xl font-semibold text-gray-900">PLM</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
{menuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.title}
|
||||
item={item}
|
||||
isOpen={openSections[item.title]}
|
||||
onToggle={() => toggleSection(item.title)}
|
||||
pathname={pathname}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItemComponent
|
||||
key={item.id} // title 대신 id 사용
|
||||
item={item}
|
||||
isOpen={openSections[item.title]}
|
||||
onToggle={() => toggleSection(item.title)}
|
||||
pathname={pathname}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 회사 정보 카드 - 항상 하단에 고정 */}
|
||||
<div className="mt-auto border-t border-gray-200 p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-3">
|
||||
@ -359,4 +256,4 @@ export function SideNav() {
|
||||
);
|
||||
}
|
||||
|
||||
export default SideNav;
|
||||
export default SideNav;
|
22
fems-app/src/types/menu.ts
Normal file
22
fems-app/src/types/menu.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// src/types/menu.ts
|
||||
|
||||
export interface Menu {
|
||||
id: string;
|
||||
menu_type: number | null;
|
||||
parent_id: string | null;
|
||||
menu_name_kor: string | null;
|
||||
menu_name_eng: string | null;
|
||||
seq: number | null;
|
||||
menu_url: string | null;
|
||||
menu_desc: string | null;
|
||||
writer: string | null;
|
||||
regdate: Date | null;
|
||||
system_name: string | null;
|
||||
isActive: boolean;
|
||||
children?: Menu[];
|
||||
}
|
||||
|
||||
export interface MenuResponse {
|
||||
success: boolean;
|
||||
data: Menu[];
|
||||
}
|
Binary file not shown.
@ -1,2 +1,3 @@
|
||||
fems:$7$101$DReOc0qh4fHrvOsM$Jfh0DIt0Llwq30My4YnsIRnlW7BUb/kDH2zqKnYiUBuOpAIntmezbF1MYTAYJ1UOo032jQyV9IUGh+oMiHMjLg==
|
||||
nodered_user:$7$101$539VdW5cs9LGnCl9$dCGBu0MZgktD2UOcbJmHZBsMoy8P9lk1zjLDl5yyzj2AV4uPh+8/1GVd/E+z+Sq4ssNIpzVT3P47yM4WJ8gpSA==
|
||||
:$7$101$b0DdJIM3X8nUFEgJ$k4Yk6IfR+iSSNexrFy0tuM5Qzu2NADWvCSF/+Zfh7UIZ0RmDUSfExHBXLLgetP5DTbiBxpzgWBHwFDaww1sgQg==
|
||||
|
@ -22216,3 +22216,404 @@ To fix this, use `chmod 0700 /mosquitto/config/passwd`.
|
||||
1732678793: New connection from ::1:59082 on port 1883.
|
||||
1732678793: New client connected from ::1:59082 as auto-072FE238-70F8-097A-36CC-1061B66C3894 (p2, c1, k60, u'fems').
|
||||
1732678793: Client auto-072FE238-70F8-097A-36CC-1061B66C3894 closed its connection.
|
||||
1733746784: mosquitto version 2.0.20 starting
|
||||
1733746784: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733746784: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$dJtDybBBk1KdSS2Y$U4NUkUrIy0B09B4nHuIyScRrd2qp7/jmB7jDhg4NJfzWKdwjPkYvdB5xQqT2atiaPM2iGVSV7LCVK+EkCitw6A==
|
||||
1733746784: Opening ipv4 listen socket on port 1883.
|
||||
1733746784: Opening ipv6 listen socket on port 1883.
|
||||
1733746784: mosquitto version 2.0.20 running
|
||||
1733746814: New connection from ::1:45084 on port 1883.
|
||||
1733746814: Client auto-572F828E-057C-F6DB-8D83-1639AB8760D7 disconnected, not authorised.
|
||||
1733746844: New connection from ::1:55774 on port 1883.
|
||||
1733746844: Client auto-20198FF6-1947-0347-9E80-8860A0920C89 disconnected, not authorised.
|
||||
1733746874: New connection from ::1:39644 on port 1883.
|
||||
1733746874: Client auto-F8760517-37D8-5D61-9A39-86BA334A9361 disconnected, not authorised.
|
||||
1733746894: mosquitto version 2.0.20 terminating
|
||||
1733746894: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733746922: mosquitto version 2.0.20 starting
|
||||
1733746922: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733746922: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$YypvzbEjQuqANPVO$h4KY9PI6SZkO1WU06d5DPpmNjuIzpDBD3vxnUUOF7lN2eUw0dxNP2+B43aDpfd2VHHuhr+YJBpkzsJreBjRXdQ==
|
||||
1733746922: Opening ipv4 listen socket on port 1883.
|
||||
1733746922: Opening ipv6 listen socket on port 1883.
|
||||
1733746922: mosquitto version 2.0.20 running
|
||||
1733746946: mosquitto version 2.0.20 terminating
|
||||
1733746946: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733746984: mosquitto version 2.0.20 starting
|
||||
1733746984: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733746984: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$rgm/wKBQ74/yE6P6$uhg5u2jsWIxc/pTQyKQRw1OoYmsqhqjw6keiOJWyg4ndYnqF+YkWWXcdma3OJ0IMa/W1saXa9TpAu/jAmoHs4A==
|
||||
1733746984: Opening ipv4 listen socket on port 1883.
|
||||
1733746984: Opening ipv6 listen socket on port 1883.
|
||||
1733746984: mosquitto version 2.0.20 running
|
||||
1733747014: New connection from ::1:45696 on port 1883.
|
||||
1733747014: Client auto-7CD31C6E-35FA-704F-8208-E321852674F0 disconnected, not authorised.
|
||||
1733747044: New connection from ::1:44206 on port 1883.
|
||||
1733747044: Client auto-4E284AEF-4DBC-19A6-66B3-0320D73767F5 disconnected, not authorised.
|
||||
1733747074: mosquitto version 2.0.20 terminating
|
||||
1733747074: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733793585: mosquitto version 2.0.20 starting
|
||||
1733793585: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733793585: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$MPfpd717JxB//Srm$Ju3HtcyJeHfFyDb5hCS9yRSVwUIjrSV7QzHaqjrl+Lam0PxMxb4PhoJ1fZdY0FS9is8Dvnsc44ITzU+VrO/j7A==
|
||||
1733793585: Opening ipv4 listen socket on port 1883.
|
||||
1733793585: Opening ipv6 listen socket on port 1883.
|
||||
1733793585: mosquitto version 2.0.20 running
|
||||
1733793614: New connection from ::1:36074 on port 1883.
|
||||
1733793614: Client auto-07C314EB-E348-3906-1BAE-8F3DC270F4A1 disconnected, not authorised.
|
||||
1733793644: New connection from ::1:34196 on port 1883.
|
||||
1733793644: Client auto-7C34FE26-77BA-198B-238F-ADB6E0A14CF6 disconnected, not authorised.
|
||||
1733793675: New connection from ::1:52750 on port 1883.
|
||||
1733793675: Client auto-20620AB8-CBF5-00A5-7EC2-99CB1939EBE6 disconnected, not authorised.
|
||||
1733793689: mosquitto version 2.0.20 terminating
|
||||
1733793689: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733796889: mosquitto version 2.0.20 starting
|
||||
1733796889: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733796889: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$Txsdrgf7PdU+ZX3l$f87d0oBkPFrqzMA7lWdU4Njmgl8zq9/GOHJEulPP+tv6/uda3KxQ0oBBphDUzg/eW92KsRkTIGWVI84RnPGKdw==
|
||||
1733796889: Opening ipv4 listen socket on port 1883.
|
||||
1733796889: Opening ipv6 listen socket on port 1883.
|
||||
1733796889: mosquitto version 2.0.20 running
|
||||
1733796890: New connection from 172.18.0.6:55320 on port 1883.
|
||||
1733796890: New client connected from 172.18.0.6:55320 as fems_realtime_39 (p2, c1, k60, u'fems').
|
||||
1733796919: New connection from ::1:57086 on port 1883.
|
||||
1733796919: Client auto-7E696C96-1328-B218-536C-FDD4F898A1CD disconnected, not authorised.
|
||||
1733796949: New connection from ::1:54028 on port 1883.
|
||||
1733796949: Client auto-3120B0FF-BBF1-FC9E-4288-663F23AE1AC1 disconnected, not authorised.
|
||||
1733796979: New connection from ::1:57326 on port 1883.
|
||||
1733796979: Client auto-E9476C3A-28EF-765B-4820-61B33CCA4F58 disconnected, not authorised.
|
||||
1733796983: Client fems_realtime_39 closed its connection.
|
||||
1733796983: mosquitto version 2.0.20 terminating
|
||||
1733796983: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733796991: mosquitto version 2.0.20 starting
|
||||
1733796991: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733796991: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$fKoDkh+cxoLe+dUf$IjdWGFe2TrhX32wRs8UYWIasSL2tHdhfT+6HHOXf5o8qj1LjTUHtm/A1Ha6xeVhVJM+N5+XmmEtrx7cKvxDacQ==
|
||||
1733796991: Opening ipv4 listen socket on port 1883.
|
||||
1733796991: Opening ipv6 listen socket on port 1883.
|
||||
1733796991: mosquitto version 2.0.20 running
|
||||
1733796992: New connection from 172.18.0.5:34224 on port 1883.
|
||||
1733796992: New client connected from 172.18.0.5:34224 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733797013: Client fems_realtime_40 closed its connection.
|
||||
1733797013: mosquitto version 2.0.20 terminating
|
||||
1733797013: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733797267: mosquitto version 2.0.20 starting
|
||||
1733797267: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733797267: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$BxZoYyWZTkUNI3b9$OKAv/Orq5gOd95q9c5kDGsQhfVB+Jcsu1ZkSqoSiPu7u55Lapyv/9ePM64zyyXzAGe+BnmovH2Et/SPJ2Ox3IQ==
|
||||
1733797267: Opening ipv4 listen socket on port 1883.
|
||||
1733797267: Opening ipv6 listen socket on port 1883.
|
||||
1733797267: mosquitto version 2.0.20 running
|
||||
1733797267: New connection from 172.18.0.7:44686 on port 1883.
|
||||
1733797267: New client connected from 172.18.0.7:44686 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733797297: New connection from ::1:50940 on port 1883.
|
||||
1733797297: Client auto-7D1AE1AA-2081-B334-FAE6-16672697E89E disconnected, not authorised.
|
||||
1733797324: Client fems_realtime_40 closed its connection.
|
||||
1733797324: mosquitto version 2.0.20 terminating
|
||||
1733797324: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733802165: mosquitto version 2.0.20 starting
|
||||
1733802165: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733802165: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$L/FjtH2VDBDEBD7w$w93DirETcZ/e+GPXXCz7TZaPvHqw9RpglNiGy6CksVYAMORUB3xq1F0aYIPmqUV+jIcDFto5YVEDtrVxFxn3NA==
|
||||
1733802165: Opening ipv4 listen socket on port 1883.
|
||||
1733802165: Opening ipv6 listen socket on port 1883.
|
||||
1733802165: mosquitto version 2.0.20 running
|
||||
1733802166: New connection from 172.18.0.8:41446 on port 1883.
|
||||
1733802166: New client connected from 172.18.0.8:41446 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733802195: New connection from ::1:35508 on port 1883.
|
||||
1733802195: Client auto-30F5AC80-B9F7-11BB-5E40-4B1213C789FB disconnected, not authorised.
|
||||
1733802225: New connection from ::1:41050 on port 1883.
|
||||
1733802225: Client auto-F2282CB1-8588-4D09-0ADE-575425FB701D disconnected, not authorised.
|
||||
1733802255: New connection from ::1:49596 on port 1883.
|
||||
1733802255: Client auto-C81B51F1-993D-E8D3-2000-E99EDC3EE77A disconnected, not authorised.
|
||||
1733802285: New connection from ::1:37162 on port 1883.
|
||||
1733802285: Client auto-A3BC53A0-DD50-9CA4-5C78-B182045216C6 disconnected, not authorised.
|
||||
1733802315: New connection from ::1:60298 on port 1883.
|
||||
1733802315: Client auto-EA6C8DFC-1509-A20C-7F6C-1B8A8FDC053F disconnected, not authorised.
|
||||
1733802345: New connection from ::1:38322 on port 1883.
|
||||
1733802345: Client auto-5BA8C8DE-DB1D-4B00-672E-97615AD78969 disconnected, not authorised.
|
||||
1733802375: New connection from ::1:42578 on port 1883.
|
||||
1733802375: Client auto-23ADC2D7-4677-498C-3B7A-ADB042138F7D disconnected, not authorised.
|
||||
1733802406: New connection from ::1:44628 on port 1883.
|
||||
1733802406: Client auto-2EF145CE-D19D-6E13-E3D3-4F1BC5FD0009 disconnected, not authorised.
|
||||
1733802436: New connection from ::1:58532 on port 1883.
|
||||
1733802436: Client auto-A1E94E1A-0DF2-8FB2-BDDF-90FDE2243C47 disconnected, not authorised.
|
||||
1733802466: New connection from ::1:32902 on port 1883.
|
||||
1733802466: Client auto-48A40F7F-6416-638B-746A-A75369F4A081 disconnected, not authorised.
|
||||
1733802496: New connection from ::1:38922 on port 1883.
|
||||
1733802496: Client auto-5C16F83A-3739-0699-4341-6EB22BDF16C8 disconnected, not authorised.
|
||||
1733802526: New connection from ::1:37138 on port 1883.
|
||||
1733802526: Client auto-E113F242-C8A9-766C-524E-11952D7FE583 disconnected, not authorised.
|
||||
1733802556: New connection from ::1:48906 on port 1883.
|
||||
1733802556: Client auto-4EEFD698-AE06-C3D6-ED93-C8274EE24836 disconnected, not authorised.
|
||||
1733802586: New connection from ::1:47202 on port 1883.
|
||||
1733802586: Client auto-40636D05-E36C-D457-2E50-B07A09E800DF disconnected, not authorised.
|
||||
1733802616: New connection from ::1:51008 on port 1883.
|
||||
1733802616: Client auto-9C39E32F-39E4-827D-BA81-BDD66D83A168 disconnected, not authorised.
|
||||
1733802646: New connection from ::1:51388 on port 1883.
|
||||
1733802646: Client auto-07F4F82A-67C0-7404-173D-7602C22BFF06 disconnected, not authorised.
|
||||
1733802676: New connection from ::1:45322 on port 1883.
|
||||
1733802676: Client auto-AFA09D6D-6391-8307-246E-851076BD2B10 disconnected, not authorised.
|
||||
1733802706: New connection from ::1:35838 on port 1883.
|
||||
1733802706: Client auto-280843B8-2387-56AA-B2E8-705578F82FD5 disconnected, not authorised.
|
||||
1733802736: New connection from ::1:40644 on port 1883.
|
||||
1733802736: Client auto-B9B99CFB-BA8F-028C-8592-31DA73C4866A disconnected, not authorised.
|
||||
1733802766: New connection from ::1:37848 on port 1883.
|
||||
1733802766: Client auto-F8DCC142-5579-BDA6-DB80-F71AE013ADB0 disconnected, not authorised.
|
||||
1733802796: New connection from ::1:59312 on port 1883.
|
||||
1733802796: Client auto-D7DA538A-410C-D4A1-1682-02CBFAC04766 disconnected, not authorised.
|
||||
1733802826: New connection from ::1:50194 on port 1883.
|
||||
1733802826: Client auto-B9006DFA-FE05-35D8-E016-3BE4BD70AA8C disconnected, not authorised.
|
||||
1733802856: New connection from ::1:48782 on port 1883.
|
||||
1733802856: Client auto-2A17AA23-7FF8-2023-7E4A-03CAD4958FB8 disconnected, not authorised.
|
||||
1733802886: New connection from ::1:41208 on port 1883.
|
||||
1733802886: Client auto-0843C297-CE19-711F-9E09-207B33FFDF09 disconnected, not authorised.
|
||||
1733802916: New connection from ::1:50386 on port 1883.
|
||||
1733802916: Client auto-480D5CAF-CD09-3815-A26F-EA65F005B257 disconnected, not authorised.
|
||||
1733802946: New connection from ::1:53822 on port 1883.
|
||||
1733802946: Client auto-188B7ED7-9A04-A9D0-41B3-4EE03393DE8C disconnected, not authorised.
|
||||
1733802976: New connection from ::1:50236 on port 1883.
|
||||
1733802976: Client auto-58C836E7-FEEF-9BED-0A2C-50AC7B597C0C disconnected, not authorised.
|
||||
1733803006: New connection from ::1:39704 on port 1883.
|
||||
1733803006: Client auto-C35FABDE-D807-F399-7432-15927F987EDC disconnected, not authorised.
|
||||
1733803036: New connection from ::1:55164 on port 1883.
|
||||
1733803036: Client auto-194A1973-A481-EAD8-AD90-3C39ED9A425B disconnected, not authorised.
|
||||
1733803067: New connection from ::1:55074 on port 1883.
|
||||
1733803067: Client auto-ABBB946B-C2C5-E79A-BB03-6DAF1BC18362 disconnected, not authorised.
|
||||
1733803097: New connection from ::1:59908 on port 1883.
|
||||
1733803097: Client auto-BA28D69D-A123-4B0F-5DD2-A293EF156D99 disconnected, not authorised.
|
||||
1733803127: New connection from ::1:60958 on port 1883.
|
||||
1733803127: Client auto-D38A00B4-547D-FBC8-300A-3D5D93F6CC4D disconnected, not authorised.
|
||||
1733803157: New connection from ::1:34326 on port 1883.
|
||||
1733803157: Client auto-B5128D39-1478-C4C7-EFF9-C20C2756313B disconnected, not authorised.
|
||||
1733803187: New connection from ::1:53612 on port 1883.
|
||||
1733803187: Client auto-B1BE1983-B3D4-3556-9B90-DD9480167D6E disconnected, not authorised.
|
||||
1733803217: New connection from ::1:36830 on port 1883.
|
||||
1733803217: Client auto-F265A9B1-D588-73F4-67E1-912CD64F5725 disconnected, not authorised.
|
||||
1733803247: New connection from ::1:53512 on port 1883.
|
||||
1733803247: Client auto-0FCC2DBE-FBB0-A9C3-3826-A973CE09754E disconnected, not authorised.
|
||||
1733803277: New connection from ::1:53792 on port 1883.
|
||||
1733803277: Client auto-675CA0D0-C7B0-9446-0FA7-B4A105B0B5EF disconnected, not authorised.
|
||||
1733803307: New connection from ::1:46304 on port 1883.
|
||||
1733803307: Client auto-0DBAF03B-9786-8165-010C-95E12443163E disconnected, not authorised.
|
||||
1733803337: New connection from ::1:46680 on port 1883.
|
||||
1733803337: Client auto-E1477F89-3598-2A96-EAD3-F5293ACB5A95 disconnected, not authorised.
|
||||
1733803367: New connection from ::1:59304 on port 1883.
|
||||
1733803367: Client auto-0F0591DD-5004-B16A-F59E-9D29456C47B8 disconnected, not authorised.
|
||||
1733803397: New connection from ::1:47688 on port 1883.
|
||||
1733803397: Client auto-FD312FCC-1AC7-B881-8B21-5E3D623953FC disconnected, not authorised.
|
||||
1733803427: New connection from ::1:43790 on port 1883.
|
||||
1733803427: Client auto-DE1ACF6F-FD60-FC42-94A5-F2DABBDF27D6 disconnected, not authorised.
|
||||
1733803457: New connection from ::1:37234 on port 1883.
|
||||
1733803457: Client auto-1C4C7E74-9FA7-D3C7-0481-0FF026EBE72A disconnected, not authorised.
|
||||
1733803487: New connection from ::1:40228 on port 1883.
|
||||
1733803487: Client auto-A881F298-762B-46D5-85F9-307B2947234A disconnected, not authorised.
|
||||
1733803517: New connection from ::1:51692 on port 1883.
|
||||
1733803517: Client auto-709A288A-DA45-F697-4E7B-A34FA0C3DCD5 disconnected, not authorised.
|
||||
1733803547: New connection from ::1:51876 on port 1883.
|
||||
1733803547: Client auto-995C43C2-5ECD-B1D9-BAA8-4AEF6F7E6875 disconnected, not authorised.
|
||||
1733803577: New connection from ::1:44538 on port 1883.
|
||||
1733803577: Client auto-06604FFA-71FF-ED04-2091-AD3B11462CD0 disconnected, not authorised.
|
||||
1733803607: New connection from ::1:41484 on port 1883.
|
||||
1733803607: Client auto-F8D87C06-20A9-75B6-AE4A-5A831B6834E5 disconnected, not authorised.
|
||||
1733803637: New connection from ::1:36316 on port 1883.
|
||||
1733803637: Client auto-3B76DD31-B241-83A4-928D-15038160A44F disconnected, not authorised.
|
||||
1733803667: New connection from ::1:48728 on port 1883.
|
||||
1733803667: Client auto-FF51EF25-41EE-30D3-E5D5-0437205760AD disconnected, not authorised.
|
||||
1733803697: New connection from ::1:56798 on port 1883.
|
||||
1733803697: Client auto-DE4FF3BB-F3EB-1259-8F0B-1C69C8933ADC disconnected, not authorised.
|
||||
1733803728: New connection from ::1:32904 on port 1883.
|
||||
1733803728: Client auto-66B3D737-A6F7-1D59-F426-5670410DC6A4 disconnected, not authorised.
|
||||
1733803758: New connection from ::1:35696 on port 1883.
|
||||
1733803758: Client auto-3F0FA16C-C454-DE38-0D4E-1F6F4737A08C disconnected, not authorised.
|
||||
1733803788: New connection from ::1:52024 on port 1883.
|
||||
1733803788: Client auto-A63C645E-E4DA-43CB-815A-8DC3B5FE4692 disconnected, not authorised.
|
||||
1733803818: New connection from ::1:48398 on port 1883.
|
||||
1733803818: Client auto-A2D70B68-55DB-09BC-DC43-CD4F23793BEE disconnected, not authorised.
|
||||
1733803848: New connection from ::1:34054 on port 1883.
|
||||
1733803848: Client auto-9BF182B0-9687-2484-EE98-A141A9501BC1 disconnected, not authorised.
|
||||
1733803878: New connection from ::1:39044 on port 1883.
|
||||
1733803878: Client auto-61677D6F-DE51-0531-A26D-44932C477FE7 disconnected, not authorised.
|
||||
1733803908: New connection from ::1:48482 on port 1883.
|
||||
1733803908: Client auto-7E7D865B-12C7-B6C2-2231-F2F78473E521 disconnected, not authorised.
|
||||
1733803938: New connection from ::1:39220 on port 1883.
|
||||
1733803938: Client auto-2311C7B9-E342-4BED-90E6-E26CAE339CEB disconnected, not authorised.
|
||||
1733803966: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733803968: New connection from ::1:51592 on port 1883.
|
||||
1733803968: Client auto-F25A06D7-D34D-DBFD-CE93-DB2079A64CF0 disconnected, not authorised.
|
||||
1733803998: New connection from ::1:33878 on port 1883.
|
||||
1733803998: Client auto-B9DD940A-9C37-AD60-69E8-88D51B55A7F0 disconnected, not authorised.
|
||||
1733804028: New connection from ::1:52648 on port 1883.
|
||||
1733804028: Client auto-5E3EFCE1-2139-F413-0AFF-8CFEF525A1BE disconnected, not authorised.
|
||||
1733804058: New connection from ::1:35762 on port 1883.
|
||||
1733804058: Client auto-F90A42AB-E809-3684-0991-C59FFDFBB178 disconnected, not authorised.
|
||||
1733804088: New connection from ::1:59566 on port 1883.
|
||||
1733804088: Client auto-54C7281F-1154-5AF6-4B61-F6C075B29B67 disconnected, not authorised.
|
||||
1733804118: New connection from ::1:51950 on port 1883.
|
||||
1733804118: Client auto-F622FFF2-1DBE-EB50-C9E9-839C364F093A disconnected, not authorised.
|
||||
1733804148: New connection from ::1:57656 on port 1883.
|
||||
1733804148: Client auto-8432B312-1A99-B2FC-9A29-FB5A28A646D4 disconnected, not authorised.
|
||||
1733804178: New connection from ::1:43324 on port 1883.
|
||||
1733804178: Client auto-8821E22B-CEA3-4AE3-682A-A4ED312C9CB2 disconnected, not authorised.
|
||||
1733804208: New connection from ::1:39172 on port 1883.
|
||||
1733804208: Client auto-5F045DDC-403A-D1E2-03BE-D6F32AFDE00A disconnected, not authorised.
|
||||
1733804238: New connection from ::1:37896 on port 1883.
|
||||
1733804238: Client auto-55999725-A35A-E314-170E-1D36D890C476 disconnected, not authorised.
|
||||
1733804268: New connection from ::1:35024 on port 1883.
|
||||
1733804268: Client auto-7AB29C76-B114-1AA7-DFEE-130850757D49 disconnected, not authorised.
|
||||
1733804298: New connection from ::1:44198 on port 1883.
|
||||
1733804298: Client auto-51B68FFB-B294-B83C-B58A-08A893E3AD75 disconnected, not authorised.
|
||||
1733804329: New connection from ::1:51110 on port 1883.
|
||||
1733804329: Client auto-4B2D8D5A-5BC3-3070-4949-D6581C247785 disconnected, not authorised.
|
||||
1733804359: New connection from ::1:52310 on port 1883.
|
||||
1733804359: Client auto-6137BE24-5E91-4DF3-6397-C4F16CFC8272 disconnected, not authorised.
|
||||
1733804389: New connection from ::1:38398 on port 1883.
|
||||
1733804389: Client auto-FD5BB219-E7C0-8CD2-BDE7-A1219C8FE5D5 disconnected, not authorised.
|
||||
1733804419: New connection from ::1:45514 on port 1883.
|
||||
1733804419: Client auto-0CA17FFF-7022-D54D-EC4A-8C3DE537FFDC disconnected, not authorised.
|
||||
1733804449: New connection from ::1:53710 on port 1883.
|
||||
1733804449: Client auto-F5DE69A9-86CE-7DF8-5D74-C1B04EDBB1DD disconnected, not authorised.
|
||||
1733804479: New connection from ::1:39020 on port 1883.
|
||||
1733804479: Client auto-B69B1F58-9379-E177-AF58-A94C2341A5D5 disconnected, not authorised.
|
||||
1733804509: New connection from ::1:58458 on port 1883.
|
||||
1733804509: Client auto-66673B3F-2B98-FCBC-0AC0-36E2362473E5 disconnected, not authorised.
|
||||
1733804539: New connection from ::1:51754 on port 1883.
|
||||
1733804539: Client auto-82A973FB-ED30-6D07-94B7-ACBCC91F04F9 disconnected, not authorised.
|
||||
1733804569: New connection from ::1:55928 on port 1883.
|
||||
1733804569: Client auto-7DD5AF85-2A71-7A95-3CD0-654BE4C5993E disconnected, not authorised.
|
||||
1733804599: New connection from ::1:59806 on port 1883.
|
||||
1733804599: Client auto-BF4DF154-2CC8-95E8-B060-D9A959CC55FE disconnected, not authorised.
|
||||
1733804629: New connection from ::1:58588 on port 1883.
|
||||
1733804629: Client auto-0710D5BA-D0C4-D011-0933-06D2432EE2EF disconnected, not authorised.
|
||||
1733804659: New connection from ::1:60802 on port 1883.
|
||||
1733804659: Client auto-9A5D6B6B-11CE-5369-AD55-92F0F20CAD25 disconnected, not authorised.
|
||||
1733804689: New connection from ::1:42376 on port 1883.
|
||||
1733804689: Client auto-04B6089E-BE47-DB4D-839D-682BF1E4F6D3 disconnected, not authorised.
|
||||
1733804719: New connection from ::1:33148 on port 1883.
|
||||
1733804719: Client auto-722EF924-93AF-6FE2-2AE9-936FE880350E disconnected, not authorised.
|
||||
1733804749: New connection from ::1:45634 on port 1883.
|
||||
1733804749: Client auto-4BBDF083-A4DD-5F64-E80B-1CA74FFC7103 disconnected, not authorised.
|
||||
1733804779: New connection from ::1:38892 on port 1883.
|
||||
1733804779: Client auto-E81C6BB4-6215-A93E-AF5D-F1DC7F1FC306 disconnected, not authorised.
|
||||
1733804809: New connection from ::1:33208 on port 1883.
|
||||
1733804809: Client auto-9E6EE048-F315-38DF-5221-1E3365180880 disconnected, not authorised.
|
||||
1733804839: New connection from ::1:37102 on port 1883.
|
||||
1733804839: Client auto-9244AEC7-20D0-8C37-9AC0-4CBF2E964BBE disconnected, not authorised.
|
||||
1733804869: New connection from ::1:34664 on port 1883.
|
||||
1733804869: Client auto-57D3F277-BD83-E19C-E2E3-E6AC8829C080 disconnected, not authorised.
|
||||
1733804899: New connection from ::1:51484 on port 1883.
|
||||
1733804899: Client auto-BDADBF77-3B95-ACE9-1ABB-CC01BCE92B4B disconnected, not authorised.
|
||||
1733804929: New connection from ::1:58436 on port 1883.
|
||||
1733804929: Client auto-D5AB95B9-DF2B-AE94-D8B6-368C470DAC72 disconnected, not authorised.
|
||||
1733804959: New connection from ::1:52714 on port 1883.
|
||||
1733804959: Client auto-32784D3F-9DBB-F9BA-9E9F-C5CD4C957AC5 disconnected, not authorised.
|
||||
1733804989: New connection from ::1:59460 on port 1883.
|
||||
1733804989: Client auto-8CC7D2F9-4485-5199-EDFC-B8AB50CE4BE5 disconnected, not authorised.
|
||||
1733805019: New connection from ::1:35070 on port 1883.
|
||||
1733805019: Client auto-3DD86BB0-3DE7-3098-4DA8-D982BF01DAE7 disconnected, not authorised.
|
||||
1733805049: New connection from ::1:55410 on port 1883.
|
||||
1733805049: Client auto-5BEF5698-4990-8256-F023-3AADC53611E4 disconnected, not authorised.
|
||||
1733805080: New connection from ::1:44898 on port 1883.
|
||||
1733805080: Client auto-70114D43-ADA8-149C-FB1D-1C91598868AA disconnected, not authorised.
|
||||
1733805110: New connection from ::1:51912 on port 1883.
|
||||
1733805110: Client auto-BB3F70FA-5941-7A6D-7FE9-AF4D6848FC58 disconnected, not authorised.
|
||||
1733805140: New connection from ::1:42926 on port 1883.
|
||||
1733805140: Client auto-A389AEA5-7456-180D-FD14-F876C1A29A72 disconnected, not authorised.
|
||||
1733805170: New connection from ::1:51948 on port 1883.
|
||||
1733805170: Client auto-3EDCBC11-76A2-FE05-1887-31C871D28111 disconnected, not authorised.
|
||||
1733805200: New connection from ::1:46558 on port 1883.
|
||||
1733805200: Client auto-5B894356-D45E-12DC-7702-8784AFDD011E disconnected, not authorised.
|
||||
1733805230: New connection from ::1:52734 on port 1883.
|
||||
1733805230: Client auto-1ACAD26C-6234-EED2-E564-0446B4A4494A disconnected, not authorised.
|
||||
1733805692: Client fems_realtime_40 has exceeded timeout, disconnecting.
|
||||
1733805697: New connection from 172.18.0.8:48928 on port 1883.
|
||||
1733805697: New client connected from 172.18.0.8:48928 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733805699: New connection from ::1:55522 on port 1883.
|
||||
1733805699: Client auto-3898921C-E5AF-B224-4EA3-6E64A984B5CD disconnected, not authorised.
|
||||
1733805729: New connection from ::1:57326 on port 1883.
|
||||
1733805729: Client auto-4A851718-E113-CBD9-C746-57AD62F5686E disconnected, not authorised.
|
||||
1733805759: New connection from ::1:56960 on port 1883.
|
||||
1733805759: Client auto-71816837-E492-EA7D-C230-36FCDFE85656 disconnected, not authorised.
|
||||
1733805767: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733805789: New connection from ::1:47168 on port 1883.
|
||||
1733805789: Client auto-3F3709B7-7E1A-A9CA-B72D-6B811672D1AE disconnected, not authorised.
|
||||
1733806449: Client fems_realtime_40 has exceeded timeout, disconnecting.
|
||||
1733806454: New connection from 172.18.0.8:58112 on port 1883.
|
||||
1733806454: New client connected from 172.18.0.8:58112 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733806456: New connection from ::1:52250 on port 1883.
|
||||
1733806456: Client auto-4614D65F-79B5-5B8C-7C70-5EE1893D755A disconnected, not authorised.
|
||||
1733806486: New connection from ::1:46120 on port 1883.
|
||||
1733806486: Client auto-8591D680-5D3A-BACF-7FA0-C1366E100F4A disconnected, not authorised.
|
||||
1733807416: Client fems_realtime_40 has exceeded timeout, disconnecting.
|
||||
1733807421: New connection from 172.18.0.8:49896 on port 1883.
|
||||
1733807421: New client connected from 172.18.0.8:49896 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733807423: New connection from ::1:50100 on port 1883.
|
||||
1733807423: Client auto-B5795A21-8A3F-01DF-810C-069C39332B6E disconnected, not authorised.
|
||||
1733807453: New connection from ::1:56446 on port 1883.
|
||||
1733807453: Client auto-F0781E40-5C08-9A14-4E82-23F66B49E91A disconnected, not authorised.
|
||||
1733807483: New connection from ::1:52606 on port 1883.
|
||||
1733807483: Client auto-079B6FE0-E18F-A73B-3F76-6D818BCC48A5 disconnected, not authorised.
|
||||
1733807513: New connection from ::1:37808 on port 1883.
|
||||
1733807513: Client auto-CD3A4FD1-9391-78E1-C397-930F8CDCA29D disconnected, not authorised.
|
||||
1733807543: New connection from ::1:54906 on port 1883.
|
||||
1733807543: Client auto-E033985C-E258-25D0-D57C-0E80F1D7EE2E disconnected, not authorised.
|
||||
1733807568: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733807573: New connection from ::1:47354 on port 1883.
|
||||
1733807573: Client auto-DCD5185A-84A0-3A2F-7EB1-19682EE35593 disconnected, not authorised.
|
||||
1733807603: New connection from ::1:46396 on port 1883.
|
||||
1733807603: Client auto-F41A50C1-9D6C-07AD-12A6-7D9B5705989D disconnected, not authorised.
|
||||
1733807633: New connection from ::1:37438 on port 1883.
|
||||
1733807633: Client auto-C5178851-AC29-7C69-5439-589055EF49BC disconnected, not authorised.
|
||||
1733807663: New connection from ::1:38384 on port 1883.
|
||||
1733807663: Client auto-F8DF2B82-9958-5AAB-3EFF-32E044F2DE20 disconnected, not authorised.
|
||||
1733807693: New connection from ::1:52872 on port 1883.
|
||||
1733807693: Client auto-699C20E4-F6B8-5C4B-9667-6D79D7D37AE3 disconnected, not authorised.
|
||||
1733807703: mosquitto version 2.0.20 terminating
|
||||
1733807703: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733808853: mosquitto version 2.0.20 starting
|
||||
1733808853: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733808853: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$LhtVgKHX/PmfN+Si$BjcRmSp0ofqZb2lprexg/jBsTABgdINmPuEAwbCCeGSfFitU8NjyYb2sjPKTDyGeKPZBgnR2XASn3oHNzspuuQ==
|
||||
1733808853: Opening ipv4 listen socket on port 1883.
|
||||
1733808853: Opening ipv6 listen socket on port 1883.
|
||||
1733808853: mosquitto version 2.0.20 running
|
||||
1733808854: New connection from 172.18.0.6:43370 on port 1883.
|
||||
1733808854: New client connected from 172.18.0.6:43370 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733808883: New connection from ::1:42106 on port 1883.
|
||||
1733808883: Client auto-67C0AD38-75D7-5774-4DED-27FAF36796A3 disconnected, not authorised.
|
||||
1733808908: mosquitto version 2.0.20 terminating
|
||||
1733808908: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733808919: mosquitto version 2.0.20 starting
|
||||
1733808919: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733808919: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$StGGKNuLvn6XTjIq$GZ1dkDRLc7teB4O5W7e2acBOuKxPS0USxlIf5d89IQW+qilDaNI9BcH8/WsUWRjVBQKeUsQ/3xT5LeqYtNh0aQ==
|
||||
1733808919: Opening ipv4 listen socket on port 1883.
|
||||
1733808919: Opening ipv6 listen socket on port 1883.
|
||||
1733808919: mosquitto version 2.0.20 running
|
||||
1733808920: New connection from 172.18.0.6:58428 on port 1883.
|
||||
1733808920: New client connected from 172.18.0.6:58428 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733808949: New connection from ::1:48408 on port 1883.
|
||||
1733808949: Client auto-CBEDDC69-B051-F5A4-56E7-41B44F3D72D3 disconnected, not authorised.
|
||||
1733808959: Client fems_realtime_40 closed its connection.
|
||||
1733808959: mosquitto version 2.0.20 terminating
|
||||
1733808959: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733808971: mosquitto version 2.0.20 starting
|
||||
1733808971: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733808971: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$KV2MgKrHDABloVU1$kXMy0KaP/DN2geeucsDrTOwrAy10GC1jcbEX34Eq8Hw7LDi2YrKfaCfnuXvdcMijrwoR+S8wD8GUAgr5HIDK/Q==
|
||||
1733808971: Opening ipv4 listen socket on port 1883.
|
||||
1733808971: Opening ipv6 listen socket on port 1883.
|
||||
1733808971: mosquitto version 2.0.20 running
|
||||
1733808971: New connection from 172.18.0.9:42624 on port 1883.
|
||||
1733808971: New client connected from 172.18.0.9:42624 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733808996: mosquitto version 2.0.20 terminating
|
||||
1733808996: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733812940: mosquitto version 2.0.20 starting
|
||||
1733812940: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733812940: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$m054NDx6NX4LxlDR$sqaZceKf07blp8sC1EXWHgyshh/Sx1fyGJaIPFS/BgTWr+o+50L2oNfv392G6uYNU5ITtPbYubuzIQfe29lpIw==
|
||||
1733812940: Opening ipv4 listen socket on port 1883.
|
||||
1733812940: Opening ipv6 listen socket on port 1883.
|
||||
1733812940: mosquitto version 2.0.20 running
|
||||
1733812941: mosquitto version 2.0.20 terminating
|
||||
1733812941: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
1733812972: mosquitto version 2.0.20 starting
|
||||
1733812972: Config loaded from /mosquitto/config/mosquitto.conf.
|
||||
1733812972: Warning: Invalid line in password file '/mosquitto/data/passwd': :$7$101$b0DdJIM3X8nUFEgJ$k4Yk6IfR+iSSNexrFy0tuM5Qzu2NADWvCSF/+Zfh7UIZ0RmDUSfExHBXLLgetP5DTbiBxpzgWBHwFDaww1sgQg==
|
||||
1733812972: Opening ipv4 listen socket on port 1883.
|
||||
1733812972: Opening ipv6 listen socket on port 1883.
|
||||
1733812972: mosquitto version 2.0.20 running
|
||||
1733812973: New connection from 172.18.0.7:47208 on port 1883.
|
||||
1733812973: New client connected from 172.18.0.7:47208 as fems_realtime_40 (p2, c1, k60, u'fems').
|
||||
1733812976: Client fems_realtime_40 closed its connection.
|
||||
1733812976: mosquitto version 2.0.20 terminating
|
||||
1733812976: Saving in-memory database to /mosquitto/data//mosquitto.db.
|
||||
|
@ -6,19 +6,19 @@
|
||||
"auditLog": "/app/logs/error/.f0712455ac9b956018e7ebc08dc33061795e3736-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1732487954429,
|
||||
"name": "/app/logs/error/error-2024-11-25.log",
|
||||
"hash": "b9d0d44fee4f722166086db00108ccc797926c564d71c563068c9563f4711946"
|
||||
"date": 1733746784906,
|
||||
"name": "/app/logs/error/error-2024-12-09.log",
|
||||
"hash": "bc01734a47c5ab9a0c8c5579b91d673c834b9da80bf2bec63403efb28be39c35"
|
||||
},
|
||||
{
|
||||
"date": 1732570927269,
|
||||
"name": "/app/logs/error/error-2024-11-26.log",
|
||||
"hash": "38556fbbe3e1d2d094f6be5fddae74f769846d7424704a8ad290acc03c5baa12"
|
||||
"date": 1733793585406,
|
||||
"name": "/app/logs/error/error-2024-12-10.log",
|
||||
"hash": "bb9f2826cd5ea0ca46e9f22cc120339198f29e37cf5d03b8f53b36a37ac45b22"
|
||||
},
|
||||
{
|
||||
"date": 1732667260450,
|
||||
"name": "/app/logs/error/error-2024-11-27.log",
|
||||
"hash": "aaac408d9061c832f7dc73f8e3c456e371d23d73aed57068d6e215532ff4bac5"
|
||||
"date": 1733879841776,
|
||||
"name": "/app/logs/error/error-2024-12-11.log",
|
||||
"hash": "39195d6169da96b7fe430ade1b2115a82fb0a51334153ceb54541d28d6f739ee"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
|
@ -1,3 +0,0 @@
|
||||
{"environment":"development","level":"error","message":"Failed to start server: Failed to connect to required services","service":"fems-edge","stack":"Error: Failed to connect to required services\n at waitForServices (/app/src/app.js:35:9)\n at async bootstrap (/app/src/app.js:43:5)","timestamp":"2024-11-25 07:44:19"}
|
||||
{"environment":"development","level":"error","message":"Failed to start server: Failed to connect to required services","service":"fems-edge","stack":"Error: Failed to connect to required services\n at waitForServices (/app/src/app.js:35:9)\n at async bootstrap (/app/src/app.js:43:5)","timestamp":"2024-11-25 07:49:53"}
|
||||
{"environment":"development","level":"error","message":"Failed to start server: Failed to connect to required services","service":"fems-edge","stack":"Error: Failed to connect to required services\n at waitForServices (/app/src/app.js:35:9)\n at async bootstrap (/app/src/app.js:43:5)","timestamp":"2024-11-25 07:51:00"}
|
@ -6,19 +6,19 @@
|
||||
"auditLog": "/app/logs/info/.17ff23bf9d3be0bd8551fdd86d0d3cca3a97cd90-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1732487954432,
|
||||
"name": "/app/logs/info/info-2024-11-25.log",
|
||||
"hash": "566bad7335558b7e07afab5362037c2a7e880d250529a7508c299dcfd99a1ea3"
|
||||
"date": 1733746784908,
|
||||
"name": "/app/logs/info/info-2024-12-09.log",
|
||||
"hash": "91a97133406f7c8225f6a4c44c7e5b284852ffe413bfba6b024440a56cef6046"
|
||||
},
|
||||
{
|
||||
"date": 1732548728076,
|
||||
"name": "/app/logs/info/info-2024-11-26.log",
|
||||
"hash": "fd8c633eeb46930b2788b53ce7ebae1dd2b427ddb03b46ee39a86207d7172f2e"
|
||||
"date": 1733793585410,
|
||||
"name": "/app/logs/info/info-2024-12-10.log",
|
||||
"hash": "9adf5bcedf67ed51d1e795830fd55f28eafff19169585bf2109a3e8a1eea2ad4"
|
||||
},
|
||||
{
|
||||
"date": 1732633663556,
|
||||
"name": "/app/logs/info/info-2024-11-27.log",
|
||||
"hash": "210aa82c16bb88b4a750a458183869412cff9f0967c8c2c898d021e84b1bcb2e"
|
||||
"date": 1733879841786,
|
||||
"name": "/app/logs/info/info-2024-12-11.log",
|
||||
"hash": "e966ad92bddef5e4c0517226872d8bab241ca53eec10b0d6c43a1dd46388bfef"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
|
@ -6,19 +6,19 @@
|
||||
"auditLog": "/app/logs/system/.d741815bd363f7141809635bfe8d8a1642abfc24-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1732487954433,
|
||||
"name": "/app/logs/system/system-2024-11-25.log",
|
||||
"hash": "17d3c14f32ab81d008830a3947ececd8a1cae5001d71409c72dd4464d15cb140"
|
||||
"date": 1733746784910,
|
||||
"name": "/app/logs/system/system-2024-12-09.log",
|
||||
"hash": "29e3a71219f6eb5844373ab72f70cd54d1a44dfc6491ee42ecef0931f5031459"
|
||||
},
|
||||
{
|
||||
"date": 1732548728182,
|
||||
"name": "/app/logs/system/system-2024-11-26.log",
|
||||
"hash": "b8f3a25c82337b2bb870ab9e4705680a040528cdc876977febe22f71cd99e359"
|
||||
"date": 1733793585414,
|
||||
"name": "/app/logs/system/system-2024-12-10.log",
|
||||
"hash": "44911f386576690f54e37dcf880b071f931b3f0927f8c2f97eff948eef82de2a"
|
||||
},
|
||||
{
|
||||
"date": 1732633663573,
|
||||
"name": "/app/logs/system/system-2024-11-27.log",
|
||||
"hash": "664d6884001d7c6bf73c07086d76b456c8c65e4bdebe6679895cae0ce18f72e3"
|
||||
"date": 1733879841794,
|
||||
"name": "/app/logs/system/system-2024-12-11.log",
|
||||
"hash": "377a78384280ae0a2ffba508995bcc46a4a718c069702599347ef1306ef47163"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
|
4
fems-timescaledb/yarn.lock
Normal file
4
fems-timescaledb/yarn.lock
Normal file
@ -0,0 +1,4 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user