From 198f678b68ee08977ef2a20941028ee228e429b3 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 4 Nov 2025 17:35:02 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B1=84=EB=B2=88=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/numberingRuleController.ts | 69 +++- .../src/services/numberingRuleService.ts | 321 ++++++++++++++++-- .../numbering-rule/NumberingRuleDesigner.tsx | 122 +++---- .../EnhancedInteractiveScreenViewer.tsx | 50 ++- .../screen/InteractiveScreenViewer.tsx | 16 +- .../webtype-configs/TextTypeConfigPanel.tsx | 76 ++++- frontend/lib/api/numberingRule.ts | 60 +++- .../registry/components/table-list/types.ts | 2 + .../text-input/TextInputComponent.tsx | 116 +++++-- .../text-input/TextInputConfigPanel.tsx | 74 +++- frontend/lib/utils/autoGeneration.ts | 6 +- frontend/lib/utils/buttonActions.ts | 51 +++ frontend/types/screen-legacy-backup.ts | 2 + frontend/types/screen-management.ts | 14 + 14 files changed, 808 insertions(+), 171 deletions(-) diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 42b6172f..f37bc542 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -2,15 +2,15 @@ * 채번 규칙 관리 컨트롤러 */ -import { Router, Request, Response } from "express"; -import { authenticateToken } from "../middleware/authMiddleware"; +import { Router, Response } from "express"; +import { authenticateToken, AuthenticatedRequest } from "../middleware/authMiddleware"; import { numberingRuleService } from "../services/numberingRuleService"; import { logger } from "../utils/logger"; const router = Router(); -// 규칙 목록 조회 -router.get("/", authenticateToken, async (req: Request, res: Response) => { +// 규칙 목록 조회 (전체) +router.get("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { const companyCode = req.user!.companyCode; 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 { 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 userId = req.user!.userId; 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 { ruleId } = req.params; 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 { 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 { ruleId } = req.params; try { 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) { logger.error("코드 생성 실패", { 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 { ruleId } = req.params; diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index c61fce29..cad0727e 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -26,6 +26,8 @@ interface NumberingRuleConfig { tableName?: string; columnName?: string; companyCode?: string; + menuObjid?: number; + scopeType?: string; createdAt?: string; updatedAt?: string; createdBy?: string; @@ -33,7 +35,7 @@ interface NumberingRuleConfig { class NumberingRuleService { /** - * 규칙 목록 조회 + * 규칙 목록 조회 (전체) */ async getRuleList(companyCode: string): Promise { try { @@ -78,11 +80,16 @@ class NumberingRuleService { 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; } - logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, { companyCode }); + logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, { + companyCode, + }); return result.rows; } catch (error: any) { logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`); @@ -90,10 +97,170 @@ class NumberingRuleService { } } + /** + * 현재 메뉴에서 사용 가능한 규칙 목록 조회 + */ + async getAvailableRulesForMenu( + companyCode: string, + menuObjid?: number + ): Promise { + 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 { + async getRuleById( + ruleId: string, + companyCode: string + ): Promise { const pool = getPool(); const query = ` SELECT @@ -106,7 +273,7 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_id AS "menuId", + menu_objid AS "menuObjid", scope_type AS "scopeType", created_at AS "createdAt", updated_at AS "updatedAt", @@ -223,7 +390,10 @@ class NumberingRuleService { } await client.query("COMMIT"); - logger.info("채번 규칙 생성 완료", { ruleId: config.ruleId, companyCode }); + logger.info("채번 규칙 생성 완료", { + ruleId: config.ruleId, + companyCode, + }); return { ...ruleResult.rows[0], parts }; } catch (error: any) { await client.query("ROLLBACK"); @@ -364,9 +534,63 @@ class NumberingRuleService { } /** - * 코드 생성 + * 코드 미리보기 (순번 증가 없음) */ - async generateCode(ruleId: string, companyCode: string): Promise { + async previewCode(ruleId: string, companyCode: string): Promise { + 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 { const pool = getPool(); const client = await pool.connect(); @@ -386,37 +610,44 @@ class NumberingRuleService { const autoConfig = part.autoConfig || {}; switch (part.partType) { - case "prefix": - return autoConfig.prefix || "PREFIX"; - case "sequence": { + // 순번 (자동 증가 숫자) const length = autoConfig.sequenceLength || 4; return String(rule.currentSequence || 1).padStart(length, "0"); } - case "date": - return this.formatDate(new Date(), autoConfig.dateFormat || "YYYYMMDD"); - - case "year": { - const format = autoConfig.dateFormat || "YYYY"; - const year = new Date().getFullYear(); - return format === "YY" ? String(year).slice(-2) : String(year); + case "number": { + // 숫자 (고정 자릿수) + const length = autoConfig.numberLength || 4; + const value = autoConfig.numberValue || 1; + return String(value).padStart(length, "0"); } - case "month": - return String(new Date().getMonth() + 1).padStart(2, "0"); + case "date": { + // 날짜 (다양한 날짜 형식) + return this.formatDate( + new Date(), + autoConfig.dateFormat || "YYYYMMDD" + ); + } - case "custom": - return autoConfig.value || "CUSTOM"; + case "text": { + // 텍스트 (고정 문자열) + return autoConfig.textValue || "TEXT"; + } default: + logger.warn("알 수 없는 파트 타입", { partType: part.partType }); 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) { await client.query( "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"); - logger.info("코드 생성 완료", { ruleId, generatedCode }); - return generatedCode; + logger.info("코드 할당 완료", { ruleId, allocatedCode, companyCode }); + return allocatedCode; } catch (error: any) { await client.query("ROLLBACK"); - logger.error("코드 생성 실패", { error: error.message }); + logger.error("코드 할당 실패", { + ruleId, + companyCode, + error: error.message, + stack: error.stack, + }); throw error; } finally { client.release(); } } + /** + * @deprecated 기존 generateCode는 allocateCode를 사용하세요 + */ + async generateCode(ruleId: string, companyCode: string): Promise { + logger.warn( + "generateCode는 deprecated 되었습니다. previewCode 또는 allocateCode를 사용하세요" + ); + return this.allocateCode(ruleId, companyCode); + } + private formatDate(date: Date, format: string): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); switch (format) { - case "YYYY": return String(year); - case "YY": return String(year).slice(-2); - case "YYYYMM": return `${year}${month}`; - 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}`; + case "YYYY": + return String(year); + case "YY": + return String(year).slice(-2); + case "YYYYMM": + return `${year}${month}`; + 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}`; } } diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 96c88201..3886ea36 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -109,9 +109,7 @@ export const NumberingRuleDesigner: React.FC = ({ if (!prev) return null; return { ...prev, - parts: prev.parts - .filter((part) => part.id !== partId) - .map((part, index) => ({ ...part, order: index + 1 })), + parts: prev.parts.filter((part) => part.id !== partId).map((part, index) => ({ ...part, order: index + 1 })), }; }); @@ -132,7 +130,7 @@ export const NumberingRuleDesigner: React.FC = ({ setLoading(true); try { const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId); - + let response; if (existing) { response = await updateNumberingRule(currentRule.ruleId, currentRule); @@ -170,29 +168,32 @@ export const NumberingRuleDesigner: React.FC = ({ toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`); }, []); - const handleDeleteSavedRule = useCallback(async (ruleId: string) => { - setLoading(true); - try { - const response = await deleteNumberingRule(ruleId); - - if (response.success) { - setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId)); - - if (selectedRuleId === ruleId) { - setSelectedRuleId(null); - setCurrentRule(null); + const handleDeleteSavedRule = useCallback( + async (ruleId: string) => { + setLoading(true); + try { + const response = await deleteNumberingRule(ruleId); + + if (response.success) { + setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId)); + + if (selectedRuleId === ruleId) { + setSelectedRuleId(null); + setCurrentRule(null); + } + + toast.success("규칙이 삭제되었습니다"); + } else { + toast.error(response.error || "삭제 실패"); } - - toast.success("규칙이 삭제되었습니다"); - } else { - toast.error(response.error || "삭제 실패"); + } catch (error: any) { + toast.error(`삭제 실패: ${error.message}`); + } finally { + setLoading(false); } - } catch (error: any) { - toast.error(`삭제 실패: ${error.message}`); - } finally { - setLoading(false); - } - }, [selectedRuleId]); + }, + [selectedRuleId], + ); const handleNewRule = useCallback(() => { const newRule: NumberingRuleConfig = { @@ -207,7 +208,7 @@ export const NumberingRuleDesigner: React.FC = ({ setSelectedRuleId(newRule.ruleId); setCurrentRule(newRule); - + toast.success("새 규칙이 생성되었습니다"); }, []); @@ -228,35 +229,29 @@ export const NumberingRuleDesigner: React.FC = ({ ) : (

{leftTitle}

)} -
{loading ? (
-

로딩 중...

+

로딩 중...

) : savedRules.length === 0 ? ( -
-

저장된 규칙이 없습니다

+
+

저장된 규칙이 없습니다

) : ( savedRules.map((rule) => ( handleSelectRule(rule)} @@ -265,9 +260,7 @@ export const NumberingRuleDesigner: React.FC = ({
{rule.ruleName} -

- 규칙 {rule.parts.length}개 -

+

규칙 {rule.parts.length}개

@@ -292,19 +285,15 @@ export const NumberingRuleDesigner: React.FC = ({
{/* 구분선 */} -
+
{/* 우측: 편집 영역 */}
{!currentRule ? (
-

- 규칙을 선택해주세요 -

-

- 좌측에서 규칙을 선택하거나 새로 생성하세요 -

+

규칙을 선택해주세요

+

좌측에서 규칙을 선택하거나 새로 생성하세요

) : ( @@ -322,12 +311,7 @@ export const NumberingRuleDesigner: React.FC = ({ ) : (

{rightTitle}

)} -
@@ -336,9 +320,7 @@ export const NumberingRuleDesigner: React.FC = ({ - setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value })) - } + onChange={(e) => setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))} className="h-9" placeholder="예: 프로젝트 코드" /> @@ -348,9 +330,7 @@ export const NumberingRuleDesigner: React.FC = ({ -

- {currentRule.scopeType === "menu" - ? "이 규칙이 설정된 상위 메뉴의 모든 하위 메뉴에서 사용 가능합니다" +

+ {currentRule.scopeType === "menu" + ? "⚠️ 현재 화면이 속한 2레벨 메뉴와 그 하위 메뉴(3레벨 이상)에서만 사용됩니다. 형제 메뉴와 구분하여 채번 규칙을 관리할 때 유용합니다." : "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}

@@ -380,16 +360,14 @@ export const NumberingRuleDesigner: React.FC = ({

코드 구성

- + {currentRule.parts.length}/{maxRules}
{currentRule.parts.length === 0 ? ( -
-

- 규칙을 추가하여 코드를 구성하세요 -

+
+

규칙을 추가하여 코드를 구성하세요

) : (
@@ -416,11 +394,7 @@ export const NumberingRuleDesigner: React.FC = ({ 규칙 추가 - diff --git a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx index 1d43b550..f1acae0b 100644 --- a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx +++ b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx @@ -84,7 +84,7 @@ export const EnhancedInteractiveScreenViewer: React.FC { + async (autoValueType: string, ruleId?: string): Promise => { const now = new Date(); switch (autoValueType) { case "current_datetime": @@ -99,6 +99,20 @@ export const EnhancedInteractiveScreenViewer: React.FC { const widgetComponents = allComponents.filter((c) => c.type === "widget") as WidgetComponent[]; - const autoValueUpdates: Record = {}; + + const loadAutoValues = async () => { + const autoValueUpdates: Record = {}; - for (const widget of widgetComponents) { - const fieldName = widget.columnName || widget.id; - const currentValue = finalFormData[fieldName]; + for (const widget of widgetComponents) { + const fieldName = widget.columnName || widget.id; + const currentValue = finalFormData[fieldName]; - // 자동값이 설정되어 있고 현재 값이 없는 경우 - if (widget.inputType === "auto" && widget.autoValueType && !currentValue) { - const autoValue = generateAutoValue(widget.autoValueType); - if (autoValue) { - autoValueUpdates[fieldName] = autoValue; + // 자동값이 설정되어 있고 현재 값이 없는 경우 + if (widget.inputType === "auto" && widget.autoValueType && !currentValue) { + const autoValue = await generateAutoValue( + widget.autoValueType, + (widget as any).numberingRuleId // 채번 규칙 ID + ); + if (autoValue) { + autoValueUpdates[fieldName] = autoValue; + } } } - } - if (Object.keys(autoValueUpdates).length > 0) { - setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates })); - } + if (Object.keys(autoValueUpdates).length > 0) { + setLocalFormData((prev) => ({ ...prev, ...autoValueUpdates })); + } + }; + + loadAutoValues(); }, [allComponents, finalFormData, generateAutoValue]); // 향상된 저장 핸들러 diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 4b292541..a9cc663d 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -136,7 +136,7 @@ export const InteractiveScreenViewer: React.FC = ( : null; // 자동값 생성 함수 - const generateAutoValue = useCallback((autoValueType: string): string => { + const generateAutoValue = useCallback(async (autoValueType: string, ruleId?: string): Promise => { const now = new Date(); switch (autoValueType) { case "current_datetime": @@ -152,6 +152,20 @@ export const InteractiveScreenViewer: React.FC = ( return crypto.randomUUID(); case "sequence": 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: return ""; } diff --git a/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx index a8eee28d..cb46ec50 100644 --- a/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx @@ -7,6 +7,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; import { TextTypeConfig } from "@/types/screen"; +import { getAvailableNumberingRules } from "@/lib/api/numberingRule"; +import { NumberingRuleConfig } from "@/types/numbering-rule"; interface TextTypeConfigPanelProps { config: TextTypeConfig; @@ -26,9 +28,14 @@ export const TextTypeConfigPanel: React.FC = ({ config autoInput: false, autoValueType: "current_datetime" as const, customValue: "", + numberingRuleId: "", ...config, }; + // 채번 규칙 목록 상태 + const [numberingRules, setNumberingRules] = useState([]); + const [loadingRules, setLoadingRules] = useState(false); + // 로컬 상태로 실시간 입력 관리 const [localValues, setLocalValues] = useState({ minLength: safeConfig.minLength?.toString() || "", @@ -41,8 +48,33 @@ export const TextTypeConfigPanel: React.FC = ({ config autoInput: safeConfig.autoInput, autoValueType: safeConfig.autoValueType, 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가 변경될 때 로컬 상태 동기화 useEffect(() => { setLocalValues({ @@ -56,6 +88,7 @@ export const TextTypeConfigPanel: React.FC = ({ config autoInput: safeConfig.autoInput, autoValueType: safeConfig.autoValueType, customValue: safeConfig.customValue, + numberingRuleId: safeConfig.numberingRuleId, }); }, [ safeConfig.minLength, @@ -68,6 +101,7 @@ export const TextTypeConfigPanel: React.FC = ({ config safeConfig.autoInput, safeConfig.autoValueType, safeConfig.customValue, + safeConfig.numberingRuleId, ]); const updateConfig = (key: keyof TextTypeConfig, value: any) => { @@ -90,16 +124,10 @@ export const TextTypeConfigPanel: React.FC = ({ config autoInput: key === "autoInput" ? value : localValues.autoInput, autoValueType: key === "autoValueType" ? value : localValues.autoValueType, customValue: key === "customValue" ? value : localValues.customValue, + numberingRuleId: key === "numberingRuleId" ? value : localValues.numberingRuleId, }; const newConfig = JSON.parse(JSON.stringify(currentValues)); - // console.log("📝 TextTypeConfig 업데이트:", { - // key, - // value, - // oldConfig: safeConfig, - // newConfig, - // localValues, - // }); setTimeout(() => { onConfigChange(newConfig); @@ -236,11 +264,45 @@ export const TextTypeConfigPanel: React.FC = ({ config 현재 사용자 고유 ID (UUID) 순번 + 채번 규칙 사용자 정의
+ {localValues.autoValueType === "numbering_rule" && ( +
+ + +

+ 현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다 +

+
+ )} + {localValues.autoValueType === "custom" && (
)} + + {/* 채번 규칙 선택 */} + {config.autoGeneration?.type === "numbering_rule" && ( +
+ + +

+ 현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다 +

+
+ )}
)} diff --git a/frontend/lib/utils/autoGeneration.ts b/frontend/lib/utils/autoGeneration.ts index d5f01db8..0d225cdd 100644 --- a/frontend/lib/utils/autoGeneration.ts +++ b/frontend/lib/utils/autoGeneration.ts @@ -207,11 +207,12 @@ export class AutoGenerationUtils { * 자동생성 타입별 설명 가져오기 */ static getTypeDescription(type: AutoGenerationType): string { - const descriptions: Record = { + const descriptions: Record = { uuid: "고유 식별자 (UUID) 생성", current_user: "현재 로그인한 사용자 ID", current_time: "현재 날짜/시간", sequence: "순차적 번호 생성", + numbering_rule: "채번 규칙 기반 코드 생성", random_string: "랜덤 문자열 생성", random_number: "랜덤 숫자 생성", company_code: "현재 회사 코드", @@ -246,6 +247,9 @@ export class AutoGenerationUtils { case "sequence": return `${options.prefix || ""}1${options.suffix || ""}`; + case "numbering_rule": + return "CODE-20251104-001"; // 채번 규칙 미리보기 + case "random_string": return `${options.prefix || ""}ABC123${options.suffix || ""}`; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 753deea5..771b10b9 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -246,6 +246,50 @@ export class ButtonActionExecutor { companyCodeValue, // ✅ 최종 회사 코드 값 }); + // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가) + console.log("🔍 채번 규칙 할당 체크 시작"); + console.log("📦 현재 formData:", JSON.stringify(formData, null, 2)); + + const fieldsWithNumbering: Record = {}; + + // 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 = { ...formData, writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId @@ -254,6 +298,13 @@ export class ButtonActionExecutor { 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({ screenId, tableName, diff --git a/frontend/types/screen-legacy-backup.ts b/frontend/types/screen-legacy-backup.ts index 762d4a8a..9c33b4d7 100644 --- a/frontend/types/screen-legacy-backup.ts +++ b/frontend/types/screen-legacy-backup.ts @@ -162,6 +162,7 @@ export type AutoGenerationType = | "current_user" // 현재 사용자 ID | "current_time" // 현재 시간 | "sequence" // 시퀀스 번호 + | "numbering_rule" // 채번 규칙 | "random_string" // 랜덤 문자열 | "random_number" // 랜덤 숫자 | "company_code" // 회사 코드 @@ -178,6 +179,7 @@ export interface AutoGenerationConfig { suffix?: string; // 접미사 format?: string; // 시간 형식 (current_time용) startValue?: number; // 시퀀스 시작값 + numberingRuleId?: string; // 채번 규칙 ID (numbering_rule 타입용) }; } diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 56a9ba2d..d83a6354 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -251,8 +251,22 @@ export interface TextTypeConfig { pattern?: string; format?: "none" | "email" | "phone" | "url" | "korean" | "english"; placeholder?: string; + defaultValue?: string; multiline?: boolean; rows?: number; + // 자동입력 관련 설정 + autoInput?: boolean; + autoValueType?: + | "current_datetime" + | "current_date" + | "current_time" + | "current_user" + | "uuid" + | "sequence" + | "numbering_rule" + | "custom"; + customValue?: string; + numberingRuleId?: string; // 채번 규칙 ID } /**