feat: Implement automatic serial number generation and reference handling in mold management
- Enhanced the `createMoldSerial` function to automatically generate serial numbers based on defined numbering rules when the serial number is not provided. - Integrated error handling for the automatic numbering process, ensuring robust logging for success and failure cases. - Updated the `NumberingRuleService` to support reference column handling, allowing for dynamic prefix generation based on related data. - Modified the frontend components to accommodate new reference configurations, improving user experience in managing numbering rules. These changes significantly enhance the mold management functionality by automating serial number generation and improving the flexibility of numbering rules.
This commit is contained in:
parent
2b4b7819c5
commit
4d6783e508
|
|
@ -233,8 +233,35 @@ export async function createMoldSerial(req: AuthenticatedRequest, res: Response)
|
||||||
const { moldCode } = req.params;
|
const { moldCode } = req.params;
|
||||||
const { serial_number, status, progress, work_description, manager, completion_date, remarks } = req.body;
|
const { serial_number, status, progress, work_description, manager, completion_date, remarks } = req.body;
|
||||||
|
|
||||||
if (!serial_number) {
|
let finalSerialNumber = serial_number;
|
||||||
res.status(400).json({ success: false, message: "일련번호는 필수입니다." });
|
|
||||||
|
// 일련번호가 비어있으면 채번 규칙으로 자동 생성
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +271,7 @@ export async function createMoldSerial(req: AuthenticatedRequest, res: Response)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
const params = [
|
const params = [
|
||||||
companyCode, moldCode, serial_number, status || "STORED",
|
companyCode, moldCode, finalSerialNumber, status || "STORED",
|
||||||
progress || 0, work_description || null, manager || null,
|
progress || 0, work_description || null, manager || null,
|
||||||
completion_date || null, remarks || null, userId,
|
completion_date || null, remarks || null, userId,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,16 @@ class NumberingRuleService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "reference": {
|
||||||
|
const refColumn = autoConfig.referenceColumnName;
|
||||||
|
if (refColumn && formData && formData[refColumn]) {
|
||||||
|
prefixParts.push(String(formData[refColumn]));
|
||||||
|
} else {
|
||||||
|
prefixParts.push("");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1245,6 +1255,14 @@ class NumberingRuleService {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "reference": {
|
||||||
|
const refColumn = autoConfig.referenceColumnName;
|
||||||
|
if (refColumn && formData && formData[refColumn]) {
|
||||||
|
return String(formData[refColumn]);
|
||||||
|
}
|
||||||
|
return "REF";
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.warn("알 수 없는 파트 타입", { partType: part.partType });
|
logger.warn("알 수 없는 파트 타입", { partType: part.partType });
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -1375,6 +1393,13 @@ class NumberingRuleService {
|
||||||
|
|
||||||
return catMapping2?.format || "CATEGORY";
|
return catMapping2?.format || "CATEGORY";
|
||||||
}
|
}
|
||||||
|
case "reference": {
|
||||||
|
const refCol2 = autoConfig.referenceColumnName;
|
||||||
|
if (refCol2 && formData && formData[refCol2]) {
|
||||||
|
return String(formData[refCol2]);
|
||||||
|
}
|
||||||
|
return "REF";
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -1524,6 +1549,15 @@ class NumberingRuleService {
|
||||||
return "";
|
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:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2691,6 +2691,32 @@ export class TableManagementService {
|
||||||
logger.info(`created_date 자동 추가: ${data.created_date}`);
|
logger.info(`created_date 자동 추가: ${data.created_date}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 채번 자동 적용: input_type = 'numbering'인 컬럼에 값이 비어있으면 자동 채번
|
||||||
|
try {
|
||||||
|
const companyCode = data.company_code || "*";
|
||||||
|
const numberingColsResult = await query<any>(
|
||||||
|
`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 skippedColumns: string[] = [];
|
||||||
const existingColumns = Object.keys(data).filter((col) => {
|
const existingColumns = Object.keys(data).filter((col) => {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ interface AutoConfigPanelProps {
|
||||||
config?: any;
|
config?: any;
|
||||||
onChange: (config: any) => void;
|
onChange: (config: any) => void;
|
||||||
isPreview?: boolean;
|
isPreview?: boolean;
|
||||||
|
tableName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableInfo {
|
interface TableInfo {
|
||||||
|
|
@ -37,6 +38,7 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
|
||||||
config = {},
|
config = {},
|
||||||
onChange,
|
onChange,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
|
tableName,
|
||||||
}) => {
|
}) => {
|
||||||
// 1. 순번 (자동 증가)
|
// 1. 순번 (자동 증가)
|
||||||
if (partType === "sequence") {
|
if (partType === "sequence") {
|
||||||
|
|
@ -161,6 +163,18 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. 참조 (마스터-디테일 분번)
|
||||||
|
if (partType === "reference") {
|
||||||
|
return (
|
||||||
|
<ReferenceConfigSection
|
||||||
|
config={config}
|
||||||
|
onChange={onChange}
|
||||||
|
isPreview={isPreview}
|
||||||
|
tableName={tableName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1088,3 +1102,94 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function ReferenceConfigSection({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
isPreview,
|
||||||
|
tableName,
|
||||||
|
}: {
|
||||||
|
config: any;
|
||||||
|
onChange: (c: any) => void;
|
||||||
|
isPreview: boolean;
|
||||||
|
tableName?: string;
|
||||||
|
}) {
|
||||||
|
const [columns, setColumns] = useState<ColumnInfo[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium sm:text-sm">참조 컬럼</Label>
|
||||||
|
<Select
|
||||||
|
value={config.referenceColumnName || ""}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange({ ...config, referenceColumnName: value })
|
||||||
|
}
|
||||||
|
disabled={isPreview || loadingCols}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
loadingCols
|
||||||
|
? "로딩 중..."
|
||||||
|
: columns.length === 0
|
||||||
|
? "엔티티 컬럼 없음"
|
||||||
|
: "컬럼 선택"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<SelectItem
|
||||||
|
key={col.columnName}
|
||||||
|
value={col.columnName}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{col.displayName} ({col.columnName})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
|
||||||
|
마스터 테이블과 연결된 엔티티/채번 컬럼의 값을 코드에 포함합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface NumberingRuleCardProps {
|
||||||
onUpdate: (updates: Partial<NumberingRulePart>) => void;
|
onUpdate: (updates: Partial<NumberingRulePart>) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
isPreview?: boolean;
|
isPreview?: boolean;
|
||||||
|
tableName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
|
export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
|
||||||
|
|
@ -23,6 +24,7 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
|
tableName,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card className="border-border bg-card flex-1">
|
<Card className="border-border bg-card flex-1">
|
||||||
|
|
@ -57,6 +59,7 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
|
||||||
date: { dateFormat: "YYYYMMDD" },
|
date: { dateFormat: "YYYYMMDD" },
|
||||||
text: { textValue: "CODE" },
|
text: { textValue: "CODE" },
|
||||||
category: { categoryKey: "", categoryMappings: [] },
|
category: { categoryKey: "", categoryMappings: [] },
|
||||||
|
reference: { referenceColumnName: "" },
|
||||||
};
|
};
|
||||||
onUpdate({
|
onUpdate({
|
||||||
partType: newPartType,
|
partType: newPartType,
|
||||||
|
|
@ -105,6 +108,7 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
|
||||||
config={part.autoConfig}
|
config={part.autoConfig}
|
||||||
onChange={(autoConfig) => onUpdate({ autoConfig })}
|
onChange={(autoConfig) => onUpdate({ autoConfig })}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
|
tableName={tableName}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ManualConfigPanel
|
<ManualConfigPanel
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
onUpdate={(updates) => handleUpdatePart(part.order, updates)}
|
onUpdate={(updates) => handleUpdatePart(part.order, updates)}
|
||||||
onDelete={() => handleDeletePart(part.order)}
|
onDelete={() => handleDeletePart(part.order)}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
|
tableName={selectedColumn?.tableName}
|
||||||
/>
|
/>
|
||||||
{/* 카드 하단에 구분자 설정 (마지막 파트 제외) */}
|
{/* 카드 하단에 구분자 설정 (마지막 파트 제외) */}
|
||||||
{index < currentRule.parts.length - 1 && (
|
{index < currentRule.parts.length - 1 && (
|
||||||
|
|
|
||||||
|
|
@ -619,45 +619,40 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
try {
|
try {
|
||||||
// 채번 규칙 ID 캐싱 (한 번만 조회)
|
// 채번 규칙 ID 캐싱 (한 번만 조회)
|
||||||
if (!numberingRuleIdRef.current) {
|
if (!numberingRuleIdRef.current) {
|
||||||
const { getTableColumns } = await import("@/lib/api/tableManagement");
|
// table_name + column_name 기반으로 채번 규칙 조회
|
||||||
const columnsResponse = await getTableColumns(tableName);
|
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) {
|
if (onFormDataChange && columnName) {
|
||||||
console.warn("테이블 컬럼 정보 조회 실패:", columnsResponse);
|
onFormDataChange(`${columnName}_numberingRuleId`, ruleResponse.data.data.ruleId);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} 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;
|
const numberingRuleId = numberingRuleIdRef.current;
|
||||||
|
|
||||||
if (!numberingRuleId) {
|
if (!numberingRuleId) {
|
||||||
console.warn("채번 규칙 ID가 설정되지 않았습니다. 테이블 관리에서 설정하세요.", { tableName, columnName });
|
console.warn("채번 규칙을 찾을 수 없습니다. 옵션설정 > 채번설정에서 규칙을 생성하세요.", { tableName, columnName });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,16 @@ export const StatusCountComponent: React.FC<StatusCountComponentProps> = ({
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-management/data/${tableName}`, {
|
const res = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
||||||
params: {
|
page: 1,
|
||||||
autoFilter: "true",
|
size: 9999,
|
||||||
[relationColumn]: parentValue,
|
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<string, number> = {};
|
const grouped: Record<string, number> = {};
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
|
@ -69,7 +71,7 @@ export const StatusCountComponent: React.FC<StatusCountComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCount = (item: StatusCountItem) => {
|
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);
|
return Object.values(counts).reduce((sum, c) => sum + c, 0);
|
||||||
}
|
}
|
||||||
const values = item.value.split(",").map((v) => v.trim());
|
const values = item.value.split(",").map((v) => v.trim());
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,47 @@ export const StatusCountConfigPanel: React.FC<StatusCountConfigPanelProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 상태 컬럼의 카테고리 값 로드
|
||||||
|
const [statusCategoryValues, setStatusCategoryValues] = useState<Array<{ value: string; label: string }>>([]);
|
||||||
|
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) => ({
|
const tableComboItems = tables.map((t) => ({
|
||||||
value: t.tableName,
|
value: t.tableName,
|
||||||
label: t.displayName,
|
label: t.displayName,
|
||||||
|
|
@ -370,15 +411,52 @@ export const StatusCountConfigPanel: React.FC<StatusCountConfigPanelProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{loadingCategoryValues && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" /> 카테고리 값 로딩...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{items.map((item: StatusCountItem, i: number) => (
|
{items.map((item: StatusCountItem, i: number) => (
|
||||||
<div key={i} className="space-y-1 rounded-md border p-2">
|
<div key={i} className="space-y-1 rounded-md border p-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Input
|
{statusCategoryValues.length > 0 ? (
|
||||||
value={item.value}
|
<Select
|
||||||
onChange={(e) => handleItemChange(i, "value", e.target.value)}
|
value={item.value || ""}
|
||||||
placeholder="상태값 (예: IN_USE)"
|
onValueChange={(v) => {
|
||||||
className="h-7 text-xs"
|
handleItemChange(i, "value", v);
|
||||||
/>
|
if (v === "__ALL__" && !item.label) {
|
||||||
|
handleItemChange(i, "label", "전체");
|
||||||
|
} else {
|
||||||
|
const catVal = statusCategoryValues.find((cv) => cv.value === v);
|
||||||
|
if (catVal && !item.label) {
|
||||||
|
handleItemChange(i, "label", catVal.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 flex-1 text-xs">
|
||||||
|
<SelectValue placeholder="카테고리 값 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__ALL__" className="text-xs font-medium">
|
||||||
|
전체
|
||||||
|
</SelectItem>
|
||||||
|
{statusCategoryValues.map((cv) => (
|
||||||
|
<SelectItem key={cv.value} value={cv.value} className="text-xs">
|
||||||
|
{cv.label} ({cv.value})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={item.value}
|
||||||
|
onChange={(e) => handleItemChange(i, "value", e.target.value)}
|
||||||
|
placeholder="상태값 (예: IN_USE)"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -418,6 +496,12 @@ export const StatusCountConfigPanel: React.FC<StatusCountConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{!loadingCategoryValues && statusCategoryValues.length === 0 && config.tableName && config.statusColumn && (
|
||||||
|
<p className="text-[10px] text-amber-600">
|
||||||
|
카테고리 값이 없습니다. 옵션설정 > 카테고리설정에서 값을 추가하거나 직접 입력하세요.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@
|
||||||
* 코드 파트 유형 (5가지)
|
* 코드 파트 유형 (5가지)
|
||||||
*/
|
*/
|
||||||
export type CodePartType =
|
export type CodePartType =
|
||||||
| "sequence" // 순번 (자동 증가 숫자)
|
| "sequence" // 순번 (자동 증가 숫자)
|
||||||
| "number" // 숫자 (고정 자릿수)
|
| "number" // 숫자 (고정 자릿수)
|
||||||
| "date" // 날짜 (다양한 날짜 형식)
|
| "date" // 날짜 (다양한 날짜 형식)
|
||||||
| "text" // 문자 (텍스트)
|
| "text" // 문자 (텍스트)
|
||||||
| "category"; // 카테고리 (카테고리 값에 따른 형식)
|
| "category" // 카테고리 (카테고리 값에 따른 형식)
|
||||||
|
| "reference"; // 참조 (다른 컬럼의 값을 가져옴, 마스터-디테일 분번용)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 생성 방식
|
* 생성 방식
|
||||||
|
|
@ -77,6 +78,9 @@ export interface NumberingRulePart {
|
||||||
// 카테고리용
|
// 카테고리용
|
||||||
categoryKey?: string; // 카테고리 키 (테이블.컬럼 형식, 예: "item_info.type")
|
categoryKey?: string; // 카테고리 키 (테이블.컬럼 형식, 예: "item_info.type")
|
||||||
categoryMappings?: CategoryFormatMapping[]; // 카테고리 값별 형식 매핑
|
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: "date", label: "날짜", description: "날짜 형식 (2025-11-04)" },
|
||||||
{ value: "text", label: "문자", description: "텍스트 또는 코드" },
|
{ value: "text", label: "문자", description: "텍스트 또는 코드" },
|
||||||
{ value: "category", label: "카테고리", description: "카테고리 값에 따른 형식" },
|
{ value: "category", label: "카테고리", description: "카테고리 값에 따른 형식" },
|
||||||
|
{ value: "reference", label: "참조", description: "다른 컬럼 값 참조 (마스터 키 분번)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DATE_FORMAT_OPTIONS: Array<{ value: DateFormat; label: string; example: string }> = [
|
export const DATE_FORMAT_OPTIONS: Array<{ value: DateFormat; label: string; example: string }> = [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue