Compare commits
No commits in common. "f6a02b5182a4f0b3900dc8da9188ad995dcce1e7" and "db31b0218085b4bf1a25a1cf0c9b5e19f06df616" have entirely different histories.
f6a02b5182
...
db31b02180
|
|
@ -138,7 +138,6 @@ import categoryTreeRoutes from "./routes/categoryTreeRoutes"; // 카테고리
|
||||||
import processWorkStandardRoutes from "./routes/processWorkStandardRoutes"; // 공정 작업기준
|
import processWorkStandardRoutes from "./routes/processWorkStandardRoutes"; // 공정 작업기준
|
||||||
import aiAssistantProxy from "./routes/aiAssistantProxy"; // AI 어시스턴트 API 프록시 (같은 포트로 서비스)
|
import aiAssistantProxy from "./routes/aiAssistantProxy"; // AI 어시스턴트 API 프록시 (같은 포트로 서비스)
|
||||||
import auditLogRoutes from "./routes/auditLogRoutes"; // 통합 변경 이력
|
import auditLogRoutes from "./routes/auditLogRoutes"; // 통합 변경 이력
|
||||||
import moldRoutes from "./routes/moldRoutes"; // 금형 관리
|
|
||||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||||
|
|
@ -327,7 +326,6 @@ app.use("/api/category-value-cascading", categoryValueCascadingRoutes); // 카
|
||||||
app.use("/api/category-tree", categoryTreeRoutes); // 카테고리 트리 (테스트)
|
app.use("/api/category-tree", categoryTreeRoutes); // 카테고리 트리 (테스트)
|
||||||
app.use("/api/process-work-standard", processWorkStandardRoutes); // 공정 작업기준
|
app.use("/api/process-work-standard", processWorkStandardRoutes); // 공정 작업기준
|
||||||
app.use("/api/audit-log", auditLogRoutes); // 통합 변경 이력
|
app.use("/api/audit-log", auditLogRoutes); // 통합 변경 이력
|
||||||
app.use("/api/mold", moldRoutes); // 금형 관리
|
|
||||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||||
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트)
|
||||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||||
|
|
|
||||||
|
|
@ -818,13 +818,13 @@ export const getCategoryValueCascadingParentOptions = async (
|
||||||
|
|
||||||
const group = groupResult.rows[0];
|
const group = groupResult.rows[0];
|
||||||
|
|
||||||
// 부모 카테고리 값 조회 (category_values에서)
|
// 부모 카테고리 값 조회 (table_column_category_values에서)
|
||||||
let optionsQuery = `
|
let optionsQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
value_code as value,
|
value_code as value,
|
||||||
value_label as label,
|
value_label as label,
|
||||||
value_order as display_order
|
value_order as display_order
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
|
|
@ -916,13 +916,13 @@ export const getCategoryValueCascadingChildOptions = async (
|
||||||
|
|
||||||
const group = groupResult.rows[0];
|
const group = groupResult.rows[0];
|
||||||
|
|
||||||
// 자식 카테고리 값 조회 (category_values에서)
|
// 자식 카테고리 값 조회 (table_column_category_values에서)
|
||||||
let optionsQuery = `
|
let optionsQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
value_code as value,
|
value_code as value,
|
||||||
value_label as label,
|
value_label as label,
|
||||||
value_order as display_order
|
value_order as display_order
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
|
|
|
||||||
|
|
@ -417,10 +417,10 @@ export class EntityJoinController {
|
||||||
// 1. 현재 테이블의 Entity 조인 설정 조회
|
// 1. 현재 테이블의 Entity 조인 설정 조회
|
||||||
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
|
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
|
||||||
|
|
||||||
// 🆕 화면 디자이너용: category_values는 카테고리 드롭다운용이므로 제외
|
// 🆕 화면 디자이너용: table_column_category_values는 카테고리 드롭다운용이므로 제외
|
||||||
// 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨
|
// 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨
|
||||||
const joinConfigs = allJoinConfigs.filter(
|
const joinConfigs = allJoinConfigs.filter(
|
||||||
(config) => config.referenceTable !== "category_values"
|
(config) => config.referenceTable !== "table_column_category_values"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (joinConfigs.length === 0) {
|
if (joinConfigs.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,470 +0,0 @@
|
||||||
import { Response } from "express";
|
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
|
||||||
import { query } from "../database/db";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 금형 마스터 CRUD
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export async function getMoldList(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { mold_code, mold_name, mold_type, operation_status } = req.query;
|
|
||||||
|
|
||||||
const conditions: string[] = [];
|
|
||||||
const params: any[] = [];
|
|
||||||
let paramIndex = 1;
|
|
||||||
|
|
||||||
if (companyCode === "*") {
|
|
||||||
// 최고 관리자: 전체 조회
|
|
||||||
} else {
|
|
||||||
conditions.push(`company_code = $${paramIndex}`);
|
|
||||||
params.push(companyCode);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mold_code) {
|
|
||||||
conditions.push(`mold_code ILIKE $${paramIndex}`);
|
|
||||||
params.push(`%${mold_code}%`);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
if (mold_name) {
|
|
||||||
conditions.push(`mold_name ILIKE $${paramIndex}`);
|
|
||||||
params.push(`%${mold_name}%`);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
if (mold_type) {
|
|
||||||
conditions.push(`mold_type = $${paramIndex}`);
|
|
||||||
params.push(mold_type);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
if (operation_status) {
|
|
||||||
conditions.push(`operation_status = $${paramIndex}`);
|
|
||||||
params.push(operation_status);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
||||||
const sql = `SELECT * FROM mold_mng ${whereClause} ORDER BY created_date DESC`;
|
|
||||||
const result = await query(sql, params);
|
|
||||||
|
|
||||||
logger.info("금형 목록 조회", { companyCode, count: result.length });
|
|
||||||
res.json({ success: true, data: result });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("금형 목록 조회 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMoldDetail(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
|
|
||||||
let sql: string;
|
|
||||||
let params: any[];
|
|
||||||
|
|
||||||
if (companyCode === "*") {
|
|
||||||
sql = `SELECT * FROM mold_mng WHERE mold_code = $1 LIMIT 1`;
|
|
||||||
params = [moldCode];
|
|
||||||
} else {
|
|
||||||
sql = `SELECT * FROM mold_mng WHERE mold_code = $1 AND company_code = $2 LIMIT 1`;
|
|
||||||
params = [moldCode, companyCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query(sql, params);
|
|
||||||
if (result.length === 0) {
|
|
||||||
res.status(404).json({ success: false, message: "금형을 찾을 수 없습니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, data: result[0] });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("금형 상세 조회 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMold(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const userId = req.user!.userId;
|
|
||||||
const {
|
|
||||||
mold_code, mold_name, mold_type, category, manufacturer,
|
|
||||||
manufacturing_number, manufacturing_date, cavity_count,
|
|
||||||
shot_count, mold_quantity, base_input_qty, operation_status,
|
|
||||||
remarks, image_path, memo,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (!mold_code || !mold_name) {
|
|
||||||
res.status(400).json({ success: false, message: "금형코드와 금형명은 필수입니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO mold_mng (
|
|
||||||
company_code, mold_code, mold_name, mold_type, category,
|
|
||||||
manufacturer, manufacturing_number, manufacturing_date,
|
|
||||||
cavity_count, shot_count, mold_quantity, base_input_qty,
|
|
||||||
operation_status, remarks, image_path, memo, writer
|
|
||||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const params = [
|
|
||||||
companyCode, mold_code, mold_name, mold_type || null, category || null,
|
|
||||||
manufacturer || null, manufacturing_number || null, manufacturing_date || null,
|
|
||||||
cavity_count || 0, shot_count || 0, mold_quantity || 1, base_input_qty || 0,
|
|
||||||
operation_status || "ACTIVE", remarks || null, image_path || null, memo || null, userId,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await query(sql, params);
|
|
||||||
logger.info("금형 생성", { companyCode, moldCode: mold_code });
|
|
||||||
res.json({ success: true, data: result[0], message: "금형이 등록되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === "23505") {
|
|
||||||
res.status(409).json({ success: false, message: "이미 존재하는 금형코드입니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.error("금형 생성 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateMold(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
const {
|
|
||||||
mold_name, mold_type, category, manufacturer,
|
|
||||||
manufacturing_number, manufacturing_date, cavity_count,
|
|
||||||
shot_count, mold_quantity, base_input_qty, operation_status,
|
|
||||||
remarks, image_path, memo,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
UPDATE mold_mng SET
|
|
||||||
mold_name = COALESCE($1, mold_name),
|
|
||||||
mold_type = $2, category = $3, manufacturer = $4,
|
|
||||||
manufacturing_number = $5, manufacturing_date = $6,
|
|
||||||
cavity_count = COALESCE($7, cavity_count),
|
|
||||||
shot_count = COALESCE($8, shot_count),
|
|
||||||
mold_quantity = COALESCE($9, mold_quantity),
|
|
||||||
base_input_qty = COALESCE($10, base_input_qty),
|
|
||||||
operation_status = COALESCE($11, operation_status),
|
|
||||||
remarks = $12, image_path = $13, memo = $14,
|
|
||||||
updated_date = NOW()
|
|
||||||
WHERE mold_code = $15 AND company_code = $16
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const params = [
|
|
||||||
mold_name, mold_type, category, manufacturer,
|
|
||||||
manufacturing_number, manufacturing_date,
|
|
||||||
cavity_count, shot_count, mold_quantity, base_input_qty,
|
|
||||||
operation_status, remarks, image_path, memo,
|
|
||||||
moldCode, companyCode,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await query(sql, params);
|
|
||||||
if (result.length === 0) {
|
|
||||||
res.status(404).json({ success: false, message: "금형을 찾을 수 없습니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("금형 수정", { companyCode, moldCode });
|
|
||||||
res.json({ success: true, data: result[0], message: "금형이 수정되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("금형 수정 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteMold(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
|
|
||||||
// 관련 데이터 먼저 삭제
|
|
||||||
await query(`DELETE FROM mold_serial WHERE mold_code = $1 AND company_code = $2`, [moldCode, companyCode]);
|
|
||||||
await query(`DELETE FROM mold_inspection_item WHERE mold_code = $1 AND company_code = $2`, [moldCode, companyCode]);
|
|
||||||
await query(`DELETE FROM mold_part WHERE mold_code = $1 AND company_code = $2`, [moldCode, companyCode]);
|
|
||||||
|
|
||||||
const result = await query(
|
|
||||||
`DELETE FROM mold_mng WHERE mold_code = $1 AND company_code = $2 RETURNING id`,
|
|
||||||
[moldCode, companyCode]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
res.status(404).json({ success: false, message: "금형을 찾을 수 없습니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("금형 삭제", { companyCode, moldCode });
|
|
||||||
res.json({ success: true, message: "금형이 삭제되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("금형 삭제 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 일련번호 CRUD
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export async function getMoldSerials(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
|
|
||||||
const sql = `SELECT * FROM mold_serial WHERE mold_code = $1 AND company_code = $2 ORDER BY serial_number`;
|
|
||||||
const result = await query(sql, [moldCode, companyCode]);
|
|
||||||
|
|
||||||
res.json({ success: true, data: result });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("일련번호 목록 조회 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMoldSerial(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const userId = req.user!.userId;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
const { serial_number, status, progress, work_description, manager, completion_date, remarks } = req.body;
|
|
||||||
|
|
||||||
if (!serial_number) {
|
|
||||||
res.status(400).json({ success: false, message: "일련번호는 필수입니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO mold_serial (company_code, mold_code, serial_number, status, progress, work_description, manager, completion_date, remarks, writer)
|
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const params = [
|
|
||||||
companyCode, moldCode, serial_number, status || "STORED",
|
|
||||||
progress || 0, work_description || null, manager || null,
|
|
||||||
completion_date || null, remarks || null, userId,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await query(sql, params);
|
|
||||||
res.json({ success: true, data: result[0], message: "일련번호가 등록되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === "23505") {
|
|
||||||
res.status(409).json({ success: false, message: "이미 존재하는 일련번호입니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.error("일련번호 생성 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteMoldSerial(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const result = await query(
|
|
||||||
`DELETE FROM mold_serial WHERE id = $1 AND company_code = $2 RETURNING id`,
|
|
||||||
[id, companyCode]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
res.status(404).json({ success: false, message: "일련번호를 찾을 수 없습니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, message: "일련번호가 삭제되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("일련번호 삭제 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 점검항목 CRUD
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export async function getMoldInspections(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
|
|
||||||
const sql = `SELECT * FROM mold_inspection_item WHERE mold_code = $1 AND company_code = $2 ORDER BY created_date`;
|
|
||||||
const result = await query(sql, [moldCode, companyCode]);
|
|
||||||
|
|
||||||
res.json({ success: true, data: result });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("점검항목 조회 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMoldInspection(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const userId = req.user!.userId;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
const {
|
|
||||||
inspection_item, inspection_cycle, inspection_method,
|
|
||||||
inspection_content, lower_limit, upper_limit, unit,
|
|
||||||
is_active, checklist, remarks,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (!inspection_item) {
|
|
||||||
res.status(400).json({ success: false, message: "점검항목명은 필수입니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO mold_inspection_item (
|
|
||||||
company_code, mold_code, inspection_item, inspection_cycle,
|
|
||||||
inspection_method, inspection_content, lower_limit, upper_limit,
|
|
||||||
unit, is_active, checklist, remarks, writer
|
|
||||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const params = [
|
|
||||||
companyCode, moldCode, inspection_item, inspection_cycle || null,
|
|
||||||
inspection_method || null, inspection_content || null,
|
|
||||||
lower_limit || null, upper_limit || null, unit || null,
|
|
||||||
is_active || "Y", checklist || null, remarks || null, userId,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await query(sql, params);
|
|
||||||
res.json({ success: true, data: result[0], message: "점검항목이 등록되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("점검항목 생성 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteMoldInspection(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const result = await query(
|
|
||||||
`DELETE FROM mold_inspection_item WHERE id = $1 AND company_code = $2 RETURNING id`,
|
|
||||||
[id, companyCode]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
res.status(404).json({ success: false, message: "점검항목을 찾을 수 없습니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, message: "점검항목이 삭제되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("점검항목 삭제 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 부품 CRUD
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export async function getMoldParts(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
|
|
||||||
const sql = `SELECT * FROM mold_part WHERE mold_code = $1 AND company_code = $2 ORDER BY created_date`;
|
|
||||||
const result = await query(sql, [moldCode, companyCode]);
|
|
||||||
|
|
||||||
res.json({ success: true, data: result });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("부품 목록 조회 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMoldPart(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const userId = req.user!.userId;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
const {
|
|
||||||
part_name, replacement_cycle, unit, specification,
|
|
||||||
manufacturer, manufacturer_code, image_path, remarks,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (!part_name) {
|
|
||||||
res.status(400).json({ success: false, message: "부품명은 필수입니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO mold_part (
|
|
||||||
company_code, mold_code, part_name, replacement_cycle,
|
|
||||||
unit, specification, manufacturer, manufacturer_code,
|
|
||||||
image_path, remarks, writer
|
|
||||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const params = [
|
|
||||||
companyCode, moldCode, part_name, replacement_cycle || null,
|
|
||||||
unit || null, specification || null, manufacturer || null,
|
|
||||||
manufacturer_code || null, image_path || null, remarks || null, userId,
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await query(sql, params);
|
|
||||||
res.json({ success: true, data: result[0], message: "부품이 등록되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("부품 생성 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteMoldPart(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const result = await query(
|
|
||||||
`DELETE FROM mold_part WHERE id = $1 AND company_code = $2 RETURNING id`,
|
|
||||||
[id, companyCode]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
res.status(404).json({ success: false, message: "부품을 찾을 수 없습니다." });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, message: "부품이 삭제되었습니다." });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("부품 삭제 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 일련번호 현황 집계
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
export async function getMoldSerialSummary(req: AuthenticatedRequest, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const companyCode = req.user!.companyCode;
|
|
||||||
const { moldCode } = req.params;
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total,
|
|
||||||
COUNT(*) FILTER (WHERE status = 'IN_USE') as in_use,
|
|
||||||
COUNT(*) FILTER (WHERE status = 'REPAIR') as repair,
|
|
||||||
COUNT(*) FILTER (WHERE status = 'STORED') as stored,
|
|
||||||
COUNT(*) FILTER (WHERE status = 'DISPOSED') as disposed
|
|
||||||
FROM mold_serial
|
|
||||||
WHERE mold_code = $1 AND company_code = $2
|
|
||||||
`;
|
|
||||||
const result = await query(sql, [moldCode, companyCode]);
|
|
||||||
|
|
||||||
res.json({ success: true, data: result[0] });
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("일련번호 현황 조회 오류", error);
|
|
||||||
res.status(500).json({ success: false, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
|
||||||
import {
|
|
||||||
getMoldList,
|
|
||||||
getMoldDetail,
|
|
||||||
createMold,
|
|
||||||
updateMold,
|
|
||||||
deleteMold,
|
|
||||||
getMoldSerials,
|
|
||||||
createMoldSerial,
|
|
||||||
deleteMoldSerial,
|
|
||||||
getMoldInspections,
|
|
||||||
createMoldInspection,
|
|
||||||
deleteMoldInspection,
|
|
||||||
getMoldParts,
|
|
||||||
createMoldPart,
|
|
||||||
deleteMoldPart,
|
|
||||||
getMoldSerialSummary,
|
|
||||||
} from "../controllers/moldController";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
router.use(authenticateToken);
|
|
||||||
|
|
||||||
// 금형 마스터
|
|
||||||
router.get("/", getMoldList);
|
|
||||||
router.get("/:moldCode", getMoldDetail);
|
|
||||||
router.post("/", createMold);
|
|
||||||
router.put("/:moldCode", updateMold);
|
|
||||||
router.delete("/:moldCode", deleteMold);
|
|
||||||
|
|
||||||
// 일련번호
|
|
||||||
router.get("/:moldCode/serials", getMoldSerials);
|
|
||||||
router.post("/:moldCode/serials", createMoldSerial);
|
|
||||||
router.delete("/serials/:id", deleteMoldSerial);
|
|
||||||
|
|
||||||
// 일련번호 현황 집계
|
|
||||||
router.get("/:moldCode/serial-summary", getMoldSerialSummary);
|
|
||||||
|
|
||||||
// 점검항목
|
|
||||||
router.get("/:moldCode/inspections", getMoldInspections);
|
|
||||||
router.post("/:moldCode/inspections", createMoldInspection);
|
|
||||||
router.delete("/inspections/:id", deleteMoldInspection);
|
|
||||||
|
|
||||||
// 부품
|
|
||||||
router.get("/:moldCode/parts", getMoldParts);
|
|
||||||
router.post("/:moldCode/parts", createMoldPart);
|
|
||||||
router.delete("/parts/:id", deleteMoldPart);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
@ -92,7 +92,7 @@ export class EntityJoinService {
|
||||||
|
|
||||||
if (column.input_type === "category") {
|
if (column.input_type === "category") {
|
||||||
// 카테고리 타입: reference 정보가 비어있어도 자동 설정
|
// 카테고리 타입: reference 정보가 비어있어도 자동 설정
|
||||||
referenceTable = referenceTable || "category_values";
|
referenceTable = referenceTable || "table_column_category_values";
|
||||||
referenceColumn = referenceColumn || "value_code";
|
referenceColumn = referenceColumn || "value_code";
|
||||||
displayColumn = displayColumn || "value_label";
|
displayColumn = displayColumn || "value_label";
|
||||||
|
|
||||||
|
|
@ -308,7 +308,7 @@ export class EntityJoinService {
|
||||||
const usedAliasesForColumns = new Set<string>();
|
const usedAliasesForColumns = new Set<string>();
|
||||||
|
|
||||||
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
|
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
|
||||||
// (category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요)
|
// (table_column_category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요)
|
||||||
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
|
||||||
if (
|
if (
|
||||||
!acc.some(
|
!acc.some(
|
||||||
|
|
@ -336,7 +336,7 @@ export class EntityJoinService {
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
usedAliasesForColumns.add(alias);
|
usedAliasesForColumns.add(alias);
|
||||||
// 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (category_values 대응)
|
// 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (table_column_category_values 대응)
|
||||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
aliasMap.set(aliasKey, alias);
|
aliasMap.set(aliasKey, alias);
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -455,10 +455,9 @@ export class EntityJoinService {
|
||||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
const alias = aliasMap.get(aliasKey);
|
const alias = aliasMap.get(aliasKey);
|
||||||
|
|
||||||
// category_values는 특별한 조인 조건 필요 (회사별 필터링)
|
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링)
|
||||||
// is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
|
if (config.referenceTable === "table_column_category_values") {
|
||||||
if (config.referenceTable === "category_values") {
|
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`;
|
||||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// user_info는 전역 테이블이므로 company_code 조건 없이 조인
|
// user_info는 전역 테이블이므로 company_code 조건 없이 조인
|
||||||
|
|
@ -529,10 +528,10 @@ export class EntityJoinService {
|
||||||
return "join";
|
return "join";
|
||||||
}
|
}
|
||||||
|
|
||||||
// category_values는 특수 조인 조건이 필요하므로 캐시 불가
|
// table_column_category_values는 특수 조인 조건이 필요하므로 캐시 불가
|
||||||
if (config.referenceTable === "category_values") {
|
if (config.referenceTable === "table_column_category_values") {
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 category_values는 캐시 전략 불가: ${config.sourceColumn}`
|
`🎯 table_column_category_values는 캐시 전략 불가: ${config.sourceColumn}`
|
||||||
);
|
);
|
||||||
return "join";
|
return "join";
|
||||||
}
|
}
|
||||||
|
|
@ -724,10 +723,10 @@ export class EntityJoinService {
|
||||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||||
const alias = aliasMap.get(aliasKey);
|
const alias = aliasMap.get(aliasKey);
|
||||||
|
|
||||||
// category_values는 특별한 조인 조건 필요 (회사별 필터링만)
|
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만)
|
||||||
// is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
|
if (config.referenceTable === "table_column_category_values") {
|
||||||
if (config.referenceTable === "category_values") {
|
// 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외)
|
||||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code`;
|
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT`;
|
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT`;
|
||||||
|
|
|
||||||
|
|
@ -3098,7 +3098,7 @@ export class MenuCopyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allValuesResult = await client.query(
|
const allValuesResult = await client.query(
|
||||||
`SELECT * FROM category_values
|
`SELECT * FROM table_column_category_values
|
||||||
WHERE company_code = $1
|
WHERE company_code = $1
|
||||||
AND (${columnConditions.join(" OR ")})
|
AND (${columnConditions.join(" OR ")})
|
||||||
ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`,
|
ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`,
|
||||||
|
|
@ -3115,7 +3115,7 @@ export class MenuCopyService {
|
||||||
// 5. 대상 회사에 이미 존재하는 값 한 번에 조회
|
// 5. 대상 회사에 이미 존재하는 값 한 번에 조회
|
||||||
const existingValuesResult = await client.query(
|
const existingValuesResult = await client.query(
|
||||||
`SELECT value_id, table_name, column_name, value_code
|
`SELECT value_id, table_name, column_name, value_code
|
||||||
FROM category_values WHERE company_code = $1`,
|
FROM table_column_category_values WHERE company_code = $1`,
|
||||||
[targetCompanyCode]
|
[targetCompanyCode]
|
||||||
);
|
);
|
||||||
const existingValueKeys = new Map(
|
const existingValueKeys = new Map(
|
||||||
|
|
@ -3194,7 +3194,7 @@ export class MenuCopyService {
|
||||||
});
|
});
|
||||||
|
|
||||||
const insertResult = await client.query(
|
const insertResult = await client.query(
|
||||||
`INSERT INTO category_values (
|
`INSERT INTO table_column_category_values (
|
||||||
table_name, column_name, value_code, value_label, value_order,
|
table_name, column_name, value_code, value_label, value_order,
|
||||||
parent_value_id, depth, description, color, icon,
|
parent_value_id, depth, description, color, icon,
|
||||||
is_active, is_default, created_at, created_by, company_code, menu_objid
|
is_active, is_default, created_at, created_by, company_code, menu_objid
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class TableCategoryValueService {
|
||||||
tc.column_name AS "columnLabel",
|
tc.column_name AS "columnLabel",
|
||||||
COUNT(cv.value_id) AS "valueCount"
|
COUNT(cv.value_id) AS "valueCount"
|
||||||
FROM table_type_columns tc
|
FROM table_type_columns tc
|
||||||
LEFT JOIN category_values cv
|
LEFT JOIN table_column_category_values cv
|
||||||
ON tc.table_name = cv.table_name
|
ON tc.table_name = cv.table_name
|
||||||
AND tc.column_name = cv.column_name
|
AND tc.column_name = cv.column_name
|
||||||
AND cv.is_active = true
|
AND cv.is_active = true
|
||||||
|
|
@ -50,7 +50,7 @@ class TableCategoryValueService {
|
||||||
tc.column_name AS "columnLabel",
|
tc.column_name AS "columnLabel",
|
||||||
COUNT(cv.value_id) AS "valueCount"
|
COUNT(cv.value_id) AS "valueCount"
|
||||||
FROM table_type_columns tc
|
FROM table_type_columns tc
|
||||||
LEFT JOIN category_values cv
|
LEFT JOIN table_column_category_values cv
|
||||||
ON tc.table_name = cv.table_name
|
ON tc.table_name = cv.table_name
|
||||||
AND tc.column_name = cv.column_name
|
AND tc.column_name = cv.column_name
|
||||||
AND cv.is_active = true
|
AND cv.is_active = true
|
||||||
|
|
@ -110,7 +110,7 @@ class TableCategoryValueService {
|
||||||
) tc
|
) tc
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT table_name, column_name, COUNT(*) as cnt
|
SELECT table_name, column_name, COUNT(*) as cnt
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE is_active = true
|
WHERE is_active = true
|
||||||
GROUP BY table_name, column_name
|
GROUP BY table_name, column_name
|
||||||
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
|
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
|
||||||
|
|
@ -133,7 +133,7 @@ class TableCategoryValueService {
|
||||||
) tc
|
) tc
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT table_name, column_name, COUNT(*) as cnt
|
SELECT table_name, column_name, COUNT(*) as cnt
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE is_active = true AND company_code = $1
|
WHERE is_active = true AND company_code = $1
|
||||||
GROUP BY table_name, column_name
|
GROUP BY table_name, column_name
|
||||||
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
|
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
|
||||||
|
|
@ -207,7 +207,7 @@ class TableCategoryValueService {
|
||||||
is_active AS "isActive",
|
is_active AS "isActive",
|
||||||
is_default AS "isDefault",
|
is_default AS "isDefault",
|
||||||
company_code AS "companyCode",
|
company_code AS "companyCode",
|
||||||
menu_objid AS "menuObjid",
|
NULL::numeric AS "menuObjid",
|
||||||
created_at AS "createdAt",
|
created_at AS "createdAt",
|
||||||
updated_at AS "updatedAt",
|
updated_at AS "updatedAt",
|
||||||
created_by AS "createdBy",
|
created_by AS "createdBy",
|
||||||
|
|
@ -289,7 +289,7 @@ class TableCategoryValueService {
|
||||||
// 최고 관리자: 모든 회사에서 중복 체크
|
// 최고 관리자: 모든 회사에서 중복 체크
|
||||||
duplicateQuery = `
|
duplicateQuery = `
|
||||||
SELECT value_id
|
SELECT value_id
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND value_code = $3
|
AND value_code = $3
|
||||||
|
|
@ -300,7 +300,7 @@ class TableCategoryValueService {
|
||||||
// 일반 회사: 자신의 회사에서만 중복 체크
|
// 일반 회사: 자신의 회사에서만 중복 체크
|
||||||
duplicateQuery = `
|
duplicateQuery = `
|
||||||
SELECT value_id
|
SELECT value_id
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND value_code = $3
|
AND value_code = $3
|
||||||
|
|
@ -316,41 +316,8 @@ class TableCategoryValueService {
|
||||||
throw new Error("이미 존재하는 코드입니다");
|
throw new Error("이미 존재하는 코드입니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 중복 체크 (같은 테이블+컬럼+회사에서 동일한 라벨명 방지)
|
|
||||||
let labelDupQuery: string;
|
|
||||||
let labelDupParams: any[];
|
|
||||||
|
|
||||||
if (companyCode === "*") {
|
|
||||||
labelDupQuery = `
|
|
||||||
SELECT value_id
|
|
||||||
FROM category_values
|
|
||||||
WHERE table_name = $1
|
|
||||||
AND column_name = $2
|
|
||||||
AND value_label = $3
|
|
||||||
AND is_active = true
|
|
||||||
`;
|
|
||||||
labelDupParams = [value.tableName, value.columnName, value.valueLabel];
|
|
||||||
} else {
|
|
||||||
labelDupQuery = `
|
|
||||||
SELECT value_id
|
|
||||||
FROM category_values
|
|
||||||
WHERE table_name = $1
|
|
||||||
AND column_name = $2
|
|
||||||
AND value_label = $3
|
|
||||||
AND company_code = $4
|
|
||||||
AND is_active = true
|
|
||||||
`;
|
|
||||||
labelDupParams = [value.tableName, value.columnName, value.valueLabel, companyCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelDupResult = await pool.query(labelDupQuery, labelDupParams);
|
|
||||||
|
|
||||||
if (labelDupResult.rows.length > 0) {
|
|
||||||
throw new Error(`이미 동일한 이름의 카테고리 값이 존재합니다: "${value.valueLabel}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertQuery = `
|
const insertQuery = `
|
||||||
INSERT INTO category_values (
|
INSERT INTO table_column_category_values (
|
||||||
table_name, column_name, value_code, value_label, value_order,
|
table_name, column_name, value_code, value_label, value_order,
|
||||||
parent_value_id, depth, description, color, icon,
|
parent_value_id, depth, description, color, icon,
|
||||||
is_active, is_default, company_code, menu_objid, created_by
|
is_active, is_default, company_code, menu_objid, created_by
|
||||||
|
|
@ -458,32 +425,6 @@ class TableCategoryValueService {
|
||||||
values.push(updates.isDefault);
|
values.push(updates.isDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 수정 시 중복 체크 (자기 자신 제외)
|
|
||||||
if (updates.valueLabel !== undefined) {
|
|
||||||
const currentRow = await pool.query(
|
|
||||||
`SELECT table_name, column_name, company_code FROM category_values WHERE value_id = $1`,
|
|
||||||
[valueId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentRow.rows.length > 0) {
|
|
||||||
const { table_name, column_name, company_code } = currentRow.rows[0];
|
|
||||||
const labelDupResult = await pool.query(
|
|
||||||
`SELECT value_id FROM category_values
|
|
||||||
WHERE table_name = $1
|
|
||||||
AND column_name = $2
|
|
||||||
AND value_label = $3
|
|
||||||
AND company_code = $4
|
|
||||||
AND is_active = true
|
|
||||||
AND value_id != $5`,
|
|
||||||
[table_name, column_name, updates.valueLabel, company_code, valueId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (labelDupResult.rows.length > 0) {
|
|
||||||
throw new Error(`이미 동일한 이름의 카테고리 값이 존재합니다: "${updates.valueLabel}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setClauses.push(`updated_at = NOW()`);
|
setClauses.push(`updated_at = NOW()`);
|
||||||
setClauses.push(`updated_by = $${paramIndex++}`);
|
setClauses.push(`updated_by = $${paramIndex++}`);
|
||||||
values.push(userId);
|
values.push(userId);
|
||||||
|
|
@ -495,7 +436,7 @@ class TableCategoryValueService {
|
||||||
// 최고 관리자: 모든 카테고리 값 수정 가능
|
// 최고 관리자: 모든 카테고리 값 수정 가능
|
||||||
values.push(valueId);
|
values.push(valueId);
|
||||||
updateQuery = `
|
updateQuery = `
|
||||||
UPDATE category_values
|
UPDATE table_column_category_values
|
||||||
SET ${setClauses.join(", ")}
|
SET ${setClauses.join(", ")}
|
||||||
WHERE value_id = $${paramIndex++}
|
WHERE value_id = $${paramIndex++}
|
||||||
RETURNING
|
RETURNING
|
||||||
|
|
@ -518,7 +459,7 @@ class TableCategoryValueService {
|
||||||
// 일반 회사: 자신의 카테고리 값만 수정 가능
|
// 일반 회사: 자신의 카테고리 값만 수정 가능
|
||||||
values.push(valueId, companyCode);
|
values.push(valueId, companyCode);
|
||||||
updateQuery = `
|
updateQuery = `
|
||||||
UPDATE category_values
|
UPDATE table_column_category_values
|
||||||
SET ${setClauses.join(", ")}
|
SET ${setClauses.join(", ")}
|
||||||
WHERE value_id = $${paramIndex++}
|
WHERE value_id = $${paramIndex++}
|
||||||
AND company_code = $${paramIndex++}
|
AND company_code = $${paramIndex++}
|
||||||
|
|
@ -575,14 +516,14 @@ class TableCategoryValueService {
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
valueQuery = `
|
valueQuery = `
|
||||||
SELECT table_name, column_name, value_code
|
SELECT table_name, column_name, value_code
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE value_id = $1
|
WHERE value_id = $1
|
||||||
`;
|
`;
|
||||||
valueParams = [valueId];
|
valueParams = [valueId];
|
||||||
} else {
|
} else {
|
||||||
valueQuery = `
|
valueQuery = `
|
||||||
SELECT table_name, column_name, value_code
|
SELECT table_name, column_name, value_code
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE value_id = $1
|
WHERE value_id = $1
|
||||||
AND company_code = $2
|
AND company_code = $2
|
||||||
`;
|
`;
|
||||||
|
|
@ -694,10 +635,10 @@ class TableCategoryValueService {
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
query = `
|
query = `
|
||||||
WITH RECURSIVE category_tree AS (
|
WITH RECURSIVE category_tree AS (
|
||||||
SELECT value_id FROM category_values WHERE parent_value_id = $1
|
SELECT value_id FROM table_column_category_values WHERE parent_value_id = $1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT cv.value_id
|
SELECT cv.value_id
|
||||||
FROM category_values cv
|
FROM table_column_category_values cv
|
||||||
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
||||||
)
|
)
|
||||||
SELECT value_id FROM category_tree
|
SELECT value_id FROM category_tree
|
||||||
|
|
@ -706,11 +647,11 @@ class TableCategoryValueService {
|
||||||
} else {
|
} else {
|
||||||
query = `
|
query = `
|
||||||
WITH RECURSIVE category_tree AS (
|
WITH RECURSIVE category_tree AS (
|
||||||
SELECT value_id FROM category_values
|
SELECT value_id FROM table_column_category_values
|
||||||
WHERE parent_value_id = $1 AND company_code = $2
|
WHERE parent_value_id = $1 AND company_code = $2
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT cv.value_id
|
SELECT cv.value_id
|
||||||
FROM category_values cv
|
FROM table_column_category_values cv
|
||||||
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
|
||||||
WHERE cv.company_code = $2
|
WHERE cv.company_code = $2
|
||||||
)
|
)
|
||||||
|
|
@ -756,10 +697,10 @@ class TableCategoryValueService {
|
||||||
let labelParams: any[];
|
let labelParams: any[];
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
labelQuery = `SELECT value_label FROM category_values WHERE value_id = $1`;
|
labelQuery = `SELECT value_label FROM table_column_category_values WHERE value_id = $1`;
|
||||||
labelParams = [id];
|
labelParams = [id];
|
||||||
} else {
|
} else {
|
||||||
labelQuery = `SELECT value_label FROM category_values WHERE value_id = $1 AND company_code = $2`;
|
labelQuery = `SELECT value_label FROM table_column_category_values WHERE value_id = $1 AND company_code = $2`;
|
||||||
labelParams = [id, companyCode];
|
labelParams = [id, companyCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -789,10 +730,10 @@ class TableCategoryValueService {
|
||||||
let deleteParams: any[];
|
let deleteParams: any[];
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
deleteQuery = `DELETE FROM category_values WHERE value_id = $1`;
|
deleteQuery = `DELETE FROM table_column_category_values WHERE value_id = $1`;
|
||||||
deleteParams = [id];
|
deleteParams = [id];
|
||||||
} else {
|
} else {
|
||||||
deleteQuery = `DELETE FROM category_values WHERE value_id = $1 AND company_code = $2`;
|
deleteQuery = `DELETE FROM table_column_category_values WHERE value_id = $1 AND company_code = $2`;
|
||||||
deleteParams = [id, companyCode];
|
deleteParams = [id, companyCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,7 +770,7 @@ class TableCategoryValueService {
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 카테고리 값 일괄 삭제 가능
|
// 최고 관리자: 모든 카테고리 값 일괄 삭제 가능
|
||||||
deleteQuery = `
|
deleteQuery = `
|
||||||
UPDATE category_values
|
UPDATE table_column_category_values
|
||||||
SET is_active = false, updated_at = NOW(), updated_by = $2
|
SET is_active = false, updated_at = NOW(), updated_by = $2
|
||||||
WHERE value_id = ANY($1::int[])
|
WHERE value_id = ANY($1::int[])
|
||||||
`;
|
`;
|
||||||
|
|
@ -837,7 +778,7 @@ class TableCategoryValueService {
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값만 일괄 삭제 가능
|
// 일반 회사: 자신의 카테고리 값만 일괄 삭제 가능
|
||||||
deleteQuery = `
|
deleteQuery = `
|
||||||
UPDATE category_values
|
UPDATE table_column_category_values
|
||||||
SET is_active = false, updated_at = NOW(), updated_by = $3
|
SET is_active = false, updated_at = NOW(), updated_by = $3
|
||||||
WHERE value_id = ANY($1::int[])
|
WHERE value_id = ANY($1::int[])
|
||||||
AND company_code = $2
|
AND company_code = $2
|
||||||
|
|
@ -878,7 +819,7 @@ class TableCategoryValueService {
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 카테고리 값 순서 변경 가능
|
// 최고 관리자: 모든 카테고리 값 순서 변경 가능
|
||||||
updateQuery = `
|
updateQuery = `
|
||||||
UPDATE category_values
|
UPDATE table_column_category_values
|
||||||
SET value_order = $1, updated_at = NOW()
|
SET value_order = $1, updated_at = NOW()
|
||||||
WHERE value_id = $2
|
WHERE value_id = $2
|
||||||
`;
|
`;
|
||||||
|
|
@ -886,7 +827,7 @@ class TableCategoryValueService {
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값만 순서 변경 가능
|
// 일반 회사: 자신의 카테고리 값만 순서 변경 가능
|
||||||
updateQuery = `
|
updateQuery = `
|
||||||
UPDATE category_values
|
UPDATE table_column_category_values
|
||||||
SET value_order = $1, updated_at = NOW()
|
SET value_order = $1, updated_at = NOW()
|
||||||
WHERE value_id = $2
|
WHERE value_id = $2
|
||||||
AND company_code = $3
|
AND company_code = $3
|
||||||
|
|
@ -1438,23 +1379,48 @@ class TableCategoryValueService {
|
||||||
let query: string;
|
let query: string;
|
||||||
let params: any[];
|
let params: any[];
|
||||||
|
|
||||||
// is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
|
// 최고 관리자: 두 테이블 모두에서 조회 (UNION으로 병합)
|
||||||
|
// 두 번째 쿼리용 플레이스홀더: $n+1 ~ $2n
|
||||||
|
const placeholders2 = valueCodes.map((_, i) => `$${n + i + 1}`).join(", ");
|
||||||
query = `
|
query = `
|
||||||
SELECT DISTINCT value_code, value_label
|
SELECT value_code, value_label FROM (
|
||||||
FROM category_values
|
SELECT value_code, value_label
|
||||||
WHERE value_code IN (${placeholders1})
|
FROM table_column_category_values
|
||||||
|
WHERE value_code IN (${placeholders1})
|
||||||
|
AND is_active = true
|
||||||
|
UNION ALL
|
||||||
|
SELECT value_code, value_label
|
||||||
|
FROM category_values
|
||||||
|
WHERE value_code IN (${placeholders2})
|
||||||
|
AND is_active = true
|
||||||
|
) combined
|
||||||
`;
|
`;
|
||||||
params = [...valueCodes];
|
params = [...valueCodes, ...valueCodes];
|
||||||
} else {
|
} else {
|
||||||
const companyIdx = n + 1;
|
// 일반 회사: 두 테이블에서 자신의 카테고리 값 + 공통 카테고리 값 조회
|
||||||
|
// 첫 번째: $1~$n (valueCodes), $n+1 (companyCode)
|
||||||
|
// 두 번째: $n+2~$2n+1 (valueCodes), $2n+2 (companyCode)
|
||||||
|
const companyIdx1 = n + 1;
|
||||||
|
const placeholders2 = valueCodes.map((_, i) => `$${n + 1 + i + 1}`).join(", ");
|
||||||
|
const companyIdx2 = 2 * n + 2;
|
||||||
|
|
||||||
query = `
|
query = `
|
||||||
SELECT DISTINCT value_code, value_label
|
SELECT value_code, value_label FROM (
|
||||||
FROM category_values
|
SELECT value_code, value_label
|
||||||
WHERE value_code IN (${placeholders1})
|
FROM table_column_category_values
|
||||||
AND (company_code = $${companyIdx} OR company_code = '*')
|
WHERE value_code IN (${placeholders1})
|
||||||
|
AND is_active = true
|
||||||
|
AND (company_code = $${companyIdx1} OR company_code = '*')
|
||||||
|
UNION ALL
|
||||||
|
SELECT value_code, value_label
|
||||||
|
FROM category_values
|
||||||
|
WHERE value_code IN (${placeholders2})
|
||||||
|
AND is_active = true
|
||||||
|
AND (company_code = $${companyIdx2} OR company_code = '*')
|
||||||
|
) combined
|
||||||
`;
|
`;
|
||||||
params = [...valueCodes, companyCode];
|
params = [...valueCodes, companyCode, ...valueCodes, companyCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
const result = await pool.query(query, params);
|
||||||
|
|
@ -1522,7 +1488,7 @@ class TableCategoryValueService {
|
||||||
// 최고 관리자: 모든 카테고리 값 조회
|
// 최고 관리자: 모든 카테고리 값 조회
|
||||||
query = `
|
query = `
|
||||||
SELECT value_code, value_label
|
SELECT value_code, value_label
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
|
|
@ -1532,7 +1498,7 @@ class TableCategoryValueService {
|
||||||
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
||||||
query = `
|
query = `
|
||||||
SELECT value_code, value_label
|
SELECT value_code, value_label
|
||||||
FROM category_values
|
FROM table_column_category_values
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
|
|
|
||||||
|
|
@ -3505,7 +3505,7 @@ export class TableManagementService {
|
||||||
const referenceTableColumns = new Map<string, string[]>();
|
const referenceTableColumns = new Map<string, string[]>();
|
||||||
const uniqueRefTables = new Set(
|
const uniqueRefTables = new Set(
|
||||||
joinConfigs
|
joinConfigs
|
||||||
.filter((c) => c.referenceTable !== "category_values") // 카테고리는 제외
|
.filter((c) => c.referenceTable !== "table_column_category_values") // 카테고리는 제외
|
||||||
.map((c) => `${c.referenceTable}:${c.sourceColumn}`)
|
.map((c) => `${c.referenceTable}:${c.sourceColumn}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -4310,8 +4310,8 @@ export class TableManagementService {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const config of joinConfigs) {
|
for (const config of joinConfigs) {
|
||||||
// category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
|
// table_column_category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
|
||||||
if (config.referenceTable === "category_values") {
|
if (config.referenceTable === "table_column_category_values") {
|
||||||
dbJoins.push(config);
|
dbJoins.push(config);
|
||||||
console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
|
console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
try {
|
try {
|
||||||
// menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용)
|
// menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용)
|
||||||
const queryParams = menuObjid ? `?menuObjid=${menuObjid}&includeInactive=true` : "?includeInactive=true";
|
const queryParams = menuObjid ? `?menuObjid=${menuObjid}` : "";
|
||||||
const response = await apiClient.get(
|
const response = await apiClient.get(
|
||||||
`/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`,
|
`/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* - 체크박스를 통한 다중 선택 및 일괄 삭제 지원
|
* - 체크박스를 통한 다중 선택 및 일괄 삭제 지원
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
|
@ -291,10 +291,6 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||||
const [editingValue, setEditingValue] = useState<CategoryValue | null>(null);
|
const [editingValue, setEditingValue] = useState<CategoryValue | null>(null);
|
||||||
const [deletingValue, setDeletingValue] = useState<CategoryValue | null>(null);
|
const [deletingValue, setDeletingValue] = useState<CategoryValue | null>(null);
|
||||||
|
|
||||||
// 추가 모달 input ref
|
|
||||||
const addNameRef = useRef<HTMLInputElement>(null);
|
|
||||||
const addDescRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// 폼 상태
|
// 폼 상태
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
valueCode: "",
|
valueCode: "",
|
||||||
|
|
@ -512,15 +508,7 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||||
const response = await createCategoryValue(input);
|
const response = await createCategoryValue(input);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success("카테고리가 추가되었습니다");
|
toast.success("카테고리가 추가되었습니다");
|
||||||
// 폼 초기화 (모달은 닫지 않고 연속 입력)
|
setIsAddModalOpen(false);
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
valueCode: "",
|
|
||||||
valueLabel: "",
|
|
||||||
description: "",
|
|
||||||
color: "",
|
|
||||||
}));
|
|
||||||
setTimeout(() => addNameRef.current?.focus(), 50);
|
|
||||||
// 기존 펼침 상태 유지하면서 데이터 새로고침
|
// 기존 펼침 상태 유지하면서 데이터 새로고침
|
||||||
await loadTree(true);
|
await loadTree(true);
|
||||||
// 부모 노드만 펼치기 (하위 추가 시)
|
// 부모 노드만 펼치기 (하위 추가 시)
|
||||||
|
|
@ -758,17 +746,9 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||||
이름 <span className="text-destructive">*</span>
|
이름 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
ref={addNameRef}
|
|
||||||
id="valueLabel"
|
id="valueLabel"
|
||||||
value={formData.valueLabel}
|
value={formData.valueLabel}
|
||||||
onChange={(e) => setFormData({ ...formData, valueLabel: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, valueLabel: e.target.value })}
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
addDescRef.current?.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="카테고리 이름을 입력하세요"
|
placeholder="카테고리 이름을 입력하세요"
|
||||||
className="h-9 text-sm"
|
className="h-9 text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
@ -779,17 +759,9 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||||
설명
|
설명
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
ref={addDescRef}
|
|
||||||
id="description"
|
id="description"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleAdd();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="선택 사항"
|
placeholder="선택 사항"
|
||||||
className="h-9 text-sm"
|
className="h-9 text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
@ -812,7 +784,7 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
||||||
onClick={() => setIsAddModalOpen(false)}
|
onClick={() => setIsAddModalOpen(false)}
|
||||||
className="h-9 flex-1 text-sm sm:flex-none"
|
className="h-9 flex-1 text-sm sm:flex-none"
|
||||||
>
|
>
|
||||||
닫기
|
취소
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAdd} className="h-9 flex-1 text-sm sm:flex-none">
|
<Button onClick={handleAdd} className="h-9 flex-1 text-sm sm:flex-none">
|
||||||
추가
|
추가
|
||||||
|
|
|
||||||
|
|
@ -796,7 +796,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
|
|
||||||
console.log(`📡 [RepeaterInput] 카테고리 매핑 로드: ${tableName}/${columnName}`);
|
console.log(`📡 [RepeaterInput] 카테고리 매핑 로드: ${tableName}/${columnName}`);
|
||||||
|
|
||||||
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
const mapping: Record<string, { label: string; color: string }> = {};
|
const mapping: Record<string, { label: string; color: string }> = {};
|
||||||
|
|
@ -838,7 +838,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
||||||
try {
|
try {
|
||||||
console.log(`📡 [RepeaterInput] 조인 테이블 카테고리 매핑 로드: ${joinedTableName}/${columnName}`);
|
console.log(`📡 [RepeaterInput] 조인 테이블 카테고리 매핑 로드: ${joinedTableName}/${columnName}`);
|
||||||
|
|
||||||
const response = await apiClient.get(`/table-categories/${joinedTableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${joinedTableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
const mapping: Record<string, { label: string; color: string }> = {};
|
const mapping: Record<string, { label: string; color: string }> = {};
|
||||||
|
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
import { apiClient } from "./client";
|
|
||||||
|
|
||||||
export interface MoldInfo {
|
|
||||||
id: string;
|
|
||||||
company_code: string;
|
|
||||||
mold_code: string;
|
|
||||||
mold_name: string;
|
|
||||||
mold_type: string | null;
|
|
||||||
category: string | null;
|
|
||||||
manufacturer: string | null;
|
|
||||||
manufacturing_number: string | null;
|
|
||||||
manufacturing_date: string | null;
|
|
||||||
cavity_count: number;
|
|
||||||
shot_count: number;
|
|
||||||
mold_quantity: number;
|
|
||||||
base_input_qty: number;
|
|
||||||
operation_status: string;
|
|
||||||
remarks: string | null;
|
|
||||||
image_path: string | null;
|
|
||||||
memo: string | null;
|
|
||||||
created_date: string;
|
|
||||||
updated_date: string;
|
|
||||||
writer: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoldSerial {
|
|
||||||
id: string;
|
|
||||||
company_code: string;
|
|
||||||
mold_code: string;
|
|
||||||
serial_number: string;
|
|
||||||
status: string;
|
|
||||||
progress: number;
|
|
||||||
work_description: string | null;
|
|
||||||
manager: string | null;
|
|
||||||
completion_date: string | null;
|
|
||||||
remarks: string | null;
|
|
||||||
created_date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoldInspectionItem {
|
|
||||||
id: string;
|
|
||||||
company_code: string;
|
|
||||||
mold_code: string;
|
|
||||||
inspection_item: string;
|
|
||||||
inspection_cycle: string | null;
|
|
||||||
inspection_method: string | null;
|
|
||||||
inspection_content: string | null;
|
|
||||||
lower_limit: string | null;
|
|
||||||
upper_limit: string | null;
|
|
||||||
unit: string | null;
|
|
||||||
is_active: string;
|
|
||||||
checklist: string | null;
|
|
||||||
remarks: string | null;
|
|
||||||
created_date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoldPart {
|
|
||||||
id: string;
|
|
||||||
company_code: string;
|
|
||||||
mold_code: string;
|
|
||||||
part_name: string;
|
|
||||||
replacement_cycle: string | null;
|
|
||||||
unit: string | null;
|
|
||||||
specification: string | null;
|
|
||||||
manufacturer: string | null;
|
|
||||||
manufacturer_code: string | null;
|
|
||||||
image_path: string | null;
|
|
||||||
remarks: string | null;
|
|
||||||
created_date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MoldSerialSummary {
|
|
||||||
total: number;
|
|
||||||
in_use: number;
|
|
||||||
repair: number;
|
|
||||||
stored: number;
|
|
||||||
disposed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
|
||||||
success: boolean;
|
|
||||||
data?: T;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResponse = async <T>(promise: Promise<any>): Promise<ApiResponse<T>> => {
|
|
||||||
try {
|
|
||||||
const response = await promise;
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error.response?.data?.message || error.message || "오류가 발생했습니다.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 금형 마스터
|
|
||||||
export const getMoldList = (params?: Record<string, string>) =>
|
|
||||||
handleResponse<MoldInfo[]>(apiClient.get("/mold", { params }));
|
|
||||||
|
|
||||||
export const getMoldDetail = (moldCode: string) =>
|
|
||||||
handleResponse<MoldInfo>(apiClient.get(`/mold/${moldCode}`));
|
|
||||||
|
|
||||||
export const createMold = (data: Partial<MoldInfo>) =>
|
|
||||||
handleResponse<MoldInfo>(apiClient.post("/mold", data));
|
|
||||||
|
|
||||||
export const updateMold = (moldCode: string, data: Partial<MoldInfo>) =>
|
|
||||||
handleResponse<MoldInfo>(apiClient.put(`/mold/${moldCode}`, data));
|
|
||||||
|
|
||||||
export const deleteMold = (moldCode: string) =>
|
|
||||||
handleResponse<void>(apiClient.delete(`/mold/${moldCode}`));
|
|
||||||
|
|
||||||
// 일련번호
|
|
||||||
export const getMoldSerials = (moldCode: string) =>
|
|
||||||
handleResponse<MoldSerial[]>(apiClient.get(`/mold/${moldCode}/serials`));
|
|
||||||
|
|
||||||
export const createMoldSerial = (moldCode: string, data: Partial<MoldSerial>) =>
|
|
||||||
handleResponse<MoldSerial>(apiClient.post(`/mold/${moldCode}/serials`, data));
|
|
||||||
|
|
||||||
export const deleteMoldSerial = (id: string) =>
|
|
||||||
handleResponse<void>(apiClient.delete(`/mold/serials/${id}`));
|
|
||||||
|
|
||||||
export const getMoldSerialSummary = (moldCode: string) =>
|
|
||||||
handleResponse<MoldSerialSummary>(apiClient.get(`/mold/${moldCode}/serial-summary`));
|
|
||||||
|
|
||||||
// 점검항목
|
|
||||||
export const getMoldInspections = (moldCode: string) =>
|
|
||||||
handleResponse<MoldInspectionItem[]>(apiClient.get(`/mold/${moldCode}/inspections`));
|
|
||||||
|
|
||||||
export const createMoldInspection = (moldCode: string, data: Partial<MoldInspectionItem>) =>
|
|
||||||
handleResponse<MoldInspectionItem>(apiClient.post(`/mold/${moldCode}/inspections`, data));
|
|
||||||
|
|
||||||
export const deleteMoldInspection = (id: string) =>
|
|
||||||
handleResponse<void>(apiClient.delete(`/mold/inspections/${id}`));
|
|
||||||
|
|
||||||
// 부품
|
|
||||||
export const getMoldParts = (moldCode: string) =>
|
|
||||||
handleResponse<MoldPart[]>(apiClient.get(`/mold/${moldCode}/parts`));
|
|
||||||
|
|
||||||
export const createMoldPart = (moldCode: string, data: Partial<MoldPart>) =>
|
|
||||||
handleResponse<MoldPart>(apiClient.post(`/mold/${moldCode}/parts`, data));
|
|
||||||
|
|
||||||
export const deleteMoldPart = (id: string) =>
|
|
||||||
handleResponse<void>(apiClient.delete(`/mold/parts/${id}`));
|
|
||||||
|
|
@ -367,7 +367,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
|
|
||||||
for (const columnName of categoryColumns) {
|
for (const columnName of categoryColumns) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`);
|
||||||
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,6 @@ import "./v2-split-line/SplitLineRenderer"; // V2 캔버스 분할선
|
||||||
import "./v2-bom-tree/BomTreeRenderer"; // BOM 트리 뷰
|
import "./v2-bom-tree/BomTreeRenderer"; // BOM 트리 뷰
|
||||||
import "./v2-bom-item-editor/BomItemEditorRenderer"; // BOM 하위품목 편집기
|
import "./v2-bom-item-editor/BomItemEditorRenderer"; // BOM 하위품목 편집기
|
||||||
import "./v2-approval-step/ApprovalStepRenderer"; // 결재 단계 시각화
|
import "./v2-approval-step/ApprovalStepRenderer"; // 결재 단계 시각화
|
||||||
import "./v2-status-count/StatusCountRenderer"; // 상태별 카운트 카드
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 초기화 함수
|
* 컴포넌트 초기화 함수
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export function RepeaterTable({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data?.success && response.data.data) {
|
if (response.data?.success && response.data.data) {
|
||||||
const options = response.data.data.map((item: any) => ({
|
const options = response.data.data.map((item: any) => ({
|
||||||
|
|
|
||||||
|
|
@ -139,22 +139,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const cascadingRole = config?.cascadingRole || componentConfig?.cascadingRole || "child";
|
const cascadingRole = config?.cascadingRole || componentConfig?.cascadingRole || "child";
|
||||||
const cascadingParentField = config?.cascadingParentField || componentConfig?.cascadingParentField;
|
const cascadingParentField = config?.cascadingParentField || componentConfig?.cascadingParentField;
|
||||||
|
|
||||||
if (component.columnName === "data_type" || component.columnName === "unit") {
|
|
||||||
console.log("🔥🔥🔥 [PLC 캐스케이딩 디버그]", {
|
|
||||||
columnName: component.columnName,
|
|
||||||
categoryRelationCode,
|
|
||||||
cascadingRole,
|
|
||||||
cascadingParentField,
|
|
||||||
configKeys: config ? Object.keys(config) : "null",
|
|
||||||
componentConfigKeys: componentConfig ? Object.keys(componentConfig) : "null",
|
|
||||||
configCategoryRelationCode: config?.categoryRelationCode,
|
|
||||||
componentConfigCategoryRelationCode: componentConfig?.categoryRelationCode,
|
|
||||||
webType,
|
|
||||||
formDataKeys: formData ? Object.keys(formData) : "null",
|
|
||||||
parentValue: cascadingParentField && formData ? formData[cascadingParentField] : "N/A",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 계층구조 역할 설정 (대분류/중분류/소분류)
|
// 🆕 계층구조 역할 설정 (대분류/중분류/소분류)
|
||||||
// 1순위: 동적으로 조회된 값 (테이블 타입관리에서 설정)
|
// 1순위: 동적으로 조회된 값 (테이블 타입관리에서 설정)
|
||||||
// 2순위: config에서 전달된 값
|
// 2순위: config에서 전달된 값
|
||||||
|
|
|
||||||
|
|
@ -1588,7 +1588,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
const columnName = col.columnName || col.column_name;
|
const columnName = col.columnName || col.column_name;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
@ -1650,7 +1650,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
const columnName = col.columnName || col.column_name;
|
const columnName = col.columnName || col.column_name;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
|
||||||
|
|
@ -1298,8 +1298,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
targetColumn = parts[1];
|
targetColumn = parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true
|
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`);
|
||||||
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true`);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
const mapping: Record<string, { label: string; color?: string }> = {};
|
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
@ -1382,7 +1381,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// inputType이 category인 경우 카테고리 매핑 로드
|
// inputType이 category인 경우 카테고리 매핑 로드
|
||||||
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
|
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
const mapping: Record<string, { label: string; color?: string }> = {};
|
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
|
||||||
|
|
@ -1362,7 +1362,7 @@ export function UniversalFormModalComponent({
|
||||||
label: String(row[optionConfig.labelColumn || "name"]),
|
label: String(row[optionConfig.labelColumn || "name"]),
|
||||||
}));
|
}));
|
||||||
} else if (optionConfig.type === "code" && optionConfig.categoryKey) {
|
} else if (optionConfig.type === "code" && optionConfig.categoryKey) {
|
||||||
// 공통코드(카테고리 컬럼): category_values 테이블에서 조회
|
// 공통코드(카테고리 컬럼): table_column_category_values 테이블에서 조회
|
||||||
// categoryKey 형식: "tableName.columnName"
|
// categoryKey 형식: "tableName.columnName"
|
||||||
const [categoryTable, categoryColumn] = optionConfig.categoryKey.split(".");
|
const [categoryTable, categoryColumn] = optionConfig.categoryKey.split(".");
|
||||||
if (categoryTable && categoryColumn) {
|
if (categoryTable && categoryColumn) {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { getCascadingRelations, getCascadingRelationByCode, CascadingRelation } from "@/lib/api/cascadingRelation";
|
import { getCascadingRelations, getCascadingRelationByCode, CascadingRelation } from "@/lib/api/cascadingRelation";
|
||||||
|
|
||||||
// 카테고리 컬럼 타입 (category_values 용)
|
// 카테고리 컬럼 타입 (table_column_category_values 용)
|
||||||
interface CategoryColumnOption {
|
interface CategoryColumnOption {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
columnName: string;
|
columnName: string;
|
||||||
|
|
|
||||||
|
|
@ -2526,7 +2526,7 @@ interface TableSectionSettingsModalProps {
|
||||||
tables: { table_name: string; comment?: string }[];
|
tables: { table_name: string; comment?: string }[];
|
||||||
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]>;
|
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]>;
|
||||||
onLoadTableColumns: (tableName: string) => void;
|
onLoadTableColumns: (tableName: string) => void;
|
||||||
// 카테고리 목록 (category_values에서 가져옴)
|
// 카테고리 목록 (table_column_category_values에서 가져옴)
|
||||||
categoryList?: { tableName: string; columnName: string; displayName?: string }[];
|
categoryList?: { tableName: string; columnName: string; displayName?: string }[];
|
||||||
onLoadCategoryList?: () => void;
|
onLoadCategoryList?: () => void;
|
||||||
// 전체 섹션 목록 (다른 섹션 필드 참조용)
|
// 전체 섹션 목록 (다른 섹션 필드 참조용)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export interface SelectOptionConfig {
|
||||||
labelColumn?: string; // 표시할 컬럼 (화면에 보여줄 텍스트)
|
labelColumn?: string; // 표시할 컬럼 (화면에 보여줄 텍스트)
|
||||||
saveColumn?: string; // 저장할 컬럼 (실제로 DB에 저장할 값, 미지정 시 valueColumn 사용)
|
saveColumn?: string; // 저장할 컬럼 (실제로 DB에 저장할 값, 미지정 시 valueColumn 사용)
|
||||||
filterCondition?: string;
|
filterCondition?: string;
|
||||||
// 카테고리 컬럼 기반 옵션 (category_values 테이블)
|
// 카테고리 컬럼 기반 옵션 (table_column_category_values 테이블)
|
||||||
// 형식: "tableName.columnName" (예: "sales_order_mng.incoterms")
|
// 형식: "tableName.columnName" (예: "sales_order_mng.incoterms")
|
||||||
categoryKey?: string;
|
categoryKey?: string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -540,7 +540,7 @@ export function BomItemEditorComponent({
|
||||||
if (alreadyLoaded) continue;
|
if (alreadyLoaded) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${mainTableName}/${col.key}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${mainTableName}/${col.key}/values`);
|
||||||
if (response.data?.success && response.data.data) {
|
if (response.data?.success && response.data.data) {
|
||||||
const options = response.data.data.map((item: any) => ({
|
const options = response.data.data.map((item: any) => ({
|
||||||
value: item.valueCode || item.value_code,
|
value: item.valueCode || item.value_code,
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export function BomTreeComponent({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLabels = async () => {
|
const loadLabels = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values?includeInactive=true`);
|
const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values`);
|
||||||
const vals = res.data?.data || [];
|
const vals = res.data?.data || [];
|
||||||
if (vals.length > 0) {
|
if (vals.length > 0) {
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
|
|
||||||
for (const columnName of categoryColumns) {
|
for (const columnName of categoryColumns) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`);
|
||||||
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
|
|
|
||||||
|
|
@ -1894,7 +1894,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
const columnName = col.columnName || col.column_name;
|
const columnName = col.columnName || col.column_name;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
@ -1972,7 +1972,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
const columnName = col.columnName || col.column_name;
|
const columnName = col.columnName || col.column_name;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback } from "react";
|
|
||||||
import { ComponentRendererProps } from "@/types/component";
|
|
||||||
import { StatusCountConfig, StatusCountItem, STATUS_COLOR_MAP } from "./types";
|
|
||||||
import { apiClient } from "@/lib/api/client";
|
|
||||||
|
|
||||||
export interface StatusCountComponentProps extends ComponentRendererProps {}
|
|
||||||
|
|
||||||
export const StatusCountComponent: React.FC<StatusCountComponentProps> = ({
|
|
||||||
component,
|
|
||||||
isDesignMode = false,
|
|
||||||
isSelected = false,
|
|
||||||
formData,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const config = (component.componentConfig || {}) as StatusCountConfig;
|
|
||||||
const [counts, setCounts] = useState<Record<string, number>>({});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
tableName,
|
|
||||||
statusColumn = "status",
|
|
||||||
relationColumn,
|
|
||||||
parentColumn,
|
|
||||||
items = [],
|
|
||||||
cardSize = "md",
|
|
||||||
} = config;
|
|
||||||
|
|
||||||
const parentValue = formData?.[parentColumn || relationColumn];
|
|
||||||
|
|
||||||
const fetchCounts = useCallback(async () => {
|
|
||||||
if (!tableName || !parentValue || isDesignMode) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await apiClient.get(`/table-management/data/${tableName}`, {
|
|
||||||
params: {
|
|
||||||
autoFilter: "true",
|
|
||||||
[relationColumn]: parentValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows: any[] = res.data?.data || res.data?.rows || res.data || [];
|
|
||||||
const grouped: Record<string, number> = {};
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const val = row[statusColumn] || "UNKNOWN";
|
|
||||||
grouped[val] = (grouped[val] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCounts(grouped);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[v2-status-count] 데이터 조회 실패:", err);
|
|
||||||
setCounts({});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [tableName, statusColumn, relationColumn, parentValue, isDesignMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCounts();
|
|
||||||
}, [fetchCounts]);
|
|
||||||
|
|
||||||
const getColorClasses = (color: string) => {
|
|
||||||
if (STATUS_COLOR_MAP[color]) return STATUS_COLOR_MAP[color];
|
|
||||||
return { bg: "bg-gray-50", text: "text-gray-600", border: "border-gray-200" };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCount = (item: StatusCountItem) => {
|
|
||||||
if (item.value === "__TOTAL__") {
|
|
||||||
return Object.values(counts).reduce((sum, c) => sum + c, 0);
|
|
||||||
}
|
|
||||||
const values = item.value.split(",").map((v) => v.trim());
|
|
||||||
return values.reduce((sum, v) => sum + (counts[v] || 0), 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: { card: "px-3 py-2", number: "text-xl", label: "text-[10px]" },
|
|
||||||
md: { card: "px-4 py-3", number: "text-2xl", label: "text-xs" },
|
|
||||||
lg: { card: "px-6 py-4", number: "text-3xl", label: "text-sm" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const sz = sizeClasses[cardSize] || sizeClasses.md;
|
|
||||||
|
|
||||||
if (isDesignMode && !parentValue) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-col gap-2 rounded-lg border border-dashed border-gray-300 p-3">
|
|
||||||
{title && <div className="text-xs font-medium text-muted-foreground">{title}</div>}
|
|
||||||
<div className="flex flex-1 items-center justify-center gap-2">
|
|
||||||
{(items.length > 0 ? items : [{ label: "상태1", color: "green" }, { label: "상태2", color: "blue" }, { label: "상태3", color: "orange" }]).map(
|
|
||||||
(item: any, i: number) => {
|
|
||||||
const colors = getColorClasses(item.color || "gray");
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`flex flex-1 flex-col items-center rounded-lg border ${colors.border} ${colors.bg} ${sz.card}`}
|
|
||||||
>
|
|
||||||
<span className={`font-bold ${colors.text} ${sz.number}`}>0</span>
|
|
||||||
<span className={`${colors.text} ${sz.label}`}>{item.label}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-col gap-2">
|
|
||||||
{title && <div className="text-sm font-medium text-foreground">{title}</div>}
|
|
||||||
<div className="flex flex-1 items-stretch gap-2">
|
|
||||||
{items.map((item, i) => {
|
|
||||||
const colors = getColorClasses(item.color);
|
|
||||||
const count = getCount(item);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.value + i}
|
|
||||||
className={`flex flex-1 flex-col items-center justify-center rounded-lg border ${colors.border} ${colors.bg} ${sz.card} transition-shadow hover:shadow-sm`}
|
|
||||||
>
|
|
||||||
<span className={`font-bold ${colors.text} ${sz.number}`}>
|
|
||||||
{loading ? "-" : count}
|
|
||||||
</span>
|
|
||||||
<span className={`mt-0.5 ${colors.text} ${sz.label}`}>{item.label}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StatusCountWrapper: React.FC<StatusCountComponentProps> = (props) => {
|
|
||||||
return <StatusCountComponent {...props} />;
|
|
||||||
};
|
|
||||||
|
|
@ -1,424 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import { StatusCountConfig, StatusCountItem, STATUS_COLOR_MAP } from "./types";
|
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
|
||||||
import { entityJoinApi, EntityJoinConfig } from "@/lib/api/entityJoin";
|
|
||||||
import { Plus, Trash2, Loader2, Check, ChevronsUpDown } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export interface StatusCountConfigPanelProps {
|
|
||||||
config: StatusCountConfig;
|
|
||||||
onChange: (config: Partial<StatusCountConfig>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLOR_OPTIONS = Object.keys(STATUS_COLOR_MAP);
|
|
||||||
|
|
||||||
interface SearchableComboboxProps {
|
|
||||||
value: string;
|
|
||||||
onSelect: (value: string) => void;
|
|
||||||
items: Array<{ value: string; label: string; sublabel?: string }>;
|
|
||||||
placeholder: string;
|
|
||||||
searchPlaceholder: string;
|
|
||||||
emptyText: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchableCombobox: React.FC<SearchableComboboxProps> = ({
|
|
||||||
value,
|
|
||||||
onSelect,
|
|
||||||
items,
|
|
||||||
placeholder,
|
|
||||||
searchPlaceholder,
|
|
||||||
emptyText,
|
|
||||||
disabled,
|
|
||||||
loading,
|
|
||||||
}) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-8 items-center gap-1 text-xs text-muted-foreground">
|
|
||||||
<Loader2 className="h-3 w-3 animate-spin" /> 로딩중...
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedItem = items.find((item) => item.value === value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={open}
|
|
||||||
disabled={disabled}
|
|
||||||
className="h-8 w-full justify-between text-xs font-normal"
|
|
||||||
>
|
|
||||||
<span className="truncate">
|
|
||||||
{selectedItem
|
|
||||||
? selectedItem.sublabel
|
|
||||||
? `${selectedItem.label} (${selectedItem.sublabel})`
|
|
||||||
: selectedItem.label
|
|
||||||
: placeholder}
|
|
||||||
</span>
|
|
||||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="p-0"
|
|
||||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder={searchPlaceholder} className="text-xs" />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty className="py-3 text-center text-xs">
|
|
||||||
{emptyText}
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{items.map((item) => (
|
|
||||||
<CommandItem
|
|
||||||
key={item.value}
|
|
||||||
value={`${item.label} ${item.sublabel || ""} ${item.value}`}
|
|
||||||
onSelect={() => {
|
|
||||||
onSelect(item.value === value ? "" : item.value);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-3 w-3",
|
|
||||||
value === item.value ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>{item.label}</span>
|
|
||||||
{item.sublabel && (
|
|
||||||
<span className="text-[10px] text-muted-foreground">
|
|
||||||
{item.sublabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StatusCountConfigPanel: React.FC<StatusCountConfigPanelProps> = ({
|
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}) => {
|
|
||||||
const items = config.items || [];
|
|
||||||
const [tables, setTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
|
||||||
const [columns, setColumns] = useState<Array<{ columnName: string; columnLabel: string }>>([]);
|
|
||||||
const [entityJoins, setEntityJoins] = useState<EntityJoinConfig[]>([]);
|
|
||||||
const [loadingTables, setLoadingTables] = useState(false);
|
|
||||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
|
||||||
const [loadingJoins, setLoadingJoins] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadTables = async () => {
|
|
||||||
setLoadingTables(true);
|
|
||||||
try {
|
|
||||||
const result = await tableTypeApi.getTables();
|
|
||||||
setTables(
|
|
||||||
(result || []).map((t: any) => ({
|
|
||||||
tableName: t.tableName || t.table_name,
|
|
||||||
displayName: t.displayName || t.tableName || t.table_name,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("테이블 목록 로드 실패:", err);
|
|
||||||
} finally {
|
|
||||||
setLoadingTables(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadTables();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!config.tableName) {
|
|
||||||
setColumns([]);
|
|
||||||
setEntityJoins([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadColumns = async () => {
|
|
||||||
setLoadingColumns(true);
|
|
||||||
try {
|
|
||||||
const result = await tableTypeApi.getColumns(config.tableName);
|
|
||||||
setColumns(
|
|
||||||
(result || []).map((c: any) => ({
|
|
||||||
columnName: c.columnName || c.column_name,
|
|
||||||
columnLabel: c.columnLabel || c.column_label || c.displayName || c.columnName || c.column_name,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("컬럼 목록 로드 실패:", err);
|
|
||||||
} finally {
|
|
||||||
setLoadingColumns(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadEntityJoins = async () => {
|
|
||||||
setLoadingJoins(true);
|
|
||||||
try {
|
|
||||||
const result = await entityJoinApi.getEntityJoinConfigs(config.tableName);
|
|
||||||
setEntityJoins(result?.joinConfigs || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("엔티티 조인 설정 로드 실패:", err);
|
|
||||||
setEntityJoins([]);
|
|
||||||
} finally {
|
|
||||||
setLoadingJoins(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadColumns();
|
|
||||||
loadEntityJoins();
|
|
||||||
}, [config.tableName]);
|
|
||||||
|
|
||||||
const handleChange = (key: keyof StatusCountConfig, value: any) => {
|
|
||||||
onChange({ [key]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleItemChange = (index: number, key: keyof StatusCountItem, value: string) => {
|
|
||||||
const newItems = [...items];
|
|
||||||
newItems[index] = { ...newItems[index], [key]: value };
|
|
||||||
handleChange("items", newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addItem = () => {
|
|
||||||
handleChange("items", [
|
|
||||||
...items,
|
|
||||||
{ value: "", label: "새 상태", color: "gray" },
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeItem = (index: number) => {
|
|
||||||
handleChange(
|
|
||||||
"items",
|
|
||||||
items.filter((_: StatusCountItem, i: number) => i !== index)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tableComboItems = tables.map((t) => ({
|
|
||||||
value: t.tableName,
|
|
||||||
label: t.displayName,
|
|
||||||
sublabel: t.displayName !== t.tableName ? t.tableName : undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const columnComboItems = columns.map((c) => ({
|
|
||||||
value: c.columnName,
|
|
||||||
label: c.columnLabel,
|
|
||||||
sublabel: c.columnLabel !== c.columnName ? c.columnName : undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const relationComboItems = entityJoins.map((ej) => {
|
|
||||||
const refTableLabel = tables.find((t) => t.tableName === ej.referenceTable)?.displayName || ej.referenceTable;
|
|
||||||
return {
|
|
||||||
value: `${ej.sourceColumn}::${ej.referenceTable}.${ej.referenceColumn}`,
|
|
||||||
label: `${ej.sourceColumn} -> ${refTableLabel}`,
|
|
||||||
sublabel: `${ej.referenceTable}.${ej.referenceColumn}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRelationValue = config.relationColumn && config.parentColumn
|
|
||||||
? relationComboItems.find((item) => {
|
|
||||||
const [srcCol] = item.value.split("::");
|
|
||||||
return srcCol === config.relationColumn;
|
|
||||||
})?.value || ""
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="text-sm font-medium">상태별 카운트 설정</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">제목</Label>
|
|
||||||
<Input
|
|
||||||
value={config.title || ""}
|
|
||||||
onChange={(e) => handleChange("title", e.target.value)}
|
|
||||||
placeholder="일련번호 현황"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">테이블</Label>
|
|
||||||
<SearchableCombobox
|
|
||||||
value={config.tableName || ""}
|
|
||||||
onSelect={(v) => {
|
|
||||||
onChange({ tableName: v, statusColumn: "", relationColumn: "", parentColumn: "" });
|
|
||||||
}}
|
|
||||||
items={tableComboItems}
|
|
||||||
placeholder="테이블 선택"
|
|
||||||
searchPlaceholder="테이블명 또는 라벨 검색..."
|
|
||||||
emptyText="테이블을 찾을 수 없습니다"
|
|
||||||
loading={loadingTables}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">상태 컬럼</Label>
|
|
||||||
<SearchableCombobox
|
|
||||||
value={config.statusColumn || ""}
|
|
||||||
onSelect={(v) => handleChange("statusColumn", v)}
|
|
||||||
items={columnComboItems}
|
|
||||||
placeholder={config.tableName ? "상태 컬럼 선택" : "테이블을 먼저 선택"}
|
|
||||||
searchPlaceholder="컬럼명 또는 라벨 검색..."
|
|
||||||
emptyText="컬럼을 찾을 수 없습니다"
|
|
||||||
disabled={!config.tableName}
|
|
||||||
loading={loadingColumns}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">엔티티 관계</Label>
|
|
||||||
{loadingJoins ? (
|
|
||||||
<div className="flex h-8 items-center gap-1 text-xs text-muted-foreground">
|
|
||||||
<Loader2 className="h-3 w-3 animate-spin" /> 로딩중...
|
|
||||||
</div>
|
|
||||||
) : entityJoins.length > 0 ? (
|
|
||||||
<SearchableCombobox
|
|
||||||
value={currentRelationValue}
|
|
||||||
onSelect={(v) => {
|
|
||||||
if (!v) {
|
|
||||||
onChange({ relationColumn: "", parentColumn: "" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [sourceCol, refPart] = v.split("::");
|
|
||||||
const [refTable, refCol] = refPart.split(".");
|
|
||||||
onChange({ relationColumn: sourceCol, parentColumn: refCol });
|
|
||||||
}}
|
|
||||||
items={relationComboItems}
|
|
||||||
placeholder="엔티티 관계 선택"
|
|
||||||
searchPlaceholder="관계 검색..."
|
|
||||||
emptyText="엔티티 관계가 없습니다"
|
|
||||||
disabled={!config.tableName}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-md border border-dashed p-2">
|
|
||||||
<p className="text-center text-[10px] text-muted-foreground">
|
|
||||||
{config.tableName ? "설정된 엔티티 관계가 없습니다" : "테이블을 먼저 선택하세요"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{config.relationColumn && config.parentColumn && (
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
자식 FK: <span className="font-medium">{config.relationColumn}</span>
|
|
||||||
{" -> "}
|
|
||||||
부모 매칭: <span className="font-medium">{config.parentColumn}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">카드 크기</Label>
|
|
||||||
<Select
|
|
||||||
value={config.cardSize || "md"}
|
|
||||||
onValueChange={(v) => handleChange("cardSize", v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="sm" className="text-xs">작게</SelectItem>
|
|
||||||
<SelectItem value="md" className="text-xs">보통</SelectItem>
|
|
||||||
<SelectItem value="lg" className="text-xs">크게</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-xs">상태 항목</Label>
|
|
||||||
<Button variant="ghost" size="sm" onClick={addItem} className="h-6 px-2 text-xs">
|
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
|
||||||
추가
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{items.map((item: StatusCountItem, i: number) => (
|
|
||||||
<div key={i} className="space-y-1 rounded-md border p-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Input
|
|
||||||
value={item.value}
|
|
||||||
onChange={(e) => handleItemChange(i, "value", e.target.value)}
|
|
||||||
placeholder="상태값 (예: IN_USE)"
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeItem(i)}
|
|
||||||
className="h-7 w-7 shrink-0 p-0"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Input
|
|
||||||
value={item.label}
|
|
||||||
onChange={(e) => handleItemChange(i, "label", e.target.value)}
|
|
||||||
placeholder="표시 라벨"
|
|
||||||
className="h-7 text-xs"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={item.color}
|
|
||||||
onValueChange={(v) => handleItemChange(i, "color", v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-7 w-24 shrink-0 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{COLOR_OPTIONS.map((c) => (
|
|
||||||
<SelectItem key={c} value={c} className="text-xs">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div
|
|
||||||
className={`h-3 w-3 rounded-full ${STATUS_COLOR_MAP[c].bg} border ${STATUS_COLOR_MAP[c].border}`}
|
|
||||||
/>
|
|
||||||
{c}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
||||||
import { V2StatusCountDefinition } from "./index";
|
|
||||||
import { StatusCountComponent } from "./StatusCountComponent";
|
|
||||||
|
|
||||||
export class StatusCountRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = V2StatusCountDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
|
||||||
return <StatusCountComponent {...this.props} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusCountRenderer.registerSelf();
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
|
||||||
import { ComponentCategory } from "@/types/component";
|
|
||||||
import { StatusCountWrapper } from "./StatusCountComponent";
|
|
||||||
import { StatusCountConfigPanel } from "./StatusCountConfigPanel";
|
|
||||||
|
|
||||||
export const V2StatusCountDefinition = createComponentDefinition({
|
|
||||||
id: "v2-status-count",
|
|
||||||
name: "상태별 카운트",
|
|
||||||
nameEng: "Status Count",
|
|
||||||
description: "관련 테이블의 상태별 데이터 건수를 카드 형태로 표시하는 범용 컴포넌트",
|
|
||||||
category: ComponentCategory.DISPLAY,
|
|
||||||
webType: "text",
|
|
||||||
component: StatusCountWrapper,
|
|
||||||
configPanel: StatusCountConfigPanel,
|
|
||||||
defaultConfig: {
|
|
||||||
title: "상태 현황",
|
|
||||||
tableName: "",
|
|
||||||
statusColumn: "status",
|
|
||||||
relationColumn: "",
|
|
||||||
parentColumn: "",
|
|
||||||
items: [
|
|
||||||
{ value: "ACTIVE", label: "사용중", color: "blue" },
|
|
||||||
{ value: "STANDBY", label: "대기", color: "green" },
|
|
||||||
{ value: "REPAIR", label: "수리중", color: "orange" },
|
|
||||||
{ value: "DISPOSED", label: "폐기", color: "red" },
|
|
||||||
],
|
|
||||||
cardSize: "md",
|
|
||||||
},
|
|
||||||
defaultSize: { width: 800, height: 100 },
|
|
||||||
icon: "BarChart3",
|
|
||||||
tags: ["상태", "카운트", "통계", "현황", "v2"],
|
|
||||||
version: "1.0.0",
|
|
||||||
author: "개발팀",
|
|
||||||
});
|
|
||||||
|
|
||||||
export type { StatusCountConfig, StatusCountItem } from "./types";
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { ComponentConfig } from "@/types/component";
|
|
||||||
|
|
||||||
export interface StatusCountItem {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
color: string; // "green" | "blue" | "orange" | "red" | "gray" | "purple" | hex color
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StatusCountConfig extends ComponentConfig {
|
|
||||||
title?: string;
|
|
||||||
tableName: string;
|
|
||||||
statusColumn: string;
|
|
||||||
relationColumn: string;
|
|
||||||
parentColumn?: string;
|
|
||||||
items: StatusCountItem[];
|
|
||||||
showTotal?: boolean;
|
|
||||||
cardSize?: "sm" | "md" | "lg";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const STATUS_COLOR_MAP: Record<string, { bg: string; text: string; border: string }> = {
|
|
||||||
green: { bg: "bg-emerald-50", text: "text-emerald-600", border: "border-emerald-200" },
|
|
||||||
blue: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" },
|
|
||||||
orange: { bg: "bg-orange-50", text: "text-orange-500", border: "border-orange-200" },
|
|
||||||
red: { bg: "bg-red-50", text: "text-red-500", border: "border-red-200" },
|
|
||||||
gray: { bg: "bg-gray-50", text: "text-gray-500", border: "border-gray-200" },
|
|
||||||
purple: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200" },
|
|
||||||
yellow: { bg: "bg-yellow-50", text: "text-yellow-600", border: "border-yellow-200" },
|
|
||||||
cyan: { bg: "bg-cyan-50", text: "text-cyan-600", border: "border-cyan-200" },
|
|
||||||
};
|
|
||||||
|
|
@ -1441,8 +1441,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
targetColumn = parts[1];
|
targetColumn = parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true
|
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`);
|
||||||
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true`);
|
|
||||||
|
|
||||||
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
const mapping: Record<string, { label: string; color?: string }> = {};
|
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
@ -1525,7 +1524,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
// inputType이 category인 경우 카테고리 매핑 로드
|
// inputType이 category인 경우 카테고리 매핑 로드
|
||||||
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
|
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values?includeInactive=true`);
|
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`);
|
||||||
|
|
||||||
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
|
||||||
const mapping: Record<string, { label: string; color?: string }> = {};
|
const mapping: Record<string, { label: string; color?: string }> = {};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue