Compare commits
10 Commits
532c80a86b
...
fef2f4a132
| Author | SHA1 | Date |
|---|---|---|
|
|
fef2f4a132 | |
|
|
35ec16084f | |
|
|
84f3ae4e6f | |
|
|
6534d03ecd | |
|
|
32d4575fb5 | |
|
|
6ebe551caa | |
|
|
abdb6b17f8 | |
|
|
e7ecd0a863 | |
|
|
23911d3dd8 | |
|
|
668b45d4ea |
|
|
@ -20,8 +20,9 @@ export class CommonCodeController {
|
||||||
*/
|
*/
|
||||||
async getCategories(req: AuthenticatedRequest, res: Response) {
|
async getCategories(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { search, isActive, page = "1", size = "20" } = req.query;
|
const { search, isActive, page = "1", size = "20", menuObjid } = req.query;
|
||||||
const userCompanyCode = req.user?.companyCode;
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
const menuObjidNum = menuObjid ? Number(menuObjid) : undefined;
|
||||||
|
|
||||||
const categories = await this.commonCodeService.getCategories(
|
const categories = await this.commonCodeService.getCategories(
|
||||||
{
|
{
|
||||||
|
|
@ -35,7 +36,8 @@ export class CommonCodeController {
|
||||||
page: parseInt(page as string),
|
page: parseInt(page as string),
|
||||||
size: parseInt(size as string),
|
size: parseInt(size as string),
|
||||||
},
|
},
|
||||||
userCompanyCode
|
userCompanyCode,
|
||||||
|
menuObjidNum
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -61,8 +63,9 @@ export class CommonCodeController {
|
||||||
async getCodes(req: AuthenticatedRequest, res: Response) {
|
async getCodes(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { categoryCode } = req.params;
|
const { categoryCode } = req.params;
|
||||||
const { search, isActive, page, size } = req.query;
|
const { search, isActive, page, size, menuObjid } = req.query;
|
||||||
const userCompanyCode = req.user?.companyCode;
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
const menuObjidNum = menuObjid ? Number(menuObjid) : undefined;
|
||||||
|
|
||||||
const result = await this.commonCodeService.getCodes(
|
const result = await this.commonCodeService.getCodes(
|
||||||
categoryCode,
|
categoryCode,
|
||||||
|
|
@ -77,7 +80,8 @@ export class CommonCodeController {
|
||||||
page: page ? parseInt(page as string) : undefined,
|
page: page ? parseInt(page as string) : undefined,
|
||||||
size: size ? parseInt(size as string) : undefined,
|
size: size ? parseInt(size as string) : undefined,
|
||||||
},
|
},
|
||||||
userCompanyCode
|
userCompanyCode,
|
||||||
|
menuObjidNum
|
||||||
);
|
);
|
||||||
|
|
||||||
// 프론트엔드가 기대하는 형식으로 데이터 변환
|
// 프론트엔드가 기대하는 형식으로 데이터 변환
|
||||||
|
|
@ -131,6 +135,7 @@ export class CommonCodeController {
|
||||||
const categoryData: CreateCategoryData = req.body;
|
const categoryData: CreateCategoryData = req.body;
|
||||||
const userId = req.user?.userId || "SYSTEM";
|
const userId = req.user?.userId || "SYSTEM";
|
||||||
const companyCode = req.user?.companyCode || "*";
|
const companyCode = req.user?.companyCode || "*";
|
||||||
|
const menuObjid = req.body.menuObjid;
|
||||||
|
|
||||||
// 입력값 검증
|
// 입력값 검증
|
||||||
if (!categoryData.categoryCode || !categoryData.categoryName) {
|
if (!categoryData.categoryCode || !categoryData.categoryName) {
|
||||||
|
|
@ -140,10 +145,18 @@ export class CommonCodeController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!menuObjid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "메뉴 OBJID는 필수입니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const category = await this.commonCodeService.createCategory(
|
const category = await this.commonCodeService.createCategory(
|
||||||
categoryData,
|
categoryData,
|
||||||
userId,
|
userId,
|
||||||
companyCode
|
companyCode,
|
||||||
|
Number(menuObjid)
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
|
|
@ -263,6 +276,7 @@ export class CommonCodeController {
|
||||||
const codeData: CreateCodeData = req.body;
|
const codeData: CreateCodeData = req.body;
|
||||||
const userId = req.user?.userId || "SYSTEM";
|
const userId = req.user?.userId || "SYSTEM";
|
||||||
const companyCode = req.user?.companyCode || "*";
|
const companyCode = req.user?.companyCode || "*";
|
||||||
|
const menuObjid = req.body.menuObjid;
|
||||||
|
|
||||||
// 입력값 검증
|
// 입력값 검증
|
||||||
if (!codeData.codeValue || !codeData.codeName) {
|
if (!codeData.codeValue || !codeData.codeName) {
|
||||||
|
|
@ -272,11 +286,19 @@ export class CommonCodeController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!menuObjid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "메뉴 OBJID는 필수입니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const code = await this.commonCodeService.createCode(
|
const code = await this.commonCodeService.createCode(
|
||||||
categoryCode,
|
categoryCode,
|
||||||
codeData,
|
codeData,
|
||||||
userId,
|
userId,
|
||||||
companyCode
|
companyCode,
|
||||||
|
Number(menuObjid)
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,24 @@ router.get("/available/:menuObjid?", authenticateToken, async (req: Authenticate
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined;
|
const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined;
|
||||||
|
|
||||||
|
logger.info("메뉴별 채번 규칙 조회 요청", { menuObjid, companyCode });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid);
|
const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid);
|
||||||
|
|
||||||
|
logger.info("✅ 메뉴별 채번 규칙 조회 성공 (컨트롤러)", {
|
||||||
|
companyCode,
|
||||||
|
menuObjid,
|
||||||
|
rulesCount: rules.length
|
||||||
|
});
|
||||||
|
|
||||||
return res.json({ success: true, data: rules });
|
return res.json({ success: true, data: rules });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("메뉴별 사용 가능한 규칙 조회 실패", {
|
logger.error("❌ 메뉴별 사용 가능한 규칙 조회 실패 (컨트롤러)", {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
|
errorCode: error.code,
|
||||||
|
errorStack: error.stack,
|
||||||
|
companyCode,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
});
|
});
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
|
@ -100,6 +112,17 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const ruleConfig = req.body;
|
const ruleConfig = req.body;
|
||||||
|
|
||||||
|
logger.info("🔍 [POST /numbering-rules] 채번 규칙 생성 요청:", {
|
||||||
|
companyCode,
|
||||||
|
userId,
|
||||||
|
ruleId: ruleConfig.ruleId,
|
||||||
|
ruleName: ruleConfig.ruleName,
|
||||||
|
scopeType: ruleConfig.scopeType,
|
||||||
|
menuObjid: ruleConfig.menuObjid,
|
||||||
|
tableName: ruleConfig.tableName,
|
||||||
|
partsCount: ruleConfig.parts?.length,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ruleConfig.ruleId || !ruleConfig.ruleName) {
|
if (!ruleConfig.ruleId || !ruleConfig.ruleName) {
|
||||||
return res.status(400).json({ success: false, error: "규칙 ID와 규칙명은 필수입니다" });
|
return res.status(400).json({ success: false, error: "규칙 ID와 규칙명은 필수입니다" });
|
||||||
|
|
@ -110,12 +133,22 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId);
|
const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId);
|
||||||
|
|
||||||
|
logger.info("✅ [POST /numbering-rules] 채번 규칙 생성 성공:", {
|
||||||
|
ruleId: newRule.ruleId,
|
||||||
|
menuObjid: newRule.menuObjid,
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(201).json({ success: true, data: newRule });
|
return res.status(201).json({ success: true, data: newRule });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === "23505") {
|
if (error.code === "23505") {
|
||||||
return res.status(409).json({ success: false, error: "이미 존재하는 규칙 ID입니다" });
|
return res.status(409).json({ success: false, error: "이미 존재하는 규칙 ID입니다" });
|
||||||
}
|
}
|
||||||
logger.error("규칙 생성 실패", { error: error.message });
|
logger.error("❌ [POST /numbering-rules] 규칙 생성 실패:", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,29 @@ export const getScreen = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 화면에 할당된 메뉴 조회
|
||||||
|
export const getScreenMenu = async (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { companyCode } = req.user as any;
|
||||||
|
|
||||||
|
const menuInfo = await screenManagementService.getMenuByScreen(
|
||||||
|
parseInt(id),
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true, data: menuInfo });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("화면 메뉴 조회 실패:", error);
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ success: false, message: "화면 메뉴 조회에 실패했습니다." });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 화면 생성
|
// 화면 생성
|
||||||
export const createScreen = async (
|
export const createScreen = async (
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,31 @@ export const getCategoryColumns = async (req: AuthenticatedRequest, res: Respons
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 목록 조회 (메뉴 스코프 적용)
|
* 카테고리 값 목록 조회 (메뉴 스코프 적용)
|
||||||
|
*
|
||||||
|
* Query Parameters:
|
||||||
|
* - menuObjid: 메뉴 OBJID (선택사항, 제공 시 형제 메뉴의 카테고리 값 포함)
|
||||||
|
* - includeInactive: 비활성 값 포함 여부
|
||||||
*/
|
*/
|
||||||
export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
|
export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { tableName, columnName } = req.params;
|
const { tableName, columnName } = req.params;
|
||||||
const includeInactive = req.query.includeInactive === "true";
|
const includeInactive = req.query.includeInactive === "true";
|
||||||
|
const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined;
|
||||||
|
|
||||||
|
logger.info("카테고리 값 조회 요청", {
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
menuObjid,
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
const values = await tableCategoryValueService.getCategoryValues(
|
const values = await tableCategoryValueService.getCategoryValues(
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
companyCode,
|
companyCode,
|
||||||
includeInactive
|
includeInactive,
|
||||||
|
menuObjid // ← menuObjid 전달
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -61,18 +74,37 @@ export const getCategoryValues = async (req: AuthenticatedRequest, res: Response
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 추가
|
* 카테고리 값 추가 (메뉴 스코프)
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - menuObjid: 메뉴 OBJID (필수)
|
||||||
|
* - 나머지 카테고리 값 정보
|
||||||
*/
|
*/
|
||||||
export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
|
export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const value = req.body;
|
const { menuObjid, ...value } = req.body;
|
||||||
|
|
||||||
|
if (!menuObjid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "menuObjid는 필수입니다",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("카테고리 값 추가 요청", {
|
||||||
|
tableName: value.tableName,
|
||||||
|
columnName: value.columnName,
|
||||||
|
menuObjid,
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
const newValue = await tableCategoryValueService.addCategoryValue(
|
const newValue = await tableCategoryValueService.addCategoryValue(
|
||||||
value,
|
value,
|
||||||
companyCode,
|
companyCode,
|
||||||
userId
|
userId,
|
||||||
|
Number(menuObjid) // ← menuObjid 전달
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
|
|
|
||||||
|
|
@ -1599,3 +1599,114 @@ export async function toggleLogTable(
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
||||||
|
*
|
||||||
|
* @route GET /api/table-management/menu/:menuObjid/category-columns
|
||||||
|
* @description 형제 메뉴들의 화면에서 사용하는 테이블의 input_type='category' 컬럼 조회
|
||||||
|
*/
|
||||||
|
export async function getCategoryColumnsByMenu(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { menuObjid } = req.params;
|
||||||
|
const companyCode = req.user?.companyCode;
|
||||||
|
|
||||||
|
logger.info("📥 메뉴별 카테고리 컬럼 조회 요청", { menuObjid, companyCode });
|
||||||
|
|
||||||
|
if (!menuObjid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "메뉴 OBJID가 필요합니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 형제 메뉴 조회
|
||||||
|
const { getSiblingMenuObjids } = await import("../services/menuService");
|
||||||
|
const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid));
|
||||||
|
|
||||||
|
logger.info("✅ 형제 메뉴 조회 완료", { siblingObjids });
|
||||||
|
|
||||||
|
// 2. 형제 메뉴들이 사용하는 테이블 조회
|
||||||
|
const { getPool } = await import("../database/db");
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const tablesQuery = `
|
||||||
|
SELECT DISTINCT sd.table_name
|
||||||
|
FROM screen_menu_assignments sma
|
||||||
|
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
||||||
|
WHERE sma.menu_objid = ANY($1)
|
||||||
|
AND sma.company_code = $2
|
||||||
|
AND sd.table_name IS NOT NULL
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]);
|
||||||
|
const tableNames = tablesResult.rows.map((row: any) => row.table_name);
|
||||||
|
|
||||||
|
logger.info("✅ 형제 메뉴 테이블 조회 완료", { tableNames, count: tableNames.length });
|
||||||
|
|
||||||
|
if (tableNames.length === 0) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: [],
|
||||||
|
message: "형제 메뉴에 연결된 테이블이 없습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 테이블들의 카테고리 타입 컬럼 조회 (테이블 라벨 포함)
|
||||||
|
logger.info("🔍 카테고리 컬럼 쿼리 준비", { tableNames, companyCode });
|
||||||
|
|
||||||
|
const columnsQuery = `
|
||||||
|
SELECT
|
||||||
|
ttc.table_name AS "tableName",
|
||||||
|
COALESCE(
|
||||||
|
tl.table_label,
|
||||||
|
initcap(replace(ttc.table_name, '_', ' '))
|
||||||
|
) AS "tableLabel",
|
||||||
|
ttc.column_name AS "columnName",
|
||||||
|
COALESCE(
|
||||||
|
cl.column_label,
|
||||||
|
initcap(replace(ttc.column_name, '_', ' '))
|
||||||
|
) AS "columnLabel",
|
||||||
|
ttc.input_type AS "inputType"
|
||||||
|
FROM table_type_columns ttc
|
||||||
|
LEFT JOIN column_labels cl
|
||||||
|
ON ttc.table_name = cl.table_name
|
||||||
|
AND ttc.column_name = cl.column_name
|
||||||
|
LEFT JOIN table_labels tl
|
||||||
|
ON ttc.table_name = tl.table_name
|
||||||
|
WHERE ttc.table_name = ANY($1)
|
||||||
|
AND ttc.company_code = $2
|
||||||
|
AND ttc.input_type = 'category'
|
||||||
|
ORDER BY ttc.table_name, ttc.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
logger.info("🔍 카테고리 컬럼 쿼리 실행 중...");
|
||||||
|
const columnsResult = await pool.query(columnsQuery, [tableNames, companyCode]);
|
||||||
|
logger.info("✅ 카테고리 컬럼 쿼리 완료", { rowCount: columnsResult.rows.length });
|
||||||
|
|
||||||
|
logger.info("✅ 카테고리 컬럼 조회 완료", {
|
||||||
|
columnCount: columnsResult.rows.length
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: columnsResult.rows,
|
||||||
|
message: "카테고리 컬럼 조회 성공",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("❌ 메뉴별 카테고리 컬럼 조회 실패");
|
||||||
|
logger.error("에러 메시지:", error.message);
|
||||||
|
logger.error("에러 스택:", error.stack);
|
||||||
|
logger.error("에러 전체:", error);
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "카테고리 컬럼 조회에 실패했습니다.",
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack, // 디버깅용
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
import {
|
import {
|
||||||
getScreens,
|
getScreens,
|
||||||
getScreen,
|
getScreen,
|
||||||
|
getScreenMenu,
|
||||||
createScreen,
|
createScreen,
|
||||||
updateScreen,
|
updateScreen,
|
||||||
updateScreenInfo,
|
updateScreenInfo,
|
||||||
|
|
@ -33,6 +34,7 @@ router.use(authenticateToken);
|
||||||
// 화면 관리
|
// 화면 관리
|
||||||
router.get("/screens", getScreens);
|
router.get("/screens", getScreens);
|
||||||
router.get("/screens/:id", getScreen);
|
router.get("/screens/:id", getScreen);
|
||||||
|
router.get("/screens/:id/menu", getScreenMenu); // 화면에 할당된 메뉴 조회
|
||||||
router.post("/screens", createScreen);
|
router.post("/screens", createScreen);
|
||||||
router.put("/screens/:id", updateScreen);
|
router.put("/screens/:id", updateScreen);
|
||||||
router.put("/screens/:id/info", updateScreenInfo); // 화면 정보만 수정
|
router.put("/screens/:id/info", updateScreenInfo); // 화면 정보만 수정
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
getLogConfig,
|
getLogConfig,
|
||||||
getLogData,
|
getLogData,
|
||||||
toggleLogTable,
|
toggleLogTable,
|
||||||
|
getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회
|
||||||
} from "../controllers/tableManagementController";
|
} from "../controllers/tableManagementController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
@ -187,4 +188,14 @@ router.get("/tables/:tableName/log", getLogData);
|
||||||
*/
|
*/
|
||||||
router.post("/tables/:tableName/log/toggle", toggleLogTable);
|
router.post("/tables/:tableName/log/toggle", toggleLogTable);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 메뉴 기반 카테고리 관리 API
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
||||||
|
* GET /api/table-management/menu/:menuObjid/category-columns
|
||||||
|
*/
|
||||||
|
router.get("/menu/:menuObjid/category-columns", getCategoryColumnsByMenu);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export class CommonCodeService {
|
||||||
/**
|
/**
|
||||||
* 카테고리 목록 조회
|
* 카테고리 목록 조회
|
||||||
*/
|
*/
|
||||||
async getCategories(params: GetCategoriesParams, userCompanyCode?: string) {
|
async getCategories(params: GetCategoriesParams, userCompanyCode?: string, menuObjid?: number) {
|
||||||
try {
|
try {
|
||||||
const { search, isActive, page = 1, size = 20 } = params;
|
const { search, isActive, page = 1, size = 20 } = params;
|
||||||
|
|
||||||
|
|
@ -74,6 +74,16 @@ export class CommonCodeService {
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
// 메뉴별 필터링 (형제 메뉴 포함)
|
||||||
|
if (menuObjid) {
|
||||||
|
const { getSiblingMenuObjids } = await import('./menuService');
|
||||||
|
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
|
||||||
|
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
|
||||||
|
values.push(siblingMenuObjids);
|
||||||
|
paramIndex++;
|
||||||
|
logger.info(`메뉴별 코드 카테고리 필터링: ${menuObjid}, 형제 메뉴: ${siblingMenuObjids.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
if (userCompanyCode && userCompanyCode !== "*") {
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
whereConditions.push(`company_code = $${paramIndex}`);
|
whereConditions.push(`company_code = $${paramIndex}`);
|
||||||
|
|
@ -142,15 +152,43 @@ export class CommonCodeService {
|
||||||
async getCodes(
|
async getCodes(
|
||||||
categoryCode: string,
|
categoryCode: string,
|
||||||
params: GetCodesParams,
|
params: GetCodesParams,
|
||||||
userCompanyCode?: string
|
userCompanyCode?: string,
|
||||||
|
menuObjid?: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { search, isActive, page = 1, size = 20 } = params;
|
const { search, isActive, page = 1, size = 20 } = params;
|
||||||
|
|
||||||
|
logger.info(`🔍 [getCodes] 코드 조회 시작:`, {
|
||||||
|
categoryCode,
|
||||||
|
menuObjid,
|
||||||
|
hasMenuObjid: !!menuObjid,
|
||||||
|
userCompanyCode,
|
||||||
|
search,
|
||||||
|
isActive,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
|
||||||
const whereConditions: string[] = ["code_category = $1"];
|
const whereConditions: string[] = ["code_category = $1"];
|
||||||
const values: any[] = [categoryCode];
|
const values: any[] = [categoryCode];
|
||||||
let paramIndex = 2;
|
let paramIndex = 2;
|
||||||
|
|
||||||
|
// 메뉴별 필터링 (형제 메뉴 포함)
|
||||||
|
if (menuObjid) {
|
||||||
|
const { getSiblingMenuObjids } = await import('./menuService');
|
||||||
|
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
|
||||||
|
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
|
||||||
|
values.push(siblingMenuObjids);
|
||||||
|
paramIndex++;
|
||||||
|
logger.info(`📋 [getCodes] 메뉴별 코드 필터링:`, {
|
||||||
|
menuObjid,
|
||||||
|
siblingMenuObjids,
|
||||||
|
siblingCount: siblingMenuObjids.length,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`⚠️ [getCodes] menuObjid 없음 - 전역 코드 조회`);
|
||||||
|
}
|
||||||
|
|
||||||
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
if (userCompanyCode && userCompanyCode !== "*") {
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
whereConditions.push(`company_code = $${paramIndex}`);
|
whereConditions.push(`company_code = $${paramIndex}`);
|
||||||
|
|
@ -178,6 +216,13 @@ export class CommonCodeService {
|
||||||
|
|
||||||
const offset = (page - 1) * size;
|
const offset = (page - 1) * size;
|
||||||
|
|
||||||
|
logger.info(`📝 [getCodes] 실행할 쿼리:`, {
|
||||||
|
whereClause,
|
||||||
|
values,
|
||||||
|
whereConditions,
|
||||||
|
paramIndex,
|
||||||
|
});
|
||||||
|
|
||||||
// 코드 조회
|
// 코드 조회
|
||||||
const codes = await query<CodeInfo>(
|
const codes = await query<CodeInfo>(
|
||||||
`SELECT * FROM code_info
|
`SELECT * FROM code_info
|
||||||
|
|
@ -196,9 +241,20 @@ export class CommonCodeService {
|
||||||
const total = parseInt(countResult?.count || "0");
|
const total = parseInt(countResult?.count || "0");
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"})`
|
`✅ [getCodes] 코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"}, menuObjid: ${menuObjid || "없음"})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.info(`📊 [getCodes] 조회된 코드 상세:`, {
|
||||||
|
categoryCode,
|
||||||
|
menuObjid,
|
||||||
|
codes: codes.map((c) => ({
|
||||||
|
code_value: c.code_value,
|
||||||
|
code_name: c.code_name,
|
||||||
|
menu_objid: c.menu_objid,
|
||||||
|
company_code: c.company_code,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
return { data: codes, total };
|
return { data: codes, total };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`코드 조회 중 오류 (${categoryCode}):`, error);
|
logger.error(`코드 조회 중 오류 (${categoryCode}):`, error);
|
||||||
|
|
@ -212,14 +268,15 @@ export class CommonCodeService {
|
||||||
async createCategory(
|
async createCategory(
|
||||||
data: CreateCategoryData,
|
data: CreateCategoryData,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
companyCode: string
|
companyCode: string,
|
||||||
|
menuObjid: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const category = await queryOne<CodeCategory>(
|
const category = await queryOne<CodeCategory>(
|
||||||
`INSERT INTO code_category
|
`INSERT INTO code_category
|
||||||
(category_code, category_name, category_name_eng, description, sort_order,
|
(category_code, category_name, category_name_eng, description, sort_order,
|
||||||
is_active, company_code, created_by, updated_by, created_date, updated_date)
|
is_active, menu_objid, company_code, created_by, updated_by, created_date, updated_date)
|
||||||
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, $8, NOW(), NOW())
|
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, $8, $9, NOW(), NOW())
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
data.categoryCode,
|
data.categoryCode,
|
||||||
|
|
@ -227,6 +284,7 @@ export class CommonCodeService {
|
||||||
data.categoryNameEng || null,
|
data.categoryNameEng || null,
|
||||||
data.description || null,
|
data.description || null,
|
||||||
data.sortOrder || 0,
|
data.sortOrder || 0,
|
||||||
|
menuObjid,
|
||||||
companyCode,
|
companyCode,
|
||||||
createdBy,
|
createdBy,
|
||||||
createdBy,
|
createdBy,
|
||||||
|
|
@ -234,7 +292,7 @@ export class CommonCodeService {
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`카테고리 생성 완료: ${data.categoryCode} (회사: ${companyCode})`
|
`카테고리 생성 완료: ${data.categoryCode} (메뉴: ${menuObjid}, 회사: ${companyCode})`
|
||||||
);
|
);
|
||||||
return category;
|
return category;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -352,14 +410,15 @@ export class CommonCodeService {
|
||||||
categoryCode: string,
|
categoryCode: string,
|
||||||
data: CreateCodeData,
|
data: CreateCodeData,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
companyCode: string
|
companyCode: string,
|
||||||
|
menuObjid: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const code = await queryOne<CodeInfo>(
|
const code = await queryOne<CodeInfo>(
|
||||||
`INSERT INTO code_info
|
`INSERT INTO code_info
|
||||||
(code_category, code_value, code_name, code_name_eng, description, sort_order,
|
(code_category, code_value, code_name, code_name_eng, description, sort_order,
|
||||||
is_active, company_code, created_by, updated_by, created_date, updated_date)
|
is_active, menu_objid, company_code, created_by, updated_by, created_date, updated_date)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, NOW(), NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, NOW(), NOW())
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
categoryCode,
|
categoryCode,
|
||||||
|
|
@ -368,6 +427,7 @@ export class CommonCodeService {
|
||||||
data.codeNameEng || null,
|
data.codeNameEng || null,
|
||||||
data.description || null,
|
data.description || null,
|
||||||
data.sortOrder || 0,
|
data.sortOrder || 0,
|
||||||
|
menuObjid,
|
||||||
companyCode,
|
companyCode,
|
||||||
createdBy,
|
createdBy,
|
||||||
createdBy,
|
createdBy,
|
||||||
|
|
@ -375,7 +435,7 @@ export class CommonCodeService {
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`코드 생성 완료: ${categoryCode}.${data.codeValue} (회사: ${companyCode})`
|
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode})`
|
||||||
);
|
);
|
||||||
return code;
|
return code;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { getPool } from "../database/db";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 관련 유틸리티 서비스
|
||||||
|
*
|
||||||
|
* 메뉴 스코프 기반 데이터 공유를 위한 형제 메뉴 조회 기능 제공
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴의 형제 메뉴 및 자식 메뉴 OBJID 목록 조회
|
||||||
|
* (같은 부모를 가진 메뉴들 + 자식 메뉴들)
|
||||||
|
*
|
||||||
|
* 메뉴 스코프 규칙:
|
||||||
|
* - 같은 부모를 가진 형제 메뉴들은 카테고리/채번규칙을 공유
|
||||||
|
* - 자식 메뉴의 데이터도 부모 메뉴에서 조회 가능 (3레벨까지만 존재)
|
||||||
|
* - 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환
|
||||||
|
* - 메뉴를 찾을 수 없으면 안전하게 자기 자신만 반환
|
||||||
|
*
|
||||||
|
* @param menuObjid 현재 메뉴의 OBJID
|
||||||
|
* @returns 형제 메뉴 + 자식 메뉴 OBJID 배열 (자기 자신 포함, 정렬됨)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // 영업관리 (200)
|
||||||
|
* // ├── 고객관리 (201)
|
||||||
|
* // │ └── 고객등록 (211)
|
||||||
|
* // ├── 계약관리 (202)
|
||||||
|
* // └── 주문관리 (203)
|
||||||
|
*
|
||||||
|
* await getSiblingMenuObjids(201);
|
||||||
|
* // 결과: [201, 202, 203, 211] - 형제(202, 203) + 자식(211)
|
||||||
|
*/
|
||||||
|
export async function getSiblingMenuObjids(menuObjid: number): Promise<number[]> {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("메뉴 스코프 조회 시작", { menuObjid });
|
||||||
|
|
||||||
|
// 1. 현재 메뉴 자신을 포함
|
||||||
|
const menuObjids = [menuObjid];
|
||||||
|
|
||||||
|
// 2. 현재 메뉴의 자식 메뉴들 조회
|
||||||
|
const childrenQuery = `
|
||||||
|
SELECT objid FROM menu_info
|
||||||
|
WHERE parent_obj_id = $1
|
||||||
|
ORDER BY objid
|
||||||
|
`;
|
||||||
|
const childrenResult = await pool.query(childrenQuery, [menuObjid]);
|
||||||
|
|
||||||
|
const childObjids = childrenResult.rows.map((row) => Number(row.objid));
|
||||||
|
|
||||||
|
// 3. 자신 + 자식을 합쳐서 정렬
|
||||||
|
const allObjids = Array.from(new Set([...menuObjids, ...childObjids])).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
logger.debug("메뉴 스코프 조회 완료", {
|
||||||
|
menuObjid,
|
||||||
|
childCount: childObjids.length,
|
||||||
|
totalCount: allObjids.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return allObjids;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("메뉴 스코프 조회 실패", {
|
||||||
|
menuObjid,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
// 에러 발생 시 안전하게 자기 자신만 반환
|
||||||
|
return [menuObjid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 여러 메뉴의 형제 메뉴 OBJID 합집합 조회
|
||||||
|
*
|
||||||
|
* 여러 메뉴에 속한 모든 형제 메뉴를 중복 제거하여 반환
|
||||||
|
*
|
||||||
|
* @param menuObjids 메뉴 OBJID 배열
|
||||||
|
* @returns 모든 형제 메뉴 OBJID 배열 (중복 제거, 정렬됨)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // 서로 다른 부모를 가진 메뉴들의 형제를 모두 조회
|
||||||
|
* await getAllSiblingMenuObjids([201, 301]);
|
||||||
|
* // 201의 형제: [201, 202, 203]
|
||||||
|
* // 301의 형제: [301, 302]
|
||||||
|
* // 결과: [201, 202, 203, 301, 302]
|
||||||
|
*/
|
||||||
|
export async function getAllSiblingMenuObjids(
|
||||||
|
menuObjids: number[]
|
||||||
|
): Promise<number[]> {
|
||||||
|
if (!menuObjids || menuObjids.length === 0) {
|
||||||
|
logger.warn("getAllSiblingMenuObjids: 빈 배열 입력");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSiblings = new Set<number>();
|
||||||
|
|
||||||
|
for (const objid of menuObjids) {
|
||||||
|
const siblings = await getSiblingMenuObjids(objid);
|
||||||
|
siblings.forEach((s) => allSiblings.add(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Array.from(allSiblings).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
logger.info("여러 메뉴의 형제 조회 완료", {
|
||||||
|
inputMenus: menuObjids,
|
||||||
|
resultCount: result.length,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴 정보 조회
|
||||||
|
*
|
||||||
|
* @param menuObjid 메뉴 OBJID
|
||||||
|
* @returns 메뉴 정보 (없으면 null)
|
||||||
|
*/
|
||||||
|
export async function getMenuInfo(menuObjid: number): Promise<any | null> {
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
objid,
|
||||||
|
parent_obj_id AS "parentObjId",
|
||||||
|
menu_name_kor AS "menuNameKor",
|
||||||
|
menu_name_eng AS "menuNameEng",
|
||||||
|
menu_url AS "menuUrl",
|
||||||
|
company_code AS "companyCode"
|
||||||
|
FROM menu_info
|
||||||
|
WHERE objid = $1
|
||||||
|
`;
|
||||||
|
const result = await pool.query(query, [menuObjid]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("메뉴 정보 조회 실패", { menuObjid, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import { getPool } from "../database/db";
|
import { getPool } from "../database/db";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { getSiblingMenuObjids } from "./menuService";
|
||||||
|
|
||||||
interface NumberingRulePart {
|
interface NumberingRulePart {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -150,22 +151,33 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 메뉴에서 사용 가능한 규칙 목록 조회
|
* 현재 메뉴에서 사용 가능한 규칙 목록 조회 (메뉴 스코프)
|
||||||
|
*
|
||||||
|
* 메뉴 스코프 규칙:
|
||||||
|
* - menuObjid가 제공되면 형제 메뉴의 채번 규칙 포함
|
||||||
|
* - 우선순위: menu (형제 메뉴) > table > global
|
||||||
*/
|
*/
|
||||||
async getAvailableRulesForMenu(
|
async getAvailableRulesForMenu(
|
||||||
companyCode: string,
|
companyCode: string,
|
||||||
menuObjid?: number
|
menuObjid?: number
|
||||||
): Promise<NumberingRuleConfig[]> {
|
): Promise<NumberingRuleConfig[]> {
|
||||||
try {
|
try {
|
||||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작", {
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작 (메뉴 스코프)", {
|
||||||
companyCode,
|
companyCode,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
|
// 1. 형제 메뉴 OBJID 조회
|
||||||
|
let siblingObjids: number[] = [];
|
||||||
|
if (menuObjid) {
|
||||||
|
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
||||||
|
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||||
|
}
|
||||||
|
|
||||||
// menuObjid가 없으면 global 규칙만 반환
|
// menuObjid가 없으면 global 규칙만 반환
|
||||||
if (!menuObjid) {
|
if (!menuObjid || siblingObjids.length === 0) {
|
||||||
let query: string;
|
let query: string;
|
||||||
let params: any[];
|
let params: any[];
|
||||||
|
|
||||||
|
|
@ -261,35 +273,13 @@ class NumberingRuleService {
|
||||||
return result.rows;
|
return result.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 메뉴의 상위 계층 조회 (2레벨 메뉴 찾기)
|
// 2. 메뉴 스코프: 형제 메뉴의 채번 규칙 조회
|
||||||
const menuHierarchyQuery = `
|
// 우선순위: menu (형제 메뉴) > table > global
|
||||||
WITH RECURSIVE menu_path AS (
|
|
||||||
SELECT objid, objid_parent, menu_level
|
|
||||||
FROM menu_info
|
|
||||||
WHERE objid = $1
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT mi.objid, mi.objid_parent, mi.menu_level
|
|
||||||
FROM menu_info mi
|
|
||||||
INNER JOIN menu_path mp ON mi.objid = mp.objid_parent
|
|
||||||
)
|
|
||||||
SELECT objid, menu_level
|
|
||||||
FROM menu_path
|
|
||||||
WHERE menu_level = 2
|
|
||||||
LIMIT 1
|
|
||||||
`;
|
|
||||||
|
|
||||||
const hierarchyResult = await pool.query(menuHierarchyQuery, [menuObjid]);
|
|
||||||
const level2MenuObjid =
|
|
||||||
hierarchyResult.rowCount > 0 ? hierarchyResult.rows[0].objid : null;
|
|
||||||
|
|
||||||
// 사용 가능한 규칙 조회 (멀티테넌시 적용)
|
|
||||||
let query: string;
|
let query: string;
|
||||||
let params: any[];
|
let params: any[];
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 규칙 조회
|
// 최고 관리자: 모든 규칙 조회 (형제 메뉴 포함)
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
rule_id AS "ruleId",
|
rule_id AS "ruleId",
|
||||||
|
|
@ -309,12 +299,22 @@ class NumberingRuleService {
|
||||||
FROM numbering_rules
|
FROM numbering_rules
|
||||||
WHERE
|
WHERE
|
||||||
scope_type = 'global'
|
scope_type = 'global'
|
||||||
OR (scope_type = 'menu' AND menu_objid = $1)
|
OR scope_type = 'table'
|
||||||
ORDER BY scope_type DESC, created_at DESC
|
OR (scope_type = 'menu' AND menu_objid = ANY($1))
|
||||||
|
OR (scope_type = 'table' AND menu_objid = ANY($1)) -- ⚠️ 임시: table 스코프도 menu_objid로 필터링
|
||||||
|
OR (scope_type = 'table' AND menu_objid IS NULL) -- ⚠️ 임시: 기존 규칙(menu_objid NULL) 포함
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($1)) THEN 1
|
||||||
|
WHEN scope_type = 'table' THEN 2
|
||||||
|
WHEN scope_type = 'global' THEN 3
|
||||||
|
END,
|
||||||
|
created_at DESC
|
||||||
`;
|
`;
|
||||||
params = [level2MenuObjid];
|
params = [siblingObjids];
|
||||||
|
logger.info("최고 관리자: 형제 메뉴 포함 채번 규칙 조회 (기존 규칙 포함)", { siblingObjids });
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 규칙만 조회
|
// 일반 회사: 자신의 규칙만 조회 (형제 메뉴 포함)
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
rule_id AS "ruleId",
|
rule_id AS "ruleId",
|
||||||
|
|
@ -335,17 +335,36 @@ class NumberingRuleService {
|
||||||
WHERE company_code = $1
|
WHERE company_code = $1
|
||||||
AND (
|
AND (
|
||||||
scope_type = 'global'
|
scope_type = 'global'
|
||||||
OR (scope_type = 'menu' AND menu_objid = $2)
|
OR scope_type = 'table'
|
||||||
|
OR (scope_type = 'menu' AND menu_objid = ANY($2))
|
||||||
|
OR (scope_type = 'table' AND menu_objid = ANY($2)) -- ⚠️ 임시: table 스코프도 menu_objid로 필터링
|
||||||
|
OR (scope_type = 'table' AND menu_objid IS NULL) -- ⚠️ 임시: 기존 규칙(menu_objid NULL) 포함
|
||||||
)
|
)
|
||||||
ORDER BY scope_type DESC, created_at DESC
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($2)) THEN 1
|
||||||
|
WHEN scope_type = 'table' THEN 2
|
||||||
|
WHEN scope_type = 'global' THEN 3
|
||||||
|
END,
|
||||||
|
created_at DESC
|
||||||
`;
|
`;
|
||||||
params = [companyCode, level2MenuObjid];
|
params = [companyCode, siblingObjids];
|
||||||
|
logger.info("회사별: 형제 메뉴 포함 채번 규칙 조회 (기존 규칙 포함)", { companyCode, siblingObjids });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("🔍 채번 규칙 쿼리 실행", {
|
||||||
|
queryPreview: query.substring(0, 200),
|
||||||
|
paramsTypes: params.map(p => typeof p),
|
||||||
|
paramsValues: params,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
const result = await pool.query(query, params);
|
||||||
|
|
||||||
|
logger.debug("채번 규칙 쿼리 성공", { ruleCount: result.rows.length });
|
||||||
|
|
||||||
// 파트 정보 추가
|
// 파트 정보 추가
|
||||||
for (const rule of result.rows) {
|
for (const rule of result.rows) {
|
||||||
|
try {
|
||||||
let partsQuery: string;
|
let partsQuery: string;
|
||||||
let partsParams: any[];
|
let partsParams: any[];
|
||||||
|
|
||||||
|
|
@ -381,12 +400,28 @@ class NumberingRuleService {
|
||||||
|
|
||||||
const partsResult = await pool.query(partsQuery, partsParams);
|
const partsResult = await pool.query(partsQuery, partsParams);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = partsResult.rows;
|
||||||
|
|
||||||
|
logger.info("✅ 규칙 파트 조회 성공", {
|
||||||
|
ruleId: rule.ruleId,
|
||||||
|
ruleName: rule.ruleName,
|
||||||
|
partsCount: partsResult.rows.length,
|
||||||
|
});
|
||||||
|
} catch (partError: any) {
|
||||||
|
logger.error("❌ 규칙 파트 조회 실패", {
|
||||||
|
ruleId: rule.ruleId,
|
||||||
|
ruleName: rule.ruleName,
|
||||||
|
error: partError.message,
|
||||||
|
errorCode: partError.code,
|
||||||
|
errorStack: partError.stack,
|
||||||
|
});
|
||||||
|
throw partError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
||||||
companyCode,
|
companyCode,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
level2MenuObjid,
|
siblingCount: siblingObjids.length,
|
||||||
count: result.rowCount,
|
count: result.rowCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -394,8 +429,11 @@ class NumberingRuleService {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("메뉴별 채번 규칙 조회 실패", {
|
logger.error("메뉴별 채번 규칙 조회 실패", {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
|
errorCode: error.code,
|
||||||
|
errorStack: error.stack,
|
||||||
companyCode,
|
companyCode,
|
||||||
menuObjid,
|
menuObjid,
|
||||||
|
siblingObjids: siblingObjids || [],
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// ✅ Prisma → Raw Query 전환 (Phase 2.1)
|
// ✅ Prisma → Raw Query 전환 (Phase 2.1)
|
||||||
import { query, transaction } from "../database/db";
|
import { query, queryOne, transaction } from "../database/db";
|
||||||
import {
|
import {
|
||||||
ScreenDefinition,
|
ScreenDefinition,
|
||||||
CreateScreenRequest,
|
CreateScreenRequest,
|
||||||
|
|
@ -1547,6 +1547,39 @@ export class ScreenManagementService {
|
||||||
return screens.map((screen) => this.mapToScreenDefinition(screen));
|
return screens.map((screen) => this.mapToScreenDefinition(screen));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 화면에 할당된 메뉴 조회 (첫 번째 할당만 반환)
|
||||||
|
* 화면 편집기에서 menuObjid를 가져오기 위해 사용
|
||||||
|
*/
|
||||||
|
async getMenuByScreen(
|
||||||
|
screenId: number,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<{ menuObjid: number; menuName?: string } | null> {
|
||||||
|
const result = await queryOne<{
|
||||||
|
menu_objid: string;
|
||||||
|
menu_name_kor?: string;
|
||||||
|
}>(
|
||||||
|
`SELECT sma.menu_objid, mi.menu_name_kor
|
||||||
|
FROM screen_menu_assignments sma
|
||||||
|
LEFT JOIN menu_info mi ON sma.menu_objid = mi.objid
|
||||||
|
WHERE sma.screen_id = $1
|
||||||
|
AND sma.company_code = $2
|
||||||
|
AND sma.is_active = 'Y'
|
||||||
|
ORDER BY sma.created_date ASC
|
||||||
|
LIMIT 1`,
|
||||||
|
[screenId, companyCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuObjid: parseInt(result.menu_objid),
|
||||||
|
menuName: result.menu_name_kor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 화면-메뉴 할당 해제 (✅ Raw Query 전환 완료)
|
* 화면-메뉴 할당 해제 (✅ Raw Query 전환 완료)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { getPool } from "../database/db";
|
import { getPool } from "../database/db";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { getSiblingMenuObjids } from "./menuService";
|
||||||
import {
|
import {
|
||||||
TableCategoryValue,
|
TableCategoryValue,
|
||||||
CategoryColumn,
|
CategoryColumn,
|
||||||
|
|
@ -79,30 +80,45 @@ class TableCategoryValueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 컬럼의 카테고리 값 목록 조회 (테이블 스코프)
|
* 특정 컬럼의 카테고리 값 목록 조회 (메뉴 스코프)
|
||||||
|
*
|
||||||
|
* 메뉴 스코프 규칙:
|
||||||
|
* - menuObjid가 제공되면 해당 메뉴와 형제 메뉴의 카테고리 값을 조회
|
||||||
|
* - menuObjid가 없으면 테이블 스코프로 동작 (하위 호환성)
|
||||||
*/
|
*/
|
||||||
async getCategoryValues(
|
async getCategoryValues(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string,
|
columnName: string,
|
||||||
companyCode: string,
|
companyCode: string,
|
||||||
includeInactive: boolean = false
|
includeInactive: boolean = false,
|
||||||
|
menuObjid?: number
|
||||||
): Promise<TableCategoryValue[]> {
|
): Promise<TableCategoryValue[]> {
|
||||||
try {
|
try {
|
||||||
logger.info("카테고리 값 목록 조회", {
|
logger.info("카테고리 값 목록 조회 (메뉴 스코프)", {
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
companyCode,
|
companyCode,
|
||||||
includeInactive,
|
includeInactive,
|
||||||
|
menuObjid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
// 1. 메뉴 스코프: 형제 메뉴 OBJID 조회
|
||||||
|
let siblingObjids: number[] = [];
|
||||||
|
if (menuObjid) {
|
||||||
|
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
||||||
|
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 카테고리 값 조회 (형제 메뉴 포함)
|
||||||
let query: string;
|
let query: string;
|
||||||
let params: any[];
|
let params: any[];
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 카테고리 값 조회
|
// 최고 관리자: 모든 카테고리 값 조회
|
||||||
|
if (menuObjid && siblingObjids.length > 0) {
|
||||||
|
// 메뉴 스코프 적용
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
value_id AS "valueId",
|
value_id AS "valueId",
|
||||||
|
|
@ -119,6 +135,36 @@ 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",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy",
|
||||||
|
updated_by AS "updatedBy"
|
||||||
|
FROM table_column_category_values
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND column_name = $2
|
||||||
|
AND menu_objid = ANY($3)
|
||||||
|
`;
|
||||||
|
params = [tableName, columnName, siblingObjids];
|
||||||
|
} else {
|
||||||
|
// 테이블 스코프 (하위 호환성)
|
||||||
|
query = `
|
||||||
|
SELECT
|
||||||
|
value_id AS "valueId",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
value_code AS "valueCode",
|
||||||
|
value_label AS "valueLabel",
|
||||||
|
value_order AS "valueOrder",
|
||||||
|
parent_value_id AS "parentValueId",
|
||||||
|
depth,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
is_active AS "isActive",
|
||||||
|
is_default AS "isDefault",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
menu_objid 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",
|
||||||
|
|
@ -128,9 +174,12 @@ class TableCategoryValueService {
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
`;
|
`;
|
||||||
params = [tableName, columnName];
|
params = [tableName, columnName];
|
||||||
|
}
|
||||||
logger.info("최고 관리자 카테고리 값 조회");
|
logger.info("최고 관리자 카테고리 값 조회");
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값만 조회
|
// 일반 회사: 자신의 카테고리 값만 조회
|
||||||
|
if (menuObjid && siblingObjids.length > 0) {
|
||||||
|
// 메뉴 스코프 적용
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
value_id AS "valueId",
|
value_id AS "valueId",
|
||||||
|
|
@ -147,6 +196,37 @@ 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",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy",
|
||||||
|
updated_by AS "updatedBy"
|
||||||
|
FROM table_column_category_values
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND column_name = $2
|
||||||
|
AND menu_objid = ANY($3)
|
||||||
|
AND company_code = $4
|
||||||
|
`;
|
||||||
|
params = [tableName, columnName, siblingObjids, companyCode];
|
||||||
|
} else {
|
||||||
|
// 테이블 스코프 (하위 호환성)
|
||||||
|
query = `
|
||||||
|
SELECT
|
||||||
|
value_id AS "valueId",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
value_code AS "valueCode",
|
||||||
|
value_label AS "valueLabel",
|
||||||
|
value_order AS "valueOrder",
|
||||||
|
parent_value_id AS "parentValueId",
|
||||||
|
depth,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
is_active AS "isActive",
|
||||||
|
is_default AS "isDefault",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
menu_objid 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",
|
||||||
|
|
@ -157,6 +237,7 @@ class TableCategoryValueService {
|
||||||
AND company_code = $3
|
AND company_code = $3
|
||||||
`;
|
`;
|
||||||
params = [tableName, columnName, companyCode];
|
params = [tableName, columnName, companyCode];
|
||||||
|
}
|
||||||
logger.info("회사별 카테고리 값 조회", { companyCode });
|
logger.info("회사별 카테고리 값 조회", { companyCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,6 +256,8 @@ class TableCategoryValueService {
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
companyCode,
|
companyCode,
|
||||||
|
menuObjid,
|
||||||
|
scopeType: menuObjid ? "menu" : "table",
|
||||||
});
|
});
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
|
|
@ -185,17 +268,31 @@ class TableCategoryValueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 추가
|
* 카테고리 값 추가 (메뉴 스코프)
|
||||||
|
*
|
||||||
|
* @param value 카테고리 값 정보
|
||||||
|
* @param companyCode 회사 코드
|
||||||
|
* @param userId 생성자 ID
|
||||||
|
* @param menuObjid 메뉴 OBJID (필수)
|
||||||
*/
|
*/
|
||||||
async addCategoryValue(
|
async addCategoryValue(
|
||||||
value: TableCategoryValue,
|
value: TableCategoryValue,
|
||||||
companyCode: string,
|
companyCode: string,
|
||||||
userId: string
|
userId: string,
|
||||||
|
menuObjid: number
|
||||||
): Promise<TableCategoryValue> {
|
): Promise<TableCategoryValue> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 중복 코드 체크 (멀티테넌시 적용)
|
logger.info("카테고리 값 추가 (메뉴 스코프)", {
|
||||||
|
tableName: value.tableName,
|
||||||
|
columnName: value.columnName,
|
||||||
|
valueCode: value.valueCode,
|
||||||
|
menuObjid,
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 중복 코드 체크 (멀티테넌시 + 메뉴 스코프)
|
||||||
let duplicateQuery: string;
|
let duplicateQuery: string;
|
||||||
let duplicateParams: any[];
|
let duplicateParams: any[];
|
||||||
|
|
||||||
|
|
@ -207,8 +304,9 @@ class TableCategoryValueService {
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND value_code = $3
|
AND value_code = $3
|
||||||
|
AND menu_objid = $4
|
||||||
`;
|
`;
|
||||||
duplicateParams = [value.tableName, value.columnName, value.valueCode];
|
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid];
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 회사에서만 중복 체크
|
// 일반 회사: 자신의 회사에서만 중복 체크
|
||||||
duplicateQuery = `
|
duplicateQuery = `
|
||||||
|
|
@ -217,9 +315,10 @@ class TableCategoryValueService {
|
||||||
WHERE table_name = $1
|
WHERE table_name = $1
|
||||||
AND column_name = $2
|
AND column_name = $2
|
||||||
AND value_code = $3
|
AND value_code = $3
|
||||||
AND company_code = $4
|
AND menu_objid = $4
|
||||||
|
AND company_code = $5
|
||||||
`;
|
`;
|
||||||
duplicateParams = [value.tableName, value.columnName, value.valueCode, companyCode];
|
duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateResult = await pool.query(duplicateQuery, duplicateParams);
|
const duplicateResult = await pool.query(duplicateQuery, duplicateParams);
|
||||||
|
|
@ -232,8 +331,8 @@ class TableCategoryValueService {
|
||||||
INSERT INTO table_column_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, created_by
|
is_active, is_default, company_code, menu_objid, created_by
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING
|
RETURNING
|
||||||
value_id AS "valueId",
|
value_id AS "valueId",
|
||||||
table_name AS "tableName",
|
table_name AS "tableName",
|
||||||
|
|
@ -249,6 +348,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",
|
||||||
created_at AS "createdAt",
|
created_at AS "createdAt",
|
||||||
created_by AS "createdBy"
|
created_by AS "createdBy"
|
||||||
`;
|
`;
|
||||||
|
|
@ -267,6 +367,7 @@ class TableCategoryValueService {
|
||||||
value.isActive !== false,
|
value.isActive !== false,
|
||||||
value.isDefault || false,
|
value.isDefault || false,
|
||||||
companyCode,
|
companyCode,
|
||||||
|
menuObjid, // ← 메뉴 OBJID 저장
|
||||||
userId,
|
userId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -274,6 +375,7 @@ class TableCategoryValueService {
|
||||||
valueId: result.rows[0].valueId,
|
valueId: result.rows[0].valueId,
|
||||||
tableName: value.tableName,
|
tableName: value.tableName,
|
||||||
columnName: value.columnName,
|
columnName: value.columnName,
|
||||||
|
menuObjid,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi } from "@/lib/api/screen";
|
||||||
|
|
@ -21,9 +21,13 @@ import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감
|
||||||
|
|
||||||
export default function ScreenViewPage() {
|
export default function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const screenId = parseInt(params.screenId as string);
|
const screenId = parseInt(params.screenId as string);
|
||||||
|
|
||||||
|
// URL 쿼리에서 menuObjid 가져오기 (메뉴 스코프)
|
||||||
|
const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined;
|
||||||
|
|
||||||
// 🆕 현재 로그인한 사용자 정보
|
// 🆕 현재 로그인한 사용자 정보
|
||||||
const { user, userName, companyCode } = useAuth();
|
const { user, userName, companyCode } = useAuth();
|
||||||
|
|
||||||
|
|
@ -399,11 +403,13 @@ export default function ScreenViewPage() {
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
isDesignMode={false}
|
isDesignMode={false}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
|
menuObjid={menuObjid}
|
||||||
screenId={screenId}
|
screenId={screenId}
|
||||||
tableName={screen?.tableName}
|
tableName={screen?.tableName}
|
||||||
userId={user?.userId}
|
userId={user?.userId}
|
||||||
userName={userName}
|
userName={userName}
|
||||||
companyCode={companyCode}
|
companyCode={companyCode}
|
||||||
|
menuObjid={menuObjid}
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
sortBy={tableSortBy}
|
sortBy={tableSortBy}
|
||||||
sortOrder={tableSortOrder}
|
sortOrder={tableSortOrder}
|
||||||
|
|
@ -463,11 +469,13 @@ export default function ScreenViewPage() {
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
isDesignMode={false}
|
isDesignMode={false}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
|
menuObjid={menuObjid}
|
||||||
screenId={screenId}
|
screenId={screenId}
|
||||||
tableName={screen?.tableName}
|
tableName={screen?.tableName}
|
||||||
userId={user?.userId}
|
userId={user?.userId}
|
||||||
userName={userName}
|
userName={userName}
|
||||||
companyCode={companyCode}
|
companyCode={companyCode}
|
||||||
|
menuObjid={menuObjid}
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
sortBy={tableSortBy}
|
sortBy={tableSortBy}
|
||||||
sortOrder={tableSortOrder}
|
sortOrder={tableSortOrder}
|
||||||
|
|
|
||||||
|
|
@ -274,10 +274,14 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||||
// 할당된 화면이 있으면 첫 번째 화면으로 이동
|
// 할당된 화면이 있으면 첫 번째 화면으로 이동
|
||||||
const firstScreen = assignedScreens[0];
|
const firstScreen = assignedScreens[0];
|
||||||
|
|
||||||
// 관리자 모드 상태를 쿼리 파라미터로 전달
|
// 관리자 모드 상태와 menuObjid를 쿼리 파라미터로 전달
|
||||||
const screenPath = isAdminMode
|
const params = new URLSearchParams();
|
||||||
? `/screens/${firstScreen.screenId}?mode=admin`
|
if (isAdminMode) {
|
||||||
: `/screens/${firstScreen.screenId}`;
|
params.set("mode", "admin");
|
||||||
|
}
|
||||||
|
params.set("menuObjid", menuObjid.toString());
|
||||||
|
|
||||||
|
const screenPath = `/screens/${firstScreen.screenId}?${params.toString()}`;
|
||||||
|
|
||||||
router.push(screenPath);
|
router.push(screenPath);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { NumberingRuleConfig, NumberingRulePart } from "@/types/numbering-rule";
|
||||||
import { NumberingRuleCard } from "./NumberingRuleCard";
|
import { NumberingRuleCard } from "./NumberingRuleCard";
|
||||||
import { NumberingRulePreview } from "./NumberingRulePreview";
|
import { NumberingRulePreview } from "./NumberingRulePreview";
|
||||||
import {
|
import {
|
||||||
getNumberingRules,
|
getAvailableNumberingRules,
|
||||||
createNumberingRule,
|
createNumberingRule,
|
||||||
updateNumberingRule,
|
updateNumberingRule,
|
||||||
deleteNumberingRule,
|
deleteNumberingRule,
|
||||||
|
|
@ -26,6 +26,7 @@ interface NumberingRuleDesignerProps {
|
||||||
isPreview?: boolean;
|
isPreview?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용)
|
currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용)
|
||||||
|
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
|
|
@ -36,6 +37,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
className = "",
|
className = "",
|
||||||
currentTableName,
|
currentTableName,
|
||||||
|
menuObjid,
|
||||||
}) => {
|
}) => {
|
||||||
const [savedRules, setSavedRules] = useState<NumberingRuleConfig[]>([]);
|
const [savedRules, setSavedRules] = useState<NumberingRuleConfig[]>([]);
|
||||||
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
|
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
|
||||||
|
|
@ -53,7 +55,20 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
const loadRules = useCallback(async () => {
|
const loadRules = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getNumberingRules();
|
console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작:", {
|
||||||
|
menuObjid,
|
||||||
|
hasMenuObjid: !!menuObjid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await getAvailableNumberingRules(menuObjid);
|
||||||
|
|
||||||
|
console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답:", {
|
||||||
|
menuObjid,
|
||||||
|
success: response.success,
|
||||||
|
rulesCount: response.data?.length || 0,
|
||||||
|
rules: response.data,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setSavedRules(response.data);
|
setSavedRules(response.data);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -64,7 +79,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [menuObjid]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentRule) {
|
if (currentRule) {
|
||||||
|
|
@ -133,19 +148,23 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
try {
|
try {
|
||||||
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
|
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
|
||||||
|
|
||||||
// 저장 전에 현재 화면의 테이블명 자동 설정
|
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
||||||
const ruleToSave = {
|
const ruleToSave = {
|
||||||
...currentRule,
|
...currentRule,
|
||||||
scopeType: "table" as const, // 항상 table로 고정
|
scopeType: "table" as const, // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
||||||
tableName: currentTableName || currentRule.tableName || "", // 현재 테이블명 자동 설정
|
tableName: currentTableName || currentRule.tableName || "", // 현재 테이블명 자동 설정
|
||||||
|
menuObjid: menuObjid || currentRule.menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("💾 채번 규칙 저장:", {
|
console.log("💾 채번 규칙 저장:", {
|
||||||
currentTableName,
|
currentTableName,
|
||||||
|
menuObjid,
|
||||||
"currentRule.tableName": currentRule.tableName,
|
"currentRule.tableName": currentRule.tableName,
|
||||||
|
"currentRule.menuObjid": currentRule.menuObjid,
|
||||||
"ruleToSave.tableName": ruleToSave.tableName,
|
"ruleToSave.tableName": ruleToSave.tableName,
|
||||||
|
"ruleToSave.menuObjid": ruleToSave.menuObjid,
|
||||||
"ruleToSave.scopeType": ruleToSave.scopeType,
|
"ruleToSave.scopeType": ruleToSave.scopeType,
|
||||||
ruleToSave
|
ruleToSave,
|
||||||
});
|
});
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
@ -213,7 +232,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNewRule = useCallback(() => {
|
const handleNewRule = useCallback(() => {
|
||||||
console.log("📋 새 규칙 생성 - currentTableName:", currentTableName);
|
console.log("📋 새 규칙 생성:", { currentTableName, menuObjid });
|
||||||
|
|
||||||
const newRule: NumberingRuleConfig = {
|
const newRule: NumberingRuleConfig = {
|
||||||
ruleId: `rule-${Date.now()}`,
|
ruleId: `rule-${Date.now()}`,
|
||||||
|
|
@ -222,8 +241,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
separator: "-",
|
separator: "-",
|
||||||
resetPeriod: "none",
|
resetPeriod: "none",
|
||||||
currentSequence: 1,
|
currentSequence: 1,
|
||||||
scopeType: "table", // 기본값을 table로 설정
|
scopeType: "table", // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
||||||
tableName: currentTableName || "", // 현재 화면의 테이블명 자동 설정
|
tableName: currentTableName || "", // 현재 화면의 테이블명 자동 설정
|
||||||
|
menuObjid: menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("📋 생성된 규칙 정보:", newRule);
|
console.log("📋 생성된 규칙 정보:", newRule);
|
||||||
|
|
@ -232,7 +252,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
setCurrentRule(newRule);
|
setCurrentRule(newRule);
|
||||||
|
|
||||||
toast.success("새 규칙이 생성되었습니다");
|
toast.success("새 규칙이 생성되었습니다");
|
||||||
}, [currentTableName]);
|
}, [currentTableName, menuObjid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex h-full gap-4 ${className}`}>
|
<div className={`flex h-full gap-4 ${className}`}>
|
||||||
|
|
@ -273,7 +293,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
savedRules.map((rule) => (
|
savedRules.map((rule) => (
|
||||||
<Card
|
<Card
|
||||||
key={rule.ruleId}
|
key={rule.ruleId}
|
||||||
className={`py-2 border-border hover:bg-accent cursor-pointer transition-colors ${
|
className={`border-border hover:bg-accent cursor-pointer py-2 transition-colors ${
|
||||||
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelectRule(rule)}
|
onClick={() => handleSelectRule(rule)}
|
||||||
|
|
@ -356,7 +376,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
{currentTableName && (
|
{currentTableName && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-medium">적용 테이블</Label>
|
<Label className="text-sm font-medium">적용 테이블</Label>
|
||||||
<div className="flex h-9 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground">
|
<div className="border-input bg-muted text-muted-foreground flex h-9 items-center rounded-md border px-3 text-sm">
|
||||||
{currentTableName}
|
{currentTableName}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ interface InteractiveScreenViewerProps {
|
||||||
id: number;
|
id: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
};
|
};
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
|
||||||
// 새로운 검증 관련 옵션들
|
// 새로운 검증 관련 옵션들
|
||||||
enableEnhancedValidation?: boolean;
|
enableEnhancedValidation?: boolean;
|
||||||
tableColumns?: ColumnInfo[];
|
tableColumns?: ColumnInfo[];
|
||||||
|
|
@ -76,6 +77,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
screenInfo,
|
screenInfo,
|
||||||
|
menuObjid, // 🆕 메뉴 OBJID
|
||||||
enableEnhancedValidation = false,
|
enableEnhancedValidation = false,
|
||||||
tableColumns = [],
|
tableColumns = [],
|
||||||
showValidationPanel = false,
|
showValidationPanel = false,
|
||||||
|
|
@ -1090,15 +1092,12 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
const widget = comp as WidgetComponent;
|
const widget = comp as WidgetComponent;
|
||||||
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
|
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
|
||||||
|
|
||||||
console.log("🔍 InteractiveScreenViewer - Code 위젯 (공통코드 선택):", {
|
console.log(`🔍 [InteractiveScreenViewer] Code 위젯 렌더링:`, {
|
||||||
componentId: widget.id,
|
componentId: widget.id,
|
||||||
widgetType: widget.widgetType,
|
|
||||||
columnName: widget.columnName,
|
columnName: widget.columnName,
|
||||||
fieldName,
|
|
||||||
currentValue,
|
|
||||||
formData,
|
|
||||||
config,
|
|
||||||
codeCategory: config?.codeCategory,
|
codeCategory: config?.codeCategory,
|
||||||
|
menuObjid,
|
||||||
|
hasMenuObjid: !!menuObjid,
|
||||||
});
|
});
|
||||||
|
|
||||||
// code 타입은 공통코드 선택박스로 처리
|
// code 타입은 공통코드 선택박스로 처리
|
||||||
|
|
@ -1117,6 +1116,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
required: required,
|
required: required,
|
||||||
placeholder: config?.placeholder || "코드를 선택하세요...",
|
placeholder: config?.placeholder || "코드를 선택하세요...",
|
||||||
className: "w-full h-full",
|
className: "w-full h-full",
|
||||||
|
menuObjid: menuObjid, // 🆕 메뉴 OBJID 전달
|
||||||
}}
|
}}
|
||||||
config={{
|
config={{
|
||||||
...config,
|
...config,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ interface RealtimePreviewProps {
|
||||||
userId?: string; // 🆕 현재 사용자 ID
|
userId?: string; // 🆕 현재 사용자 ID
|
||||||
userName?: string; // 🆕 현재 사용자 이름
|
userName?: string; // 🆕 현재 사용자 이름
|
||||||
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID (코드/카테고리 스코프용)
|
||||||
selectedRowsData?: any[];
|
selectedRowsData?: any[];
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||||
flowSelectedData?: any[];
|
flowSelectedData?: any[];
|
||||||
|
|
@ -107,6 +108,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
userId, // 🆕 사용자 ID
|
userId, // 🆕 사용자 ID
|
||||||
userName, // 🆕 사용자 이름
|
userName, // 🆕 사용자 이름
|
||||||
companyCode, // 🆕 회사 코드
|
companyCode, // 🆕 회사 코드
|
||||||
|
menuObjid, // 🆕 메뉴 OBJID
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
flowSelectedData,
|
flowSelectedData,
|
||||||
|
|
@ -344,6 +346,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
userId={userId}
|
userId={userId}
|
||||||
userName={userName}
|
userName={userName}
|
||||||
companyCode={companyCode}
|
companyCode={companyCode}
|
||||||
|
menuObjid={menuObjid}
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
onSelectedRowsChange={onSelectedRowsChange}
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
flowSelectedData={flowSelectedData}
|
flowSelectedData={flowSelectedData}
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
});
|
});
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// 🆕 화면에 할당된 메뉴 OBJID
|
||||||
|
const [menuObjid, setMenuObjid] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
// 메뉴 할당 모달 상태
|
// 메뉴 할당 모달 상태
|
||||||
const [showMenuAssignmentModal, setShowMenuAssignmentModal] = useState(false);
|
const [showMenuAssignmentModal, setShowMenuAssignmentModal] = useState(false);
|
||||||
|
|
||||||
|
|
@ -880,6 +883,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
|
|
||||||
const loadLayout = async () => {
|
const loadLayout = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 🆕 화면에 할당된 메뉴 조회
|
||||||
|
const menuInfo = await screenApi.getScreenMenu(selectedScreen.screenId);
|
||||||
|
if (menuInfo) {
|
||||||
|
setMenuObjid(menuInfo.menuObjid);
|
||||||
|
console.log("🔗 화면에 할당된 메뉴:", menuInfo);
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ 화면에 할당된 메뉴가 없습니다");
|
||||||
|
}
|
||||||
|
|
||||||
const response = await screenApi.getLayout(selectedScreen.screenId);
|
const response = await screenApi.getLayout(selectedScreen.screenId);
|
||||||
if (response) {
|
if (response) {
|
||||||
// 🔄 마이그레이션 필요 여부 확인
|
// 🔄 마이그레이션 필요 여부 확인
|
||||||
|
|
@ -4205,6 +4217,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
currentResolution={screenResolution}
|
currentResolution={screenResolution}
|
||||||
onResolutionChange={handleResolutionChange}
|
onResolutionChange={handleResolutionChange}
|
||||||
allComponents={layout.components} // 🆕 플로우 위젯 감지용
|
allComponents={layout.components} // 🆕 플로우 위젯 감지용
|
||||||
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
@ -4497,6 +4510,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
onDragStart={(e) => startComponentDrag(component, e)}
|
onDragStart={(e) => startComponentDrag(component, e)}
|
||||||
onDragEnd={endDrag}
|
onDragEnd={endDrag}
|
||||||
selectedScreen={selectedScreen}
|
selectedScreen={selectedScreen}
|
||||||
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
// onZoneComponentDrop 제거
|
// onZoneComponentDrop 제거
|
||||||
onZoneClick={handleZoneClick}
|
onZoneClick={handleZoneClick}
|
||||||
// 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영)
|
// 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,8 @@ interface UnifiedPropertiesPanelProps {
|
||||||
onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void;
|
onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void;
|
||||||
// 🆕 플로우 위젯 감지용
|
// 🆕 플로우 위젯 감지용
|
||||||
allComponents?: ComponentData[];
|
allComponents?: ComponentData[];
|
||||||
|
// 🆕 메뉴 OBJID (코드/카테고리 스코프용)
|
||||||
|
menuObjid?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
@ -98,6 +100,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
currentTableName,
|
currentTableName,
|
||||||
dragState,
|
dragState,
|
||||||
onStyleChange,
|
onStyleChange,
|
||||||
|
menuObjid,
|
||||||
currentResolution,
|
currentResolution,
|
||||||
onResolutionChange,
|
onResolutionChange,
|
||||||
allComponents = [], // 🆕 기본값 빈 배열
|
allComponents = [], // 🆕 기본값 빈 배열
|
||||||
|
|
@ -685,6 +688,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
||||||
tableColumns={currentTable?.columns || []}
|
tableColumns={currentTable?.columns || []}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
onChange={(newConfig) => {
|
onChange={(newConfig) => {
|
||||||
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
||||||
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||||
|
|
@ -848,6 +852,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
|
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
|
||||||
tableColumns={currentTable?.columns || []}
|
tableColumns={currentTable?.columns || []}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
onChange={(newConfig) => {
|
onChange={(newConfig) => {
|
||||||
console.log("🔄 DynamicComponentConfigPanel onChange (widget):", newConfig);
|
console.log("🔄 DynamicComponentConfigPanel onChange (widget):", newConfig);
|
||||||
// 전체 componentConfig를 업데이트
|
// 전체 componentConfig를 업데이트
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface TextTypeConfigPanelProps {
|
||||||
config: TextTypeConfig;
|
config: TextTypeConfig;
|
||||||
onConfigChange: (config: TextTypeConfig) => void;
|
onConfigChange: (config: TextTypeConfig) => void;
|
||||||
tableName?: string; // 화면의 테이블명 (선택)
|
tableName?: string; // 화면의 테이블명 (선택)
|
||||||
menuObjid?: number; // 메뉴 objid (선택)
|
menuObjid?: number; // 메뉴 objid (선택) - 사용자가 선택한 부모 메뉴
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
|
|
@ -45,6 +45,10 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||||
const [loadingRules, setLoadingRules] = useState(false);
|
const [loadingRules, setLoadingRules] = useState(false);
|
||||||
|
|
||||||
|
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
|
||||||
|
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
||||||
|
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(menuObjid);
|
||||||
|
|
||||||
// 로컬 상태로 실시간 입력 관리
|
// 로컬 상태로 실시간 입력 관리
|
||||||
const [localValues, setLocalValues] = useState({
|
const [localValues, setLocalValues] = useState({
|
||||||
minLength: safeConfig.minLength?.toString() || "",
|
minLength: safeConfig.minLength?.toString() || "",
|
||||||
|
|
@ -60,31 +64,61 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
numberingRuleId: safeConfig.numberingRuleId,
|
numberingRuleId: safeConfig.numberingRuleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 채번 규칙 목록 로드
|
// 부모 메뉴 목록 로드 (최상위 메뉴 또는 레벨 2 메뉴)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadParentMenus = async () => {
|
||||||
|
try {
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
|
||||||
|
// 관리자 메뉴와 사용자 메뉴 모두 가져오기
|
||||||
|
const [adminResponse, userResponse] = await Promise.all([
|
||||||
|
apiClient.get("/admin/menus", { params: { menuType: "0" } }),
|
||||||
|
apiClient.get("/admin/menus", { params: { menuType: "1" } })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allMenus = [
|
||||||
|
...(adminResponse.data?.data || []),
|
||||||
|
...(userResponse.data?.data || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// 레벨 2 이하 메뉴만 선택 가능 (부모가 있는 메뉴)
|
||||||
|
const parentMenuList = allMenus.filter((menu: any) => {
|
||||||
|
const level = menu.lev || menu.LEV || 0;
|
||||||
|
return level >= 2; // 레벨 2 이상만 표시 (형제 메뉴가 있을 가능성)
|
||||||
|
});
|
||||||
|
|
||||||
|
setParentMenus(parentMenuList);
|
||||||
|
console.log("✅ 부모 메뉴 목록 로드:", parentMenuList.length);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 부모 메뉴 목록 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadParentMenus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRules = async () => {
|
const loadRules = async () => {
|
||||||
console.log("🔄 채번 규칙 로드 시작:", {
|
console.log("🔄 채번 규칙 로드 시작:", {
|
||||||
autoValueType: localValues.autoValueType,
|
autoValueType: localValues.autoValueType,
|
||||||
|
selectedMenuObjid,
|
||||||
tableName,
|
tableName,
|
||||||
hasTableName: !!tableName,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 메뉴를 선택하지 않으면 로드하지 않음
|
||||||
|
if (!selectedMenuObjid) {
|
||||||
|
console.warn("⚠️ 메뉴를 선택해야 채번 규칙을 조회할 수 있습니다");
|
||||||
|
setNumberingRules([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingRules(true);
|
setLoadingRules(true);
|
||||||
try {
|
try {
|
||||||
let response;
|
// 선택된 메뉴의 채번 규칙 조회 (메뉴 스코프)
|
||||||
|
console.log("📋 메뉴 기반 채번 규칙 조회 API 호출:", { menuObjid: selectedMenuObjid });
|
||||||
// 테이블명이 있으면 테이블 기반 필터링 사용
|
const response = await getAvailableNumberingRules(selectedMenuObjid);
|
||||||
if (tableName) {
|
|
||||||
console.log("📋 테이블 기반 채번 규칙 조회 API 호출:", { tableName });
|
|
||||||
response = await getAvailableNumberingRulesForScreen(tableName);
|
|
||||||
console.log("📋 API 응답:", response);
|
console.log("📋 API 응답:", response);
|
||||||
} else {
|
|
||||||
// 테이블명이 없으면 빈 배열 (테이블 필수)
|
|
||||||
console.warn("⚠️ 테이블명이 없어 채번 규칙을 조회할 수 없습니다");
|
|
||||||
setNumberingRules([]);
|
|
||||||
setLoadingRules(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setNumberingRules(response.data);
|
setNumberingRules(response.data);
|
||||||
|
|
@ -93,7 +127,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
rules: response.data.map((r: any) => ({
|
rules: response.data.map((r: any) => ({
|
||||||
ruleId: r.ruleId,
|
ruleId: r.ruleId,
|
||||||
ruleName: r.ruleName,
|
ruleName: r.ruleName,
|
||||||
tableName: r.tableName,
|
menuObjid: selectedMenuObjid,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -115,7 +149,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
} else {
|
} else {
|
||||||
console.log("⏭️ autoValueType !== 'numbering_rule', 규칙 로드 스킵:", localValues.autoValueType);
|
console.log("⏭️ autoValueType !== 'numbering_rule', 규칙 로드 스킵:", localValues.autoValueType);
|
||||||
}
|
}
|
||||||
}, [localValues.autoValueType, tableName]);
|
}, [localValues.autoValueType, selectedMenuObjid]);
|
||||||
|
|
||||||
// config가 변경될 때 로컬 상태 동기화
|
// config가 변경될 때 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -314,7 +348,54 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
console.log("🔍 메뉴 선택 UI 렌더링 체크:", {
|
||||||
|
autoValueType: localValues.autoValueType,
|
||||||
|
isNumberingRule: localValues.autoValueType === "numbering_rule",
|
||||||
|
parentMenusCount: parentMenus.length,
|
||||||
|
selectedMenuObjid,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
|
||||||
{localValues.autoValueType === "numbering_rule" && (
|
{localValues.autoValueType === "numbering_rule" && (
|
||||||
|
<>
|
||||||
|
{/* 부모 메뉴 선택 */}
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="parentMenu" className="text-sm font-medium">
|
||||||
|
대상 메뉴 선택 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedMenuObjid?.toString() || ""}
|
||||||
|
onValueChange={(value) => setSelectedMenuObjid(parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1 h-8 w-full px-2 py-0 text-xs">
|
||||||
|
<SelectValue placeholder="채번 규칙을 사용할 메뉴 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{parentMenus.length === 0 ? (
|
||||||
|
<SelectItem value="no-menus" disabled>
|
||||||
|
사용 가능한 메뉴가 없습니다
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
parentMenus.map((menu) => {
|
||||||
|
const objid = menu.objid || menu.OBJID;
|
||||||
|
const menuName = menu.menu_name_kor || menu.MENU_NAME_KOR;
|
||||||
|
return (
|
||||||
|
<SelectItem key={objid} value={objid.toString()}>
|
||||||
|
{menuName}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
|
이 필드가 어느 메뉴에서 사용될 것인지 선택하세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 채번 규칙 선택 */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="numberingRuleId" className="text-sm font-medium">
|
<Label htmlFor="numberingRuleId" className="text-sm font-medium">
|
||||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||||
|
|
@ -322,15 +403,25 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
<Select
|
<Select
|
||||||
value={localValues.numberingRuleId}
|
value={localValues.numberingRuleId}
|
||||||
onValueChange={(value) => updateConfig("numberingRuleId", value)}
|
onValueChange={(value) => updateConfig("numberingRuleId", value)}
|
||||||
disabled={loadingRules}
|
disabled={loadingRules || !selectedMenuObjid}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
<SelectTrigger className="mt-1 h-8 w-full px-2 py-0 text-xs">
|
||||||
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
!selectedMenuObjid
|
||||||
|
? "먼저 메뉴를 선택하세요"
|
||||||
|
: loadingRules
|
||||||
|
? "규칙 로딩 중..."
|
||||||
|
: "채번 규칙 선택"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{numberingRules.length === 0 ? (
|
{numberingRules.length === 0 ? (
|
||||||
<SelectItem value="no-rules" disabled>
|
<SelectItem value="no-rules" disabled>
|
||||||
사용 가능한 규칙이 없습니다
|
{!selectedMenuObjid
|
||||||
|
? "메뉴를 먼저 선택하세요"
|
||||||
|
: "사용 가능한 규칙이 없습니다"}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : (
|
) : (
|
||||||
numberingRules.map((rule) => (
|
numberingRules.map((rule) => (
|
||||||
|
|
@ -342,9 +433,10 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-muted-foreground mt-1 text-[10px]">
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
선택한 메뉴와 형제 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{localValues.autoValueType === "custom" && (
|
{localValues.autoValueType === "custom" && (
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,52 @@ import { CategoryValueManager } from "@/components/table-category/CategoryValueM
|
||||||
import { GripVertical } from "lucide-react";
|
import { GripVertical } from "lucide-react";
|
||||||
|
|
||||||
interface CategoryWidgetProps {
|
interface CategoryWidgetProps {
|
||||||
widgetId: string;
|
widgetId?: string;
|
||||||
tableName: string; // 현재 화면의 테이블
|
tableName?: string; // 현재 화면의 테이블 (옵션 - 형제 메뉴 전체 표시)
|
||||||
|
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프) - 필수
|
||||||
|
component?: any; // DynamicComponentRenderer에서 전달되는 컴포넌트 정보
|
||||||
|
[key: string]: any; // 추가 props 허용
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 관리 위젯 (좌우 분할)
|
* 카테고리 관리 위젯 (좌우 분할)
|
||||||
* - 좌측: 현재 테이블의 카테고리 타입 컬럼 목록
|
* - 좌측: 형제 메뉴들의 모든 카테고리 타입 컬럼 목록 (메뉴 스코프)
|
||||||
* - 우측: 선택된 컬럼의 카테고리 값 관리 (테이블 스코프)
|
* - 우측: 선택된 컬럼의 카테고리 값 관리 (메뉴 스코프)
|
||||||
*/
|
*/
|
||||||
export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...props }: CategoryWidgetProps) {
|
||||||
|
// menuObjid가 없으면 경고 로그
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log("🔍 CategoryWidget 받은 props:", {
|
||||||
|
widgetId,
|
||||||
|
tableName,
|
||||||
|
menuObjid,
|
||||||
|
hasComponent: !!component,
|
||||||
|
propsKeys: Object.keys(props),
|
||||||
|
propsMenuObjid: props.menuObjid,
|
||||||
|
allProps: { widgetId, tableName, menuObjid, ...props },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!menuObjid && !props.menuObjid) {
|
||||||
|
console.warn("⚠️ CategoryWidget: menuObjid가 전달되지 않았습니다", {
|
||||||
|
component,
|
||||||
|
props,
|
||||||
|
allAvailableProps: { widgetId, tableName, menuObjid, ...props }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("✅ CategoryWidget 렌더링", {
|
||||||
|
widgetId,
|
||||||
|
tableName,
|
||||||
|
menuObjid: menuObjid || props.menuObjid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [menuObjid, widgetId, tableName, component, props]);
|
||||||
|
// menuObjid 우선순위: 직접 전달된 값 > props에서 추출한 값
|
||||||
|
const effectiveMenuObjid = menuObjid || props.menuObjid;
|
||||||
|
|
||||||
const [selectedColumn, setSelectedColumn] = useState<{
|
const [selectedColumn, setSelectedColumn] = useState<{
|
||||||
columnName: string;
|
columnName: string;
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
|
tableName: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [leftWidth, setLeftWidth] = useState(15); // 초기값 15%
|
const [leftWidth, setLeftWidth] = useState(15); // 초기값 15%
|
||||||
|
|
@ -66,9 +99,10 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||||
<CategoryColumnList
|
<CategoryColumnList
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
selectedColumn={selectedColumn?.columnName || null}
|
selectedColumn={selectedColumn?.columnName || null}
|
||||||
onColumnSelect={(columnName, columnLabel) =>
|
onColumnSelect={(columnName, columnLabel, tableName) =>
|
||||||
setSelectedColumn({ columnName, columnLabel })
|
setSelectedColumn({ columnName, columnLabel, tableName })
|
||||||
}
|
}
|
||||||
|
menuObjid={effectiveMenuObjid}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -84,9 +118,10 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||||
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
|
<div style={{ width: `${100 - leftWidth - 1}%` }} className="pl-3">
|
||||||
{selectedColumn ? (
|
{selectedColumn ? (
|
||||||
<CategoryValueManager
|
<CategoryValueManager
|
||||||
tableName={tableName}
|
tableName={selectedColumn.tableName}
|
||||||
columnName={selectedColumn.columnName}
|
columnName={selectedColumn.columnName}
|
||||||
columnLabel={selectedColumn.columnLabel}
|
columnLabel={selectedColumn.columnLabel}
|
||||||
|
menuObjid={effectiveMenuObjid}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
|
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||||
import { FolderTree, Loader2 } from "lucide-react";
|
import { FolderTree, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
interface CategoryColumn {
|
interface CategoryColumn {
|
||||||
|
tableName: string;
|
||||||
|
tableLabel?: string; // 테이블 라벨 추가
|
||||||
columnName: string;
|
columnName: string;
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
inputType: string;
|
inputType: string;
|
||||||
|
|
@ -13,94 +15,85 @@ interface CategoryColumn {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryColumnListProps {
|
interface CategoryColumnListProps {
|
||||||
tableName: string;
|
tableName: string; // 현재 화면의 테이블 (사용하지 않음 - 형제 메뉴 전체 표시)
|
||||||
selectedColumn: string | null;
|
selectedColumn: string | null;
|
||||||
onColumnSelect: (columnName: string, columnLabel: string) => void;
|
onColumnSelect: (columnName: string, columnLabel: string, tableName: string) => void;
|
||||||
|
menuObjid?: number; // 현재 메뉴 OBJID (필수)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 컬럼 목록 (좌측 패널)
|
* 카테고리 컬럼 목록 (좌측 패널)
|
||||||
* - 현재 테이블에서 input_type='category'인 컬럼들을 표시 (테이블 스코프)
|
* - 형제 메뉴들의 모든 카테고리 타입 컬럼을 표시 (메뉴 스코프)
|
||||||
*/
|
*/
|
||||||
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }: CategoryColumnListProps) {
|
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) {
|
||||||
const [columns, setColumns] = useState<CategoryColumn[]>([]);
|
const [columns, setColumns] = useState<CategoryColumn[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCategoryColumns();
|
if (menuObjid) {
|
||||||
}, [tableName]);
|
loadCategoryColumnsByMenu();
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ menuObjid가 없어서 카테고리 컬럼을 로드할 수 없습니다");
|
||||||
|
setColumns([]);
|
||||||
|
}
|
||||||
|
}, [menuObjid]);
|
||||||
|
|
||||||
const loadCategoryColumns = async () => {
|
const loadCategoryColumnsByMenu = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// table_type_columns에서 input_type = 'category'인 컬럼 조회
|
console.log("🔍 형제 메뉴의 카테고리 컬럼 조회 시작", { menuObjid });
|
||||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
|
||||||
|
|
||||||
console.log("🔍 테이블 컬럼 API 응답:", {
|
// 새 API: 형제 메뉴들의 카테고리 컬럼 조회
|
||||||
tableName,
|
const response = await apiClient.get(`/table-management/menu/${menuObjid}/category-columns`);
|
||||||
|
|
||||||
|
console.log("✅ 메뉴별 카테고리 컬럼 API 응답:", {
|
||||||
|
menuObjid,
|
||||||
response: response.data,
|
response: response.data,
|
||||||
type: typeof response.data,
|
|
||||||
isArray: Array.isArray(response.data),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// API 응답 구조 파싱 (여러 가능성 대응)
|
let categoryColumns: any[] = [];
|
||||||
let allColumns: any[] = [];
|
|
||||||
|
|
||||||
if (Array.isArray(response.data)) {
|
if (response.data.success && response.data.data) {
|
||||||
// response.data가 직접 배열인 경우
|
categoryColumns = response.data.data;
|
||||||
allColumns = response.data;
|
} else if (Array.isArray(response.data)) {
|
||||||
} else if (response.data.data && response.data.data.columns && Array.isArray(response.data.data.columns)) {
|
categoryColumns = response.data;
|
||||||
// response.data.data.columns가 배열인 경우 (table-management API)
|
|
||||||
allColumns = response.data.data.columns;
|
|
||||||
} else if (response.data.data && Array.isArray(response.data.data)) {
|
|
||||||
// response.data.data가 배열인 경우
|
|
||||||
allColumns = response.data.data;
|
|
||||||
} else if (response.data.columns && Array.isArray(response.data.columns)) {
|
|
||||||
// response.data.columns가 배열인 경우
|
|
||||||
allColumns = response.data.columns;
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ 예상하지 못한 API 응답 구조:", response.data);
|
console.warn("⚠️ 예상하지 못한 API 응답 구조:", response.data);
|
||||||
allColumns = [];
|
categoryColumns = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔍 파싱된 컬럼 목록:", {
|
console.log("✅ 카테고리 컬럼 파싱 완료:", {
|
||||||
totalColumns: allColumns.length,
|
|
||||||
sample: allColumns.slice(0, 3),
|
|
||||||
});
|
|
||||||
|
|
||||||
// category 타입만 필터링
|
|
||||||
const categoryColumns = allColumns.filter(
|
|
||||||
(col: any) => col.inputType === "category" || col.input_type === "category",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ 카테고리 컬럼:", {
|
|
||||||
count: categoryColumns.length,
|
count: categoryColumns.length,
|
||||||
columns: categoryColumns.map((c: any) => ({
|
columns: categoryColumns.map((c: any) => ({
|
||||||
name: c.columnName || c.column_name,
|
table: c.tableName,
|
||||||
type: c.inputType || c.input_type,
|
column: c.columnName,
|
||||||
|
label: c.columnLabel,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 각 컬럼의 값 개수 가져오기
|
||||||
const columnsWithCount = await Promise.all(
|
const columnsWithCount = await Promise.all(
|
||||||
categoryColumns.map(async (col: any) => {
|
categoryColumns.map(async (col: any) => {
|
||||||
const colName = col.columnName || col.column_name;
|
const colTable = col.tableName;
|
||||||
const colLabel = col.columnLabel || col.column_label || col.displayName || colName;
|
const colName = col.columnName;
|
||||||
|
const colLabel = col.columnLabel || colName;
|
||||||
|
|
||||||
// 각 컬럼의 값 개수 가져오기
|
|
||||||
let valueCount = 0;
|
let valueCount = 0;
|
||||||
try {
|
try {
|
||||||
const valuesResult = await getCategoryValues(tableName, colName, false);
|
const valuesResult = await getCategoryValues(colTable, colName, false, menuObjid);
|
||||||
if (valuesResult.success && valuesResult.data) {
|
if (valuesResult.success && valuesResult.data) {
|
||||||
valueCount = valuesResult.data.length;
|
valueCount = valuesResult.data.length;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`항목 개수 조회 실패 (${colName}):`, error);
|
console.error(`항목 개수 조회 실패 (${colTable}.${colName}):`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
tableName: colTable,
|
||||||
|
tableLabel: col.tableLabel || colTable, // 테이블 라벨 추가
|
||||||
columnName: colName,
|
columnName: colName,
|
||||||
columnLabel: colLabel,
|
columnLabel: colLabel,
|
||||||
inputType: col.inputType || col.input_type,
|
inputType: col.inputType,
|
||||||
valueCount,
|
valueCount,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
@ -111,7 +104,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||||
// 첫 번째 컬럼 자동 선택
|
// 첫 번째 컬럼 자동 선택
|
||||||
if (columnsWithCount.length > 0 && !selectedColumn) {
|
if (columnsWithCount.length > 0 && !selectedColumn) {
|
||||||
const firstCol = columnsWithCount[0];
|
const firstCol = columnsWithCount[0];
|
||||||
onColumnSelect(firstCol.columnName, firstCol.columnLabel);
|
onColumnSelect(firstCol.columnName, firstCol.columnLabel, firstCol.tableName);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 카테고리 컬럼 조회 실패:", error);
|
console.error("❌ 카테고리 컬럼 조회 실패:", error);
|
||||||
|
|
@ -152,10 +145,12 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => {
|
||||||
|
const uniqueKey = `${column.tableName}.${column.columnName}`;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={column.columnName}
|
key={uniqueKey}
|
||||||
onClick={() => onColumnSelect(column.columnName, column.columnLabel || column.columnName)}
|
onClick={() => onColumnSelect(column.columnName, column.columnLabel || column.columnName, column.tableName)}
|
||||||
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
|
className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${
|
||||||
selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -166,13 +161,15 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
<h4 className="text-sm font-semibold">{column.columnLabel || column.columnName}</h4>
|
||||||
|
<p className="text-muted-foreground text-xs">{column.tableLabel || column.tableName}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground text-xs font-medium">
|
<span className="text-muted-foreground text-xs font-medium">
|
||||||
{column.valueCount !== undefined ? `${column.valueCount}개` : "..."}
|
{column.valueCount !== undefined ? `${column.valueCount}개` : "..."}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface CategoryValueManagerProps {
|
||||||
columnName: string;
|
columnName: string;
|
||||||
columnLabel: string;
|
columnLabel: string;
|
||||||
onValueCountChange?: (count: number) => void;
|
onValueCountChange?: (count: number) => void;
|
||||||
|
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
|
|
@ -36,6 +37,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
columnName,
|
columnName,
|
||||||
columnLabel,
|
columnLabel,
|
||||||
onValueCountChange,
|
onValueCountChange,
|
||||||
|
menuObjid,
|
||||||
}) => {
|
}) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [values, setValues] = useState<TableCategoryValue[]>([]);
|
const [values, setValues] = useState<TableCategoryValue[]>([]);
|
||||||
|
|
@ -81,7 +83,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// includeInactive: true로 비활성 값도 포함
|
// includeInactive: true로 비활성 값도 포함
|
||||||
const response = await getCategoryValues(tableName, columnName, true);
|
const response = await getCategoryValues(tableName, columnName, true, menuObjid);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setValues(response.data);
|
setValues(response.data);
|
||||||
setFilteredValues(response.data);
|
setFilteredValues(response.data);
|
||||||
|
|
@ -101,11 +103,23 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||||
|
|
||||||
const handleAddValue = async (newValue: TableCategoryValue) => {
|
const handleAddValue = async (newValue: TableCategoryValue) => {
|
||||||
try {
|
try {
|
||||||
const response = await addCategoryValue({
|
if (!menuObjid) {
|
||||||
|
toast({
|
||||||
|
title: "오류",
|
||||||
|
description: "메뉴 정보가 없습니다. 카테고리 값을 추가할 수 없습니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await addCategoryValue(
|
||||||
|
{
|
||||||
...newValue,
|
...newValue,
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
});
|
},
|
||||||
|
menuObjid
|
||||||
|
);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
await loadCategoryValues();
|
await loadCategoryValues();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ export function useTableCodeCategory(tableName?: string, columnName?: string) {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!tableName || !columnName) return null;
|
if (!tableName || !columnName) return null;
|
||||||
|
|
||||||
console.log(`🔍 [React Query] 테이블 코드 카테고리 조회: ${tableName}.${columnName}`);
|
|
||||||
const columns = await tableTypeApi.getColumns(tableName);
|
const columns = await tableTypeApi.getColumns(tableName);
|
||||||
const targetColumn = columns.find((col) => col.columnName === columnName);
|
const targetColumn = columns.find((col) => col.columnName === columnName);
|
||||||
|
|
||||||
|
|
@ -41,7 +40,6 @@ export function useTableCodeCategory(tableName?: string, columnName?: string) {
|
||||||
? targetColumn.codeCategory
|
? targetColumn.codeCategory
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
console.log(`✅ [React Query] 테이블 코드 카테고리 결과: ${tableName}.${columnName} -> ${codeCategory}`);
|
|
||||||
return codeCategory;
|
return codeCategory;
|
||||||
},
|
},
|
||||||
enabled: !!(tableName && columnName),
|
enabled: !!(tableName && columnName),
|
||||||
|
|
@ -51,14 +49,32 @@ export function useTableCodeCategory(tableName?: string, columnName?: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 코드 옵션 조회 (select용)
|
// 코드 옵션 조회 (select용)
|
||||||
export function useCodeOptions(codeCategory?: string, enabled: boolean = true) {
|
export function useCodeOptions(codeCategory?: string, enabled: boolean = true, menuObjid?: number) {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: queryKeys.codes.options(codeCategory || ""),
|
queryKey: menuObjid
|
||||||
|
? [...queryKeys.codes.options(codeCategory || ""), 'menu', menuObjid]
|
||||||
|
: queryKeys.codes.options(codeCategory || ""),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!codeCategory || codeCategory === "none") return [];
|
if (!codeCategory || codeCategory === "none") return [];
|
||||||
|
|
||||||
console.log(`🔍 [React Query] 코드 옵션 조회: ${codeCategory}`);
|
console.log(`🔍 [useCodeOptions] 코드 옵션 조회 시작:`, {
|
||||||
const response = await commonCodeApi.codes.getList(codeCategory, { isActive: true });
|
codeCategory,
|
||||||
|
menuObjid,
|
||||||
|
hasMenuObjid: !!menuObjid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await commonCodeApi.codes.getList(codeCategory, {
|
||||||
|
isActive: true,
|
||||||
|
menuObjid
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📦 [useCodeOptions] API 응답:`, {
|
||||||
|
codeCategory,
|
||||||
|
menuObjid,
|
||||||
|
success: response.success,
|
||||||
|
dataCount: response.data?.length || 0,
|
||||||
|
rawData: response.data,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
const options = response.data.map((code: any) => {
|
const options = response.data.map((code: any) => {
|
||||||
|
|
@ -73,7 +89,13 @@ export function useCodeOptions(codeCategory?: string, enabled: boolean = true) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ [React Query] 코드 옵션 결과: ${codeCategory} (${options.length}개)`);
|
console.log(`✅ [useCodeOptions] 옵션 변환 완료:`, {
|
||||||
|
codeCategory,
|
||||||
|
menuObjid,
|
||||||
|
optionsCount: options.length,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,8 @@ export const useMenu = (user: any, authLoading: boolean) => {
|
||||||
if (assignedScreens.length > 0) {
|
if (assignedScreens.length > 0) {
|
||||||
// 할당된 화면이 있으면 첫 번째 화면으로 이동
|
// 할당된 화면이 있으면 첫 번째 화면으로 이동
|
||||||
const firstScreen = assignedScreens[0];
|
const firstScreen = assignedScreens[0];
|
||||||
router.push(`/screens/${firstScreen.screenId}`);
|
// menuObjid를 쿼리 파라미터로 전달
|
||||||
|
router.push(`/screens/${firstScreen.screenId}?menuObjid=${menuObjid}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,14 @@ export const commonCodeApi = {
|
||||||
/**
|
/**
|
||||||
* 카테고리별 코드 목록 조회
|
* 카테고리별 코드 목록 조회
|
||||||
*/
|
*/
|
||||||
async getList(categoryCode: string, params?: GetCodesQuery): Promise<ApiResponse<CodeInfo[]>> {
|
async getList(categoryCode: string, params?: GetCodesQuery & { menuObjid?: number }): Promise<ApiResponse<CodeInfo[]>> {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
if (params?.search) searchParams.append("search", params.search);
|
if (params?.search) searchParams.append("search", params.search);
|
||||||
if (params?.isActive !== undefined) searchParams.append("isActive", params.isActive.toString());
|
if (params?.isActive !== undefined) searchParams.append("isActive", params.isActive.toString());
|
||||||
if (params?.page !== undefined) searchParams.append("page", params.page.toString());
|
if (params?.page !== undefined) searchParams.append("page", params.page.toString());
|
||||||
if (params?.size !== undefined) searchParams.append("size", params.size.toString());
|
if (params?.size !== undefined) searchParams.append("size", params.size.toString());
|
||||||
|
if (params?.menuObjid !== undefined) searchParams.append("menuObjid", params.menuObjid.toString());
|
||||||
|
|
||||||
const queryString = searchParams.toString();
|
const queryString = searchParams.toString();
|
||||||
const url = `/common-codes/categories/${categoryCode}/codes${queryString ? `?${queryString}` : ""}`;
|
const url = `/common-codes/categories/${categoryCode}/codes${queryString ? `?${queryString}` : ""}`;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,12 @@ export const screenApi = {
|
||||||
} as ScreenDefinition;
|
} as ScreenDefinition;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 화면에 할당된 메뉴 조회
|
||||||
|
getScreenMenu: async (screenId: number): Promise<{ menuObjid: number; menuName?: string } | null> => {
|
||||||
|
const response = await apiClient.get(`/screen-management/screens/${screenId}/menu`);
|
||||||
|
return response.data?.data || null;
|
||||||
|
},
|
||||||
|
|
||||||
// 화면 생성
|
// 화면 생성
|
||||||
createScreen: async (screenData: CreateScreenRequest): Promise<ScreenDefinition> => {
|
createScreen: async (screenData: CreateScreenRequest): Promise<ScreenDefinition> => {
|
||||||
const response = await apiClient.post("/screen-management/screens", screenData);
|
const response = await apiClient.post("/screen-management/screens", screenData);
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,30 @@ export async function getCategoryColumns(tableName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 목록 조회 (테이블 스코프)
|
* 카테고리 값 목록 조회 (메뉴 스코프)
|
||||||
|
*
|
||||||
|
* @param tableName 테이블명
|
||||||
|
* @param columnName 컬럼명
|
||||||
|
* @param includeInactive 비활성 값 포함 여부
|
||||||
|
* @param menuObjid 메뉴 OBJID (선택사항, 제공 시 형제 메뉴의 카테고리 값 포함)
|
||||||
*/
|
*/
|
||||||
export async function getCategoryValues(
|
export async function getCategoryValues(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string,
|
columnName: string,
|
||||||
includeInactive: boolean = false
|
includeInactive: boolean = false,
|
||||||
|
menuObjid?: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const params: any = { includeInactive };
|
||||||
|
if (menuObjid) {
|
||||||
|
params.menuObjid = menuObjid;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiClient.get<{
|
const response = await apiClient.get<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: TableCategoryValue[];
|
data: TableCategoryValue[];
|
||||||
}>(`/table-categories/${tableName}/${columnName}/values`, {
|
}>(`/table-categories/${tableName}/${columnName}/values`, {
|
||||||
params: { includeInactive },
|
params,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -43,14 +54,23 @@ export async function getCategoryValues(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 추가
|
* 카테고리 값 추가 (메뉴 스코프)
|
||||||
|
*
|
||||||
|
* @param value 카테고리 값 정보
|
||||||
|
* @param menuObjid 메뉴 OBJID (필수)
|
||||||
*/
|
*/
|
||||||
export async function addCategoryValue(value: TableCategoryValue) {
|
export async function addCategoryValue(
|
||||||
|
value: TableCategoryValue,
|
||||||
|
menuObjid: number
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<{
|
const response = await apiClient.post<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: TableCategoryValue;
|
data: TableCategoryValue;
|
||||||
}>("/table-categories/values", value);
|
}>("/table-categories/values", {
|
||||||
|
...value,
|
||||||
|
menuObjid, // ← menuObjid 포함
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 값 추가 실패:", error);
|
console.error("카테고리 값 추가 실패:", error);
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ export interface DynamicComponentRendererProps {
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
|
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID (메뉴 스코프 - 카테고리/채번)
|
||||||
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
|
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
|
||||||
userId?: string; // 🆕 현재 사용자 ID
|
userId?: string; // 🆕 현재 사용자 ID
|
||||||
userName?: string; // 🆕 현재 사용자 이름
|
userName?: string; // 🆕 현재 사용자 이름
|
||||||
|
|
@ -224,6 +225,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
tableName,
|
tableName,
|
||||||
menuId, // 🆕 메뉴 ID
|
menuId, // 🆕 메뉴 ID
|
||||||
|
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
|
||||||
selectedScreen, // 🆕 화면 정보
|
selectedScreen, // 🆕 화면 정보
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
@ -319,6 +321,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onChange: handleChange, // 개선된 onChange 핸들러 전달
|
onChange: handleChange, // 개선된 onChange 핸들러 전달
|
||||||
tableName,
|
tableName,
|
||||||
menuId, // 🆕 메뉴 ID
|
menuId, // 🆕 메뉴 ID
|
||||||
|
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
|
||||||
selectedScreen, // 🆕 화면 정보
|
selectedScreen, // 🆕 화면 정보
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
|
||||||
|
|
@ -552,8 +552,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
margin: "0",
|
margin: "0",
|
||||||
lineHeight: "1.25",
|
lineHeight: "1.25",
|
||||||
boxShadow: componentConfig.disabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
boxShadow: componentConfig.disabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||||
// isInteractive 모드에서는 사용자 스타일 우선 적용 (width/height 제외)
|
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height 제외)
|
||||||
...(isInteractive && component.style ? Object.fromEntries(
|
...(component.style ? Object.fromEntries(
|
||||||
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')
|
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')
|
||||||
) : {}),
|
) : {}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface NumberingRuleWrapperProps {
|
||||||
onChange?: (config: NumberingRuleComponentConfig) => void;
|
onChange?: (config: NumberingRuleComponentConfig) => void;
|
||||||
isPreview?: boolean;
|
isPreview?: boolean;
|
||||||
tableName?: string; // 현재 화면의 테이블명
|
tableName?: string; // 현재 화면의 테이블명
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberingRuleWrapper: React.FC<NumberingRuleWrapperProps> = ({
|
export const NumberingRuleWrapper: React.FC<NumberingRuleWrapperProps> = ({
|
||||||
|
|
@ -16,8 +17,14 @@ export const NumberingRuleWrapper: React.FC<NumberingRuleWrapperProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
tableName,
|
tableName,
|
||||||
|
menuObjid,
|
||||||
}) => {
|
}) => {
|
||||||
console.log("📋 NumberingRuleWrapper: 테이블명 전달", { tableName, config });
|
console.log("📋 NumberingRuleWrapper: 테이블명 + menuObjid 전달", {
|
||||||
|
tableName,
|
||||||
|
menuObjid,
|
||||||
|
hasMenuObjid: !!menuObjid,
|
||||||
|
config
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
|
|
@ -26,6 +33,7 @@ export const NumberingRuleWrapper: React.FC<NumberingRuleWrapperProps> = ({
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
className="h-full"
|
className="h-full"
|
||||||
currentTableName={tableName} // 테이블명 전달
|
currentTableName={tableName} // 테이블명 전달
|
||||||
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export interface SelectBasicComponentProps {
|
||||||
onDragStart?: () => void;
|
onDragStart?: () => void;
|
||||||
onDragEnd?: () => void;
|
onDragEnd?: () => void;
|
||||||
value?: any; // 외부에서 전달받는 값
|
value?: any; // 외부에서 전달받는 값
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,19 +47,9 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
value: externalValue, // 명시적으로 value prop 받기
|
value: externalValue, // 명시적으로 value prop 받기
|
||||||
|
menuObjid, // 🆕 메뉴 OBJID
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 🚨 최우선 디버깅: 컴포넌트가 실행되는지 확인
|
|
||||||
console.log("🚨🚨🚨 SelectBasicComponent 실행됨!!!", {
|
|
||||||
componentId: component?.id,
|
|
||||||
componentType: component?.type,
|
|
||||||
webType: component?.webType,
|
|
||||||
tableName: component?.tableName,
|
|
||||||
columnName: component?.columnName,
|
|
||||||
screenId,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
|
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
|
||||||
|
|
@ -77,30 +68,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// autocomplete의 경우 검색어 관리
|
// autocomplete의 경우 검색어 관리
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
console.log("🔍 SelectBasicComponent 초기화 (React Query):", {
|
|
||||||
componentId: component.id,
|
|
||||||
externalValue,
|
|
||||||
componentConfigValue: componentConfig?.value,
|
|
||||||
webTypeConfigValue: (props as any).webTypeConfig?.value,
|
|
||||||
configValue: config?.value,
|
|
||||||
finalSelectedValue: externalValue || config?.value || "",
|
|
||||||
tableName: component.tableName,
|
|
||||||
columnName: component.columnName,
|
|
||||||
staticCodeCategory: config?.codeCategory,
|
|
||||||
// React Query 디버깅 정보
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
mountCount: ++(window as any).selectMountCount || ((window as any).selectMountCount = 1),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 언마운트 시 로깅
|
|
||||||
useEffect(() => {
|
|
||||||
const componentId = component.id;
|
|
||||||
console.log(`🔍 [${componentId}] SelectBasicComponent 마운트됨`);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log(`🔍 [${componentId}] SelectBasicComponent 언마운트됨`);
|
|
||||||
};
|
|
||||||
}, [component.id]);
|
|
||||||
|
|
||||||
const selectRef = useRef<HTMLDivElement>(null);
|
const selectRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -115,11 +82,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 (메모이제이션)
|
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 (메모이제이션)
|
||||||
const codeCategory = useMemo(() => {
|
const codeCategory = useMemo(() => {
|
||||||
const category = dynamicCodeCategory || staticCodeCategory;
|
const category = dynamicCodeCategory || staticCodeCategory;
|
||||||
console.log(`🔑 [${component.id}] 코드 카테고리 결정:`, {
|
|
||||||
dynamicCodeCategory,
|
|
||||||
staticCodeCategory,
|
|
||||||
finalCategory: category,
|
|
||||||
});
|
|
||||||
return category;
|
return category;
|
||||||
}, [dynamicCodeCategory, staticCodeCategory, component.id]);
|
}, [dynamicCodeCategory, staticCodeCategory, component.id]);
|
||||||
|
|
||||||
|
|
@ -132,34 +94,27 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
options: codeOptions,
|
options: codeOptions,
|
||||||
isLoading: isLoadingCodes,
|
isLoading: isLoadingCodes,
|
||||||
isFetching,
|
isFetching,
|
||||||
} = useCodeOptions(codeCategory, isCodeCategoryValid);
|
} = useCodeOptions(codeCategory, isCodeCategoryValid, menuObjid);
|
||||||
|
|
||||||
// React Query 상태 디버깅
|
// 디버깅: menuObjid가 제대로 전달되는지 확인
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(`🎯 [${component.id}] React Query 상태:`, {
|
if (codeCategory && codeCategory !== "none") {
|
||||||
|
console.log(`🎯 [SelectBasicComponent ${component.id}] 코드 옵션 로드:`, {
|
||||||
codeCategory,
|
codeCategory,
|
||||||
|
menuObjid,
|
||||||
|
hasMenuObjid: !!menuObjid,
|
||||||
isCodeCategoryValid,
|
isCodeCategoryValid,
|
||||||
codeOptionsLength: codeOptions.length,
|
codeOptionsCount: codeOptions.length,
|
||||||
isLoadingCodes,
|
isLoading: isLoadingCodes,
|
||||||
isFetching,
|
|
||||||
cacheStatus: isFetching ? "FETCHING" : "FROM_CACHE",
|
|
||||||
});
|
});
|
||||||
}, [component.id, codeCategory, isCodeCategoryValid, codeOptions.length, isLoadingCodes, isFetching]);
|
}
|
||||||
|
}, [component.id, codeCategory, menuObjid, codeOptions.length, isLoadingCodes, isCodeCategoryValid]);
|
||||||
|
|
||||||
// 외부 value prop 변경 시 selectedValue 업데이트
|
// 외부 value prop 변경 시 selectedValue 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newValue = externalValue || config?.value || "";
|
const newValue = externalValue || config?.value || "";
|
||||||
// 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리)
|
// 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리)
|
||||||
if (newValue !== selectedValue) {
|
if (newValue !== selectedValue) {
|
||||||
console.log(`🔄 SelectBasicComponent value 업데이트: "${selectedValue}" → "${newValue}"`);
|
|
||||||
console.log("🔍 업데이트 조건 분석:", {
|
|
||||||
externalValue,
|
|
||||||
componentConfigValue: componentConfig?.value,
|
|
||||||
configValue: config?.value,
|
|
||||||
newValue,
|
|
||||||
selectedValue,
|
|
||||||
shouldUpdate: newValue !== selectedValue,
|
|
||||||
});
|
|
||||||
setSelectedValue(newValue);
|
setSelectedValue(newValue);
|
||||||
}
|
}
|
||||||
}, [externalValue, config?.value]);
|
}, [externalValue, config?.value]);
|
||||||
|
|
@ -188,23 +143,12 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const labelMatch = options.find((option) => option.label === selectedValue);
|
const labelMatch = options.find((option) => option.label === selectedValue);
|
||||||
if (labelMatch) {
|
if (labelMatch) {
|
||||||
newLabel = labelMatch.label;
|
newLabel = labelMatch.label;
|
||||||
console.log(`🔍 [${component.id}] 코드명으로 매치 발견: "${selectedValue}" → "${newLabel}"`);
|
|
||||||
} else {
|
} else {
|
||||||
// 2) selectedValue가 코드값인 경우라면 원래 로직대로 라벨을 찾되, 없으면 원값 표시
|
// 2) selectedValue가 코드값인 경우라면 원래 로직대로 라벨을 찾되, 없으면 원값 표시
|
||||||
newLabel = selectedValue; // 코드값 그대로 표시 (예: "555")
|
newLabel = selectedValue; // 코드값 그대로 표시 (예: "555")
|
||||||
console.log(`🔍 [${component.id}] 코드값 원본 유지: "${selectedValue}"`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🏷️ [${component.id}] 라벨 업데이트:`, {
|
|
||||||
selectedValue,
|
|
||||||
selectedOption: selectedOption ? { value: selectedOption.value, label: selectedOption.label } : null,
|
|
||||||
newLabel,
|
|
||||||
optionsCount: options.length,
|
|
||||||
allOptionsValues: options.map((o) => o.value),
|
|
||||||
allOptionsLabels: options.map((o) => o.label),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newLabel !== selectedLabel) {
|
if (newLabel !== selectedLabel) {
|
||||||
setSelectedLabel(newLabel);
|
setSelectedLabel(newLabel);
|
||||||
}
|
}
|
||||||
|
|
@ -214,15 +158,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (isDesignMode) return;
|
if (isDesignMode) return;
|
||||||
|
|
||||||
console.log(`🖱️ [${component.id}] 드롭다운 토글 (React Query): ${isOpen} → ${!isOpen}`);
|
|
||||||
console.log(`📊 [${component.id}] 현재 상태:`, {
|
|
||||||
codeCategory,
|
|
||||||
isLoadingCodes,
|
|
||||||
codeOptionsLength: codeOptions.length,
|
|
||||||
tableName: component.tableName,
|
|
||||||
columnName: component.columnName,
|
|
||||||
});
|
|
||||||
|
|
||||||
// React Query가 자동으로 캐시 관리하므로 수동 새로고침 불필요
|
// React Query가 자동으로 캐시 관리하므로 수동 새로고침 불필요
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
@ -240,17 +175,8 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
|
|
||||||
// 인터랙티브 모드에서 폼 데이터 업데이트 (TextInputComponent와 동일한 로직)
|
// 인터랙티브 모드에서 폼 데이터 업데이트 (TextInputComponent와 동일한 로직)
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
console.log(`📤 SelectBasicComponent -> onFormDataChange 호출: ${component.columnName} = "${value}"`);
|
|
||||||
onFormDataChange(component.columnName, value);
|
onFormDataChange(component.columnName, value);
|
||||||
} else {
|
|
||||||
console.log("❌ SelectBasicComponent onFormDataChange 조건 미충족:", {
|
|
||||||
isInteractive,
|
|
||||||
hasOnFormDataChange: !!onFormDataChange,
|
|
||||||
hasColumnName: !!component.columnName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ [${component.id}] 옵션 선택:`, { value, label });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 외부 클릭 시 드롭다운 닫기
|
// 외부 클릭 시 드롭다운 닫기
|
||||||
|
|
@ -278,12 +204,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
// 모든 옵션 가져오기
|
// 모든 옵션 가져오기
|
||||||
const getAllOptions = () => {
|
const getAllOptions = () => {
|
||||||
const configOptions = config.options || [];
|
const configOptions = config.options || [];
|
||||||
console.log(`🔧 [${component.id}] 옵션 병합:`, {
|
|
||||||
codeOptionsLength: codeOptions.length,
|
|
||||||
codeOptions: codeOptions.map((o: Option) => ({ value: o.value, label: o.label })),
|
|
||||||
configOptionsLength: configOptions.length,
|
|
||||||
configOptions: configOptions.map((o: Option) => ({ value: o.value, label: o.label })),
|
|
||||||
});
|
|
||||||
return [...codeOptions, ...configOptions];
|
return [...codeOptions, ...configOptions];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,49 +15,86 @@ export interface TextInputConfigPanelProps {
|
||||||
config: TextInputConfig;
|
config: TextInputConfig;
|
||||||
onChange: (config: Partial<TextInputConfig>) => void;
|
onChange: (config: Partial<TextInputConfig>) => void;
|
||||||
screenTableName?: string; // 🆕 현재 화면의 테이블명
|
screenTableName?: string; // 🆕 현재 화면의 테이블명
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID (사용자 선택)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TextInput 설정 패널
|
* TextInput 설정 패널
|
||||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||||
*/
|
*/
|
||||||
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange, screenTableName }) => {
|
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange, screenTableName, menuObjid }) => {
|
||||||
// 채번 규칙 목록 상태
|
// 채번 규칙 목록 상태
|
||||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||||
const [loadingRules, setLoadingRules] = useState(false);
|
const [loadingRules, setLoadingRules] = useState(false);
|
||||||
|
|
||||||
// 채번 규칙 목록 로드
|
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
|
||||||
|
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// useState 초기값에서 저장된 값 복원 (우선순위: 저장된 값 > menuObjid prop)
|
||||||
|
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
|
||||||
|
return config.autoGeneration?.selectedMenuObjid || menuObjid;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loadingMenus, setLoadingMenus] = useState(false);
|
||||||
|
|
||||||
|
// 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMenus = async () => {
|
||||||
|
setLoadingMenus(true);
|
||||||
|
try {
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const response = await apiClient.get("/admin/menus");
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
const allMenus = response.data.data;
|
||||||
|
|
||||||
|
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
|
||||||
|
const level2UserMenus = allMenus.filter((menu: any) =>
|
||||||
|
menu.menu_type === '1' && menu.lev === 2
|
||||||
|
);
|
||||||
|
|
||||||
|
setParentMenus(level2UserMenus);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("부모 메뉴 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingMenus(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadMenus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRules = async () => {
|
const loadRules = async () => {
|
||||||
|
// autoGeneration.type이 numbering_rule이 아니면 로드하지 않음
|
||||||
|
if (config.autoGeneration?.type !== "numbering_rule") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메뉴가 선택되지 않았으면 로드하지 않음
|
||||||
|
if (!selectedMenuObjid) {
|
||||||
|
setNumberingRules([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingRules(true);
|
setLoadingRules(true);
|
||||||
try {
|
try {
|
||||||
let response;
|
const response = await getAvailableNumberingRules(selectedMenuObjid);
|
||||||
|
|
||||||
// 🆕 테이블명이 있으면 테이블 기반 필터링, 없으면 전체 조회
|
|
||||||
if (screenTableName) {
|
|
||||||
console.log("🔍 TextInputConfigPanel: 테이블 기반 채번 규칙 로드", { screenTableName });
|
|
||||||
response = await getAvailableNumberingRulesForScreen(screenTableName);
|
|
||||||
} else {
|
|
||||||
console.log("🔍 TextInputConfigPanel: 전체 채번 규칙 로드 (테이블명 없음)");
|
|
||||||
response = await getAvailableNumberingRules();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setNumberingRules(response.data);
|
setNumberingRules(response.data);
|
||||||
console.log("✅ 채번 규칙 로드 완료:", response.data.length, "개");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("채번 규칙 목록 로드 실패:", error);
|
console.error("채번 규칙 목록 로드 실패:", error);
|
||||||
|
setNumberingRules([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRules(false);
|
setLoadingRules(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// autoGeneration.type이 numbering_rule일 때만 로드
|
|
||||||
if (config.autoGeneration?.type === "numbering_rule") {
|
|
||||||
loadRules();
|
loadRules();
|
||||||
}
|
}, [selectedMenuObjid, config.autoGeneration?.type]);
|
||||||
}, [config.autoGeneration?.type, screenTableName]);
|
|
||||||
|
|
||||||
const handleChange = (key: keyof TextInputConfig, value: any) => {
|
const handleChange = (key: keyof TextInputConfig, value: any) => {
|
||||||
onChange({ [key]: value });
|
onChange({ [key]: value });
|
||||||
|
|
@ -157,6 +194,55 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||||
|
|
||||||
{/* 채번 규칙 선택 */}
|
{/* 채번 규칙 선택 */}
|
||||||
{config.autoGeneration?.type === "numbering_rule" && (
|
{config.autoGeneration?.type === "numbering_rule" && (
|
||||||
|
<>
|
||||||
|
{/* 부모 메뉴 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="targetMenu">
|
||||||
|
대상 메뉴 선택 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedMenuObjid?.toString() || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const menuObjid = parseInt(value);
|
||||||
|
setSelectedMenuObjid(menuObjid);
|
||||||
|
|
||||||
|
// 컴포넌트 설정에 저장하여 언마운트 시에도 유지
|
||||||
|
handleChange("autoGeneration", {
|
||||||
|
...config.autoGeneration,
|
||||||
|
selectedMenuObjid: menuObjid,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={loadingMenus}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{parentMenus.length === 0 ? (
|
||||||
|
<SelectItem value="no-menus" disabled>
|
||||||
|
사용 가능한 메뉴가 없습니다
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
parentMenus.map((menu) => (
|
||||||
|
<SelectItem key={menu.objid} value={menu.objid.toString()}>
|
||||||
|
{menu.menu_name_kor}
|
||||||
|
{menu.menu_name_eng && (
|
||||||
|
<span className="text-muted-foreground ml-2 text-xs">
|
||||||
|
({menu.menu_name_eng})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
이 입력 필드가 어느 메뉴에 속할지 선택하세요 (해당 메뉴의 채번규칙이 적용됩니다)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 채번 규칙 선택 (메뉴 선택 후) */}
|
||||||
|
{selectedMenuObjid ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="numberingRuleId">
|
<Label htmlFor="numberingRuleId">
|
||||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||||
|
|
@ -181,7 +267,7 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{numberingRules.length === 0 ? (
|
{numberingRules.length === 0 ? (
|
||||||
<SelectItem value="no-rules" disabled>
|
<SelectItem value="no-rules" disabled>
|
||||||
사용 가능한 규칙이 없습니다
|
선택된 메뉴에 사용 가능한 규칙이 없습니다
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : (
|
) : (
|
||||||
numberingRules.map((rule) => (
|
numberingRules.map((rule) => (
|
||||||
|
|
@ -198,9 +284,15 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
선택된 메뉴 및 형제 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||||
|
먼저 대상 메뉴를 선택하세요
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ export interface ComponentConfigPanelProps {
|
||||||
screenTableName?: string; // 화면에서 지정한 테이블명
|
screenTableName?: string; // 화면에서 지정한 테이블명
|
||||||
tableColumns?: any[]; // 테이블 컬럼 정보
|
tableColumns?: any[]; // 테이블 컬럼 정보
|
||||||
tables?: any[]; // 전체 테이블 목록
|
tables?: any[]; // 전체 테이블 목록
|
||||||
|
menuObjid?: number; // 🆕 메뉴 OBJID (코드/카테고리/채번규칙 스코프용)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> = ({
|
export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> = ({
|
||||||
|
|
@ -116,6 +117,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
screenTableName,
|
screenTableName,
|
||||||
tableColumns,
|
tableColumns,
|
||||||
tables,
|
tables,
|
||||||
|
menuObjid,
|
||||||
}) => {
|
}) => {
|
||||||
// 모든 useState를 최상단에 선언 (Hooks 규칙)
|
// 모든 useState를 최상단에 선언 (Hooks 규칙)
|
||||||
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
|
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
|
||||||
|
|
@ -259,6 +261,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
|
tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
|
||||||
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
|
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
|
||||||
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
|
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
|
||||||
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue