카테고리 트리 기능 추가 및 관련 API 구현
- 카테고리 트리 컨트롤러와 서비스 추가: 트리 구조를 지원하는 카테고리 값 관리 기능을 구현하였습니다. - 카테고리 트리 API 클라이언트 추가: CRUD 작업을 위한 API 클라이언트를 구현하였습니다. - 카테고리 값 관리 컴포넌트 및 설정 패널 추가: 사용자 인터페이스에서 카테고리 값을 관리할 수 있도록 트리 구조 기반의 컴포넌트를 추가하였습니다. - 관련 라우트 및 레지스트리 업데이트: 카테고리 트리 관련 라우트를 추가하고, 컴포넌트 레지스트리에 등록하였습니다. 이로 인해 카테고리 관리의 효율성이 향상되었습니다.
This commit is contained in:
@@ -83,6 +83,7 @@ import cascadingConditionRoutes from "./routes/cascadingConditionRoutes"; // 조
|
||||
import cascadingMutualExclusionRoutes from "./routes/cascadingMutualExclusionRoutes"; // 상호 배제 관리
|
||||
import cascadingHierarchyRoutes from "./routes/cascadingHierarchyRoutes"; // 다단계 계층 관리
|
||||
import categoryValueCascadingRoutes from "./routes/categoryValueCascadingRoutes"; // 카테고리 값 연쇄관계
|
||||
import categoryTreeRoutes from "./routes/categoryTreeRoutes"; // 카테고리 트리 (테스트)
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
@@ -262,6 +263,7 @@ app.use("/api/cascading-conditions", cascadingConditionRoutes); // 조건부 연
|
||||
app.use("/api/cascading-exclusions", cascadingMutualExclusionRoutes); // 상호 배제 관리
|
||||
app.use("/api/cascading-hierarchy", cascadingHierarchyRoutes); // 다단계 계층 관리
|
||||
app.use("/api/category-value-cascading", categoryValueCascadingRoutes); // 카테고리 값 연쇄관계
|
||||
app.use("/api/category-tree", categoryTreeRoutes); // 카테고리 트리 (테스트)
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
|
||||
226
backend-node/src/controllers/categoryTreeController.ts
Normal file
226
backend-node/src/controllers/categoryTreeController.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* 카테고리 트리 컨트롤러 (테스트용)
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from "express";
|
||||
import { categoryTreeService, CreateCategoryValueInput, UpdateCategoryValueInput } from "../services/categoryTreeService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 인증된 사용자 타입
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
companyCode: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 트리 조회
|
||||
* GET /api/category-tree/test/:tableName/:columnName
|
||||
*/
|
||||
router.get("/test/:tableName/:columnName", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
const tree = await categoryTreeService.getCategoryTree(companyCode, tableName, columnName);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: tree,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 트리 조회 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 목록 조회 (플랫 리스트)
|
||||
* GET /api/category-tree/test/:tableName/:columnName/flat
|
||||
*/
|
||||
router.get("/test/:tableName/:columnName/flat", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
const list = await categoryTreeService.getCategoryList(companyCode, tableName, columnName);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: list,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 목록 조회 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 값 단일 조회
|
||||
* GET /api/category-tree/test/value/:valueId
|
||||
*/
|
||||
router.get("/test/value/:valueId", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { valueId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
const value = await categoryTreeService.getCategoryValue(companyCode, Number(valueId));
|
||||
|
||||
if (!value) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "카테고리 값을 찾을 수 없습니다",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: value,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 조회 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 값 생성
|
||||
* POST /api/category-tree/test/value
|
||||
*/
|
||||
router.post("/test/value", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const input: CreateCategoryValueInput = req.body;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const createdBy = req.user?.userId;
|
||||
|
||||
if (!input.tableName || !input.columnName || !input.valueCode || !input.valueLabel) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "tableName, columnName, valueCode, valueLabel은 필수입니다",
|
||||
});
|
||||
}
|
||||
|
||||
const value = await categoryTreeService.createCategoryValue(companyCode, input, createdBy);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: value,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 생성 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 값 수정
|
||||
* PUT /api/category-tree/test/value/:valueId
|
||||
*/
|
||||
router.put("/test/value/:valueId", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { valueId } = req.params;
|
||||
const input: UpdateCategoryValueInput = req.body;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const updatedBy = req.user?.userId;
|
||||
|
||||
const value = await categoryTreeService.updateCategoryValue(companyCode, Number(valueId), input, updatedBy);
|
||||
|
||||
if (!value) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "카테고리 값을 찾을 수 없습니다",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: value,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 수정 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 카테고리 값 삭제
|
||||
* DELETE /api/category-tree/test/value/:valueId
|
||||
*/
|
||||
router.delete("/test/value/:valueId", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { valueId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
const success = await categoryTreeService.deleteCategoryValue(companyCode, Number(valueId));
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "카테고리 값을 찾을 수 없습니다",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "삭제되었습니다",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 삭제 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 테이블의 카테고리 컬럼 목록 조회
|
||||
* GET /api/category-tree/test/columns/:tableName
|
||||
*/
|
||||
router.get("/test/columns/:tableName", async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
const columns = await categoryTreeService.getCategoryColumns(companyCode, tableName);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: columns,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 컬럼 목록 조회 API 오류", { error: err.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -59,3 +59,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,3 +55,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,3 +71,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,3 +59,4 @@ export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
8
backend-node/src/routes/categoryTreeRoutes.ts
Normal file
8
backend-node/src/routes/categoryTreeRoutes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 카테고리 트리 라우트 (테스트용)
|
||||
*/
|
||||
|
||||
import categoryTreeController from "../controllers/categoryTreeController";
|
||||
|
||||
export default categoryTreeController;
|
||||
|
||||
513
backend-node/src/services/categoryTreeService.ts
Normal file
513
backend-node/src/services/categoryTreeService.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
/**
|
||||
* 카테고리 트리 서비스 (테스트용)
|
||||
* - 트리 구조 지원 (최대 3단계: 대분류/중분류/소분류)
|
||||
*/
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 카테고리 값 타입
|
||||
export interface CategoryValue {
|
||||
valueId: number;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
valueCode: string;
|
||||
valueLabel: string;
|
||||
valueOrder: number;
|
||||
parentValueId: number | null;
|
||||
depth: number;
|
||||
path: string | null;
|
||||
description: string | null;
|
||||
color: string | null;
|
||||
icon: string | null;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
companyCode: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdBy: string | null;
|
||||
updatedBy: string | null;
|
||||
children?: CategoryValue[];
|
||||
}
|
||||
|
||||
// 카테고리 값 생성 입력
|
||||
export interface CreateCategoryValueInput {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
valueCode: string;
|
||||
valueLabel: string;
|
||||
valueOrder?: number;
|
||||
parentValueId?: number | null;
|
||||
description?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
// 카테고리 값 수정 입력
|
||||
export interface UpdateCategoryValueInput {
|
||||
valueCode?: string;
|
||||
valueLabel?: string;
|
||||
valueOrder?: number;
|
||||
parentValueId?: number | null;
|
||||
description?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
class CategoryTreeService {
|
||||
/**
|
||||
* 카테고리 값 목록 조회 (트리 구조로 반환)
|
||||
*/
|
||||
async getCategoryTree(companyCode: string, tableName: string, columnName: string): Promise<CategoryValue[]> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
logger.info("카테고리 트리 조회 시작", { companyCode, tableName, columnName });
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
path,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy",
|
||||
updated_by AS "updatedBy"
|
||||
FROM category_values_test
|
||||
WHERE (company_code = $1 OR company_code = '*')
|
||||
AND table_name = $2
|
||||
AND column_name = $3
|
||||
ORDER BY depth ASC, value_order ASC, value_label ASC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode, tableName, columnName]);
|
||||
const flatList = result.rows as CategoryValue[];
|
||||
|
||||
const tree = this.buildTree(flatList);
|
||||
|
||||
logger.info("카테고리 트리 조회 완료", {
|
||||
tableName,
|
||||
columnName,
|
||||
totalCount: flatList.length,
|
||||
rootCount: tree.length,
|
||||
});
|
||||
|
||||
return tree;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 트리 조회 실패", { error: err.message, tableName, columnName });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 목록 조회 (플랫 리스트)
|
||||
*/
|
||||
async getCategoryList(companyCode: string, tableName: string, columnName: string): Promise<CategoryValue[]> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
path,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt"
|
||||
FROM category_values_test
|
||||
WHERE (company_code = $1 OR company_code = '*')
|
||||
AND table_name = $2
|
||||
AND column_name = $3
|
||||
ORDER BY depth ASC, value_order ASC, value_label ASC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode, tableName, columnName]);
|
||||
return result.rows as CategoryValue[];
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 목록 조회 실패", { error: err.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 단일 조회
|
||||
*/
|
||||
async getCategoryValue(companyCode: string, valueId: number): Promise<CategoryValue | null> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
path,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt"
|
||||
FROM category_values_test
|
||||
WHERE (company_code = $1 OR company_code = '*') AND value_id = $2
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode, valueId]);
|
||||
return result.rows[0] || null;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 조회 실패", { error: err.message, valueId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 생성
|
||||
*/
|
||||
async createCategoryValue(companyCode: string, input: CreateCategoryValueInput, createdBy?: string): Promise<CategoryValue> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
// depth 계산
|
||||
let depth = 1;
|
||||
let path = input.valueLabel;
|
||||
|
||||
if (input.parentValueId) {
|
||||
const parent = await this.getCategoryValue(companyCode, input.parentValueId);
|
||||
if (parent) {
|
||||
depth = parent.depth + 1;
|
||||
path = parent.path ? `${parent.path}/${input.valueLabel}` : input.valueLabel;
|
||||
|
||||
if (depth > 3) {
|
||||
throw new Error("카테고리는 최대 3단계까지만 가능합니다");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO category_values_test (
|
||||
table_name, column_name, value_code, value_label, value_order,
|
||||
parent_value_id, depth, path, description, color, icon,
|
||||
is_active, is_default, company_code, created_by, updated_by
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $15
|
||||
)
|
||||
RETURNING
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
path,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt"
|
||||
`;
|
||||
|
||||
const params = [
|
||||
input.tableName,
|
||||
input.columnName,
|
||||
input.valueCode,
|
||||
input.valueLabel,
|
||||
input.valueOrder ?? 0,
|
||||
input.parentValueId ?? null,
|
||||
depth,
|
||||
path,
|
||||
input.description ?? null,
|
||||
input.color ?? null,
|
||||
input.icon ?? null,
|
||||
input.isActive ?? true,
|
||||
input.isDefault ?? false,
|
||||
companyCode,
|
||||
createdBy ?? null,
|
||||
];
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("카테고리 값 생성 완료", {
|
||||
valueId: result.rows[0].valueId,
|
||||
valueLabel: input.valueLabel,
|
||||
depth,
|
||||
});
|
||||
|
||||
return result.rows[0];
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 생성 실패", { error: err.message, input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 수정
|
||||
*/
|
||||
async updateCategoryValue(
|
||||
companyCode: string,
|
||||
valueId: number,
|
||||
input: UpdateCategoryValueInput,
|
||||
updatedBy?: string
|
||||
): Promise<CategoryValue | null> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const current = await this.getCategoryValue(companyCode, valueId);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let newPath = current.path;
|
||||
let newDepth = current.depth;
|
||||
|
||||
if (input.valueLabel && input.valueLabel !== current.valueLabel) {
|
||||
if (current.parentValueId) {
|
||||
const parent = await this.getCategoryValue(companyCode, current.parentValueId);
|
||||
if (parent && parent.path) {
|
||||
newPath = `${parent.path}/${input.valueLabel}`;
|
||||
} else {
|
||||
newPath = input.valueLabel;
|
||||
}
|
||||
} else {
|
||||
newPath = input.valueLabel;
|
||||
}
|
||||
}
|
||||
|
||||
if (input.parentValueId !== undefined && input.parentValueId !== current.parentValueId) {
|
||||
if (input.parentValueId) {
|
||||
const newParent = await this.getCategoryValue(companyCode, input.parentValueId);
|
||||
if (newParent) {
|
||||
newDepth = newParent.depth + 1;
|
||||
const label = input.valueLabel ?? current.valueLabel;
|
||||
newPath = newParent.path ? `${newParent.path}/${label}` : label;
|
||||
|
||||
if (newDepth > 3) {
|
||||
throw new Error("카테고리는 최대 3단계까지만 가능합니다");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newDepth = 1;
|
||||
newPath = input.valueLabel ?? current.valueLabel;
|
||||
}
|
||||
}
|
||||
|
||||
const query = `
|
||||
UPDATE category_values_test
|
||||
SET
|
||||
value_code = COALESCE($3, value_code),
|
||||
value_label = COALESCE($4, value_label),
|
||||
value_order = COALESCE($5, value_order),
|
||||
parent_value_id = $6,
|
||||
depth = $7,
|
||||
path = $8,
|
||||
description = COALESCE($9, description),
|
||||
color = COALESCE($10, color),
|
||||
icon = COALESCE($11, icon),
|
||||
is_active = COALESCE($12, is_active),
|
||||
is_default = COALESCE($13, is_default),
|
||||
updated_at = NOW(),
|
||||
updated_by = $14
|
||||
WHERE (company_code = $1 OR company_code = '*') AND value_id = $2
|
||||
RETURNING
|
||||
value_id AS "valueId",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
value_code AS "valueCode",
|
||||
value_label AS "valueLabel",
|
||||
value_order AS "valueOrder",
|
||||
parent_value_id AS "parentValueId",
|
||||
depth,
|
||||
path,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active AS "isActive",
|
||||
is_default AS "isDefault",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt"
|
||||
`;
|
||||
|
||||
const params = [
|
||||
companyCode,
|
||||
valueId,
|
||||
input.valueCode ?? null,
|
||||
input.valueLabel ?? null,
|
||||
input.valueOrder ?? null,
|
||||
input.parentValueId !== undefined ? input.parentValueId : current.parentValueId,
|
||||
newDepth,
|
||||
newPath,
|
||||
input.description ?? null,
|
||||
input.color ?? null,
|
||||
input.icon ?? null,
|
||||
input.isActive ?? null,
|
||||
input.isDefault ?? null,
|
||||
updatedBy ?? null,
|
||||
];
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
if (input.valueLabel || input.parentValueId !== undefined) {
|
||||
await this.updateChildrenPaths(companyCode, valueId, newPath || "");
|
||||
}
|
||||
|
||||
logger.info("카테고리 값 수정 완료", { valueId });
|
||||
|
||||
return result.rows[0] || null;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 수정 실패", { error: err.message, valueId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 삭제 (하위 항목도 함께 삭제)
|
||||
*/
|
||||
async deleteCategoryValue(companyCode: string, valueId: number): Promise<boolean> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
DELETE FROM category_values_test
|
||||
WHERE (company_code = $1 OR company_code = '*') AND value_id = $2
|
||||
RETURNING value_id
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode, valueId]);
|
||||
|
||||
if (result.rowCount && result.rowCount > 0) {
|
||||
logger.info("카테고리 값 삭제 완료", { valueId });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 값 삭제 실패", { error: err.message, valueId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 항목들의 path 업데이트
|
||||
*/
|
||||
private async updateChildrenPaths(companyCode: string, parentValueId: number, parentPath: string): Promise<void> {
|
||||
const pool = getPool();
|
||||
|
||||
const query = `
|
||||
SELECT value_id, value_label
|
||||
FROM category_values_test
|
||||
WHERE (company_code = $1 OR company_code = '*') AND parent_value_id = $2
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode, parentValueId]);
|
||||
|
||||
for (const child of result.rows) {
|
||||
const newPath = `${parentPath}/${child.value_label}`;
|
||||
|
||||
await pool.query(`UPDATE category_values_test SET path = $1, updated_at = NOW() WHERE value_id = $2`, [
|
||||
newPath,
|
||||
child.value_id,
|
||||
]);
|
||||
|
||||
await this.updateChildrenPaths(companyCode, child.value_id, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫 리스트를 트리 구조로 변환
|
||||
*/
|
||||
private buildTree(flatList: CategoryValue[]): CategoryValue[] {
|
||||
const map = new Map<number, CategoryValue>();
|
||||
const roots: CategoryValue[] = [];
|
||||
|
||||
for (const item of flatList) {
|
||||
map.set(item.valueId, { ...item, children: [] });
|
||||
}
|
||||
|
||||
for (const item of flatList) {
|
||||
const node = map.get(item.valueId)!;
|
||||
|
||||
if (item.parentValueId && map.has(item.parentValueId)) {
|
||||
const parent = map.get(item.parentValueId)!;
|
||||
parent.children = parent.children || [];
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 카테고리 컬럼 목록 조회
|
||||
*/
|
||||
async getCategoryColumns(companyCode: string, tableName: string): Promise<{ columnName: string; columnLabel: string }[]> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT DISTINCT column_name AS "columnName", column_label AS "columnLabel"
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND input_type = 'category'
|
||||
AND (company_code = $2 OR company_code = '*')
|
||||
ORDER BY column_name
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [tableName, companyCode]);
|
||||
return result.rows;
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error("카테고리 컬럼 목록 조회 실패", { error: err.message, tableName });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const categoryTreeService = new CategoryTreeService();
|
||||
|
||||
Reference in New Issue
Block a user