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