jskim-node #394
|
|
@ -14,6 +14,35 @@ interface NumberingRulePart {
|
||||||
autoConfig?: any;
|
autoConfig?: any;
|
||||||
manualConfig?: any;
|
manualConfig?: any;
|
||||||
generatedValue?: string;
|
generatedValue?: string;
|
||||||
|
separatorAfter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파트 배열에서 autoConfig.separatorAfter를 파트 레벨로 추출
|
||||||
|
*/
|
||||||
|
function extractSeparatorAfterFromParts(parts: any[]): any[] {
|
||||||
|
return parts.map((part) => {
|
||||||
|
if (part.autoConfig?.separatorAfter !== undefined) {
|
||||||
|
part.separatorAfter = part.autoConfig.separatorAfter;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파트별 개별 구분자를 사용하여 코드 결합
|
||||||
|
* 마지막 파트의 separatorAfter는 무시됨
|
||||||
|
*/
|
||||||
|
function joinPartsWithSeparators(partValues: string[], sortedParts: any[], globalSeparator: string): string {
|
||||||
|
let result = "";
|
||||||
|
partValues.forEach((val, idx) => {
|
||||||
|
result += val;
|
||||||
|
if (idx < partValues.length - 1) {
|
||||||
|
const sep = sortedParts[idx].separatorAfter ?? sortedParts[idx].autoConfig?.separatorAfter ?? globalSeparator;
|
||||||
|
result += sep;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NumberingRuleConfig {
|
interface NumberingRuleConfig {
|
||||||
|
|
@ -141,7 +170,7 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const partsResult = await pool.query(partsQuery, partsParams);
|
const partsResult = await pool.query(partsQuery, partsParams);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, {
|
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, {
|
||||||
|
|
@ -274,7 +303,7 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const partsResult = await pool.query(partsQuery, partsParams);
|
const partsResult = await pool.query(partsQuery, partsParams);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.rows;
|
return result.rows;
|
||||||
|
|
@ -381,7 +410,7 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const partsResult = await pool.query(partsQuery, partsParams);
|
const partsResult = await pool.query(partsQuery, partsParams);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
|
|
||||||
logger.info("✅ 규칙 파트 조회 성공", {
|
logger.info("✅ 규칙 파트 조회 성공", {
|
||||||
ruleId: rule.ruleId,
|
ruleId: rule.ruleId,
|
||||||
|
|
@ -517,7 +546,7 @@ class NumberingRuleService {
|
||||||
companyCode === "*" ? rule.companyCode : companyCode,
|
companyCode === "*" ? rule.companyCode : companyCode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`화면용 채번 규칙 조회 완료: ${result.rows.length}개`, {
|
logger.info(`화면용 채번 규칙 조회 완료: ${result.rows.length}개`, {
|
||||||
|
|
@ -633,7 +662,7 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const partsResult = await pool.query(partsQuery, partsParams);
|
const partsResult = await pool.query(partsQuery, partsParams);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
|
|
||||||
return rule;
|
return rule;
|
||||||
}
|
}
|
||||||
|
|
@ -708,17 +737,25 @@ class NumberingRuleService {
|
||||||
manual_config AS "manualConfig"
|
manual_config AS "manualConfig"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// auto_config에 separatorAfter 포함
|
||||||
|
const autoConfigWithSep = { ...(part.autoConfig || {}), separatorAfter: part.separatorAfter ?? "-" };
|
||||||
|
|
||||||
const partResult = await client.query(insertPartQuery, [
|
const partResult = await client.query(insertPartQuery, [
|
||||||
config.ruleId,
|
config.ruleId,
|
||||||
part.order,
|
part.order,
|
||||||
part.partType,
|
part.partType,
|
||||||
part.generationMethod,
|
part.generationMethod,
|
||||||
JSON.stringify(part.autoConfig || {}),
|
JSON.stringify(autoConfigWithSep),
|
||||||
JSON.stringify(part.manualConfig || {}),
|
JSON.stringify(part.manualConfig || {}),
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
parts.push(partResult.rows[0]);
|
const savedPart = partResult.rows[0];
|
||||||
|
// autoConfig에서 separatorAfter를 추출하여 파트 레벨로 이동
|
||||||
|
if (savedPart.autoConfig?.separatorAfter !== undefined) {
|
||||||
|
savedPart.separatorAfter = savedPart.autoConfig.separatorAfter;
|
||||||
|
}
|
||||||
|
parts.push(savedPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
|
|
@ -820,17 +857,23 @@ class NumberingRuleService {
|
||||||
manual_config AS "manualConfig"
|
manual_config AS "manualConfig"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const autoConfigWithSep = { ...(part.autoConfig || {}), separatorAfter: part.separatorAfter ?? "-" };
|
||||||
|
|
||||||
const partResult = await client.query(insertPartQuery, [
|
const partResult = await client.query(insertPartQuery, [
|
||||||
ruleId,
|
ruleId,
|
||||||
part.order,
|
part.order,
|
||||||
part.partType,
|
part.partType,
|
||||||
part.generationMethod,
|
part.generationMethod,
|
||||||
JSON.stringify(part.autoConfig || {}),
|
JSON.stringify(autoConfigWithSep),
|
||||||
JSON.stringify(part.manualConfig || {}),
|
JSON.stringify(part.manualConfig || {}),
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
parts.push(partResult.rows[0]);
|
const savedPart = partResult.rows[0];
|
||||||
|
if (savedPart.autoConfig?.separatorAfter !== undefined) {
|
||||||
|
savedPart.separatorAfter = savedPart.autoConfig.separatorAfter;
|
||||||
|
}
|
||||||
|
parts.push(savedPart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1053,7 +1096,8 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const previewCode = parts.join(rule.separator || "");
|
const sortedRuleParts = rule.parts.sort((a: any, b: any) => a.order - b.order);
|
||||||
|
const previewCode = joinPartsWithSeparators(parts, sortedRuleParts, rule.separator || "");
|
||||||
logger.info("코드 미리보기 생성", {
|
logger.info("코드 미리보기 생성", {
|
||||||
ruleId,
|
ruleId,
|
||||||
previewCode,
|
previewCode,
|
||||||
|
|
@ -1164,8 +1208,8 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const separator = rule.separator || "";
|
const sortedPartsForTemplate = rule.parts.sort((a: any, b: any) => a.order - b.order);
|
||||||
const previewTemplate = previewParts.join(separator);
|
const previewTemplate = joinPartsWithSeparators(previewParts, sortedPartsForTemplate, rule.separator || "");
|
||||||
|
|
||||||
// 사용자 입력 코드에서 수동 입력 부분 추출
|
// 사용자 입력 코드에서 수동 입력 부분 추출
|
||||||
// 예: 템플릿 "R-____-XXX", 사용자입력 "R-MYVALUE-012" → "MYVALUE" 추출
|
// 예: 템플릿 "R-____-XXX", 사용자입력 "R-MYVALUE-012" → "MYVALUE" 추출
|
||||||
|
|
@ -1382,7 +1426,8 @@ class NumberingRuleService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allocatedCode = parts.join(rule.separator || "");
|
const sortedPartsForAlloc = rule.parts.sort((a: any, b: any) => a.order - b.order);
|
||||||
|
const allocatedCode = joinPartsWithSeparators(parts, sortedPartsForAlloc, rule.separator || "");
|
||||||
|
|
||||||
// 순번이 있는 경우에만 증가
|
// 순번이 있는 경우에만 증가
|
||||||
const hasSequence = rule.parts.some(
|
const hasSequence = rule.parts.some(
|
||||||
|
|
@ -1541,7 +1586,7 @@ class NumberingRuleService {
|
||||||
rule.ruleId,
|
rule.ruleId,
|
||||||
companyCode === "*" ? rule.companyCode : companyCode,
|
companyCode === "*" ? rule.companyCode : companyCode,
|
||||||
]);
|
]);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("[테스트] 채번 규칙 목록 조회 완료", {
|
logger.info("[테스트] 채번 규칙 목록 조회 완료", {
|
||||||
|
|
@ -1634,7 +1679,7 @@ class NumberingRuleService {
|
||||||
rule.ruleId,
|
rule.ruleId,
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
|
|
||||||
logger.info("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", {
|
logger.info("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", {
|
||||||
ruleId: rule.ruleId,
|
ruleId: rule.ruleId,
|
||||||
|
|
@ -1754,12 +1799,14 @@ class NumberingRuleService {
|
||||||
auto_config, manual_config, company_code, created_at
|
auto_config, manual_config, company_code, created_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
||||||
`;
|
`;
|
||||||
|
const autoConfigWithSep = { ...(part.autoConfig || {}), separatorAfter: part.separatorAfter ?? "-" };
|
||||||
|
|
||||||
await client.query(partInsertQuery, [
|
await client.query(partInsertQuery, [
|
||||||
config.ruleId,
|
config.ruleId,
|
||||||
part.order,
|
part.order,
|
||||||
part.partType,
|
part.partType,
|
||||||
part.generationMethod,
|
part.generationMethod,
|
||||||
JSON.stringify(part.autoConfig || {}),
|
JSON.stringify(autoConfigWithSep),
|
||||||
JSON.stringify(part.manualConfig || {}),
|
JSON.stringify(part.manualConfig || {}),
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
|
|
@ -1914,7 +1961,7 @@ class NumberingRuleService {
|
||||||
rule.ruleId,
|
rule.ruleId,
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
|
|
||||||
logger.info("카테고리 조건 매칭 채번 규칙 찾음", {
|
logger.info("카테고리 조건 매칭 채번 규칙 찾음", {
|
||||||
ruleId: rule.ruleId,
|
ruleId: rule.ruleId,
|
||||||
|
|
@ -1973,7 +2020,7 @@ class NumberingRuleService {
|
||||||
rule.ruleId,
|
rule.ruleId,
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
|
|
||||||
logger.info("기본 채번 규칙 찾음 (카테고리 조건 없음)", {
|
logger.info("기본 채번 규칙 찾음 (카테고리 조건 없음)", {
|
||||||
ruleId: rule.ruleId,
|
ruleId: rule.ruleId,
|
||||||
|
|
@ -2056,7 +2103,7 @@ class NumberingRuleService {
|
||||||
rule.ruleId,
|
rule.ruleId,
|
||||||
companyCode,
|
companyCode,
|
||||||
]);
|
]);
|
||||||
rule.parts = partsResult.rows;
|
rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.rows;
|
return result.rows;
|
||||||
|
|
|
||||||
|
|
@ -1607,7 +1607,8 @@ export class TableManagementService {
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
actualValue,
|
actualValue,
|
||||||
paramIndex
|
paramIndex,
|
||||||
|
operator
|
||||||
);
|
);
|
||||||
|
|
||||||
case "entity":
|
case "entity":
|
||||||
|
|
@ -1620,7 +1621,14 @@ export class TableManagementService {
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 기본 문자열 검색 (actualValue 사용)
|
// operator에 따라 정확 일치 또는 부분 일치 검색
|
||||||
|
if (operator === "equals") {
|
||||||
|
return {
|
||||||
|
whereClause: `${columnName}::text = $${paramIndex}`,
|
||||||
|
values: [String(actualValue)],
|
||||||
|
paramCount: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||||
values: [`%${actualValue}%`],
|
values: [`%${actualValue}%`],
|
||||||
|
|
@ -1634,10 +1642,19 @@ export class TableManagementService {
|
||||||
);
|
);
|
||||||
// 오류 시 기본 검색으로 폴백
|
// 오류 시 기본 검색으로 폴백
|
||||||
let fallbackValue = value;
|
let fallbackValue = value;
|
||||||
|
let fallbackOperator = "contains";
|
||||||
if (typeof value === "object" && value !== null && "value" in value) {
|
if (typeof value === "object" && value !== null && "value" in value) {
|
||||||
fallbackValue = value.value;
|
fallbackValue = value.value;
|
||||||
|
fallbackOperator = value.operator || "contains";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fallbackOperator === "equals") {
|
||||||
|
return {
|
||||||
|
whereClause: `${columnName}::text = $${paramIndex}`,
|
||||||
|
values: [String(fallbackValue)],
|
||||||
|
paramCount: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||||
values: [`%${fallbackValue}%`],
|
values: [`%${fallbackValue}%`],
|
||||||
|
|
@ -1784,7 +1801,8 @@ export class TableManagementService {
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string,
|
columnName: string,
|
||||||
value: any,
|
value: any,
|
||||||
paramIndex: number
|
paramIndex: number,
|
||||||
|
operator: string = "contains"
|
||||||
): Promise<{
|
): Promise<{
|
||||||
whereClause: string;
|
whereClause: string;
|
||||||
values: any[];
|
values: any[];
|
||||||
|
|
@ -1794,7 +1812,14 @@ export class TableManagementService {
|
||||||
const codeTypeInfo = await this.getCodeTypeInfo(tableName, columnName);
|
const codeTypeInfo = await this.getCodeTypeInfo(tableName, columnName);
|
||||||
|
|
||||||
if (!codeTypeInfo.isCodeType || !codeTypeInfo.codeCategory) {
|
if (!codeTypeInfo.isCodeType || !codeTypeInfo.codeCategory) {
|
||||||
// 코드 타입이 아니면 기본 검색
|
// 코드 타입이 아니면 operator에 따라 검색
|
||||||
|
if (operator === "equals") {
|
||||||
|
return {
|
||||||
|
whereClause: `${columnName}::text = $${paramIndex}`,
|
||||||
|
values: [String(value)],
|
||||||
|
paramCount: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
whereClause: `${columnName}::text ILIKE $${paramIndex}`,
|
||||||
values: [`%${value}%`],
|
values: [`%${value}%`],
|
||||||
|
|
@ -1802,6 +1827,15 @@ export class TableManagementService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// select 필터(equals)인 경우 정확한 코드값 매칭만 수행
|
||||||
|
if (operator === "equals") {
|
||||||
|
return {
|
||||||
|
whereClause: `${columnName}::text = $${paramIndex}`,
|
||||||
|
values: [String(value)],
|
||||||
|
paramCount: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === "string" && value.trim() !== "") {
|
if (typeof value === "string" && value.trim() !== "") {
|
||||||
// 코드값 또는 코드명으로 검색
|
// 코드값 또는 코드명으로 검색
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Card className="border-border bg-card">
|
<Card className="border-border bg-card flex-1">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Badge variant="outline" className="text-xs sm:text-sm">
|
<Badge variant="outline" className="text-xs sm:text-sm">
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
const [editingLeftTitle, setEditingLeftTitle] = useState(false);
|
const [editingLeftTitle, setEditingLeftTitle] = useState(false);
|
||||||
const [editingRightTitle, setEditingRightTitle] = useState(false);
|
const [editingRightTitle, setEditingRightTitle] = useState(false);
|
||||||
|
|
||||||
// 구분자 관련 상태
|
// 구분자 관련 상태 (개별 파트 사이 구분자)
|
||||||
const [separatorType, setSeparatorType] = useState<SeparatorType>("-");
|
const [separatorTypes, setSeparatorTypes] = useState<Record<number, SeparatorType>>({});
|
||||||
const [customSeparator, setCustomSeparator] = useState("");
|
const [customSeparators, setCustomSeparators] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
// 카테고리 조건 관련 상태 - 모든 카테고리를 테이블.컬럼 단위로 조회
|
// 카테고리 조건 관련 상태 - 모든 카테고리를 테이블.컬럼 단위로 조회
|
||||||
interface CategoryOption {
|
interface CategoryOption {
|
||||||
|
|
@ -192,48 +192,68 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
}
|
}
|
||||||
}, [currentRule, onChange]);
|
}, [currentRule, onChange]);
|
||||||
|
|
||||||
// currentRule이 변경될 때 구분자 상태 동기화
|
// currentRule이 변경될 때 파트별 구분자 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentRule) {
|
if (currentRule && currentRule.parts.length > 0) {
|
||||||
const sep = currentRule.separator ?? "-";
|
const newSepTypes: Record<number, SeparatorType> = {};
|
||||||
// 빈 문자열이면 "none"
|
const newCustomSeps: Record<number, string> = {};
|
||||||
if (sep === "") {
|
|
||||||
setSeparatorType("none");
|
currentRule.parts.forEach((part) => {
|
||||||
setCustomSeparator("");
|
const sep = part.separatorAfter ?? currentRule.separator ?? "-";
|
||||||
return;
|
if (sep === "") {
|
||||||
}
|
newSepTypes[part.order] = "none";
|
||||||
// 미리 정의된 구분자인지 확인 (none, custom 제외)
|
newCustomSeps[part.order] = "";
|
||||||
const predefinedOption = SEPARATOR_OPTIONS.find(
|
} else {
|
||||||
opt => opt.value !== "custom" && opt.value !== "none" && opt.displayValue === sep
|
const predefinedOption = SEPARATOR_OPTIONS.find(
|
||||||
);
|
opt => opt.value !== "custom" && opt.value !== "none" && opt.displayValue === sep
|
||||||
if (predefinedOption) {
|
);
|
||||||
setSeparatorType(predefinedOption.value);
|
if (predefinedOption) {
|
||||||
setCustomSeparator("");
|
newSepTypes[part.order] = predefinedOption.value;
|
||||||
} else {
|
newCustomSeps[part.order] = "";
|
||||||
// 직접 입력된 구분자
|
} else {
|
||||||
setSeparatorType("custom");
|
newSepTypes[part.order] = "custom";
|
||||||
setCustomSeparator(sep);
|
newCustomSeps[part.order] = sep;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSeparatorTypes(newSepTypes);
|
||||||
|
setCustomSeparators(newCustomSeps);
|
||||||
}
|
}
|
||||||
}, [currentRule?.ruleId]); // ruleId가 변경될 때만 실행 (규칙 선택/생성 시)
|
}, [currentRule?.ruleId]);
|
||||||
|
|
||||||
// 구분자 변경 핸들러
|
// 개별 파트 구분자 변경 핸들러
|
||||||
const handleSeparatorChange = useCallback((type: SeparatorType) => {
|
const handlePartSeparatorChange = useCallback((partOrder: number, type: SeparatorType) => {
|
||||||
setSeparatorType(type);
|
setSeparatorTypes(prev => ({ ...prev, [partOrder]: type }));
|
||||||
if (type !== "custom") {
|
if (type !== "custom") {
|
||||||
const option = SEPARATOR_OPTIONS.find(opt => opt.value === type);
|
const option = SEPARATOR_OPTIONS.find(opt => opt.value === type);
|
||||||
const newSeparator = option?.displayValue ?? "";
|
const newSeparator = option?.displayValue ?? "";
|
||||||
setCurrentRule((prev) => prev ? { ...prev, separator: newSeparator } : null);
|
setCustomSeparators(prev => ({ ...prev, [partOrder]: "" }));
|
||||||
setCustomSeparator("");
|
setCurrentRule((prev) => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
parts: prev.parts.map((part) =>
|
||||||
|
part.order === partOrder ? { ...part, separatorAfter: newSeparator } : part
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 직접 입력 구분자 변경 핸들러
|
// 개별 파트 직접 입력 구분자 변경 핸들러
|
||||||
const handleCustomSeparatorChange = useCallback((value: string) => {
|
const handlePartCustomSeparatorChange = useCallback((partOrder: number, value: string) => {
|
||||||
// 최대 2자 제한
|
|
||||||
const trimmedValue = value.slice(0, 2);
|
const trimmedValue = value.slice(0, 2);
|
||||||
setCustomSeparator(trimmedValue);
|
setCustomSeparators(prev => ({ ...prev, [partOrder]: trimmedValue }));
|
||||||
setCurrentRule((prev) => prev ? { ...prev, separator: trimmedValue } : null);
|
setCurrentRule((prev) => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
parts: prev.parts.map((part) =>
|
||||||
|
part.order === partOrder ? { ...part, separatorAfter: trimmedValue } : part
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddPart = useCallback(() => {
|
const handleAddPart = useCallback(() => {
|
||||||
|
|
@ -250,6 +270,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
partType: "text",
|
partType: "text",
|
||||||
generationMethod: "auto",
|
generationMethod: "auto",
|
||||||
autoConfig: { textValue: "CODE" },
|
autoConfig: { textValue: "CODE" },
|
||||||
|
separatorAfter: "-",
|
||||||
};
|
};
|
||||||
|
|
||||||
setCurrentRule((prev) => {
|
setCurrentRule((prev) => {
|
||||||
|
|
@ -257,6 +278,10 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
return { ...prev, parts: [...prev.parts, newPart] };
|
return { ...prev, parts: [...prev.parts, newPart] };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 새 파트의 구분자 상태 초기화
|
||||||
|
setSeparatorTypes(prev => ({ ...prev, [newPart.order]: "-" }));
|
||||||
|
setCustomSeparators(prev => ({ ...prev, [newPart.order]: "" }));
|
||||||
|
|
||||||
toast.success(`규칙 ${newPart.order}가 추가되었습니다`);
|
toast.success(`규칙 ${newPart.order}가 추가되었습니다`);
|
||||||
}, [currentRule, maxRules]);
|
}, [currentRule, maxRules]);
|
||||||
|
|
||||||
|
|
@ -573,42 +598,6 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 두 번째 줄: 구분자 설정 */}
|
|
||||||
<div className="flex items-end gap-3">
|
|
||||||
<div className="w-48 space-y-2">
|
|
||||||
<Label className="text-sm font-medium">구분자</Label>
|
|
||||||
<Select
|
|
||||||
value={separatorType}
|
|
||||||
onValueChange={(value) => handleSeparatorChange(value as SeparatorType)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9">
|
|
||||||
<SelectValue placeholder="구분자 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{SEPARATOR_OPTIONS.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{separatorType === "custom" && (
|
|
||||||
<div className="w-32 space-y-2">
|
|
||||||
<Label className="text-sm font-medium">직접 입력</Label>
|
|
||||||
<Input
|
|
||||||
value={customSeparator}
|
|
||||||
onChange={(e) => handleCustomSeparatorChange(e.target.value)}
|
|
||||||
className="h-9"
|
|
||||||
placeholder="최대 2자"
|
|
||||||
maxLength={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-muted-foreground pb-2 text-xs">
|
|
||||||
규칙 사이에 들어갈 문자입니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -625,15 +614,48 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||||
<p className="text-muted-foreground text-xs sm:text-sm">규칙을 추가하여 코드를 구성하세요</p>
|
<p className="text-muted-foreground text-xs sm:text-sm">규칙을 추가하여 코드를 구성하세요</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="flex flex-wrap items-stretch gap-3">
|
||||||
{currentRule.parts.map((part, index) => (
|
{currentRule.parts.map((part, index) => (
|
||||||
<NumberingRuleCard
|
<React.Fragment key={`part-${part.order}-${index}`}>
|
||||||
key={`part-${part.order}-${index}`}
|
<div className="flex w-[200px] flex-col">
|
||||||
part={part}
|
<NumberingRuleCard
|
||||||
onUpdate={(updates) => handleUpdatePart(part.order, updates)}
|
part={part}
|
||||||
onDelete={() => handleDeletePart(part.order)}
|
onUpdate={(updates) => handleUpdatePart(part.order, updates)}
|
||||||
isPreview={isPreview}
|
onDelete={() => handleDeletePart(part.order)}
|
||||||
/>
|
isPreview={isPreview}
|
||||||
|
/>
|
||||||
|
{/* 카드 하단에 구분자 설정 (마지막 파트 제외) */}
|
||||||
|
{index < currentRule.parts.length - 1 && (
|
||||||
|
<div className="mt-2 flex items-center gap-1">
|
||||||
|
<span className="text-muted-foreground text-[10px] whitespace-nowrap">뒤 구분자</span>
|
||||||
|
<Select
|
||||||
|
value={separatorTypes[part.order] || "-"}
|
||||||
|
onValueChange={(value) => handlePartSeparatorChange(part.order, value as SeparatorType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 flex-1 text-[10px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{SEPARATOR_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value} className="text-xs">
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{separatorTypes[part.order] === "custom" && (
|
||||||
|
<Input
|
||||||
|
value={customSeparators[part.order] || ""}
|
||||||
|
onChange={(e) => handlePartCustomSeparatorChange(part.order, e.target.value)}
|
||||||
|
className="h-6 w-14 text-center text-[10px]"
|
||||||
|
placeholder="2자"
|
||||||
|
maxLength={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -17,75 +17,71 @@ export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
|
||||||
return "규칙을 추가해주세요";
|
return "규칙을 추가해주세요";
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = config.parts
|
const sortedParts = config.parts.sort((a, b) => a.order - b.order);
|
||||||
.sort((a, b) => a.order - b.order)
|
|
||||||
.map((part) => {
|
const partValues = sortedParts.map((part) => {
|
||||||
if (part.generationMethod === "manual") {
|
if (part.generationMethod === "manual") {
|
||||||
return part.manualConfig?.value || "XXX";
|
return part.manualConfig?.value || "XXX";
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoConfig = part.autoConfig || {};
|
||||||
|
|
||||||
|
switch (part.partType) {
|
||||||
|
case "sequence": {
|
||||||
|
const length = autoConfig.sequenceLength || 3;
|
||||||
|
const startFrom = autoConfig.startFrom || 1;
|
||||||
|
return String(startFrom).padStart(length, "0");
|
||||||
}
|
}
|
||||||
|
case "number": {
|
||||||
const autoConfig = part.autoConfig || {};
|
const length = autoConfig.numberLength || 4;
|
||||||
|
const value = autoConfig.numberValue || 0;
|
||||||
switch (part.partType) {
|
return String(value).padStart(length, "0");
|
||||||
// 1. 순번 (자동 증가)
|
}
|
||||||
case "sequence": {
|
case "date": {
|
||||||
const length = autoConfig.sequenceLength || 3;
|
const format = autoConfig.dateFormat || "YYYYMMDD";
|
||||||
const startFrom = autoConfig.startFrom || 1;
|
if (autoConfig.useColumnValue && autoConfig.sourceColumnName) {
|
||||||
return String(startFrom).padStart(length, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 숫자 (고정 자릿수)
|
|
||||||
case "number": {
|
|
||||||
const length = autoConfig.numberLength || 4;
|
|
||||||
const value = autoConfig.numberValue || 0;
|
|
||||||
return String(value).padStart(length, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 날짜
|
|
||||||
case "date": {
|
|
||||||
const format = autoConfig.dateFormat || "YYYYMMDD";
|
|
||||||
|
|
||||||
// 컬럼 기준 생성인 경우 placeholder 표시
|
|
||||||
if (autoConfig.useColumnValue && autoConfig.sourceColumnName) {
|
|
||||||
// 형식에 맞는 placeholder 반환
|
|
||||||
switch (format) {
|
|
||||||
case "YYYY": return "[YYYY]";
|
|
||||||
case "YY": return "[YY]";
|
|
||||||
case "YYYYMM": return "[YYYYMM]";
|
|
||||||
case "YYMM": return "[YYMM]";
|
|
||||||
case "YYYYMMDD": return "[YYYYMMDD]";
|
|
||||||
case "YYMMDD": return "[YYMMDD]";
|
|
||||||
default: return "[DATE]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 현재 날짜 기준 생성
|
|
||||||
const now = new Date();
|
|
||||||
const year = now.getFullYear();
|
|
||||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(now.getDate()).padStart(2, "0");
|
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case "YYYY": return String(year);
|
case "YYYY": return "[YYYY]";
|
||||||
case "YY": return String(year).slice(-2);
|
case "YY": return "[YY]";
|
||||||
case "YYYYMM": return `${year}${month}`;
|
case "YYYYMM": return "[YYYYMM]";
|
||||||
case "YYMM": return `${String(year).slice(-2)}${month}`;
|
case "YYMM": return "[YYMM]";
|
||||||
case "YYYYMMDD": return `${year}${month}${day}`;
|
case "YYYYMMDD": return "[YYYYMMDD]";
|
||||||
case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`;
|
case "YYMMDD": return "[YYMMDD]";
|
||||||
default: return `${year}${month}${day}`;
|
default: return "[DATE]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const now = new Date();
|
||||||
// 4. 문자
|
const year = now.getFullYear();
|
||||||
case "text":
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
return autoConfig.textValue || "TEXT";
|
const day = String(now.getDate()).padStart(2, "0");
|
||||||
|
switch (format) {
|
||||||
default:
|
case "YYYY": return String(year);
|
||||||
return "XXX";
|
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 "text":
|
||||||
|
return autoConfig.textValue || "TEXT";
|
||||||
|
default:
|
||||||
|
return "XXX";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return parts.join(config.separator || "");
|
// 파트별 개별 구분자로 결합
|
||||||
|
const globalSep = config.separator ?? "-";
|
||||||
|
let result = "";
|
||||||
|
partValues.forEach((val, idx) => {
|
||||||
|
result += val;
|
||||||
|
if (idx < partValues.length - 1) {
|
||||||
|
const sep = sortedParts[idx].separatorAfter ?? globalSep;
|
||||||
|
result += sep;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
|
|
|
||||||
|
|
@ -940,23 +940,35 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 타입 또는 카테고리 조회 실패 시: 현재 데이터 기반
|
// 백엔드 DISTINCT API로 전체 고유값 조회 (페이징과 무관하게 모든 값 반환)
|
||||||
|
try {
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
|
return response.data.data.map((item: any) => ({
|
||||||
|
value: String(item.value),
|
||||||
|
label: String(item.label),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// DISTINCT API 실패 시 현재 데이터 기반으로 fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: 현재 로드된 데이터에서 고유 값 추출
|
||||||
const isLabelType = ["category", "entity", "code"].includes(inputType);
|
const isLabelType = ["category", "entity", "code"].includes(inputType);
|
||||||
const labelField = isLabelType ? `${columnName}_name` : columnName;
|
const labelField = isLabelType ? `${columnName}_name` : columnName;
|
||||||
|
|
||||||
// 현재 로드된 데이터에서 고유 값 추출
|
const uniqueValuesMap = new Map<string, string>();
|
||||||
const uniqueValuesMap = new Map<string, string>(); // value -> label
|
|
||||||
|
|
||||||
data.forEach((row) => {
|
data.forEach((row) => {
|
||||||
const value = row[columnName];
|
const value = row[columnName];
|
||||||
if (value !== null && value !== undefined && value !== "") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
// 백엔드 조인된 _name 필드 사용 (없으면 원본 값)
|
|
||||||
const label = isLabelType && row[labelField] ? row[labelField] : String(value);
|
const label = isLabelType && row[labelField] ? row[labelField] : String(value);
|
||||||
uniqueValuesMap.set(String(value), label);
|
uniqueValuesMap.set(String(value), label);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map을 배열로 변환하고 라벨 기준으로 정렬
|
|
||||||
const result = Array.from(uniqueValuesMap.entries())
|
const result = Array.from(uniqueValuesMap.entries())
|
||||||
.map(([value, label]) => ({
|
.map(([value, label]) => ({
|
||||||
value: value,
|
value: value,
|
||||||
|
|
|
||||||
|
|
@ -1015,23 +1015,35 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 타입 또는 카테고리 조회 실패 시: 현재 데이터 기반
|
// 백엔드 DISTINCT API로 전체 고유값 조회 (페이징과 무관하게 모든 값 반환)
|
||||||
|
try {
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data && response.data.data.length > 0) {
|
||||||
|
return response.data.data.map((item: any) => ({
|
||||||
|
value: String(item.value),
|
||||||
|
label: String(item.label),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// DISTINCT API 실패 시 현재 데이터 기반으로 fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: 현재 로드된 데이터에서 고유 값 추출
|
||||||
const isLabelType = ["category", "entity", "code"].includes(inputType);
|
const isLabelType = ["category", "entity", "code"].includes(inputType);
|
||||||
const labelField = isLabelType ? `${columnName}_name` : columnName;
|
const labelField = isLabelType ? `${columnName}_name` : columnName;
|
||||||
|
|
||||||
// 현재 로드된 데이터에서 고유 값 추출
|
const uniqueValuesMap = new Map<string, string>();
|
||||||
const uniqueValuesMap = new Map<string, string>(); // value -> label
|
|
||||||
|
|
||||||
data.forEach((row) => {
|
data.forEach((row) => {
|
||||||
const value = row[columnName];
|
const value = row[columnName];
|
||||||
if (value !== null && value !== undefined && value !== "") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
// 백엔드 조인된 _name 필드 사용 (없으면 원본 값)
|
|
||||||
const label = isLabelType && row[labelField] ? row[labelField] : String(value);
|
const label = isLabelType && row[labelField] ? row[labelField] : String(value);
|
||||||
uniqueValuesMap.set(String(value), label);
|
uniqueValuesMap.set(String(value), label);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map을 배열로 변환하고 라벨 기준으로 정렬
|
|
||||||
const result = Array.from(uniqueValuesMap.entries())
|
const result = Array.from(uniqueValuesMap.entries())
|
||||||
.map(([value, label]) => ({
|
.map(([value, label]) => ({
|
||||||
value: value,
|
value: value,
|
||||||
|
|
|
||||||
|
|
@ -2054,11 +2054,11 @@ export class ButtonActionExecutor {
|
||||||
const { tableName, screenId } = context;
|
const { tableName, screenId } = context;
|
||||||
|
|
||||||
// 범용_폼_모달 키 찾기 (컬럼명에 따라 다를 수 있음)
|
// 범용_폼_모달 키 찾기 (컬럼명에 따라 다를 수 있음)
|
||||||
|
// initializeForm에서 __tableSection_ (더블), 수정 시 _tableSection_ (싱글) 사용
|
||||||
const universalFormModalKey = Object.keys(formData).find((key) => {
|
const universalFormModalKey = Object.keys(formData).find((key) => {
|
||||||
const value = formData[key];
|
const value = formData[key];
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||||
// _tableSection_ 키가 있는지 확인
|
return Object.keys(value).some((k) => k.startsWith("_tableSection_") || k.startsWith("__tableSection_"));
|
||||||
return Object.keys(value).some((k) => k.startsWith("_tableSection_"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!universalFormModalKey) {
|
if (!universalFormModalKey) {
|
||||||
|
|
@ -2117,11 +2117,18 @@ export class ButtonActionExecutor {
|
||||||
const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || [];
|
const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || [];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(modalData)) {
|
for (const [key, value] of Object.entries(modalData)) {
|
||||||
if (key.startsWith("_tableSection_")) {
|
// initializeForm: __tableSection_ (더블), 수정 시: _tableSection_ (싱글) → 통일 처리
|
||||||
const sectionId = key.replace("_tableSection_", "");
|
if (key.startsWith("_tableSection_") || key.startsWith("__tableSection_")) {
|
||||||
tableSectionData[sectionId] = value as any[];
|
if (Array.isArray(value)) {
|
||||||
|
const normalizedKey = key.startsWith("__tableSection_")
|
||||||
|
? key.replace("__tableSection_", "")
|
||||||
|
: key.replace("_tableSection_", "");
|
||||||
|
// 싱글 언더스코어 키(수정된 데이터)가 더블 언더스코어 키(초기 데이터)보다 우선
|
||||||
|
if (!tableSectionData[normalizedKey] || key.startsWith("_tableSection_")) {
|
||||||
|
tableSectionData[normalizedKey] = value as any[];
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (!key.startsWith("_")) {
|
} else if (!key.startsWith("_")) {
|
||||||
// _로 시작하지 않는 필드는 공통 필드로 처리
|
|
||||||
commonFieldsData[key] = value;
|
commonFieldsData[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2306,10 +2313,11 @@ export class ButtonActionExecutor {
|
||||||
// originalGroupedData 전달이 누락된 경우를 처리
|
// originalGroupedData 전달이 누락된 경우를 처리
|
||||||
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - id가 있으므로 UPDATE 시도 (폴백): id=${item.id}`);
|
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - id가 있으므로 UPDATE 시도 (폴백): id=${item.id}`);
|
||||||
|
|
||||||
// ⚠️ 중요: commonFieldsData가 item보다 우선순위가 높아야 함
|
// 마스터/디테일 테이블 분리 시: commonFieldsData 병합하지 않음
|
||||||
// item에 있는 기존 값(예: manager_id=123)이 commonFieldsData의 새 값(manager_id=234)을 덮어쓰지 않도록
|
// → 동명 컬럼(예: memo)이 마스터 값으로 덮어씌워지는 문제 방지
|
||||||
// 순서: item(기존) → commonFieldsData(새로 입력) → userInfo(메타데이터)
|
const rowToUpdate = hasSeparateTargetTable
|
||||||
const rowToUpdate = { ...item, ...commonFieldsData, ...userInfo };
|
? { ...item, ...userInfo }
|
||||||
|
: { ...commonFieldsData, ...item, ...userInfo };
|
||||||
Object.keys(rowToUpdate).forEach((key) => {
|
Object.keys(rowToUpdate).forEach((key) => {
|
||||||
if (key.startsWith("_")) {
|
if (key.startsWith("_")) {
|
||||||
delete rowToUpdate[key];
|
delete rowToUpdate[key];
|
||||||
|
|
@ -2330,17 +2338,20 @@ export class ButtonActionExecutor {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 변경 사항 확인 (공통 필드 포함)
|
// 변경 사항 확인
|
||||||
// ⚠️ 중요: commonFieldsData가 item보다 우선순위가 높아야 함 (새로 입력한 값이 기존 값을 덮어씀)
|
// 마스터/디테일 테이블이 분리된 경우(hasSeparateTargetTable):
|
||||||
const currentDataWithCommon = { ...item, ...commonFieldsData };
|
// 마스터 필드(commonFieldsData)를 디테일에 병합하지 않음
|
||||||
const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon);
|
// → 동명 컬럼(예: memo)이 마스터 값으로 덮어씌워지는 문제 방지
|
||||||
|
// 같은 테이블인 경우: 공통 필드 병합 유지 (공유 필드 업데이트 필요)
|
||||||
|
const dataForComparison = hasSeparateTargetTable ? item : { ...commonFieldsData, ...item };
|
||||||
|
const hasChanges = this.checkForChanges(originalItem, dataForComparison);
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
// 변경된 필드만 추출하여 부분 업데이트
|
// 변경된 필드만 추출하여 부분 업데이트
|
||||||
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
||||||
item.id,
|
item.id,
|
||||||
originalItem,
|
originalItem,
|
||||||
currentDataWithCommon,
|
dataForComparison,
|
||||||
saveTableName,
|
saveTableName,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ export interface NumberingRulePart {
|
||||||
partType: CodePartType; // 파트 유형
|
partType: CodePartType; // 파트 유형
|
||||||
generationMethod: GenerationMethod; // 생성 방식
|
generationMethod: GenerationMethod; // 생성 방식
|
||||||
|
|
||||||
|
// 이 파트 뒤에 붙을 구분자 (마지막 파트는 무시됨)
|
||||||
|
separatorAfter?: string;
|
||||||
|
|
||||||
// 자동 생성 설정
|
// 자동 생성 설정
|
||||||
autoConfig?: {
|
autoConfig?: {
|
||||||
// 순번용
|
// 순번용
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue