feat: implement packaging unit and item management APIs
- Added CRUD operations for packaging units and their associated items in the new `packagingController.ts`. - Implemented routes for managing packaging units and items in `packagingRoutes.ts`. - Enhanced error handling and logging for better traceability. - Ensured company code filtering for data access based on user roles. Made-with: Cursor
This commit is contained in:
parent
7269867d91
commit
09c3fa4708
|
|
@ -0,0 +1,478 @@
|
||||||
|
import { Response } from "express";
|
||||||
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { getPool } from "../database/db";
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 포장단위 (pkg_unit) CRUD
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getPkgUnits(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
let sql: string;
|
||||||
|
let params: any[];
|
||||||
|
|
||||||
|
if (companyCode === "*") {
|
||||||
|
sql = `SELECT * FROM pkg_unit ORDER BY company_code, created_date DESC`;
|
||||||
|
params = [];
|
||||||
|
} else {
|
||||||
|
sql = `SELECT * FROM pkg_unit WHERE company_code = $1 ORDER BY created_date DESC`;
|
||||||
|
params = [companyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(sql, params);
|
||||||
|
logger.info("포장단위 목록 조회", { companyCode, count: result.rowCount });
|
||||||
|
res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("포장단위 목록 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPkgUnit(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const pool = getPool();
|
||||||
|
const {
|
||||||
|
pkg_code, pkg_name, pkg_type, status,
|
||||||
|
width_mm, length_mm, height_mm,
|
||||||
|
self_weight_kg, max_load_kg, volume_l, remarks,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!pkg_code || !pkg_name) {
|
||||||
|
res.status(400).json({ success: false, message: "포장코드와 포장명은 필수입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dup = await pool.query(
|
||||||
|
`SELECT id FROM pkg_unit WHERE pkg_code = $1 AND company_code = $2`,
|
||||||
|
[pkg_code, companyCode]
|
||||||
|
);
|
||||||
|
if (dup.rowCount && dup.rowCount > 0) {
|
||||||
|
res.status(409).json({ success: false, message: "이미 존재하는 포장코드입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO pkg_unit
|
||||||
|
(company_code, pkg_code, pkg_name, pkg_type, status,
|
||||||
|
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, volume_l, remarks, writer)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||||
|
RETURNING *`,
|
||||||
|
[companyCode, pkg_code, pkg_name, pkg_type, status || "ACTIVE",
|
||||||
|
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, volume_l, remarks,
|
||||||
|
req.user!.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("포장단위 등록", { companyCode, pkg_code });
|
||||||
|
res.json({ success: true, data: result.rows[0] });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("포장단위 등록 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePkgUnit(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { id } = req.params;
|
||||||
|
const pool = getPool();
|
||||||
|
const {
|
||||||
|
pkg_name, pkg_type, status,
|
||||||
|
width_mm, length_mm, height_mm,
|
||||||
|
self_weight_kg, max_load_kg, volume_l, remarks,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE pkg_unit SET
|
||||||
|
pkg_name=$1, pkg_type=$2, status=$3,
|
||||||
|
width_mm=$4, length_mm=$5, height_mm=$6,
|
||||||
|
self_weight_kg=$7, max_load_kg=$8, volume_l=$9, remarks=$10,
|
||||||
|
updated_date=NOW(), writer=$11
|
||||||
|
WHERE id=$12 AND company_code=$13
|
||||||
|
RETURNING *`,
|
||||||
|
[pkg_name, pkg_type, status,
|
||||||
|
width_mm, length_mm, height_mm,
|
||||||
|
self_weight_kg, max_load_kg, volume_l, remarks,
|
||||||
|
req.user!.userId, id, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("포장단위 수정", { companyCode, id });
|
||||||
|
res.json({ success: true, data: result.rows[0] });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("포장단위 수정 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePkgUnit(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
await client.query("BEGIN");
|
||||||
|
await client.query(
|
||||||
|
`DELETE FROM pkg_unit_item WHERE pkg_code = (SELECT pkg_code FROM pkg_unit WHERE id=$1 AND company_code=$2) AND company_code=$2`,
|
||||||
|
[id, companyCode]
|
||||||
|
);
|
||||||
|
const result = await client.query(
|
||||||
|
`DELETE FROM pkg_unit WHERE id=$1 AND company_code=$2 RETURNING id`,
|
||||||
|
[id, companyCode]
|
||||||
|
);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("포장단위 삭제", { companyCode, id });
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
logger.error("포장단위 삭제 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 포장단위 매칭품목 (pkg_unit_item) CRUD
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getPkgUnitItems(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { pkgCode } = req.params;
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM pkg_unit_item WHERE pkg_code=$1 AND company_code=$2 ORDER BY created_date DESC`,
|
||||||
|
[pkgCode, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("매칭품목 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPkgUnitItem(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const pool = getPool();
|
||||||
|
const { pkg_code, item_number, pkg_qty } = req.body;
|
||||||
|
|
||||||
|
if (!pkg_code || !item_number) {
|
||||||
|
res.status(400).json({ success: false, message: "포장코드와 품번은 필수입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO pkg_unit_item (company_code, pkg_code, item_number, pkg_qty, writer)
|
||||||
|
VALUES ($1,$2,$3,$4,$5)
|
||||||
|
RETURNING *`,
|
||||||
|
[companyCode, pkg_code, item_number, pkg_qty, req.user!.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("매칭품목 추가", { companyCode, pkg_code, item_number });
|
||||||
|
res.json({ success: true, data: result.rows[0] });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("매칭품목 추가 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePkgUnitItem(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { id } = req.params;
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`DELETE FROM pkg_unit_item WHERE id=$1 AND company_code=$2 RETURNING id`,
|
||||||
|
[id, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("매칭품목 삭제", { companyCode, id });
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("매칭품목 삭제 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 적재함 (loading_unit) CRUD
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getLoadingUnits(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
let sql: string;
|
||||||
|
let params: any[];
|
||||||
|
|
||||||
|
if (companyCode === "*") {
|
||||||
|
sql = `SELECT * FROM loading_unit ORDER BY company_code, created_date DESC`;
|
||||||
|
params = [];
|
||||||
|
} else {
|
||||||
|
sql = `SELECT * FROM loading_unit WHERE company_code = $1 ORDER BY created_date DESC`;
|
||||||
|
params = [companyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(sql, params);
|
||||||
|
logger.info("적재함 목록 조회", { companyCode, count: result.rowCount });
|
||||||
|
res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("적재함 목록 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLoadingUnit(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const pool = getPool();
|
||||||
|
const {
|
||||||
|
loading_code, loading_name, loading_type, status,
|
||||||
|
width_mm, length_mm, height_mm,
|
||||||
|
self_weight_kg, max_load_kg, max_stack, remarks,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!loading_code || !loading_name) {
|
||||||
|
res.status(400).json({ success: false, message: "적재함코드와 적재함명은 필수입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dup = await pool.query(
|
||||||
|
`SELECT id FROM loading_unit WHERE loading_code=$1 AND company_code=$2`,
|
||||||
|
[loading_code, companyCode]
|
||||||
|
);
|
||||||
|
if (dup.rowCount && dup.rowCount > 0) {
|
||||||
|
res.status(409).json({ success: false, message: "이미 존재하는 적재함코드입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO loading_unit
|
||||||
|
(company_code, loading_code, loading_name, loading_type, status,
|
||||||
|
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, max_stack, remarks, writer)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||||
|
RETURNING *`,
|
||||||
|
[companyCode, loading_code, loading_name, loading_type, status || "ACTIVE",
|
||||||
|
width_mm, length_mm, height_mm, self_weight_kg, max_load_kg, max_stack, remarks,
|
||||||
|
req.user!.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("적재함 등록", { companyCode, loading_code });
|
||||||
|
res.json({ success: true, data: result.rows[0] });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("적재함 등록 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLoadingUnit(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { id } = req.params;
|
||||||
|
const pool = getPool();
|
||||||
|
const {
|
||||||
|
loading_name, loading_type, status,
|
||||||
|
width_mm, length_mm, height_mm,
|
||||||
|
self_weight_kg, max_load_kg, max_stack, remarks,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE loading_unit SET
|
||||||
|
loading_name=$1, loading_type=$2, status=$3,
|
||||||
|
width_mm=$4, length_mm=$5, height_mm=$6,
|
||||||
|
self_weight_kg=$7, max_load_kg=$8, max_stack=$9, remarks=$10,
|
||||||
|
updated_date=NOW(), writer=$11
|
||||||
|
WHERE id=$12 AND company_code=$13
|
||||||
|
RETURNING *`,
|
||||||
|
[loading_name, loading_type, status,
|
||||||
|
width_mm, length_mm, height_mm,
|
||||||
|
self_weight_kg, max_load_kg, max_stack, remarks,
|
||||||
|
req.user!.userId, id, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("적재함 수정", { companyCode, id });
|
||||||
|
res.json({ success: true, data: result.rows[0] });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("적재함 수정 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLoadingUnit(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
const pool = getPool();
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
await client.query("BEGIN");
|
||||||
|
await client.query(
|
||||||
|
`DELETE FROM loading_unit_pkg WHERE loading_code = (SELECT loading_code FROM loading_unit WHERE id=$1 AND company_code=$2) AND company_code=$2`,
|
||||||
|
[id, companyCode]
|
||||||
|
);
|
||||||
|
const result = await client.query(
|
||||||
|
`DELETE FROM loading_unit WHERE id=$1 AND company_code=$2 RETURNING id`,
|
||||||
|
[id, companyCode]
|
||||||
|
);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("적재함 삭제", { companyCode, id });
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
logger.error("적재함 삭제 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// 적재함 포장구성 (loading_unit_pkg) CRUD
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getLoadingUnitPkgs(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { loadingCode } = req.params;
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM loading_unit_pkg WHERE loading_code=$1 AND company_code=$2 ORDER BY created_date DESC`,
|
||||||
|
[loadingCode, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("적재구성 조회 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLoadingUnitPkg(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const pool = getPool();
|
||||||
|
const { loading_code, pkg_code, max_load_qty, load_method } = req.body;
|
||||||
|
|
||||||
|
if (!loading_code || !pkg_code) {
|
||||||
|
res.status(400).json({ success: false, message: "적재함코드와 포장코드는 필수입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO loading_unit_pkg (company_code, loading_code, pkg_code, max_load_qty, load_method, writer)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6)
|
||||||
|
RETURNING *`,
|
||||||
|
[companyCode, loading_code, pkg_code, max_load_qty, load_method, req.user!.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("적재구성 추가", { companyCode, loading_code, pkg_code });
|
||||||
|
res.json({ success: true, data: result.rows[0] });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("적재구성 추가 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLoadingUnitPkg(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { id } = req.params;
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`DELETE FROM loading_unit_pkg WHERE id=$1 AND company_code=$2 RETURNING id`,
|
||||||
|
[id, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("적재구성 삭제", { companyCode, id });
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("적재구성 삭제 실패", { error: error.message });
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,36 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import {
|
||||||
|
getPkgUnits, createPkgUnit, updatePkgUnit, deletePkgUnit,
|
||||||
|
getPkgUnitItems, createPkgUnitItem, deletePkgUnitItem,
|
||||||
|
getLoadingUnits, createLoadingUnit, updateLoadingUnit, deleteLoadingUnit,
|
||||||
|
getLoadingUnitPkgs, createLoadingUnitPkg, deleteLoadingUnitPkg,
|
||||||
|
} from "../controllers/packagingController";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(authenticateToken);
|
router.use(authenticateToken);
|
||||||
|
|
||||||
// TODO: 포장/적재정보 관리 API 구현 예정
|
// 포장단위
|
||||||
|
router.get("/pkg-units", getPkgUnits);
|
||||||
|
router.post("/pkg-units", createPkgUnit);
|
||||||
|
router.put("/pkg-units/:id", updatePkgUnit);
|
||||||
|
router.delete("/pkg-units/:id", deletePkgUnit);
|
||||||
|
|
||||||
|
// 포장단위 매칭품목
|
||||||
|
router.get("/pkg-unit-items/:pkgCode", getPkgUnitItems);
|
||||||
|
router.post("/pkg-unit-items", createPkgUnitItem);
|
||||||
|
router.delete("/pkg-unit-items/:id", deletePkgUnitItem);
|
||||||
|
|
||||||
|
// 적재함
|
||||||
|
router.get("/loading-units", getLoadingUnits);
|
||||||
|
router.post("/loading-units", createLoadingUnit);
|
||||||
|
router.put("/loading-units/:id", updateLoadingUnit);
|
||||||
|
router.delete("/loading-units/:id", deleteLoadingUnit);
|
||||||
|
|
||||||
|
// 적재함 포장구성
|
||||||
|
router.get("/loading-unit-pkgs/:loadingCode", getLoadingUnitPkgs);
|
||||||
|
router.post("/loading-unit-pkgs", createLoadingUnitPkg);
|
||||||
|
router.delete("/loading-unit-pkgs/:id", deleteLoadingUnitPkg);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -2346,19 +2346,24 @@ export class ScreenManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴별 화면 목록 조회 (✅ Raw Query 전환 완료)
|
* 메뉴별 화면 목록 조회
|
||||||
|
* company_code 매칭: 본인 회사 할당 + SUPER_ADMIN 글로벌 할당('*') 모두 조회
|
||||||
|
* 본인 회사 할당이 우선, 없으면 글로벌 할당 사용
|
||||||
*/
|
*/
|
||||||
async getScreensByMenu(
|
async getScreensByMenu(
|
||||||
menuObjid: number,
|
menuObjid: number,
|
||||||
companyCode: string,
|
companyCode: string,
|
||||||
): Promise<ScreenDefinition[]> {
|
): Promise<ScreenDefinition[]> {
|
||||||
const screens = await query<any>(
|
const screens = await query<any>(
|
||||||
`SELECT sd.* FROM screen_menu_assignments sma
|
`SELECT sd.*
|
||||||
|
FROM screen_menu_assignments sma
|
||||||
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
||||||
WHERE sma.menu_objid = $1
|
WHERE sma.menu_objid = $1
|
||||||
AND sma.company_code = $2
|
AND (sma.company_code = $2 OR sma.company_code = '*')
|
||||||
AND sma.is_active = 'Y'
|
AND sma.is_active = 'Y'
|
||||||
ORDER BY sma.display_order ASC`,
|
ORDER BY
|
||||||
|
CASE WHEN sma.company_code = $2 THEN 0 ELSE 1 END,
|
||||||
|
sma.display_order ASC`,
|
||||||
[menuObjid, companyCode],
|
[menuObjid, companyCode],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo, useState, useEffect } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { ScreenViewPageWrapper } from "@/app/(main)/screens/[screenId]/page";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const LoadingFallback = () => (
|
const LoadingFallback = () => (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
|
|
@ -10,11 +12,52 @@ const LoadingFallback = () => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
function ScreenCodeResolver({ screenCode }: { screenCode: string }) {
|
||||||
* 관리자 페이지를 URL 기반으로 동적 로딩하는 레지스트리.
|
const [screenId, setScreenId] = useState<number | null>(null);
|
||||||
* 사이드바 메뉴에서 접근하는 주요 페이지를 명시적으로 매핑한다.
|
const [loading, setLoading] = useState(true);
|
||||||
* 매핑되지 않은 URL은 catch-all fallback으로 처리된다.
|
|
||||||
*/
|
useEffect(() => {
|
||||||
|
const numericId = parseInt(screenCode);
|
||||||
|
if (!isNaN(numericId)) {
|
||||||
|
setScreenId(numericId);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resolve = async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/screen-management/screens", {
|
||||||
|
params: { searchTerm: screenCode, size: 50 },
|
||||||
|
});
|
||||||
|
const items = res.data?.data?.data || res.data?.data || [];
|
||||||
|
const arr = Array.isArray(items) ? items : [];
|
||||||
|
const exact = arr.find((s: any) => s.screenCode === screenCode);
|
||||||
|
const target = exact || arr[0];
|
||||||
|
if (target) setScreenId(target.screenId || target.screen_id);
|
||||||
|
} catch {
|
||||||
|
console.error("스크린 코드 변환 실패:", screenCode);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
resolve();
|
||||||
|
}, [screenCode]);
|
||||||
|
|
||||||
|
if (loading) return <LoadingFallback />;
|
||||||
|
if (!screenId) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center">
|
||||||
|
<p className="text-sm text-muted-foreground">화면을 찾을 수 없습니다 (코드: {screenCode})</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <ScreenViewPageWrapper screenIdProp={screenId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardViewPage = dynamic(
|
||||||
|
() => import("@/app/(main)/dashboard/[dashboardId]/page"),
|
||||||
|
{ ssr: false, loading: LoadingFallback },
|
||||||
|
);
|
||||||
|
|
||||||
const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||||
// 관리자 메인
|
// 관리자 메인
|
||||||
"/admin": dynamic(() => import("@/app/(main)/admin/page"), { ssr: false, loading: LoadingFallback }),
|
"/admin": dynamic(() => import("@/app/(main)/admin/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
|
@ -62,6 +105,16 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||||
"/admin/batch-management": dynamic(() => import("@/app/(main)/admin/batch-management/page"), { ssr: false, loading: LoadingFallback }),
|
"/admin/batch-management": dynamic(() => import("@/app/(main)/admin/batch-management/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
"/admin/batch-management-new": dynamic(() => import("@/app/(main)/admin/batch-management-new/page"), { ssr: false, loading: LoadingFallback }),
|
"/admin/batch-management-new": dynamic(() => import("@/app/(main)/admin/batch-management-new/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
|
||||||
|
// 결재 관리
|
||||||
|
"/admin/approvalTemplate": dynamic(() => import("@/app/(main)/admin/approvalTemplate/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
"/admin/approvalBox": dynamic(() => import("@/app/(main)/admin/approvalBox/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
"/admin/approvalMng": dynamic(() => import("@/app/(main)/admin/approvalMng/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
|
||||||
|
// 시스템
|
||||||
|
"/admin/audit-log": dynamic(() => import("@/app/(main)/admin/audit-log/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
"/admin/system-notices": dynamic(() => import("@/app/(main)/admin/system-notices/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
"/admin/aiAssistant": dynamic(() => import("@/app/(main)/admin/aiAssistant/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
|
||||||
// 기타
|
// 기타
|
||||||
"/admin/cascading-management": dynamic(() => import("@/app/(main)/admin/cascading-management/page"), { ssr: false, loading: LoadingFallback }),
|
"/admin/cascading-management": dynamic(() => import("@/app/(main)/admin/cascading-management/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
"/admin/cascading-relations": dynamic(() => import("@/app/(main)/admin/cascading-relations/page"), { ssr: false, loading: LoadingFallback }),
|
"/admin/cascading-relations": dynamic(() => import("@/app/(main)/admin/cascading-relations/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
|
|
@ -73,18 +126,115 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
|
||||||
"/admin/auto-fill": dynamic(() => import("@/app/(main)/admin/auto-fill/page"), { ssr: false, loading: LoadingFallback }),
|
"/admin/auto-fill": dynamic(() => import("@/app/(main)/admin/auto-fill/page"), { ssr: false, loading: LoadingFallback }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 매핑되지 않은 URL용 Fallback
|
const DYNAMIC_ADMIN_IMPORTS: Record<string, () => Promise<any>> = {
|
||||||
|
"/admin/aiAssistant/dashboard": () => import("@/app/(main)/admin/aiAssistant/dashboard/page"),
|
||||||
|
"/admin/aiAssistant/history": () => import("@/app/(main)/admin/aiAssistant/history/page"),
|
||||||
|
"/admin/aiAssistant/api-keys": () => import("@/app/(main)/admin/aiAssistant/api-keys/page"),
|
||||||
|
"/admin/aiAssistant/api-test": () => import("@/app/(main)/admin/aiAssistant/api-test/page"),
|
||||||
|
"/admin/aiAssistant/usage": () => import("@/app/(main)/admin/aiAssistant/usage/page"),
|
||||||
|
"/admin/aiAssistant/chat": () => import("@/app/(main)/admin/aiAssistant/chat/page"),
|
||||||
|
"/admin/screenMng/barcodeList": () => import("@/app/(main)/admin/screenMng/barcodeList/page"),
|
||||||
|
"/admin/automaticMng/batchmngList/create": () => import("@/app/(main)/admin/automaticMng/batchmngList/create/page"),
|
||||||
|
"/admin/systemMng/dataflow/node-editorList": () => import("@/app/(main)/admin/systemMng/dataflow/node-editorList/page"),
|
||||||
|
"/admin/standards/new": () => import("@/app/(main)/admin/standards/new/page"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const DYNAMIC_ADMIN_PATTERNS: Array<{
|
||||||
|
pattern: RegExp;
|
||||||
|
getImport: (match: RegExpMatchArray) => Promise<any>;
|
||||||
|
extractParams: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/userMng\/rolesList\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/userMng/rolesList/[id]/page"),
|
||||||
|
extractParams: (m) => ({ id: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/screenMng\/dashboardList\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/screenMng/dashboardList/[id]/page"),
|
||||||
|
extractParams: (m) => ({ id: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/automaticMng\/flowMgmtList\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/automaticMng/flowMgmtList/[id]/page"),
|
||||||
|
extractParams: (m) => ({ id: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/automaticMng\/batchmngList\/edit\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page"),
|
||||||
|
extractParams: (m) => ({ id: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/screenMng\/barcodeList\/designer\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/screenMng/barcodeList/designer/[labelId]/page"),
|
||||||
|
extractParams: (m) => ({ labelId: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/screenMng\/reportList\/designer\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/screenMng/reportList/designer/[reportId]/page"),
|
||||||
|
extractParams: (m) => ({ reportId: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/systemMng\/dataflow\/edit\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/systemMng/dataflow/edit/[diagramId]/page"),
|
||||||
|
extractParams: (m) => ({ diagramId: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/userMng\/companyList\/([^/]+)\/departments$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/userMng/companyList/[companyCode]/departments/page"),
|
||||||
|
extractParams: (m) => ({ companyCode: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/standards\/([^/]+)\/edit$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/standards/[webType]/edit/page"),
|
||||||
|
extractParams: (m) => ({ webType: m[1] }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/admin\/standards\/([^/]+)$/,
|
||||||
|
getImport: () => import("@/app/(main)/admin/standards/[webType]/page"),
|
||||||
|
extractParams: (m) => ({ webType: m[1] }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function DynamicAdminLoader({ url, params }: { url: string; params?: Record<string, string> }) {
|
||||||
|
const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const staticImport = DYNAMIC_ADMIN_IMPORTS[url];
|
||||||
|
if (staticImport) {
|
||||||
|
staticImport()
|
||||||
|
.then((mod) => setComponent(() => mod.default))
|
||||||
|
.catch(() => setFailed(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { pattern, getImport, extractParams } of DYNAMIC_ADMIN_PATTERNS) {
|
||||||
|
const match = url.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
getImport()
|
||||||
|
.then((mod) => setComponent(() => mod.default))
|
||||||
|
.catch(() => setFailed(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFailed(true);
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
if (failed) return <AdminPageFallback url={url} />;
|
||||||
|
if (!Component) return <LoadingFallback />;
|
||||||
|
if (params) return <Component params={Promise.resolve(params)} />;
|
||||||
|
return <Component />;
|
||||||
|
}
|
||||||
|
|
||||||
function AdminPageFallback({ url }: { url: string }) {
|
function AdminPageFallback({ url }: { url: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-lg font-semibold text-foreground">페이지 로딩 불가</p>
|
<p className="text-lg font-semibold text-foreground">페이지 로딩 불가</p>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">경로: {url}</p>
|
||||||
경로: {url}
|
<p className="mt-2 text-xs text-muted-foreground">해당 페이지가 존재하지 않습니다.</p>
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
|
||||||
AdminPageRenderer 레지스트리에 이 URL을 추가해주세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -95,15 +245,58 @@ interface AdminPageRendererProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminPageRenderer({ url }: AdminPageRendererProps) {
|
export function AdminPageRenderer({ url }: AdminPageRendererProps) {
|
||||||
const PageComponent = useMemo(() => {
|
const cleanUrl = url.split("?")[0].split("#")[0].replace(/\/$/, "");
|
||||||
// URL에서 쿼리스트링/해시 제거 후 매칭
|
|
||||||
const cleanUrl = url.split("?")[0].split("#")[0].replace(/\/$/, "");
|
|
||||||
return ADMIN_PAGE_REGISTRY[cleanUrl] || null;
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
if (!PageComponent) {
|
console.log("[AdminPageRenderer] 렌더링:", { url, cleanUrl });
|
||||||
return <AdminPageFallback url={url} />;
|
|
||||||
|
// 화면 할당: /screens/[id]
|
||||||
|
const screensIdMatch = cleanUrl.match(/^\/screens\/(\d+)$/);
|
||||||
|
if (screensIdMatch) {
|
||||||
|
console.log("[AdminPageRenderer] → /screens/[id] 매칭:", screensIdMatch[1]);
|
||||||
|
return <ScreenViewPageWrapper screenIdProp={parseInt(screensIdMatch[1])} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageComponent />;
|
// 화면 할당: /screen/[code] (구 형식)
|
||||||
|
const screenCodeMatch = cleanUrl.match(/^\/screen\/([^/]+)$/);
|
||||||
|
if (screenCodeMatch) {
|
||||||
|
console.log("[AdminPageRenderer] → /screen/[code] 매칭:", screenCodeMatch[1]);
|
||||||
|
return <ScreenCodeResolver screenCode={screenCodeMatch[1]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대시보드 할당: /dashboard/[id]
|
||||||
|
const dashboardMatch = cleanUrl.match(/^\/dashboard\/([^/]+)$/);
|
||||||
|
if (dashboardMatch) {
|
||||||
|
console.log("[AdminPageRenderer] → /dashboard/[id] 매칭:", dashboardMatch[1]);
|
||||||
|
return <DashboardViewPage params={Promise.resolve({ dashboardId: dashboardMatch[1] })} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL 직접 입력: 레지스트리 매칭
|
||||||
|
const PageComponent = useMemo(() => {
|
||||||
|
return ADMIN_PAGE_REGISTRY[cleanUrl] || null;
|
||||||
|
}, [cleanUrl]);
|
||||||
|
|
||||||
|
if (PageComponent) {
|
||||||
|
console.log("[AdminPageRenderer] → 레지스트리 매칭:", cleanUrl);
|
||||||
|
return <PageComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레지스트리에 없으면 동적 import 시도
|
||||||
|
// 동적 라우트 패턴 매칭 (params 추출)
|
||||||
|
for (const { pattern, extractParams } of DYNAMIC_ADMIN_PATTERNS) {
|
||||||
|
const match = cleanUrl.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
const params = extractParams(match);
|
||||||
|
console.log("[AdminPageRenderer] → 동적 라우트 매칭:", cleanUrl, params);
|
||||||
|
return <DynamicAdminLoader url={cleanUrl} params={params} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정적 동적 import 목록에 있으면
|
||||||
|
if (DYNAMIC_ADMIN_IMPORTS[cleanUrl]) {
|
||||||
|
console.log("[AdminPageRenderer] → 동적 import:", cleanUrl);
|
||||||
|
return <DynamicAdminLoader url={cleanUrl} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("[AdminPageRenderer] 미등록 URL:", cleanUrl);
|
||||||
|
return <AdminPageFallback url={url} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,12 +202,26 @@ const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: Exten
|
||||||
|
|
||||||
const children = convertMenuToUI(allMenus, userInfo, menuId, tabTitle);
|
const children = convertMenuToUI(allMenus, userInfo, menuId, tabTitle);
|
||||||
|
|
||||||
|
const menuUrl = menu.menu_url || menu.MENU_URL || "#";
|
||||||
|
const screenCode = menu.screen_code || menu.SCREEN_CODE || null;
|
||||||
|
const menuType = String(menu.menu_type ?? menu.MENU_TYPE ?? "");
|
||||||
|
|
||||||
|
let screenId: number | null = null;
|
||||||
|
const screensMatch = menuUrl.match(/^\/screens\/(\d+)/);
|
||||||
|
if (screensMatch) {
|
||||||
|
screenId = parseInt(screensMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: menuId,
|
id: menuId,
|
||||||
|
objid: menuId,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
tabTitle,
|
tabTitle,
|
||||||
icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || "", menu.menu_icon || menu.MENU_ICON),
|
icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || "", menu.menu_icon || menu.MENU_ICON),
|
||||||
url: menu.menu_url || menu.MENU_URL || "#",
|
url: menuUrl,
|
||||||
|
screenCode,
|
||||||
|
screenId,
|
||||||
|
menuType,
|
||||||
children: children.length > 0 ? children : undefined,
|
children: children.length > 0 ? children : undefined,
|
||||||
hasChildren: children.length > 0,
|
hasChildren: children.length > 0,
|
||||||
};
|
};
|
||||||
|
|
@ -341,42 +355,76 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
const handleMenuClick = async (menu: any) => {
|
const handleMenuClick = async (menu: any) => {
|
||||||
if (menu.hasChildren) {
|
if (menu.hasChildren) {
|
||||||
toggleMenu(menu.id);
|
toggleMenu(menu.id);
|
||||||
} else {
|
return;
|
||||||
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
|
}
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem("currentMenuName", menuName);
|
const menuName = menu.tabTitle || menu.label || menu.name || "메뉴";
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem("currentMenuName", menuName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0");
|
||||||
|
const isAdminMenu = menu.menuType === "0";
|
||||||
|
|
||||||
|
console.log("[handleMenuClick] 메뉴 클릭:", {
|
||||||
|
menuName,
|
||||||
|
menuObjid,
|
||||||
|
menuType: menu.menuType,
|
||||||
|
isAdminMenu,
|
||||||
|
screenId: menu.screenId,
|
||||||
|
screenCode: menu.screenCode,
|
||||||
|
url: menu.url,
|
||||||
|
fullMenu: menu,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 관리자 메뉴 (menu_type = 0): URL 직접 입력 → admin 탭
|
||||||
|
if (isAdminMenu) {
|
||||||
|
if (menu.url && menu.url !== "#") {
|
||||||
|
console.log("[handleMenuClick] → admin 탭:", menu.url);
|
||||||
|
openTab({ type: "admin", title: menuName, adminUrl: menu.url });
|
||||||
|
if (isMobile) setSidebarOpen(false);
|
||||||
|
} else {
|
||||||
|
toast.warning("이 메뉴에는 연결된 페이지가 없습니다.");
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 메뉴 (menu_type = 1, 2): 화면/대시보드 할당
|
||||||
|
// 1) screenId가 메뉴 URL에서 추출된 경우 바로 screen 탭
|
||||||
|
if (menu.screenId) {
|
||||||
|
console.log("[handleMenuClick] → screen 탭 (URL에서 screenId 추출):", menu.screenId);
|
||||||
|
openTab({ type: "screen", title: menuName, screenId: menu.screenId, menuObjid });
|
||||||
|
if (isMobile) setSidebarOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) screen_menu_assignments 테이블 조회
|
||||||
|
if (menuObjid) {
|
||||||
try {
|
try {
|
||||||
const menuObjid = menu.objid || menu.id;
|
console.log("[handleMenuClick] → screen_menu_assignments 조회 시도, menuObjid:", menuObjid);
|
||||||
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||||||
|
console.log("[handleMenuClick] → 조회 결과:", assignedScreens);
|
||||||
if (assignedScreens.length > 0) {
|
if (assignedScreens.length > 0) {
|
||||||
const firstScreen = assignedScreens[0];
|
console.log("[handleMenuClick] → screen 탭 (assignments):", assignedScreens[0].screenId);
|
||||||
openTab({
|
openTab({ type: "screen", title: menuName, screenId: assignedScreens[0].screenId, menuObjid });
|
||||||
type: "screen",
|
|
||||||
title: menuName,
|
|
||||||
screenId: firstScreen.screenId,
|
|
||||||
menuObjid: parseInt(menuObjid),
|
|
||||||
});
|
|
||||||
if (isMobile) setSidebarOpen(false);
|
if (isMobile) setSidebarOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
console.warn("할당된 화면 조회 실패");
|
console.error("[handleMenuClick] 할당된 화면 조회 실패:", err);
|
||||||
}
|
|
||||||
|
|
||||||
if (menu.url && menu.url !== "#") {
|
|
||||||
openTab({
|
|
||||||
type: "admin",
|
|
||||||
title: menuName,
|
|
||||||
adminUrl: menu.url,
|
|
||||||
});
|
|
||||||
if (isMobile) setSidebarOpen(false);
|
|
||||||
} else {
|
|
||||||
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) 대시보드 할당 (/dashboard/xxx) → admin 탭으로 렌더링 (AdminPageRenderer가 처리)
|
||||||
|
if (menu.url && menu.url.startsWith("/dashboard/")) {
|
||||||
|
console.log("[handleMenuClick] → 대시보드 탭:", menu.url);
|
||||||
|
openTab({ type: "admin", title: menuName, adminUrl: menu.url });
|
||||||
|
if (isMobile) setSidebarOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("[handleMenuClick] 어떤 조건에도 매칭 안 됨:", { menuName, menuType: menu.menuType, url: menu.url, screenId: menu.screenId });
|
||||||
|
toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModeSwitch = () => {
|
const handleModeSwitch = () => {
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,14 @@ function TabPageRenderer({
|
||||||
tab: { id: string; type: string; screenId?: number; menuObjid?: number; adminUrl?: string };
|
tab: { id: string; type: string; screenId?: number; menuObjid?: number; adminUrl?: string };
|
||||||
refreshKey: number;
|
refreshKey: number;
|
||||||
}) {
|
}) {
|
||||||
|
console.log("[TabPageRenderer] 탭 렌더링:", {
|
||||||
|
tabId: tab.id,
|
||||||
|
type: tab.type,
|
||||||
|
screenId: tab.screenId,
|
||||||
|
adminUrl: tab.adminUrl,
|
||||||
|
menuObjid: tab.menuObjid,
|
||||||
|
});
|
||||||
|
|
||||||
if (tab.type === "screen" && tab.screenId != null) {
|
if (tab.type === "screen" && tab.screenId != null) {
|
||||||
return (
|
return (
|
||||||
<ScreenViewPageWrapper
|
<ScreenViewPageWrapper
|
||||||
|
|
@ -244,5 +252,6 @@ function TabPageRenderer({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn("[TabPageRenderer] 렌더링 불가 - 매칭 조건 없음:", tab);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,72 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config,
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 데이터 바인딩 설정 */}
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="dataBindingEnabled"
|
||||||
|
checked={!!config.dataBinding?.sourceComponentId}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
updateConfig("dataBinding", {
|
||||||
|
sourceComponentId: config.dataBinding?.sourceComponentId || "",
|
||||||
|
sourceColumn: config.dataBinding?.sourceColumn || "",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateConfig("dataBinding", undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="dataBindingEnabled" className="text-xs font-semibold">
|
||||||
|
테이블 선택 데이터 바인딩
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.dataBinding && (
|
||||||
|
<div className="space-y-2 rounded border p-2">
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
v2-table-list에서 행 선택 시 해당 컬럼 값이 자동으로 채워집니다
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs font-medium">소스 컴포넌트 ID</Label>
|
||||||
|
<Input
|
||||||
|
value={config.dataBinding?.sourceComponentId || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateConfig("dataBinding", {
|
||||||
|
...config.dataBinding,
|
||||||
|
sourceComponentId: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="예: tbl_items"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
같은 화면 내 v2-table-list 컴포넌트의 ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs font-medium">소스 컬럼명</Label>
|
||||||
|
<Input
|
||||||
|
value={config.dataBinding?.sourceColumn || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateConfig("dataBinding", {
|
||||||
|
...config.dataBinding,
|
||||||
|
sourceColumn: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="예: item_number"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
선택된 행에서 가져올 컬럼명
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ export interface MenuItem {
|
||||||
TRANSLATED_DESC?: string;
|
TRANSLATED_DESC?: string;
|
||||||
menu_icon?: string;
|
menu_icon?: string;
|
||||||
MENU_ICON?: string;
|
MENU_ICON?: string;
|
||||||
|
screen_code?: string;
|
||||||
|
SCREEN_CODE?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuFormData {
|
export interface MenuFormData {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,78 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||||
import { V2InputDefinition } from "./index";
|
import { V2InputDefinition } from "./index";
|
||||||
import { V2Input } from "@/components/v2/V2Input";
|
import { V2Input } from "@/components/v2/V2Input";
|
||||||
import { isColumnRequiredByMeta } from "../../DynamicComponentRenderer";
|
import { isColumnRequiredByMeta } from "../../DynamicComponentRenderer";
|
||||||
|
import { v2EventBus, V2_EVENTS } from "@/lib/v2-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dataBinding이 설정된 v2-input을 위한 wrapper
|
||||||
|
* v2-table-list의 TABLE_DATA_CHANGE 이벤트를 구독하여
|
||||||
|
* 선택된 행의 특정 컬럼 값을 자동으로 formData에 반영
|
||||||
|
*/
|
||||||
|
function DataBindingWrapper({
|
||||||
|
dataBinding,
|
||||||
|
columnName,
|
||||||
|
onFormDataChange,
|
||||||
|
isInteractive,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
dataBinding: { sourceComponentId: string; sourceColumn: string };
|
||||||
|
columnName: string;
|
||||||
|
onFormDataChange?: (field: string, value: any) => void;
|
||||||
|
isInteractive?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const lastBoundValueRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dataBinding?.sourceComponentId || !dataBinding?.sourceColumn) return;
|
||||||
|
|
||||||
|
console.log("[DataBinding] 구독 시작:", {
|
||||||
|
sourceComponentId: dataBinding.sourceComponentId,
|
||||||
|
sourceColumn: dataBinding.sourceColumn,
|
||||||
|
targetColumn: columnName,
|
||||||
|
isInteractive,
|
||||||
|
hasOnFormDataChange: !!onFormDataChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_DATA_CHANGE, (payload: any) => {
|
||||||
|
console.log("[DataBinding] TABLE_DATA_CHANGE 수신:", {
|
||||||
|
payloadSource: payload.source,
|
||||||
|
expectedSource: dataBinding.sourceComponentId,
|
||||||
|
dataLength: payload.data?.length,
|
||||||
|
match: payload.source === dataBinding.sourceComponentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload.source !== dataBinding.sourceComponentId) return;
|
||||||
|
|
||||||
|
const selectedData = payload.data;
|
||||||
|
if (selectedData && selectedData.length > 0) {
|
||||||
|
const value = selectedData[0][dataBinding.sourceColumn];
|
||||||
|
console.log("[DataBinding] 바인딩 값:", { column: dataBinding.sourceColumn, value, columnName });
|
||||||
|
if (value !== lastBoundValueRef.current) {
|
||||||
|
lastBoundValueRef.current = value;
|
||||||
|
if (onFormDataChange && columnName) {
|
||||||
|
onFormDataChange(columnName, value ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lastBoundValueRef.current !== null) {
|
||||||
|
lastBoundValueRef.current = null;
|
||||||
|
if (onFormDataChange && columnName) {
|
||||||
|
onFormDataChange(columnName, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [dataBinding?.sourceComponentId, dataBinding?.sourceColumn, columnName, onFormDataChange, isInteractive]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2Input 렌더러
|
* V2Input 렌더러
|
||||||
|
|
@ -16,41 +84,37 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
|
||||||
render(): React.ReactElement {
|
render(): React.ReactElement {
|
||||||
const { component, formData, onFormDataChange, isDesignMode, isSelected, isInteractive, ...restProps } = this.props;
|
const { component, formData, onFormDataChange, isDesignMode, isSelected, isInteractive, ...restProps } = this.props;
|
||||||
|
|
||||||
// 컴포넌트 설정 추출
|
|
||||||
const config = component.componentConfig || component.config || {};
|
const config = component.componentConfig || component.config || {};
|
||||||
const columnName = component.columnName;
|
const columnName = component.columnName;
|
||||||
const tableName = component.tableName || this.props.tableName;
|
const tableName = component.tableName || this.props.tableName;
|
||||||
|
|
||||||
// formData에서 현재 값 가져오기
|
|
||||||
const currentValue = formData?.[columnName] ?? component.value ?? "";
|
const currentValue = formData?.[columnName] ?? component.value ?? "";
|
||||||
|
|
||||||
// 값 변경 핸들러
|
|
||||||
const handleChange = (value: any) => {
|
const handleChange = (value: any) => {
|
||||||
console.log("🔄 [V2InputRenderer] handleChange 호출:", {
|
|
||||||
columnName,
|
|
||||||
value,
|
|
||||||
isInteractive,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
});
|
|
||||||
if (isInteractive && onFormDataChange && columnName) {
|
if (isInteractive && onFormDataChange && columnName) {
|
||||||
onFormDataChange(columnName, value);
|
onFormDataChange(columnName, value);
|
||||||
} else {
|
|
||||||
console.warn("⚠️ [V2InputRenderer] onFormDataChange 호출 스킵:", {
|
|
||||||
isInteractive,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
columnName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 라벨: style.labelText 우선, 없으면 component.label 사용
|
|
||||||
// 🔧 style.labelDisplay를 먼저 체크 (속성 패널에서 style 객체로 업데이트하므로)
|
|
||||||
const style = component.style || {};
|
const style = component.style || {};
|
||||||
const labelDisplay = style.labelDisplay ?? (component as any).labelDisplay;
|
const labelDisplay = style.labelDisplay ?? (component as any).labelDisplay;
|
||||||
// labelDisplay: true → 라벨 표시, false → 숨김, undefined → 기존 동작 유지(숨김)
|
|
||||||
const effectiveLabel = labelDisplay === true ? style.labelText || component.label : undefined;
|
const effectiveLabel = labelDisplay === true ? style.labelText || component.label : undefined;
|
||||||
|
|
||||||
return (
|
const dataBinding = config.dataBinding || (component as any).dataBinding || config.componentConfig?.dataBinding;
|
||||||
|
|
||||||
|
if (dataBinding || (config as any).dataBinding || (component as any).dataBinding) {
|
||||||
|
console.log("[V2InputRenderer] dataBinding 탐색:", {
|
||||||
|
componentId: component.id,
|
||||||
|
columnName,
|
||||||
|
configKeys: Object.keys(config),
|
||||||
|
configDataBinding: config.dataBinding,
|
||||||
|
componentDataBinding: (component as any).dataBinding,
|
||||||
|
nestedDataBinding: config.componentConfig?.dataBinding,
|
||||||
|
finalDataBinding: dataBinding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputElement = (
|
||||||
<V2Input
|
<V2Input
|
||||||
id={component.id}
|
id={component.id}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
|
|
@ -77,10 +141,26 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
|
||||||
{...restProps}
|
{...restProps}
|
||||||
label={effectiveLabel}
|
label={effectiveLabel}
|
||||||
required={component.required || isColumnRequiredByMeta(tableName, columnName)}
|
required={component.required || isColumnRequiredByMeta(tableName, columnName)}
|
||||||
readonly={config.readonly || component.readonly}
|
readonly={config.readonly || component.readonly || !!dataBinding?.sourceComponentId}
|
||||||
disabled={config.disabled || component.disabled}
|
disabled={config.disabled || component.disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// dataBinding이 있으면 wrapper로 감싸서 이벤트 구독
|
||||||
|
if (dataBinding?.sourceComponentId && dataBinding?.sourceColumn) {
|
||||||
|
return (
|
||||||
|
<DataBindingWrapper
|
||||||
|
dataBinding={dataBinding}
|
||||||
|
columnName={columnName}
|
||||||
|
onFormDataChange={onFormDataChange}
|
||||||
|
isInteractive={isInteractive}
|
||||||
|
>
|
||||||
|
{inputElement}
|
||||||
|
</DataBindingWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5103,6 +5103,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -118,9 +118,9 @@ export interface AdditionalTabConfig {
|
||||||
// 추가 버튼 설정 (모달 화면 연결 지원)
|
// 추가 버튼 설정 (모달 화면 연결 지원)
|
||||||
addButton?: {
|
addButton?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
mode: "auto" | "modal"; // auto: 내장 폼, modal: 커스텀 모달 화면
|
mode: "auto" | "modal";
|
||||||
modalScreenId?: number; // 모달로 열 화면 ID (mode: "modal"일 때)
|
modalScreenId?: number;
|
||||||
buttonLabel?: string; // 버튼 라벨 (기본: "추가")
|
buttonLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteButton?: {
|
deleteButton?: {
|
||||||
|
|
@ -161,9 +161,9 @@ export interface SplitPanelLayoutConfig {
|
||||||
// 추가 버튼 설정 (모달 화면 연결 지원)
|
// 추가 버튼 설정 (모달 화면 연결 지원)
|
||||||
addButton?: {
|
addButton?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
mode: "auto" | "modal"; // auto: 내장 폼, modal: 커스텀 모달 화면
|
mode: "auto" | "modal";
|
||||||
modalScreenId?: number; // 모달로 열 화면 ID (mode: "modal"일 때)
|
modalScreenId?: number;
|
||||||
buttonLabel?: string; // 버튼 라벨 (기본: "추가")
|
buttonLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
columns?: Array<{
|
columns?: Array<{
|
||||||
|
|
@ -334,10 +334,10 @@ export interface SplitPanelLayoutConfig {
|
||||||
|
|
||||||
// 🆕 추가 버튼 설정 (모달 화면 연결 지원)
|
// 🆕 추가 버튼 설정 (모달 화면 연결 지원)
|
||||||
addButton?: {
|
addButton?: {
|
||||||
enabled: boolean; // 추가 버튼 표시 여부 (기본: true)
|
enabled: boolean;
|
||||||
mode: "auto" | "modal"; // auto: 내장 폼, modal: 커스텀 모달 화면
|
mode: "auto" | "modal";
|
||||||
modalScreenId?: number; // 모달로 열 화면 ID (mode: "modal"일 때)
|
modalScreenId?: number;
|
||||||
buttonLabel?: string; // 버튼 라벨 (기본: "추가")
|
buttonLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 삭제 버튼 설정
|
// 🆕 삭제 버튼 설정
|
||||||
|
|
|
||||||
|
|
@ -2080,11 +2080,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
||||||
const newSelectedRows = new Set(selectedRows);
|
const isMultiSelect = tableConfig.checkbox?.multiple !== false;
|
||||||
if (checked) {
|
let newSelectedRows: Set<string>;
|
||||||
newSelectedRows.add(rowKey);
|
|
||||||
|
if (isMultiSelect) {
|
||||||
|
newSelectedRows = new Set(selectedRows);
|
||||||
|
if (checked) {
|
||||||
|
newSelectedRows.add(rowKey);
|
||||||
|
} else {
|
||||||
|
newSelectedRows.delete(rowKey);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newSelectedRows.delete(rowKey);
|
// 단일 선택: 기존 선택 해제 후 새 항목만 선택
|
||||||
|
newSelectedRows = checked ? new Set([rowKey]) : new Set();
|
||||||
}
|
}
|
||||||
setSelectedRows(newSelectedRows);
|
setSelectedRows(newSelectedRows);
|
||||||
|
|
||||||
|
|
@ -4154,6 +4162,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
const renderCheckboxHeader = () => {
|
const renderCheckboxHeader = () => {
|
||||||
if (!tableConfig.checkbox?.selectAll) return null;
|
if (!tableConfig.checkbox?.selectAll) return null;
|
||||||
|
if (tableConfig.checkbox?.multiple === false) return null;
|
||||||
|
|
||||||
return <Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" />;
|
return <Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue