채번 자동생성기능
This commit is contained in:
parent
b8e30c9557
commit
198f678b68
|
|
@ -2,15 +2,15 @@
|
||||||
* 채번 규칙 관리 컨트롤러
|
* 채번 규칙 관리 컨트롤러
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router, Request, Response } from "express";
|
import { Router, Response } from "express";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken, AuthenticatedRequest } from "../middleware/authMiddleware";
|
||||||
import { numberingRuleService } from "../services/numberingRuleService";
|
import { numberingRuleService } from "../services/numberingRuleService";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// 규칙 목록 조회
|
// 규칙 목록 조회 (전체)
|
||||||
router.get("/", authenticateToken, async (req: Request, res: Response) => {
|
router.get("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -22,8 +22,25 @@ router.get("/", authenticateToken, async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 메뉴별 사용 가능한 규칙 조회
|
||||||
|
router.get("/available/:menuObjid?", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid);
|
||||||
|
return res.json({ success: true, data: rules });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("메뉴별 사용 가능한 규칙 조회 실패", {
|
||||||
|
error: error.message,
|
||||||
|
menuObjid,
|
||||||
|
});
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 특정 규칙 조회
|
// 특정 규칙 조회
|
||||||
router.get("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
|
router.get("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
|
|
@ -40,7 +57,7 @@ router.get("/:ruleId", authenticateToken, async (req: Request, res: Response) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// 규칙 생성
|
// 규칙 생성
|
||||||
router.post("/", authenticateToken, async (req: Request, res: Response) => {
|
router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const ruleConfig = req.body;
|
const ruleConfig = req.body;
|
||||||
|
|
@ -66,7 +83,7 @@ router.post("/", authenticateToken, async (req: Request, res: Response) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 규칙 수정
|
// 규칙 수정
|
||||||
router.put("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
|
router.put("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
const updates = req.body;
|
const updates = req.body;
|
||||||
|
|
@ -84,7 +101,7 @@ router.put("/:ruleId", authenticateToken, async (req: Request, res: Response) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
// 규칙 삭제
|
// 규칙 삭제
|
||||||
router.delete("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
|
router.delete("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
|
|
@ -100,14 +117,42 @@ router.delete("/:ruleId", authenticateToken, async (req: Request, res: Response)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 코드 생성
|
// 코드 미리보기 (순번 증가 없음)
|
||||||
router.post("/:ruleId/generate", authenticateToken, async (req: Request, res: Response) => {
|
router.post("/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const previewCode = await numberingRuleService.previewCode(ruleId, companyCode);
|
||||||
|
return res.json({ success: true, data: { generatedCode: previewCode } });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("코드 미리보기 실패", { error: error.message });
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 코드 할당 (저장 시점에 실제 순번 증가)
|
||||||
|
router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
|
||||||
|
return res.json({ success: true, data: { generatedCode: allocatedCode } });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("코드 할당 실패", { error: error.message });
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 코드 생성 (기존 호환성 유지, deprecated)
|
||||||
|
router.post("/:ruleId/generate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const generatedCode = await numberingRuleService.generateCode(ruleId, companyCode);
|
const generatedCode = await numberingRuleService.generateCode(ruleId, companyCode);
|
||||||
return res.json({ success: true, data: { code: generatedCode } });
|
return res.json({ success: true, data: { generatedCode } });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("코드 생성 실패", { error: error.message });
|
logger.error("코드 생성 실패", { error: error.message });
|
||||||
return res.status(500).json({ success: false, error: error.message });
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
|
@ -115,7 +160,7 @@ router.post("/:ruleId/generate", authenticateToken, async (req: Request, res: Re
|
||||||
});
|
});
|
||||||
|
|
||||||
// 시퀀스 초기화
|
// 시퀀스 초기화
|
||||||
router.post("/:ruleId/reset", authenticateToken, async (req: Request, res: Response) => {
|
router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
const { ruleId } = req.params;
|
const { ruleId } = req.params;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ interface NumberingRuleConfig {
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
columnName?: string;
|
columnName?: string;
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
|
menuObjid?: number;
|
||||||
|
scopeType?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
|
|
@ -33,7 +35,7 @@ interface NumberingRuleConfig {
|
||||||
|
|
||||||
class NumberingRuleService {
|
class NumberingRuleService {
|
||||||
/**
|
/**
|
||||||
* 규칙 목록 조회
|
* 규칙 목록 조회 (전체)
|
||||||
*/
|
*/
|
||||||
async getRuleList(companyCode: string): Promise<NumberingRuleConfig[]> {
|
async getRuleList(companyCode: string): Promise<NumberingRuleConfig[]> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -78,11 +80,16 @@ class NumberingRuleService {
|
||||||
ORDER BY part_order
|
ORDER BY part_order
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]);
|
const partsResult = await pool.query(partsQuery, [
|
||||||
|
rule.ruleId,
|
||||||
|
companyCode,
|
||||||
|
]);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = partsResult.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, { companyCode });
|
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, {
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
return result.rows;
|
return result.rows;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`);
|
logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`);
|
||||||
|
|
@ -90,10 +97,170 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 메뉴에서 사용 가능한 규칙 목록 조회
|
||||||
|
*/
|
||||||
|
async getAvailableRulesForMenu(
|
||||||
|
companyCode: string,
|
||||||
|
menuObjid?: number
|
||||||
|
): Promise<NumberingRuleConfig[]> {
|
||||||
|
try {
|
||||||
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작", {
|
||||||
|
companyCode,
|
||||||
|
menuObjid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
// menuObjid가 없으면 global 규칙만 반환
|
||||||
|
if (!menuObjid) {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
rule_id AS "ruleId",
|
||||||
|
rule_name AS "ruleName",
|
||||||
|
description,
|
||||||
|
separator,
|
||||||
|
reset_period AS "resetPeriod",
|
||||||
|
current_sequence AS "currentSequence",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
menu_objid AS "menuObjid",
|
||||||
|
scope_type AS "scopeType",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy"
|
||||||
|
FROM numbering_rules
|
||||||
|
WHERE (company_code = $1 OR company_code = '*')
|
||||||
|
AND scope_type = 'global'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await pool.query(query, [companyCode]);
|
||||||
|
|
||||||
|
// 파트 정보 추가
|
||||||
|
for (const rule of result.rows) {
|
||||||
|
const partsQuery = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
part_order AS "order",
|
||||||
|
part_type AS "partType",
|
||||||
|
generation_method AS "generationMethod",
|
||||||
|
auto_config AS "autoConfig",
|
||||||
|
manual_config AS "manualConfig"
|
||||||
|
FROM numbering_rule_parts
|
||||||
|
WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
|
||||||
|
ORDER BY part_order
|
||||||
|
`;
|
||||||
|
|
||||||
|
const partsResult = await pool.query(partsQuery, [
|
||||||
|
rule.ruleId,
|
||||||
|
companyCode,
|
||||||
|
]);
|
||||||
|
rule.parts = partsResult.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 메뉴의 상위 계층 조회 (2레벨 메뉴 찾기)
|
||||||
|
const menuHierarchyQuery = `
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 사용 가능한 규칙 조회
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
rule_id AS "ruleId",
|
||||||
|
rule_name AS "ruleName",
|
||||||
|
description,
|
||||||
|
separator,
|
||||||
|
reset_period AS "resetPeriod",
|
||||||
|
current_sequence AS "currentSequence",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
menu_objid AS "menuObjid",
|
||||||
|
scope_type AS "scopeType",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy"
|
||||||
|
FROM numbering_rules
|
||||||
|
WHERE (company_code = $1 OR company_code = '*')
|
||||||
|
AND (
|
||||||
|
scope_type = 'global'
|
||||||
|
OR (scope_type = 'menu' AND menu_objid = $2)
|
||||||
|
)
|
||||||
|
ORDER BY scope_type DESC, created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await pool.query(query, [companyCode, level2MenuObjid]);
|
||||||
|
|
||||||
|
// 파트 정보 추가
|
||||||
|
for (const rule of result.rows) {
|
||||||
|
const partsQuery = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
part_order AS "order",
|
||||||
|
part_type AS "partType",
|
||||||
|
generation_method AS "generationMethod",
|
||||||
|
auto_config AS "autoConfig",
|
||||||
|
manual_config AS "manualConfig"
|
||||||
|
FROM numbering_rule_parts
|
||||||
|
WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
|
||||||
|
ORDER BY part_order
|
||||||
|
`;
|
||||||
|
|
||||||
|
const partsResult = await pool.query(partsQuery, [
|
||||||
|
rule.ruleId,
|
||||||
|
companyCode,
|
||||||
|
]);
|
||||||
|
rule.parts = partsResult.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
||||||
|
companyCode,
|
||||||
|
menuObjid,
|
||||||
|
level2MenuObjid,
|
||||||
|
count: result.rowCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("메뉴별 채번 규칙 조회 실패", {
|
||||||
|
error: error.message,
|
||||||
|
companyCode,
|
||||||
|
menuObjid,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 규칙 조회
|
* 특정 규칙 조회
|
||||||
*/
|
*/
|
||||||
async getRuleById(ruleId: string, companyCode: string): Promise<NumberingRuleConfig | null> {
|
async getRuleById(
|
||||||
|
ruleId: string,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<NumberingRuleConfig | null> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -106,7 +273,7 @@ class NumberingRuleService {
|
||||||
table_name AS "tableName",
|
table_name AS "tableName",
|
||||||
column_name AS "columnName",
|
column_name AS "columnName",
|
||||||
company_code AS "companyCode",
|
company_code AS "companyCode",
|
||||||
menu_id AS "menuId",
|
menu_objid AS "menuObjid",
|
||||||
scope_type AS "scopeType",
|
scope_type AS "scopeType",
|
||||||
created_at AS "createdAt",
|
created_at AS "createdAt",
|
||||||
updated_at AS "updatedAt",
|
updated_at AS "updatedAt",
|
||||||
|
|
@ -223,7 +390,10 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
logger.info("채번 규칙 생성 완료", { ruleId: config.ruleId, companyCode });
|
logger.info("채번 규칙 생성 완료", {
|
||||||
|
ruleId: config.ruleId,
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
return { ...ruleResult.rows[0], parts };
|
return { ...ruleResult.rows[0], parts };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await client.query("ROLLBACK");
|
await client.query("ROLLBACK");
|
||||||
|
|
@ -364,9 +534,63 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 코드 생성
|
* 코드 미리보기 (순번 증가 없음)
|
||||||
*/
|
*/
|
||||||
async generateCode(ruleId: string, companyCode: string): Promise<string> {
|
async previewCode(ruleId: string, companyCode: string): Promise<string> {
|
||||||
|
const rule = await this.getRuleById(ruleId, companyCode);
|
||||||
|
if (!rule) throw new Error("규칙을 찾을 수 없습니다");
|
||||||
|
|
||||||
|
const parts = rule.parts
|
||||||
|
.sort((a: any, b: any) => a.order - b.order)
|
||||||
|
.map((part: any) => {
|
||||||
|
if (part.generationMethod === "manual") {
|
||||||
|
return part.manualConfig?.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoConfig = part.autoConfig || {};
|
||||||
|
|
||||||
|
switch (part.partType) {
|
||||||
|
case "sequence": {
|
||||||
|
// 순번 (현재 순번으로 미리보기, 증가 안 함)
|
||||||
|
const length = autoConfig.sequenceLength || 4;
|
||||||
|
return String(rule.currentSequence || 1).padStart(length, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
case "number": {
|
||||||
|
// 숫자 (고정 자릿수)
|
||||||
|
const length = autoConfig.numberLength || 4;
|
||||||
|
const value = autoConfig.numberValue || 1;
|
||||||
|
return String(value).padStart(length, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
case "date": {
|
||||||
|
// 날짜 (다양한 날짜 형식)
|
||||||
|
return this.formatDate(
|
||||||
|
new Date(),
|
||||||
|
autoConfig.dateFormat || "YYYYMMDD"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "text": {
|
||||||
|
// 텍스트 (고정 문자열)
|
||||||
|
return autoConfig.textValue || "TEXT";
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.warn("알 수 없는 파트 타입", { partType: part.partType });
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewCode = parts.join(rule.separator || "");
|
||||||
|
logger.info("코드 미리보기 생성", { ruleId, previewCode, companyCode });
|
||||||
|
return previewCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 코드 할당 (저장 시점에 실제 순번 증가)
|
||||||
|
*/
|
||||||
|
async allocateCode(ruleId: string, companyCode: string): Promise<string> {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
|
@ -386,37 +610,44 @@ class NumberingRuleService {
|
||||||
const autoConfig = part.autoConfig || {};
|
const autoConfig = part.autoConfig || {};
|
||||||
|
|
||||||
switch (part.partType) {
|
switch (part.partType) {
|
||||||
case "prefix":
|
|
||||||
return autoConfig.prefix || "PREFIX";
|
|
||||||
|
|
||||||
case "sequence": {
|
case "sequence": {
|
||||||
|
// 순번 (자동 증가 숫자)
|
||||||
const length = autoConfig.sequenceLength || 4;
|
const length = autoConfig.sequenceLength || 4;
|
||||||
return String(rule.currentSequence || 1).padStart(length, "0");
|
return String(rule.currentSequence || 1).padStart(length, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
case "date":
|
case "number": {
|
||||||
return this.formatDate(new Date(), autoConfig.dateFormat || "YYYYMMDD");
|
// 숫자 (고정 자릿수)
|
||||||
|
const length = autoConfig.numberLength || 4;
|
||||||
case "year": {
|
const value = autoConfig.numberValue || 1;
|
||||||
const format = autoConfig.dateFormat || "YYYY";
|
return String(value).padStart(length, "0");
|
||||||
const year = new Date().getFullYear();
|
|
||||||
return format === "YY" ? String(year).slice(-2) : String(year);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "month":
|
case "date": {
|
||||||
return String(new Date().getMonth() + 1).padStart(2, "0");
|
// 날짜 (다양한 날짜 형식)
|
||||||
|
return this.formatDate(
|
||||||
|
new Date(),
|
||||||
|
autoConfig.dateFormat || "YYYYMMDD"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "custom":
|
case "text": {
|
||||||
return autoConfig.value || "CUSTOM";
|
// 텍스트 (고정 문자열)
|
||||||
|
return autoConfig.textValue || "TEXT";
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
logger.warn("알 수 없는 파트 타입", { partType: part.partType });
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const generatedCode = parts.join(rule.separator || "");
|
const allocatedCode = parts.join(rule.separator || "");
|
||||||
|
|
||||||
const hasSequence = rule.parts.some((p: any) => p.partType === "sequence");
|
// 순번이 있는 경우에만 증가
|
||||||
|
const hasSequence = rule.parts.some(
|
||||||
|
(p: any) => p.partType === "sequence"
|
||||||
|
);
|
||||||
if (hasSequence) {
|
if (hasSequence) {
|
||||||
await client.query(
|
await client.query(
|
||||||
"UPDATE numbering_rules SET current_sequence = current_sequence + 1 WHERE rule_id = $1 AND company_code = $2",
|
"UPDATE numbering_rules SET current_sequence = current_sequence + 1 WHERE rule_id = $1 AND company_code = $2",
|
||||||
|
|
@ -425,30 +656,52 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
logger.info("코드 생성 완료", { ruleId, generatedCode });
|
logger.info("코드 할당 완료", { ruleId, allocatedCode, companyCode });
|
||||||
return generatedCode;
|
return allocatedCode;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await client.query("ROLLBACK");
|
await client.query("ROLLBACK");
|
||||||
logger.error("코드 생성 실패", { error: error.message });
|
logger.error("코드 할당 실패", {
|
||||||
|
ruleId,
|
||||||
|
companyCode,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 기존 generateCode는 allocateCode를 사용하세요
|
||||||
|
*/
|
||||||
|
async generateCode(ruleId: string, companyCode: string): Promise<string> {
|
||||||
|
logger.warn(
|
||||||
|
"generateCode는 deprecated 되었습니다. previewCode 또는 allocateCode를 사용하세요"
|
||||||
|
);
|
||||||
|
return this.allocateCode(ruleId, companyCode);
|
||||||
|
}
|
||||||
|
|
||||||
private formatDate(date: Date, format: string): string {
|
private formatDate(date: Date, format: string): string {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case "YYYY": return String(year);
|
case "YYYY":
|
||||||
case "YY": return String(year).slice(-2);
|
return String(year);
|
||||||
case "YYYYMM": return `${year}${month}`;
|
case "YY":
|
||||||
case "YYMM": return `${String(year).slice(-2)}${month}`;
|
return String(year).slice(-2);
|
||||||
case "YYYYMMDD": return `${year}${month}${day}`;
|
case "YYYYMM":
|
||||||
case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`;
|
return `${year}${month}`;
|
||||||
default: return `${year}${month}${day}`;
|
case "YYMM":
|
||||||
|
return `${String(year).slice(-2)}${month}`;
|
||||||
|
case "YYYYMMDD":
|
||||||
|
return `${year}${month}${day}`;
|
||||||
|
case "YYMMDD":
|
||||||
|
return `${String(year).slice(-2)}${month}${day}`;
|
||||||
|
default:
|
||||||
|
return `${year}${month}${day}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
if (!prev) return null;
|
if (!prev) return null;
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
parts: prev.parts
|
parts: prev.parts.filter((part) => part.id !== partId).map((part, index) => ({ ...part, order: index + 1 })),
|
||||||
.filter((part) => part.id !== partId)
|
|
||||||
.map((part, index) => ({ ...part, order: index + 1 })),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -132,7 +130,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
|
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (existing) {
|
if (existing) {
|
||||||
response = await updateNumberingRule(currentRule.ruleId, currentRule);
|
response = await updateNumberingRule(currentRule.ruleId, currentRule);
|
||||||
|
|
@ -170,29 +168,32 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`);
|
toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDeleteSavedRule = useCallback(async (ruleId: string) => {
|
const handleDeleteSavedRule = useCallback(
|
||||||
setLoading(true);
|
async (ruleId: string) => {
|
||||||
try {
|
setLoading(true);
|
||||||
const response = await deleteNumberingRule(ruleId);
|
try {
|
||||||
|
const response = await deleteNumberingRule(ruleId);
|
||||||
if (response.success) {
|
|
||||||
setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId));
|
if (response.success) {
|
||||||
|
setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId));
|
||||||
if (selectedRuleId === ruleId) {
|
|
||||||
setSelectedRuleId(null);
|
if (selectedRuleId === ruleId) {
|
||||||
setCurrentRule(null);
|
setSelectedRuleId(null);
|
||||||
|
setCurrentRule(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("규칙이 삭제되었습니다");
|
||||||
|
} else {
|
||||||
|
toast.error(response.error || "삭제 실패");
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
toast.success("규칙이 삭제되었습니다");
|
toast.error(`삭제 실패: ${error.message}`);
|
||||||
} else {
|
} finally {
|
||||||
toast.error(response.error || "삭제 실패");
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
},
|
||||||
toast.error(`삭제 실패: ${error.message}`);
|
[selectedRuleId],
|
||||||
} finally {
|
);
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [selectedRuleId]);
|
|
||||||
|
|
||||||
const handleNewRule = useCallback(() => {
|
const handleNewRule = useCallback(() => {
|
||||||
const newRule: NumberingRuleConfig = {
|
const newRule: NumberingRuleConfig = {
|
||||||
|
|
@ -207,7 +208,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
|
|
||||||
setSelectedRuleId(newRule.ruleId);
|
setSelectedRuleId(newRule.ruleId);
|
||||||
setCurrentRule(newRule);
|
setCurrentRule(newRule);
|
||||||
|
|
||||||
toast.success("새 규칙이 생성되었습니다");
|
toast.success("새 규칙이 생성되었습니다");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -228,35 +229,29 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<h2 className="text-sm font-semibold sm:text-base">{leftTitle}</h2>
|
<h2 className="text-sm font-semibold sm:text-base">{leftTitle}</h2>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditingLeftTitle(true)}>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6"
|
|
||||||
onClick={() => setEditingLeftTitle(true)}
|
|
||||||
>
|
|
||||||
<Edit2 className="h-3 w-3" />
|
<Edit2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleNewRule} variant="outline" className="h-9 w-full text-sm">
|
<Button onClick={handleNewRule} variant="outline" className="h-9 w-full text-sm">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />새 규칙 생성
|
||||||
새 규칙 생성
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2 overflow-y-auto">
|
<div className="flex-1 space-y-2 overflow-y-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex h-32 items-center justify-center">
|
<div className="flex h-32 items-center justify-center">
|
||||||
<p className="text-xs text-muted-foreground">로딩 중...</p>
|
<p className="text-muted-foreground text-xs">로딩 중...</p>
|
||||||
</div>
|
</div>
|
||||||
) : savedRules.length === 0 ? (
|
) : savedRules.length === 0 ? (
|
||||||
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border bg-muted/50">
|
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||||
<p className="text-xs text-muted-foreground">저장된 규칙이 없습니다</p>
|
<p className="text-muted-foreground text-xs">저장된 규칙이 없습니다</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
savedRules.map((rule) => (
|
savedRules.map((rule) => (
|
||||||
<Card
|
<Card
|
||||||
key={rule.ruleId}
|
key={rule.ruleId}
|
||||||
className={`cursor-pointer border-border transition-colors hover:bg-accent ${
|
className={`border-border hover:bg-accent cursor-pointer 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)}
|
||||||
|
|
@ -265,9 +260,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
|
<CardTitle className="text-sm font-medium">{rule.ruleName}</CardTitle>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="text-muted-foreground mt-1 text-xs">규칙 {rule.parts.length}개</p>
|
||||||
규칙 {rule.parts.length}개
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -278,7 +271,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
handleDeleteSavedRule(rule.ruleId);
|
handleDeleteSavedRule(rule.ruleId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3 text-destructive" />
|
<Trash2 className="text-destructive h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -292,19 +285,15 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 구분선 */}
|
{/* 구분선 */}
|
||||||
<div className="h-full w-px bg-border"></div>
|
<div className="bg-border h-full w-px"></div>
|
||||||
|
|
||||||
{/* 우측: 편집 영역 */}
|
{/* 우측: 편집 영역 */}
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
{!currentRule ? (
|
{!currentRule ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="mb-2 text-lg font-medium text-muted-foreground">
|
<p className="text-muted-foreground mb-2 text-lg font-medium">규칙을 선택해주세요</p>
|
||||||
규칙을 선택해주세요
|
<p className="text-muted-foreground text-sm">좌측에서 규칙을 선택하거나 새로 생성하세요</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
좌측에서 규칙을 선택하거나 새로 생성하세요
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -322,12 +311,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<h2 className="text-sm font-semibold sm:text-base">{rightTitle}</h2>
|
<h2 className="text-sm font-semibold sm:text-base">{rightTitle}</h2>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setEditingRightTitle(true)}>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6"
|
|
||||||
onClick={() => setEditingRightTitle(true)}
|
|
||||||
>
|
|
||||||
<Edit2 className="h-3 w-3" />
|
<Edit2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -336,9 +320,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<Label className="text-sm font-medium">규칙명</Label>
|
<Label className="text-sm font-medium">규칙명</Label>
|
||||||
<Input
|
<Input
|
||||||
value={currentRule.ruleName}
|
value={currentRule.ruleName}
|
||||||
onChange={(e) =>
|
onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))}
|
||||||
setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))
|
|
||||||
}
|
|
||||||
className="h-9"
|
className="h-9"
|
||||||
placeholder="예: 프로젝트 코드"
|
placeholder="예: 프로젝트 코드"
|
||||||
/>
|
/>
|
||||||
|
|
@ -348,9 +330,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<Label className="text-sm font-medium">적용 범위</Label>
|
<Label className="text-sm font-medium">적용 범위</Label>
|
||||||
<Select
|
<Select
|
||||||
value={currentRule.scopeType || "global"}
|
value={currentRule.scopeType || "global"}
|
||||||
onValueChange={(value: "global" | "menu") =>
|
onValueChange={(value: "global" | "menu") => setCurrentRule((prev) => ({ ...prev!, scopeType: value }))}
|
||||||
setCurrentRule((prev) => ({ ...prev!, scopeType: value }))
|
|
||||||
}
|
|
||||||
disabled={isPreview}
|
disabled={isPreview}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-9">
|
<SelectTrigger className="h-9">
|
||||||
|
|
@ -361,9 +341,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<SelectItem value="menu">메뉴별</SelectItem>
|
<SelectItem value="menu">메뉴별</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
{currentRule.scopeType === "menu"
|
{currentRule.scopeType === "menu"
|
||||||
? "이 규칙이 설정된 상위 메뉴의 모든 하위 메뉴에서 사용 가능합니다"
|
? "⚠️ 현재 화면이 속한 2레벨 메뉴와 그 하위 메뉴(3레벨 이상)에서만 사용됩니다. 형제 메뉴와 구분하여 채번 규칙을 관리할 때 유용합니다."
|
||||||
: "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}
|
: "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -380,16 +360,14 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold">코드 구성</h3>
|
<h3 className="text-sm font-semibold">코드 구성</h3>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-muted-foreground text-xs">
|
||||||
{currentRule.parts.length}/{maxRules}
|
{currentRule.parts.length}/{maxRules}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentRule.parts.length === 0 ? (
|
{currentRule.parts.length === 0 ? (
|
||||||
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border bg-muted/50">
|
<div className="border-border bg-muted/50 flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||||
<p className="text-xs text-muted-foreground sm:text-sm">
|
<p className="text-muted-foreground text-xs sm:text-sm">규칙을 추가하여 코드를 구성하세요</p>
|
||||||
규칙을 추가하여 코드를 구성하세요
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
||||||
|
|
@ -416,11 +394,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
규칙 추가
|
규칙 추가
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleSave} disabled={isPreview || loading} className="h-9 flex-1 text-sm">
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isPreview || loading}
|
|
||||||
className="h-9 flex-1 text-sm"
|
|
||||||
>
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-2 h-4 w-4" />
|
||||||
{loading ? "저장 중..." : "저장"}
|
{loading ? "저장 중..." : "저장"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
|
|
||||||
// 자동값 생성 함수
|
// 자동값 생성 함수
|
||||||
const generateAutoValue = useCallback(
|
const generateAutoValue = useCallback(
|
||||||
(autoValueType: string): string => {
|
async (autoValueType: string, ruleId?: string): Promise<string> => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
switch (autoValueType) {
|
switch (autoValueType) {
|
||||||
case "current_datetime":
|
case "current_datetime":
|
||||||
|
|
@ -99,6 +99,20 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
case "sequence":
|
case "sequence":
|
||||||
return `SEQ_${Date.now()}`;
|
return `SEQ_${Date.now()}`;
|
||||||
|
case "numbering_rule":
|
||||||
|
// 채번 규칙 사용
|
||||||
|
if (ruleId) {
|
||||||
|
try {
|
||||||
|
const { generateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||||
|
const response = await generateNumberingCode(ruleId);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return response.data.generatedCode;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("채번 규칙 코드 생성 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -129,24 +143,32 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
// 자동값 설정
|
// 자동값 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const widgetComponents = allComponents.filter((c) => c.type === "widget") as WidgetComponent[];
|
const widgetComponents = allComponents.filter((c) => c.type === "widget") as WidgetComponent[];
|
||||||
const autoValueUpdates: Record<string, any> = {};
|
|
||||||
|
const loadAutoValues = async () => {
|
||||||
|
const autoValueUpdates: Record<string, any> = {};
|
||||||
|
|
||||||
for (const widget of widgetComponents) {
|
for (const widget of widgetComponents) {
|
||||||
const fieldName = widget.columnName || widget.id;
|
const fieldName = widget.columnName || widget.id;
|
||||||
const currentValue = finalFormData[fieldName];
|
const currentValue = finalFormData[fieldName];
|
||||||
|
|
||||||
// 자동값이 설정되어 있고 현재 값이 없는 경우
|
// 자동값이 설정되어 있고 현재 값이 없는 경우
|
||||||
if (widget.inputType === "auto" && widget.autoValueType && !currentValue) {
|
if (widget.inputType === "auto" && widget.autoValueType && !currentValue) {
|
||||||
const autoValue = generateAutoValue(widget.autoValueType);
|
const autoValue = await generateAutoValue(
|
||||||
if (autoValue) {
|
widget.autoValueType,
|
||||||
autoValueUpdates[fieldName] = autoValue;
|
(widget as any).numberingRuleId // 채번 규칙 ID
|
||||||
|
);
|
||||||
|
if (autoValue) {
|
||||||
|
autoValueUpdates[fieldName] = autoValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(autoValueUpdates).length > 0) {
|
if (Object.keys(autoValueUpdates).length > 0) {
|
||||||
setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates }));
|
setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates }));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadAutoValues();
|
||||||
}, [allComponents, finalFormData, generateAutoValue]);
|
}, [allComponents, finalFormData, generateAutoValue]);
|
||||||
|
|
||||||
// 향상된 저장 핸들러
|
// 향상된 저장 핸들러
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 자동값 생성 함수
|
// 자동값 생성 함수
|
||||||
const generateAutoValue = useCallback((autoValueType: string): string => {
|
const generateAutoValue = useCallback(async (autoValueType: string, ruleId?: string): Promise<string> => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
switch (autoValueType) {
|
switch (autoValueType) {
|
||||||
case "current_datetime":
|
case "current_datetime":
|
||||||
|
|
@ -152,6 +152,20 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
case "sequence":
|
case "sequence":
|
||||||
return `SEQ_${Date.now()}`;
|
return `SEQ_${Date.now()}`;
|
||||||
|
case "numbering_rule":
|
||||||
|
// 채번 규칙 사용
|
||||||
|
if (ruleId) {
|
||||||
|
try {
|
||||||
|
const { generateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||||
|
const response = await generateNumberingCode(ruleId);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return response.data.generatedCode;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("채번 규칙 코드 생성 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { TextTypeConfig } from "@/types/screen";
|
import { TextTypeConfig } from "@/types/screen";
|
||||||
|
import { getAvailableNumberingRules } from "@/lib/api/numberingRule";
|
||||||
|
import { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||||
|
|
||||||
interface TextTypeConfigPanelProps {
|
interface TextTypeConfigPanelProps {
|
||||||
config: TextTypeConfig;
|
config: TextTypeConfig;
|
||||||
|
|
@ -26,9 +28,14 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
autoInput: false,
|
autoInput: false,
|
||||||
autoValueType: "current_datetime" as const,
|
autoValueType: "current_datetime" as const,
|
||||||
customValue: "",
|
customValue: "",
|
||||||
|
numberingRuleId: "",
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 채번 규칙 목록 상태
|
||||||
|
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||||
|
const [loadingRules, setLoadingRules] = useState(false);
|
||||||
|
|
||||||
// 로컬 상태로 실시간 입력 관리
|
// 로컬 상태로 실시간 입력 관리
|
||||||
const [localValues, setLocalValues] = useState({
|
const [localValues, setLocalValues] = useState({
|
||||||
minLength: safeConfig.minLength?.toString() || "",
|
minLength: safeConfig.minLength?.toString() || "",
|
||||||
|
|
@ -41,8 +48,33 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
autoInput: safeConfig.autoInput,
|
autoInput: safeConfig.autoInput,
|
||||||
autoValueType: safeConfig.autoValueType,
|
autoValueType: safeConfig.autoValueType,
|
||||||
customValue: safeConfig.customValue,
|
customValue: safeConfig.customValue,
|
||||||
|
numberingRuleId: safeConfig.numberingRuleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 채번 규칙 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRules = async () => {
|
||||||
|
setLoadingRules(true);
|
||||||
|
try {
|
||||||
|
// TODO: 현재 메뉴 objid를 화면 정보에서 가져와야 함
|
||||||
|
// 지금은 menuObjid 없이 호출 (global 규칙만 조회)
|
||||||
|
const response = await getAvailableNumberingRules();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setNumberingRules(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("채번 규칙 목록 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingRules(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// autoValueType이 numbering_rule일 때만 로드
|
||||||
|
if (localValues.autoValueType === "numbering_rule") {
|
||||||
|
loadRules();
|
||||||
|
}
|
||||||
|
}, [localValues.autoValueType]);
|
||||||
|
|
||||||
// config가 변경될 때 로컬 상태 동기화
|
// config가 변경될 때 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalValues({
|
setLocalValues({
|
||||||
|
|
@ -56,6 +88,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
autoInput: safeConfig.autoInput,
|
autoInput: safeConfig.autoInput,
|
||||||
autoValueType: safeConfig.autoValueType,
|
autoValueType: safeConfig.autoValueType,
|
||||||
customValue: safeConfig.customValue,
|
customValue: safeConfig.customValue,
|
||||||
|
numberingRuleId: safeConfig.numberingRuleId,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
safeConfig.minLength,
|
safeConfig.minLength,
|
||||||
|
|
@ -68,6 +101,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
safeConfig.autoInput,
|
safeConfig.autoInput,
|
||||||
safeConfig.autoValueType,
|
safeConfig.autoValueType,
|
||||||
safeConfig.customValue,
|
safeConfig.customValue,
|
||||||
|
safeConfig.numberingRuleId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateConfig = (key: keyof TextTypeConfig, value: any) => {
|
const updateConfig = (key: keyof TextTypeConfig, value: any) => {
|
||||||
|
|
@ -90,16 +124,10 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
autoInput: key === "autoInput" ? value : localValues.autoInput,
|
autoInput: key === "autoInput" ? value : localValues.autoInput,
|
||||||
autoValueType: key === "autoValueType" ? value : localValues.autoValueType,
|
autoValueType: key === "autoValueType" ? value : localValues.autoValueType,
|
||||||
customValue: key === "customValue" ? value : localValues.customValue,
|
customValue: key === "customValue" ? value : localValues.customValue,
|
||||||
|
numberingRuleId: key === "numberingRuleId" ? value : localValues.numberingRuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||||
// console.log("📝 TextTypeConfig 업데이트:", {
|
|
||||||
// key,
|
|
||||||
// value,
|
|
||||||
// oldConfig: safeConfig,
|
|
||||||
// newConfig,
|
|
||||||
// localValues,
|
|
||||||
// });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
|
|
@ -236,11 +264,45 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
||||||
<SelectItem value="current_user">현재 사용자</SelectItem>
|
<SelectItem value="current_user">현재 사용자</SelectItem>
|
||||||
<SelectItem value="uuid">고유 ID (UUID)</SelectItem>
|
<SelectItem value="uuid">고유 ID (UUID)</SelectItem>
|
||||||
<SelectItem value="sequence">순번</SelectItem>
|
<SelectItem value="sequence">순번</SelectItem>
|
||||||
|
<SelectItem value="numbering_rule">채번 규칙</SelectItem>
|
||||||
<SelectItem value="custom">사용자 정의</SelectItem>
|
<SelectItem value="custom">사용자 정의</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{localValues.autoValueType === "numbering_rule" && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="numberingRuleId" className="text-sm font-medium">
|
||||||
|
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={localValues.numberingRuleId}
|
||||||
|
onValueChange={(value) => updateConfig("numberingRuleId", value)}
|
||||||
|
disabled={loadingRules}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||||
|
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{numberingRules.length === 0 ? (
|
||||||
|
<SelectItem value="no-rules" disabled>
|
||||||
|
사용 가능한 규칙이 없습니다
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
numberingRules.map((rule) => (
|
||||||
|
<SelectItem key={rule.ruleId} value={rule.ruleId}>
|
||||||
|
{rule.ruleName} ({rule.ruleId})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||||
|
현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{localValues.autoValueType === "custom" && (
|
{localValues.autoValueType === "custom" && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="customValue" className="text-sm font-medium">
|
<Label htmlFor="customValue" className="text-sm font-medium">
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,25 @@ export async function getNumberingRules(): Promise<ApiResponse<NumberingRuleConf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴별 사용 가능한 채번 규칙 조회
|
||||||
|
* @param menuObjid 현재 메뉴의 objid (선택)
|
||||||
|
* @returns 사용 가능한 채번 규칙 목록
|
||||||
|
*/
|
||||||
|
export async function getAvailableNumberingRules(
|
||||||
|
menuObjid?: number
|
||||||
|
): Promise<ApiResponse<NumberingRuleConfig[]>> {
|
||||||
|
try {
|
||||||
|
const url = menuObjid
|
||||||
|
? `/numbering-rules/available/${menuObjid}`
|
||||||
|
: "/numbering-rules/available";
|
||||||
|
const response = await apiClient.get(url);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message || "사용 가능한 규칙 조회 실패" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNumberingRuleById(ruleId: string): Promise<ApiResponse<NumberingRuleConfig>> {
|
export async function getNumberingRuleById(ruleId: string): Promise<ApiResponse<NumberingRuleConfig>> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(`/numbering-rules/${ruleId}`);
|
const response = await apiClient.get(`/numbering-rules/${ruleId}`);
|
||||||
|
|
@ -62,15 +81,49 @@ export async function deleteNumberingRule(ruleId: string): Promise<ApiResponse<v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateCode(ruleId: string): Promise<ApiResponse<{ code: string }>> {
|
/**
|
||||||
|
* 코드 미리보기 (순번 증가 없음)
|
||||||
|
* 화면 표시용으로 사용
|
||||||
|
*/
|
||||||
|
export async function previewNumberingCode(
|
||||||
|
ruleId: string
|
||||||
|
): Promise<ApiResponse<{ generatedCode: string }>> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(`/numbering-rules/${ruleId}/generate`);
|
const response = await apiClient.post(`/numbering-rules/${ruleId}/preview`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: false, error: error.message || "코드 생성 실패" };
|
return { success: false, error: error.message || "코드 미리보기 실패" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 코드 할당 (저장 시점에 실제 순번 증가)
|
||||||
|
* 실제 저장할 때만 호출
|
||||||
|
*/
|
||||||
|
export async function allocateNumberingCode(
|
||||||
|
ruleId: string
|
||||||
|
): Promise<ApiResponse<{ generatedCode: string }>> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error.message || "코드 할당 실패" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 기존 generateNumberingCode는 previewNumberingCode를 사용하세요
|
||||||
|
*/
|
||||||
|
export async function generateNumberingCode(
|
||||||
|
ruleId: string
|
||||||
|
): Promise<ApiResponse<{ generatedCode: string }>> {
|
||||||
|
console.warn("generateNumberingCode는 deprecated. previewNumberingCode 사용 권장");
|
||||||
|
return previewNumberingCode(ruleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 하위 호환성을 위한 별칭
|
||||||
|
export const generateCode = generateNumberingCode;
|
||||||
|
|
||||||
export async function resetSequence(ruleId: string): Promise<ApiResponse<void>> {
|
export async function resetSequence(ruleId: string): Promise<ApiResponse<void>> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(`/numbering-rules/${ruleId}/reset`);
|
const response = await apiClient.post(`/numbering-rules/${ruleId}/reset`);
|
||||||
|
|
@ -79,3 +132,4 @@ export async function resetSequence(ruleId: string): Promise<ApiResponse<void>>
|
||||||
return { success: false, error: error.message || "시퀀스 초기화 실패" };
|
return { success: false, error: error.message || "시퀀스 초기화 실패" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export type AutoGenerationType =
|
||||||
| "current_user" // 현재 사용자 ID
|
| "current_user" // 현재 사용자 ID
|
||||||
| "current_time" // 현재 시간
|
| "current_time" // 현재 시간
|
||||||
| "sequence" // 시퀀스 번호
|
| "sequence" // 시퀀스 번호
|
||||||
|
| "numbering_rule" // 채번 규칙
|
||||||
| "random_string" // 랜덤 문자열
|
| "random_string" // 랜덤 문자열
|
||||||
| "random_number" // 랜덤 숫자
|
| "random_number" // 랜덤 숫자
|
||||||
| "company_code" // 회사 코드
|
| "company_code" // 회사 코드
|
||||||
|
|
@ -37,6 +38,7 @@ export interface AutoGenerationConfig {
|
||||||
suffix?: string; // 접미사
|
suffix?: string; // 접미사
|
||||||
format?: string; // 시간 형식 (current_time용)
|
format?: string; // 시간 형식 (current_time용)
|
||||||
startValue?: number; // 시퀀스 시작값
|
startValue?: number; // 시퀀스 시작값
|
||||||
|
numberingRuleId?: string; // 채번 규칙 ID (numbering_rule 타입용)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
// 자동생성된 값 상태
|
// 자동생성된 값 상태
|
||||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||||
|
|
||||||
|
// API 호출 중복 방지를 위한 ref
|
||||||
|
const isGeneratingRef = React.useRef(false);
|
||||||
|
const hasGeneratedRef = React.useRef(false);
|
||||||
|
|
||||||
// 테스트용: 컴포넌트 라벨에 "test"가 포함되면 강제로 UUID 자동생성 활성화
|
// 테스트용: 컴포넌트 라벨에 "test"가 포함되면 강제로 UUID 자동생성 활성화
|
||||||
const testAutoGeneration = component.label?.toLowerCase().includes("test")
|
const testAutoGeneration = component.label?.toLowerCase().includes("test")
|
||||||
|
|
@ -79,32 +83,96 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// autoGeneratedValue,
|
// autoGeneratedValue,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// 자동생성 값 생성 (컴포넌트 마운트 시 또는 폼 데이터 변경 시)
|
// 자동생성 값 생성 (컴포넌트 마운트 시 한 번만 실행)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
|
const generateAutoValue = async () => {
|
||||||
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
|
// 이미 생성 중이거나 생성 완료된 경우 중복 실행 방지
|
||||||
const currentFormValue = formData?.[component.columnName];
|
if (isGeneratingRef.current || hasGeneratedRef.current) {
|
||||||
const currentComponentValue = component.value;
|
console.log("⏭️ 중복 실행 방지:", {
|
||||||
|
isGenerating: isGeneratingRef.current,
|
||||||
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
hasGenerated: hasGeneratedRef.current,
|
||||||
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
});
|
||||||
const generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
|
return;
|
||||||
|
|
||||||
if (generatedValue) {
|
|
||||||
setAutoGeneratedValue(generatedValue);
|
|
||||||
|
|
||||||
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
|
||||||
onFormDataChange(component.columnName, generatedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
|
||||||
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
|
||||||
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
|
||||||
setAutoGeneratedValue(previewValue);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
|
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
|
||||||
|
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
|
||||||
|
const currentFormValue = formData?.[component.columnName];
|
||||||
|
const currentComponentValue = component.value;
|
||||||
|
|
||||||
|
console.log("🔧 TextInput 자동생성 체크:", {
|
||||||
|
componentId: component.id,
|
||||||
|
columnName: component.columnName,
|
||||||
|
autoGenType: testAutoGeneration.type,
|
||||||
|
ruleId: testAutoGeneration.options?.numberingRuleId,
|
||||||
|
currentFormValue,
|
||||||
|
currentComponentValue,
|
||||||
|
autoGeneratedValue,
|
||||||
|
isInteractive,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
||||||
|
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
||||||
|
isGeneratingRef.current = true; // 생성 시작 플래그
|
||||||
|
let generatedValue: string | null = null;
|
||||||
|
|
||||||
|
// 채번 규칙은 비동기로 처리
|
||||||
|
if (testAutoGeneration.type === "numbering_rule") {
|
||||||
|
const ruleId = testAutoGeneration.options?.numberingRuleId;
|
||||||
|
if (ruleId) {
|
||||||
|
try {
|
||||||
|
console.log("🚀 채번 규칙 API 호출 시작:", ruleId);
|
||||||
|
const { generateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||||
|
const response = await generateNumberingCode(ruleId);
|
||||||
|
console.log("✅ 채번 규칙 API 응답:", response);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
generatedValue = response.data.generatedCode;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 채번 규칙 코드 생성 실패:", error);
|
||||||
|
} finally {
|
||||||
|
isGeneratingRef.current = false; // 생성 완료
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ 채번 규칙 ID가 없습니다");
|
||||||
|
isGeneratingRef.current = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 기타 타입은 동기로 처리
|
||||||
|
generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
|
||||||
|
isGeneratingRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generatedValue) {
|
||||||
|
console.log("✅ 자동생성 값 설정:", generatedValue);
|
||||||
|
setAutoGeneratedValue(generatedValue);
|
||||||
|
hasGeneratedRef.current = true; // 생성 완료 플래그
|
||||||
|
|
||||||
|
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
||||||
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
|
console.log("📝 formData 업데이트:", component.columnName, generatedValue);
|
||||||
|
onFormDataChange(component.columnName, generatedValue);
|
||||||
|
|
||||||
|
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
|
||||||
|
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
|
||||||
|
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
||||||
|
onFormDataChange(ruleIdKey, testAutoGeneration.options.numberingRuleId);
|
||||||
|
console.log("📝 채번 규칙 ID 저장:", ruleIdKey, testAutoGeneration.options.numberingRuleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
||||||
|
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
||||||
|
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
||||||
|
console.log("👁️ 미리보기 값 설정:", previewValue);
|
||||||
|
setAutoGeneratedValue(previewValue);
|
||||||
|
hasGeneratedRef.current = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
generateAutoValue();
|
||||||
|
}, [testAutoGeneration.enabled, testAutoGeneration.type, component.columnName, isInteractive]);
|
||||||
|
|
||||||
// 실제 화면에서 숨김 처리된 컴포넌트는 렌더링하지 않음
|
// 실제 화면에서 숨김 처리된 컴포넌트는 렌더링하지 않음
|
||||||
if (isHidden && !isDesignMode) {
|
if (isHidden && !isDesignMode) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
@ -8,6 +8,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { TextInputConfig } from "./types";
|
import { TextInputConfig } from "./types";
|
||||||
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
||||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||||
|
import { getAvailableNumberingRules } from "@/lib/api/numberingRule";
|
||||||
|
import { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||||
|
|
||||||
export interface TextInputConfigPanelProps {
|
export interface TextInputConfigPanelProps {
|
||||||
config: TextInputConfig;
|
config: TextInputConfig;
|
||||||
|
|
@ -19,6 +21,32 @@ export interface TextInputConfigPanelProps {
|
||||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||||
*/
|
*/
|
||||||
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange }) => {
|
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange }) => {
|
||||||
|
// 채번 규칙 목록 상태
|
||||||
|
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||||
|
const [loadingRules, setLoadingRules] = useState(false);
|
||||||
|
|
||||||
|
// 채번 규칙 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRules = async () => {
|
||||||
|
setLoadingRules(true);
|
||||||
|
try {
|
||||||
|
const response = await getAvailableNumberingRules();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setNumberingRules(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("채번 규칙 목록 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingRules(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// autoGeneration.type이 numbering_rule일 때만 로드
|
||||||
|
if (config.autoGeneration?.type === "numbering_rule") {
|
||||||
|
loadRules();
|
||||||
|
}
|
||||||
|
}, [config.autoGeneration?.type]);
|
||||||
|
|
||||||
const handleChange = (key: keyof TextInputConfig, value: any) => {
|
const handleChange = (key: keyof TextInputConfig, value: any) => {
|
||||||
onChange({ [key]: value });
|
onChange({ [key]: value });
|
||||||
};
|
};
|
||||||
|
|
@ -100,6 +128,7 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||||
<SelectItem value="current_user">현재 사용자 ID</SelectItem>
|
<SelectItem value="current_user">현재 사용자 ID</SelectItem>
|
||||||
<SelectItem value="current_time">현재 시간</SelectItem>
|
<SelectItem value="current_time">현재 시간</SelectItem>
|
||||||
<SelectItem value="sequence">순차 번호</SelectItem>
|
<SelectItem value="sequence">순차 번호</SelectItem>
|
||||||
|
<SelectItem value="numbering_rule">채번 규칙</SelectItem>
|
||||||
<SelectItem value="random_string">랜덤 문자열</SelectItem>
|
<SelectItem value="random_string">랜덤 문자열</SelectItem>
|
||||||
<SelectItem value="random_number">랜덤 숫자</SelectItem>
|
<SelectItem value="random_number">랜덤 숫자</SelectItem>
|
||||||
<SelectItem value="company_code">회사 코드</SelectItem>
|
<SelectItem value="company_code">회사 코드</SelectItem>
|
||||||
|
|
@ -113,6 +142,49 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||||
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
|
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 채번 규칙 선택 */}
|
||||||
|
{config.autoGeneration?.type === "numbering_rule" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="numberingRuleId">
|
||||||
|
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={config.autoGeneration?.options?.numberingRuleId || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentConfig = config.autoGeneration!;
|
||||||
|
handleChange("autoGeneration", {
|
||||||
|
...currentConfig,
|
||||||
|
options: {
|
||||||
|
...currentConfig.options,
|
||||||
|
numberingRuleId: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={loadingRules}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{numberingRules.length === 0 ? (
|
||||||
|
<SelectItem value="no-rules" disabled>
|
||||||
|
사용 가능한 규칙이 없습니다
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
numberingRules.map((rule) => (
|
||||||
|
<SelectItem key={rule.ruleId} value={rule.ruleId}>
|
||||||
|
{rule.ruleName} ({rule.ruleId})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,11 +207,12 @@ export class AutoGenerationUtils {
|
||||||
* 자동생성 타입별 설명 가져오기
|
* 자동생성 타입별 설명 가져오기
|
||||||
*/
|
*/
|
||||||
static getTypeDescription(type: AutoGenerationType): string {
|
static getTypeDescription(type: AutoGenerationType): string {
|
||||||
const descriptions: Record<AutoGenerationType, string> = {
|
const descriptions: Record<string, string> = {
|
||||||
uuid: "고유 식별자 (UUID) 생성",
|
uuid: "고유 식별자 (UUID) 생성",
|
||||||
current_user: "현재 로그인한 사용자 ID",
|
current_user: "현재 로그인한 사용자 ID",
|
||||||
current_time: "현재 날짜/시간",
|
current_time: "현재 날짜/시간",
|
||||||
sequence: "순차적 번호 생성",
|
sequence: "순차적 번호 생성",
|
||||||
|
numbering_rule: "채번 규칙 기반 코드 생성",
|
||||||
random_string: "랜덤 문자열 생성",
|
random_string: "랜덤 문자열 생성",
|
||||||
random_number: "랜덤 숫자 생성",
|
random_number: "랜덤 숫자 생성",
|
||||||
company_code: "현재 회사 코드",
|
company_code: "현재 회사 코드",
|
||||||
|
|
@ -246,6 +247,9 @@ export class AutoGenerationUtils {
|
||||||
case "sequence":
|
case "sequence":
|
||||||
return `${options.prefix || ""}1${options.suffix || ""}`;
|
return `${options.prefix || ""}1${options.suffix || ""}`;
|
||||||
|
|
||||||
|
case "numbering_rule":
|
||||||
|
return "CODE-20251104-001"; // 채번 규칙 미리보기
|
||||||
|
|
||||||
case "random_string":
|
case "random_string":
|
||||||
return `${options.prefix || ""}ABC123${options.suffix || ""}`;
|
return `${options.prefix || ""}ABC123${options.suffix || ""}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,50 @@ export class ButtonActionExecutor {
|
||||||
companyCodeValue, // ✅ 최종 회사 코드 값
|
companyCodeValue, // ✅ 최종 회사 코드 값
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
|
||||||
|
console.log("🔍 채번 규칙 할당 체크 시작");
|
||||||
|
console.log("📦 현재 formData:", JSON.stringify(formData, null, 2));
|
||||||
|
|
||||||
|
const fieldsWithNumbering: Record<string, string> = {};
|
||||||
|
|
||||||
|
// formData에서 채번 규칙이 설정된 필드 찾기
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
if (key.endsWith("_numberingRuleId") && value) {
|
||||||
|
const fieldName = key.replace("_numberingRuleId", "");
|
||||||
|
fieldsWithNumbering[fieldName] = value as string;
|
||||||
|
console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering);
|
||||||
|
console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length);
|
||||||
|
|
||||||
|
// 각 필드에 대해 실제 코드 할당
|
||||||
|
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
|
||||||
|
try {
|
||||||
|
console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`);
|
||||||
|
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||||
|
const response = await allocateNumberingCode(ruleId);
|
||||||
|
|
||||||
|
console.log(`📡 API 응답 (${fieldName}):`, response);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const generatedCode = response.data.generatedCode;
|
||||||
|
formData[fieldName] = generatedCode;
|
||||||
|
console.log(`✅ ${fieldName} = ${generatedCode} (할당 완료)`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 채번 규칙 할당 실패 (${fieldName}):`, response.error);
|
||||||
|
toast.error(`${fieldName} 채번 규칙 할당 실패: ${response.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 채번 규칙 할당 오류 (${fieldName}):`, error);
|
||||||
|
toast.error(`${fieldName} 채번 규칙 할당 오류`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 채번 규칙 할당 완료");
|
||||||
|
console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
|
||||||
|
|
||||||
const dataWithUserInfo = {
|
const dataWithUserInfo = {
|
||||||
...formData,
|
...formData,
|
||||||
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
||||||
|
|
@ -254,6 +298,13 @@ export class ButtonActionExecutor {
|
||||||
company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
|
company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// _numberingRuleId 필드 제거 (실제 저장하지 않음)
|
||||||
|
for (const key of Object.keys(dataWithUserInfo)) {
|
||||||
|
if (key.endsWith("_numberingRuleId")) {
|
||||||
|
delete dataWithUserInfo[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveResult = await DynamicFormApi.saveFormData({
|
saveResult = await DynamicFormApi.saveFormData({
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ export type AutoGenerationType =
|
||||||
| "current_user" // 현재 사용자 ID
|
| "current_user" // 현재 사용자 ID
|
||||||
| "current_time" // 현재 시간
|
| "current_time" // 현재 시간
|
||||||
| "sequence" // 시퀀스 번호
|
| "sequence" // 시퀀스 번호
|
||||||
|
| "numbering_rule" // 채번 규칙
|
||||||
| "random_string" // 랜덤 문자열
|
| "random_string" // 랜덤 문자열
|
||||||
| "random_number" // 랜덤 숫자
|
| "random_number" // 랜덤 숫자
|
||||||
| "company_code" // 회사 코드
|
| "company_code" // 회사 코드
|
||||||
|
|
@ -178,6 +179,7 @@ export interface AutoGenerationConfig {
|
||||||
suffix?: string; // 접미사
|
suffix?: string; // 접미사
|
||||||
format?: string; // 시간 형식 (current_time용)
|
format?: string; // 시간 형식 (current_time용)
|
||||||
startValue?: number; // 시퀀스 시작값
|
startValue?: number; // 시퀀스 시작값
|
||||||
|
numberingRuleId?: string; // 채번 규칙 ID (numbering_rule 타입용)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,8 +251,22 @@ export interface TextTypeConfig {
|
||||||
pattern?: string;
|
pattern?: string;
|
||||||
format?: "none" | "email" | "phone" | "url" | "korean" | "english";
|
format?: "none" | "email" | "phone" | "url" | "korean" | "english";
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
defaultValue?: string;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
// 자동입력 관련 설정
|
||||||
|
autoInput?: boolean;
|
||||||
|
autoValueType?:
|
||||||
|
| "current_datetime"
|
||||||
|
| "current_date"
|
||||||
|
| "current_time"
|
||||||
|
| "current_user"
|
||||||
|
| "uuid"
|
||||||
|
| "sequence"
|
||||||
|
| "numbering_rule"
|
||||||
|
| "custom";
|
||||||
|
customValue?: string;
|
||||||
|
numberingRuleId?: string; // 채번 규칙 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue