Compare commits
3 Commits
88ba2f62d2
...
e171f5a503
| Author | SHA1 | Date |
|---|---|---|
|
|
e171f5a503 | |
|
|
80a7a8e455 | |
|
|
52fd370460 |
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 || {};
|
||||
|
|
|
|||
|
|
@ -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}%'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 등)만 전달해야 함
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 || "코드 할당 실패" };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
>({});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue