최초커밋

This commit is contained in:
gbpark 2024-12-12 18:20:55 +09:00
parent 1556bc2845
commit 349c1bc833
23 changed files with 2061 additions and 335 deletions

View File

@ -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

View File

@ -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;

View 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;

View 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;

View 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;

View File

@ -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;

View 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
};

View File

@ -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,

View File

@ -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!@#",

View 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>
);
};

View 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;

View File

@ -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() {

View File

@ -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;

View 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.

View File

@ -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==

View File

@ -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.

View File

@ -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"

View File

@ -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"}

View File

@ -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"

View File

@ -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"

View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1