Compare commits

...

3 Commits

Author SHA1 Message Date
kjs e171f5a503 Merge branch 'feature/v2-unified-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feature/v2-unified-renewal 2026-02-04 15:00:49 +09:00
kjs 80a7a8e455 feat: 다중 선택값 처리 로직 추가 및 개선
- 테이블 관리 서비스에서 파이프로 구분된 문자열을 처리하는 로직을 추가하여, 날짜 타입은 날짜 범위로, 그 외 타입은 다중 선택(IN 조건)으로 처리하도록 개선하였습니다.
- 엔티티 조인 검색 및 일반 컬럼 검색에서 다중 선택값을 처리하는 로직을 추가하여, 사용자 입력에 따른 필터링 기능을 강화하였습니다.
- 버튼 컴포넌트에서 기본 텍스트 결정 로직을 개선하여 다양한 소스에서 버튼 텍스트를 가져올 수 있도록 하였습니다.
- 테이블 리스트 컴포넌트에서 joinColumnMapping을 추가하여 필터링 기능을 개선하였습니다.
2026-02-04 15:00:48 +09:00
kjs 52fd370460 feat: 수동 입력 코드 처리 개선 및 사용자 입력 코드 전달 기능 추가
- allocateCode 함수에 사용자가 편집한 최종 코드를 전달하여 수동 입력 부분을 추출할 수 있도록 수정하였습니다.
- 여러 컴포넌트에서 사용자 입력 값을 처리할 수 있는 이벤트 리스너를 추가하여, 채번 생성 시 수동 입력 값을 반영하도록 개선하였습니다.
- V2Input 및 관련 컴포넌트에서 formData에 수동 입력 값을 주입하는 로직을 추가하여 사용자 경험을 향상시켰습니다.
- 코드 할당 요청 시 사용자 입력 코드와 폼 데이터를 함께 전달하여, 보다 유연한 코드 할당이 가능하도록 하였습니다.
2026-02-04 14:12:24 +09:00
16 changed files with 334 additions and 47 deletions

View File

@ -225,12 +225,12 @@ router.post("/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequ
router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
const { formData } = req.body; // 폼 데이터 (날짜 컬럼 기준 생성 시 사용)
const { formData, userInputCode } = req.body; // 폼 데이터 + 사용자가 편집한 코드
logger.info("코드 할당 요청", { ruleId, companyCode, hasFormData: !!formData });
logger.info("코드 할당 요청", { ruleId, companyCode, hasFormData: !!formData, userInputCode });
try {
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode, formData);
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode, formData, userInputCode);
logger.info("코드 할당 성공", { ruleId, allocatedCode });
return res.json({ success: true, data: { generatedCode: allocatedCode } });
} catch (error: any) {

View File

@ -886,8 +886,9 @@ class NumberingRuleService {
.sort((a: any, b: any) => a.order - b.order)
.map((part: any) => {
if (part.generationMethod === "manual") {
// 수동 입력 - 플레이스홀더 표시 (실제 값은 사용자가 입력)
return part.manualConfig?.placeholder || "____";
// 수동 입력 - 항상 ____ 마커 사용 (프론트엔드에서 편집 가능하게 처리)
// placeholder 텍스트는 프론트엔드에서 별도로 표시
return "____";
}
const autoConfig = part.autoConfig || {};
@ -1014,11 +1015,13 @@ class NumberingRuleService {
* @param ruleId ID
* @param companyCode
* @param formData ( )
* @param userInputCode ( )
*/
async allocateCode(
ruleId: string,
companyCode: string,
formData?: Record<string, any>
formData?: Record<string, any>,
userInputCode?: string
): Promise<string> {
const pool = getPool();
const client = await pool.connect();
@ -1029,11 +1032,77 @@ class NumberingRuleService {
const rule = await this.getRuleById(ruleId, companyCode);
if (!rule) throw new Error("규칙을 찾을 수 없습니다");
// 수동 입력 파트가 있고, 사용자가 입력한 코드가 있으면 수동 입력 부분 추출
const manualParts = rule.parts.filter((p: any) => p.generationMethod === "manual");
let extractedManualValues: string[] = [];
if (manualParts.length > 0 && userInputCode) {
// 프리뷰 코드를 생성해서 ____ 위치 파악
const previewParts = rule.parts
.sort((a: any, b: any) => a.order - b.order)
.map((part: any) => {
if (part.generationMethod === "manual") {
return "____";
}
const autoConfig = part.autoConfig || {};
switch (part.partType) {
case "sequence": {
const length = autoConfig.sequenceLength || 3;
return "X".repeat(length); // 순번 자리 표시
}
case "text":
return autoConfig.textValue || "";
case "date":
return "DATEPART"; // 날짜 자리 표시
default:
return "";
}
});
const separator = rule.separator || "";
const previewTemplate = previewParts.join(separator);
// 사용자 입력 코드에서 수동 입력 부분 추출
// 예: 템플릿 "R-____-XXX", 사용자입력 "R-MYVALUE-012" → "MYVALUE" 추출
const templateParts = previewTemplate.split("____");
if (templateParts.length > 1) {
let remainingCode = userInputCode;
for (let i = 0; i < templateParts.length - 1; i++) {
const prefix = templateParts[i];
const suffix = templateParts[i + 1];
// prefix 이후 부분 추출
if (prefix && remainingCode.startsWith(prefix)) {
remainingCode = remainingCode.slice(prefix.length);
}
// suffix 이전까지가 수동 입력 값
if (suffix) {
// suffix에서 순번(XXX)이나 날짜 부분을 제외한 실제 구분자 찾기
const suffixStart = suffix.replace(/X+|DATEPART/g, "");
const manualEndIndex = suffixStart ? remainingCode.indexOf(suffixStart) : remainingCode.length;
if (manualEndIndex > 0) {
extractedManualValues.push(remainingCode.slice(0, manualEndIndex));
remainingCode = remainingCode.slice(manualEndIndex);
}
} else {
extractedManualValues.push(remainingCode);
}
}
}
logger.info(`수동 입력 값 추출: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedManualValues)}`);
}
let manualPartIndex = 0;
const parts = rule.parts
.sort((a: any, b: any) => a.order - b.order)
.map((part: any) => {
if (part.generationMethod === "manual") {
return part.manualConfig?.value || "";
// 추출된 수동 입력 값 사용, 없으면 기본값 사용
const manualValue = extractedManualValues[manualPartIndex] || part.manualConfig?.value || "";
manualPartIndex++;
return manualValue;
}
const autoConfig = part.autoConfig || {};

View File

@ -1461,6 +1461,40 @@ export class TableManagementService {
});
}
// 🔧 파이프로 구분된 문자열 처리 (객체에서 추출한 actualValue도 처리)
if (typeof actualValue === "string" && actualValue.includes("|")) {
const columnInfo = await this.getColumnWebTypeInfo(
tableName,
columnName
);
// 날짜 타입이면 날짜 범위로 처리
if (
columnInfo &&
(columnInfo.webType === "date" || columnInfo.webType === "datetime")
) {
return this.buildDateRangeCondition(columnName, actualValue, paramIndex);
}
// 그 외 타입이면 다중선택(IN 조건)으로 처리
const multiValues = actualValue
.split("|")
.filter((v: string) => v.trim() !== "");
if (multiValues.length > 0) {
const placeholders = multiValues
.map((_: string, idx: number) => `$${paramIndex + idx}`)
.join(", ");
logger.info(
`🔍 다중선택 필터 적용 (객체에서 추출): ${columnName} IN (${multiValues.join(", ")})`
);
return {
whereClause: `${columnName}::text IN (${placeholders})`,
values: multiValues,
paramCount: multiValues.length,
};
}
}
// "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
if (
actualValue === "__ALL__" ||
@ -3428,15 +3462,37 @@ export class TableManagementService {
// 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색
const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`;
const alias = aliasMap.get(aliasKey);
whereConditions.push(
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
);
entitySearchColumns.push(
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
);
logger.info(
`🎯 Entity 조인 검색: ${key}${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})`
);
// 🔧 파이프로 구분된 다중 선택값 처리
if (safeValue.includes("|")) {
const multiValues = safeValue
.split("|")
.filter((v: string) => v.trim() !== "");
if (multiValues.length > 0) {
const inClause = multiValues
.map((v: string) => `'${v}'`)
.join(", ");
whereConditions.push(
`${alias}.${joinConfig.displayColumn}::text IN (${inClause})`
);
entitySearchColumns.push(
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
);
logger.info(
`🎯 Entity 조인 다중선택 검색: ${key}${joinConfig.referenceTable}.${joinConfig.displayColumn} IN (${multiValues.join(", ")}) (별칭: ${alias})`
);
}
} else {
whereConditions.push(
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
);
entitySearchColumns.push(
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
);
logger.info(
`🎯 Entity 조인 검색: ${key}${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})`
);
}
} else if (key === "writer_dept_code") {
// writer_dept_code: user_info.dept_code에서 검색
const userAliasKey = Array.from(aliasMap.keys()).find((k) =>
@ -3473,10 +3529,26 @@ export class TableManagementService {
}
} else {
// 일반 컬럼인 경우: 메인 테이블에서 검색
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
logger.info(
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
);
// 🔧 파이프로 구분된 다중 선택값 처리
if (safeValue.includes("|")) {
const multiValues = safeValue
.split("|")
.filter((v: string) => v.trim() !== "");
if (multiValues.length > 0) {
const inClause = multiValues
.map((v: string) => `'${v}'`)
.join(", ");
whereConditions.push(`main.${key}::text IN (${inClause})`);
logger.info(
`🔍 다중선택 컬럼 검색: ${key} → main.${key} IN (${multiValues.join(", ")})`
);
}
} else {
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
logger.info(
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
);
}
}
}
}

View File

@ -127,6 +127,24 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 모달이 열린 시간 추적 (저장 성공 이벤트 무시용)
const modalOpenedAtRef = React.useRef<number>(0);
// 🆕 채번 필드 수동 입력 값 변경 이벤트 리스너
useEffect(() => {
const handleNumberingValueChanged = (event: CustomEvent) => {
const { columnName, value } = event.detail;
if (columnName && modalState.isOpen) {
setFormData((prev) => ({
...prev,
[columnName]: value,
}));
}
};
window.addEventListener("numberingValueChanged", handleNumberingValueChanged as EventListener);
return () => {
window.removeEventListener("numberingValueChanged", handleNumberingValueChanged as EventListener);
};
}, [modalState.isOpen]);
// 전역 모달 이벤트 리스너
useEffect(() => {
const handleOpenModal = (event: CustomEvent) => {
@ -140,6 +158,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
splitPanelParentData,
selectedData: eventSelectedData,
selectedIds,
isCreateMode, // 🆕 복사 모드 플래그 (true면 editData가 있어도 originalData 설정 안 함)
} = event.detail;
// 🆕 모달 열린 시간 기록
@ -163,7 +182,8 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
}
// 🆕 editData가 있으면 formData와 originalData로 설정 (수정 모드)
if (editData) {
// 🔧 단, isCreateMode가 true이면 (복사 모드) originalData를 설정하지 않음 → 채번 생성 가능
if (editData && !isCreateMode) {
// 🆕 배열인 경우 두 가지 데이터를 설정:
// 1. formData: 첫 번째 요소(객체) - 일반 입력 필드용 (TextInput 등)
// 2. selectedData: 전체 배열 - 다중 항목 컴포넌트용 (SelectedItemsDetailInput 등)
@ -177,6 +197,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
setSelectedData([editData]); // 🔧 단일 객체도 배열로 변환하여 저장
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
}
} else if (editData && isCreateMode) {
// 🆕 복사 모드: formData만 설정하고 originalData는 null로 유지 (채번 생성 가능)
if (Array.isArray(editData)) {
const firstRecord = editData[0] || {};
setFormData(firstRecord);
setSelectedData(editData);
} else {
setFormData(editData);
setSelectedData([editData]);
}
setOriginalData(null); // 🔧 복사 모드에서는 originalData를 null로 설정
} else {
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
// 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 전달해야 함

View File

@ -772,12 +772,14 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try {
console.log(`🔄 [EditModal] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`);
const allocateResult = await allocateNumberingCode(ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const userInputCode = dataToSave[fieldName] as string;
console.log(`🔄 [EditModal] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}, 사용자입력: ${userInputCode}`);
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);
if (allocateResult.success && allocateResult.data?.generatedCode) {
const newCode = allocateResult.data.generatedCode;
console.log(`✅ [EditModal] ${fieldName} 새 코드 할당: ${dataToSave[fieldName]}${newCode}`);
console.log(`✅ [EditModal] ${fieldName} 새 코드 할당: ${userInputCode}${newCode}`);
dataToSave[fieldName] = newCode;
} else {
console.warn(`⚠️ [EditModal] ${fieldName} 코드 할당 실패:`, allocateResult.error);

View File

@ -834,8 +834,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
{/* 이벤트 버스 */}
<SelectItem value="event"> </SelectItem>
{/* 🔒 - , UI
{/* 복사 */}
<SelectItem value="copy"> ( )</SelectItem>
{/* 🔒 - , UI
<SelectItem value="openRelatedModal"> </SelectItem>
<SelectItem value="openModalWithData">(deprecated) + </SelectItem>
<SelectItem value="view_table_history"> </SelectItem>

View File

@ -700,9 +700,10 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
);
// 🆕 채번 API 호출 (비동기)
const generateNumberingCode = useCallback(async (ruleId: string): Promise<string> => {
// 🆕 수동 입력 부분 지원을 위해 userInputCode 파라미터 추가
const generateNumberingCode = useCallback(async (ruleId: string, userInputCode?: string, formData?: Record<string, any>): Promise<string> => {
try {
const result = await allocateNumberingCode(ruleId);
const result = await allocateNumberingCode(ruleId, userInputCode, formData);
if (result.success && result.data?.generatedCode) {
return result.data.generatedCode;
}
@ -831,7 +832,8 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
if (match) {
const ruleId = match[1];
try {
const result = await allocateNumberingCode(ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const result = await allocateNumberingCode(ruleId, undefined, newRow);
if (result.success && result.data?.generatedCode) {
newRow[key] = result.data.generatedCode;
} else {

View File

@ -625,6 +625,40 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableName, columnName, isEditMode, categoryValuesForNumbering]);
// 🆕 beforeFormSave 이벤트 리스너 - 저장 직전에 현재 조합된 값을 formData에 주입
useEffect(() => {
const inputType = propsInputType || config.inputType || config.type || "text";
if (inputType !== "numbering" || !columnName) return;
const handleBeforeFormSave = (event: CustomEvent) => {
const template = numberingTemplateRef.current;
if (!template || !template.includes("____")) return;
// 템플릿에서 prefix와 suffix 추출
const templateParts = template.split("____");
const templatePrefix = templateParts[0] || "";
const templateSuffix = templateParts.length > 1 ? templateParts.slice(1).join("") : "";
// 현재 조합된 값 생성
const currentValue = templatePrefix + manualInputValue + templateSuffix;
// formData에 직접 주입
if (event.detail?.formData && columnName) {
event.detail.formData[columnName] = currentValue;
console.log("🔧 [V2Input] beforeFormSave에서 채번 값 주입:", {
columnName,
manualInputValue,
currentValue,
});
}
};
window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
return () => {
window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
};
}, [columnName, manualInputValue, propsInputType, config.inputType, config.type]);
// 실제 표시할 값 (자동생성 값 또는 props value)
const displayValue = autoGeneratedValue ?? value;
@ -769,7 +803,19 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
const newValue = templatePrefix + newUserInput + templateSuffix;
userEditedNumberingRef.current = true;
setAutoGeneratedValue(newValue);
// 모든 방법으로 formData 업데이트 시도
onChange?.(newValue);
if (onFormDataChange && columnName) {
onFormDataChange(columnName, newValue);
}
// 커스텀 이벤트로도 전달 (최후의 보루)
if (typeof window !== "undefined" && columnName) {
window.dispatchEvent(new CustomEvent("numberingValueChanged", {
detail: { columnName, value: newValue }
}));
}
}}
placeholder="입력"
className="h-full min-w-[60px] flex-1 bg-transparent px-2 text-sm focus-visible:outline-none"

View File

@ -567,9 +567,10 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
);
// 🆕 채번 API 호출 (비동기)
const generateNumberingCode = useCallback(async (ruleId: string): Promise<string> => {
// 🆕 수동 입력 부분 지원을 위해 userInputCode 파라미터 추가
const generateNumberingCode = useCallback(async (ruleId: string, userInputCode?: string, formData?: Record<string, any>): Promise<string> => {
try {
const result = await allocateNumberingCode(ruleId);
const result = await allocateNumberingCode(ruleId, userInputCode, formData);
if (result.success && result.data?.generatedCode) {
return result.data.generatedCode;
}
@ -690,7 +691,8 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
if (match) {
const ruleId = match[1];
try {
const result = await allocateNumberingCode(ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const result = await allocateNumberingCode(ruleId, undefined, newRow);
if (result.success && result.data?.generatedCode) {
newRow[key] = result.data.generatedCode;
} else {

View File

@ -139,12 +139,20 @@ export async function previewNumberingCode(
/**
* ( )
*
* @param ruleId ID
* @param userInputCode ( )
* @param formData (/ )
*/
export async function allocateNumberingCode(
ruleId: string
ruleId: string,
userInputCode?: string,
formData?: Record<string, any>
): Promise<ApiResponse<{ generatedCode: string }>> {
try {
const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`);
const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`, {
userInputCode,
formData,
});
return response.data;
} catch (error: any) {
return { success: false, error: error.message || "코드 할당 실패" };

View File

@ -856,8 +856,10 @@ export function RepeatScreenModalComponent({
});
// 채번 API 호출 (allocate: 실제 시퀀스 증가)
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
const response = await allocateNumberingCode(rowNumbering.numberingRuleId);
const userInputCode = newRowData[rowNumbering.targetColumn] as string;
const response = await allocateNumberingCode(rowNumbering.numberingRuleId, userInputCode, newRowData);
if (response.success && response.data) {
newRowData[rowNumbering.targetColumn] = response.data.generatedCode;

View File

@ -1443,8 +1443,9 @@ export function UniversalFormModalComponent({
if (isNewRecord || hasNoValue) {
try {
// allocateNumberingCode로 실제 순번 증가
const response = await allocateNumberingCode(field.numberingRule.ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const userInputCode = mainData[field.columnName] as string;
const response = await allocateNumberingCode(field.numberingRule.ruleId, userInputCode, mainData);
if (response.success && response.data?.generatedCode) {
mainData[field.columnName] = response.data.generatedCode;
}

View File

@ -1324,7 +1324,31 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
...userStyle,
};
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
// 버튼 텍스트 결정 (다양한 소스에서 가져옴)
// "기본 버튼"은 컴포넌트 생성 시 기본값이므로 무시
const labelValue = component.label === "기본 버튼" ? undefined : component.label;
// 액션 타입에 따른 기본 텍스트 (modal 액션과 동일하게)
const actionType = processedConfig.action?.type || component.componentConfig?.action?.type;
const actionDefaultText: Record<string, string> = {
save: "저장",
delete: "삭제",
modal: "등록",
edit: "수정",
copy: "복사",
close: "닫기",
cancel: "취소",
};
const buttonContent =
processedConfig.text ||
component.webTypeConfig?.text ||
component.componentConfig?.text ||
component.config?.text ||
component.style?.labelText ||
labelValue ||
actionDefaultText[actionType as string] ||
"버튼";
return (
<>

View File

@ -25,8 +25,20 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
// 값 변경 핸들러
const handleChange = (value: any) => {
console.log("🔄 [V2InputRenderer] handleChange 호출:", {
columnName,
value,
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
});
if (isInteractive && onFormDataChange && columnName) {
onFormDataChange(columnName, value);
} else {
console.warn("⚠️ [V2InputRenderer] onFormDataChange 호출 스킵:", {
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
columnName,
});
}
};

View File

@ -459,6 +459,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
// 🆕 joinColumnMapping - filteredData에서 사용하므로 먼저 정의해야 함
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
// 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터
const filteredData = useMemo(() => {
@ -473,14 +476,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
}
// 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용)
// 2. 헤더 필터 적용 (joinColumnMapping 사용 - 조인된 컬럼과 일치해야 함)
if (Object.keys(headerFilters).length > 0) {
result = result.filter((row) => {
return Object.entries(headerFilters).every(([columnName, values]) => {
if (values.size === 0) return true;
// 여러 가능한 컬럼명 시도
const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
// joinColumnMapping을 사용하여 조인된 컬럼명 확인
const mappedColumnName = joinColumnMapping[columnName] || columnName;
// 여러 가능한 컬럼명 시도 (mappedColumnName 우선)
const cellValue = row[mappedColumnName] ?? row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : "";
return values.has(cellStr);
@ -541,7 +547,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
return result;
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]);
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups, joinColumnMapping]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
@ -554,7 +560,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [tableLabel, setTableLabel] = useState<string>("");
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
const [columnMeta, setColumnMeta] = useState<
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
>({});

View File

@ -737,7 +737,9 @@ export class ButtonActionExecutor {
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) {
try {
const allocateResult = await allocateNumberingCode(ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const userInputCode = context.formData[fieldName] as string;
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, context.formData);
if (allocateResult.success && allocateResult.data?.generatedCode) {
const newCode = allocateResult.data.generatedCode;
@ -1030,7 +1032,9 @@ export class ButtonActionExecutor {
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try {
const allocateResult = await allocateNumberingCode(ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const userInputCode = formData[fieldName] as string;
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);
if (allocateResult.success && allocateResult.data?.generatedCode) {
const newCode = allocateResult.data.generatedCode;
@ -2054,7 +2058,9 @@ export class ButtonActionExecutor {
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try {
const allocateResult = await allocateNumberingCode(ruleId);
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
const userInputCode = commonFieldsData[fieldName] as string;
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);
if (allocateResult.success && allocateResult.data?.generatedCode) {
const newCode = allocateResult.data.generatedCode;
@ -3485,10 +3491,13 @@ export class ButtonActionExecutor {
const screenModalEvent = new CustomEvent("openScreenModal", {
detail: {
screenId: config.targetScreenId,
title: config.editModalTitle || "데이터 수정",
title: isCreateMode ? config.editModalTitle || "데이터 복사" : config.editModalTitle || "데이터 수정",
description: description,
size: config.modalSize || "lg",
editData: rowData, // 🆕 수정 데이터 전달
// 🔧 복사 모드에서는 editData 대신 splitPanelParentData로 전달하여 채번이 생성되도록 함
editData: isCreateMode ? undefined : rowData,
splitPanelParentData: isCreateMode ? rowData : undefined,
isCreateMode: isCreateMode, // 🆕 복사 모드 플래그 전달
},
});
window.dispatchEvent(screenModalEvent);