diff --git a/backend-node/src/controllers/moldController.ts b/backend-node/src/controllers/moldController.ts index 4683dd75..ee500f5e 100644 --- a/backend-node/src/controllers/moldController.ts +++ b/backend-node/src/controllers/moldController.ts @@ -233,8 +233,35 @@ export async function createMoldSerial(req: AuthenticatedRequest, res: Response) const { moldCode } = req.params; const { serial_number, status, progress, work_description, manager, completion_date, remarks } = req.body; - if (!serial_number) { - res.status(400).json({ success: false, message: "일련번호는 필수입니다." }); + let finalSerialNumber = serial_number; + + // 일련번호가 비어있으면 채번 규칙으로 자동 생성 + if (!finalSerialNumber) { + try { + const { numberingRuleService } = await import("../services/numberingRuleService"); + const rule = await numberingRuleService.getNumberingRuleByColumn( + companyCode, + "mold_serial", + "serial_number" + ); + + if (rule) { + // formData에 mold_code를 포함 (reference 파트에서 참조) + const formData = { mold_code: moldCode, ...req.body }; + finalSerialNumber = await numberingRuleService.allocateCode( + rule.ruleId, + companyCode, + formData + ); + logger.info("일련번호 자동 채번 완료", { serialNumber: finalSerialNumber, ruleId: rule.ruleId }); + } + } catch (numError: any) { + logger.error("일련번호 자동 채번 실패", { error: numError.message }); + } + } + + if (!finalSerialNumber) { + res.status(400).json({ success: false, message: "일련번호를 생성할 수 없습니다. 채번 규칙을 확인해주세요." }); return; } @@ -244,7 +271,7 @@ export async function createMoldSerial(req: AuthenticatedRequest, res: Response) RETURNING * `; const params = [ - companyCode, moldCode, serial_number, status || "STORED", + companyCode, moldCode, finalSerialNumber, status || "STORED", progress || 0, work_description || null, manager || null, completion_date || null, remarks || null, userId, ]; diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 34acc44f..91ae4cb5 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -172,6 +172,16 @@ class NumberingRuleService { break; } + case "reference": { + const refColumn = autoConfig.referenceColumnName; + if (refColumn && formData && formData[refColumn]) { + prefixParts.push(String(formData[refColumn])); + } else { + prefixParts.push(""); + } + break; + } + default: break; } @@ -1245,6 +1255,14 @@ class NumberingRuleService { return ""; } + case "reference": { + const refColumn = autoConfig.referenceColumnName; + if (refColumn && formData && formData[refColumn]) { + return String(formData[refColumn]); + } + return "REF"; + } + default: logger.warn("알 수 없는 파트 타입", { partType: part.partType }); return ""; @@ -1375,6 +1393,13 @@ class NumberingRuleService { return catMapping2?.format || "CATEGORY"; } + case "reference": { + const refCol2 = autoConfig.referenceColumnName; + if (refCol2 && formData && formData[refCol2]) { + return String(formData[refCol2]); + } + return "REF"; + } default: return ""; } @@ -1524,6 +1549,15 @@ class NumberingRuleService { return ""; } + case "reference": { + const refColumn = autoConfig.referenceColumnName; + if (refColumn && formData && formData[refColumn]) { + return String(formData[refColumn]); + } + logger.warn("reference 파트: 참조 컬럼 값 없음", { refColumn, formDataKeys: formData ? Object.keys(formData) : [] }); + return ""; + } + default: return ""; } diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 0cff4f6b..d07c02d2 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2691,6 +2691,32 @@ export class TableManagementService { logger.info(`created_date 자동 추가: ${data.created_date}`); } + // 채번 자동 적용: input_type = 'numbering'인 컬럼에 값이 비어있으면 자동 채번 + try { + const companyCode = data.company_code || "*"; + const numberingColsResult = await query( + `SELECT DISTINCT column_name FROM table_type_columns + WHERE table_name = $1 AND input_type = 'numbering' + AND company_code IN ($2, '*')`, + [tableName, companyCode] + ); + + for (const row of numberingColsResult) { + const col = row.column_name; + if (!data[col] || data[col] === "" || data[col] === "자동 생성됩니다") { + const { numberingRuleService } = await import("./numberingRuleService"); + const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, tableName, col); + if (rule) { + const generatedCode = await numberingRuleService.allocateCode(rule.ruleId, companyCode, data); + data[col] = generatedCode; + logger.info(`채번 자동 적용: ${tableName}.${col} = ${generatedCode}`); + } + } + } + } catch (numErr: any) { + logger.warn(`채번 자동 적용 중 오류 (무시됨): ${numErr.message}`); + } + // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼은 무시) const skippedColumns: string[] = []; const existingColumns = Object.keys(data).filter((col) => { diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx index b51ea500..544eae9d 100644 --- a/frontend/components/numbering-rule/AutoConfigPanel.tsx +++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx @@ -18,6 +18,7 @@ interface AutoConfigPanelProps { config?: any; onChange: (config: any) => void; isPreview?: boolean; + tableName?: string; } interface TableInfo { @@ -37,6 +38,7 @@ export const AutoConfigPanel: React.FC = ({ config = {}, onChange, isPreview = false, + tableName, }) => { // 1. 순번 (자동 증가) if (partType === "sequence") { @@ -161,6 +163,18 @@ export const AutoConfigPanel: React.FC = ({ ); } + // 6. 참조 (마스터-디테일 분번) + if (partType === "reference") { + return ( + + ); + } + return null; }; @@ -1088,3 +1102,94 @@ const CategoryConfigPanel: React.FC = ({ ); }; + +function ReferenceConfigSection({ + config, + onChange, + isPreview, + tableName, +}: { + config: any; + onChange: (c: any) => void; + isPreview: boolean; + tableName?: string; +}) { + const [columns, setColumns] = useState([]); + const [loadingCols, setLoadingCols] = useState(false); + + useEffect(() => { + if (!tableName) return; + setLoadingCols(true); + + const loadEntityColumns = async () => { + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get( + `/screen-management/tables/${tableName}/columns` + ); + const allCols = response.data?.data || response.data || []; + const entityCols = allCols.filter( + (c: any) => + (c.inputType || c.input_type) === "entity" || + (c.inputType || c.input_type) === "numbering" + ); + setColumns( + entityCols.map((c: any) => ({ + columnName: c.columnName || c.column_name, + displayName: + c.columnLabel || c.column_label || c.columnName || c.column_name, + dataType: c.dataType || c.data_type || "", + inputType: c.inputType || c.input_type || "", + })) + ); + } catch { + setColumns([]); + } finally { + setLoadingCols(false); + } + }; + + loadEntityColumns(); + }, [tableName]); + + return ( +
+
+ + +

+ 마스터 테이블과 연결된 엔티티/채번 컬럼의 값을 코드에 포함합니다 +

+
+
+ ); +} diff --git a/frontend/components/numbering-rule/NumberingRuleCard.tsx b/frontend/components/numbering-rule/NumberingRuleCard.tsx index e9731017..e3dbc3ab 100644 --- a/frontend/components/numbering-rule/NumberingRuleCard.tsx +++ b/frontend/components/numbering-rule/NumberingRuleCard.tsx @@ -16,6 +16,7 @@ interface NumberingRuleCardProps { onUpdate: (updates: Partial) => void; onDelete: () => void; isPreview?: boolean; + tableName?: string; } export const NumberingRuleCard: React.FC = ({ @@ -23,6 +24,7 @@ export const NumberingRuleCard: React.FC = ({ onUpdate, onDelete, isPreview = false, + tableName, }) => { return ( @@ -57,6 +59,7 @@ export const NumberingRuleCard: React.FC = ({ date: { dateFormat: "YYYYMMDD" }, text: { textValue: "CODE" }, category: { categoryKey: "", categoryMappings: [] }, + reference: { referenceColumnName: "" }, }; onUpdate({ partType: newPartType, @@ -105,6 +108,7 @@ export const NumberingRuleCard: React.FC = ({ config={part.autoConfig} onChange={(autoConfig) => onUpdate({ autoConfig })} isPreview={isPreview} + tableName={tableName} /> ) : ( = ({ onUpdate={(updates) => handleUpdatePart(part.order, updates)} onDelete={() => handleDeletePart(part.order)} isPreview={isPreview} + tableName={selectedColumn?.tableName} /> {/* 카드 하단에 구분자 설정 (마지막 파트 제외) */} {index < currentRule.parts.length - 1 && ( diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index 219fa275..6bd19c5d 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -619,45 +619,40 @@ export const V2Input = forwardRef((props, ref) => try { // 채번 규칙 ID 캐싱 (한 번만 조회) if (!numberingRuleIdRef.current) { - const { getTableColumns } = await import("@/lib/api/tableManagement"); - const columnsResponse = await getTableColumns(tableName); + // table_name + column_name 기반으로 채번 규칙 조회 + try { + const { apiClient } = await import("@/lib/api/client"); + const ruleResponse = await apiClient.get(`/numbering-rules/by-column/${tableName}/${columnName}`); + if (ruleResponse.data?.success && ruleResponse.data?.data?.ruleId) { + numberingRuleIdRef.current = ruleResponse.data.data.ruleId; - if (!columnsResponse.success || !columnsResponse.data) { - console.warn("테이블 컬럼 정보 조회 실패:", columnsResponse); - return; - } - - const columns = columnsResponse.data.columns || columnsResponse.data; - const targetColumn = columns.find((col: { columnName: string }) => col.columnName === columnName); - - if (!targetColumn) { - console.warn("컬럼 정보를 찾을 수 없습니다:", columnName); - return; - } - - // detailSettings에서 numberingRuleId 추출 - if (targetColumn.detailSettings) { - try { - // 문자열이면 파싱, 객체면 그대로 사용 - const parsed = typeof targetColumn.detailSettings === "string" - ? JSON.parse(targetColumn.detailSettings) - : targetColumn.detailSettings; - numberingRuleIdRef.current = parsed.numberingRuleId || null; - - // 🆕 채번 규칙 ID를 formData에 저장 (저장 시 allocateCode 호출을 위해) - if (parsed.numberingRuleId && onFormDataChange && columnName) { - onFormDataChange(`${columnName}_numberingRuleId`, parsed.numberingRuleId); + if (onFormDataChange && columnName) { + onFormDataChange(`${columnName}_numberingRuleId`, ruleResponse.data.data.ruleId); } - } catch { - // JSON 파싱 실패 } + } catch { + // by-column 조회 실패 시 detailSettings fallback + try { + const { getTableColumns } = await import("@/lib/api/tableManagement"); + const columnsResponse = await getTableColumns(tableName); + if (columnsResponse.success && columnsResponse.data) { + const columns = columnsResponse.data.columns || columnsResponse.data; + const targetColumn = columns.find((col: { columnName: string }) => col.columnName === columnName); + if (targetColumn?.detailSettings) { + const parsed = typeof targetColumn.detailSettings === "string" + ? JSON.parse(targetColumn.detailSettings) + : targetColumn.detailSettings; + numberingRuleIdRef.current = parsed.numberingRuleId || null; + } + } + } catch { /* ignore */ } } } const numberingRuleId = numberingRuleIdRef.current; if (!numberingRuleId) { - console.warn("채번 규칙 ID가 설정되지 않았습니다. 테이블 관리에서 설정하세요.", { tableName, columnName }); + console.warn("채번 규칙을 찾을 수 없습니다. 옵션설정 > 채번설정에서 규칙을 생성하세요.", { tableName, columnName }); return; } diff --git a/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx b/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx index fe25bd68..d0c388e3 100644 --- a/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx +++ b/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx @@ -35,14 +35,16 @@ export const StatusCountComponent: React.FC = ({ setLoading(true); try { - const res = await apiClient.get(`/table-management/data/${tableName}`, { - params: { - autoFilter: "true", - [relationColumn]: parentValue, - }, + const res = await apiClient.post(`/table-management/tables/${tableName}/data`, { + page: 1, + size: 9999, + search: relationColumn ? { [relationColumn]: parentValue } : {}, }); - const rows: any[] = res.data?.data || res.data?.rows || res.data || []; + const responseData = res.data?.data; + const rows: any[] = Array.isArray(responseData) + ? responseData + : (responseData?.data || responseData?.rows || []); const grouped: Record = {}; for (const row of rows) { @@ -69,7 +71,7 @@ export const StatusCountComponent: React.FC = ({ }; const getCount = (item: StatusCountItem) => { - if (item.value === "__TOTAL__") { + if (item.value === "__TOTAL__" || item.value === "__ALL__") { return Object.values(counts).reduce((sum, c) => sum + c, 0); } const values = item.value.split(",").map((v) => v.trim()); diff --git a/frontend/lib/registry/components/v2-status-count/StatusCountConfigPanel.tsx b/frontend/lib/registry/components/v2-status-count/StatusCountConfigPanel.tsx index bd029ab3..cee8a432 100644 --- a/frontend/lib/registry/components/v2-status-count/StatusCountConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-status-count/StatusCountConfigPanel.tsx @@ -233,6 +233,47 @@ export const StatusCountConfigPanel: React.FC = ({ ); }; + // 상태 컬럼의 카테고리 값 로드 + const [statusCategoryValues, setStatusCategoryValues] = useState>([]); + const [loadingCategoryValues, setLoadingCategoryValues] = useState(false); + + useEffect(() => { + if (!config.tableName || !config.statusColumn) { + setStatusCategoryValues([]); + return; + } + + const loadCategoryValues = async () => { + setLoadingCategoryValues(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get( + `/table-categories/${config.tableName}/${config.statusColumn}/values` + ); + if (response.data?.success && response.data?.data) { + const flatValues: Array<{ value: string; label: string }> = []; + const flatten = (items: any[]) => { + for (const item of items) { + flatValues.push({ + value: item.valueCode || item.value_code, + label: item.valueLabel || item.value_label, + }); + if (item.children?.length > 0) flatten(item.children); + } + }; + flatten(response.data.data); + setStatusCategoryValues(flatValues); + } + } catch { + setStatusCategoryValues([]); + } finally { + setLoadingCategoryValues(false); + } + }; + + loadCategoryValues(); + }, [config.tableName, config.statusColumn]); + const tableComboItems = tables.map((t) => ({ value: t.tableName, label: t.displayName, @@ -370,15 +411,52 @@ export const StatusCountConfigPanel: React.FC = ({ + {loadingCategoryValues && ( +
+ 카테고리 값 로딩... +
+ )} + {items.map((item: StatusCountItem, i: number) => (
- handleItemChange(i, "value", e.target.value)} - placeholder="상태값 (예: IN_USE)" - className="h-7 text-xs" - /> + {statusCategoryValues.length > 0 ? ( + + ) : ( + handleItemChange(i, "value", e.target.value)} + placeholder="상태값 (예: IN_USE)" + className="h-7 text-xs" + /> + )}
))} + + {!loadingCategoryValues && statusCategoryValues.length === 0 && config.tableName && config.statusColumn && ( +

+ 카테고리 값이 없습니다. 옵션설정 > 카테고리설정에서 값을 추가하거나 직접 입력하세요. +

+ )} ); diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts index 3b14a6bc..18e1e747 100644 --- a/frontend/types/numbering-rule.ts +++ b/frontend/types/numbering-rule.ts @@ -7,11 +7,12 @@ * 코드 파트 유형 (5가지) */ export type CodePartType = - | "sequence" // 순번 (자동 증가 숫자) - | "number" // 숫자 (고정 자릿수) - | "date" // 날짜 (다양한 날짜 형식) - | "text" // 문자 (텍스트) - | "category"; // 카테고리 (카테고리 값에 따른 형식) + | "sequence" // 순번 (자동 증가 숫자) + | "number" // 숫자 (고정 자릿수) + | "date" // 날짜 (다양한 날짜 형식) + | "text" // 문자 (텍스트) + | "category" // 카테고리 (카테고리 값에 따른 형식) + | "reference"; // 참조 (다른 컬럼의 값을 가져옴, 마스터-디테일 분번용) /** * 생성 방식 @@ -77,6 +78,9 @@ export interface NumberingRulePart { // 카테고리용 categoryKey?: string; // 카테고리 키 (테이블.컬럼 형식, 예: "item_info.type") categoryMappings?: CategoryFormatMapping[]; // 카테고리 값별 형식 매핑 + + // 참조용 (마스터-디테일 분번) + referenceColumnName?: string; // 참조할 컬럼명 (FK 컬럼 등, 해당 컬럼의 값을 코드에 포함) }; // 직접 입력 설정 @@ -132,6 +136,7 @@ export const CODE_PART_TYPE_OPTIONS: Array<{ value: CodePartType; label: string; { value: "date", label: "날짜", description: "날짜 형식 (2025-11-04)" }, { value: "text", label: "문자", description: "텍스트 또는 코드" }, { value: "category", label: "카테고리", description: "카테고리 값에 따른 형식" }, + { value: "reference", label: "참조", description: "다른 컬럼 값 참조 (마스터 키 분번)" }, ]; export const DATE_FORMAT_OPTIONS: Array<{ value: DateFormat; label: string; example: string }> = [