diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts
index 6628cf4c..9fc0f079 100644
--- a/backend-node/src/services/screenManagementService.ts
+++ b/backend-node/src/services/screenManagementService.ts
@@ -2360,30 +2360,33 @@ export class ScreenManagementService {
const lockId = Buffer.from(companyCode).reduce((acc, byte) => acc + byte, 0);
await client.query('SELECT pg_advisory_xact_lock($1)', [lockId]);
- // 현재 최대 번호 조회
- const existingScreens = await client.query<{ screen_code: string }>(
- `SELECT screen_code FROM screen_definitions
- WHERE company_code = $1 AND screen_code LIKE $2
- ORDER BY screen_code DESC
- LIMIT 10`,
- [companyCode, `${companyCode}%`]
+ // 현재 최대 번호 조회 (숫자 추출 후 정렬)
+ // 패턴: COMPANY_CODE_XXX 또는 COMPANY_CODEXXX
+ const existingScreens = await client.query<{ screen_code: string; num: number }>(
+ `SELECT screen_code,
+ COALESCE(
+ NULLIF(
+ regexp_replace(screen_code, $2, '\\1'),
+ screen_code
+ )::integer,
+ 0
+ ) as num
+ FROM screen_definitions
+ WHERE company_code = $1
+ AND screen_code ~ $2
+ AND deleted_date IS NULL
+ ORDER BY num DESC
+ LIMIT 1`,
+ [companyCode, `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[_]?(\\d+)$`]
);
let maxNumber = 0;
- const pattern = new RegExp(
- `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$`
- );
-
- for (const screen of existingScreens.rows) {
- const match = screen.screen_code.match(pattern);
- if (match) {
- const number = parseInt(match[1], 10);
- if (number > maxNumber) {
- maxNumber = number;
- }
- }
+ if (existingScreens.rows.length > 0 && existingScreens.rows[0].num) {
+ maxNumber = existingScreens.rows[0].num;
}
+ console.log(`🔢 현재 최대 화면 코드 번호: ${companyCode} → ${maxNumber}`);
+
// count개의 코드를 순차적으로 생성
const codes: string[] = [];
for (let i = 0; i < count; i++) {
diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx
index c37603c5..f5e71c4c 100644
--- a/frontend/components/screen/CopyScreenModal.tsx
+++ b/frontend/components/screen/CopyScreenModal.tsx
@@ -166,18 +166,28 @@ export default function CopyScreenModal({
// linkedScreens 로딩이 완료되면 화면 코드 생성
useEffect(() => {
+ // 모달 화면들의 코드가 모두 설정되었는지 확인
+ const allModalCodesSet = linkedScreens.length === 0 ||
+ linkedScreens.every(screen => screen.newScreenCode);
+
console.log("🔍 코드 생성 조건 체크:", {
targetCompanyCode,
loadingLinkedScreens,
screenCode,
linkedScreensCount: linkedScreens.length,
+ allModalCodesSet,
});
- if (targetCompanyCode && !loadingLinkedScreens && !screenCode) {
+ // 조건: 회사 코드가 있고, 로딩이 완료되고, (메인 코드가 없거나 모달 코드가 없을 때)
+ const needsCodeGeneration = targetCompanyCode &&
+ !loadingLinkedScreens &&
+ (!screenCode || (linkedScreens.length > 0 && !allModalCodesSet));
+
+ if (needsCodeGeneration) {
console.log("✅ 화면 코드 생성 시작 (linkedScreens 개수:", linkedScreens.length, ")");
generateScreenCodes();
}
- }, [targetCompanyCode, loadingLinkedScreens, screenCode]);
+ }, [targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreens]);
// 회사 목록 조회
const loadCompanies = async () => {
diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx
index 914e34f1..7a11bdb1 100644
--- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx
+++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx
@@ -1438,7 +1438,7 @@ export function ModalRepeaterTableConfigPanel({
checked={col.dynamicDataSource?.enabled || false}
onCheckedChange={(checked) => toggleDynamicDataSource(index, checked)}
/>
-
+
컬럼 헤더 클릭으로 데이터 소스 전환 (예: 거래처별 단가, 품목별 단가)
diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx
index 869884a7..410fd9a6 100644
--- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx
+++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx
@@ -161,11 +161,11 @@ export function RepeaterTable({
: null;
return (
-
+ |
{hasDynamicSource ? (
) : (
<>
- {col.label}
- {col.required && *}
+ {col.label}
+ {col.required && *}
>
)}
- |
+
);
})}
diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts
index 028a892b..6097aaf3 100644
--- a/frontend/lib/registry/components/modal-repeater-table/types.ts
+++ b/frontend/lib/registry/components/modal-repeater-table/types.ts
@@ -49,7 +49,7 @@ export interface RepeaterColumnConfig {
required?: boolean; // 필수 입력 여부
defaultValue?: string | number | boolean; // 기본값
selectOptions?: { value: string; label: string }[]; // select일 때 옵션
-
+
// 컬럼 매핑 설정
mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정
@@ -142,16 +142,16 @@ export interface MultiTableJoinStep {
export interface ColumnMapping {
/** 매핑 타입 */
type: "source" | "reference" | "manual";
-
+
/** 매핑 타입별 설정 */
// type: "source" - 소스 테이블 (모달에서 선택한 항목)의 컬럼에서 가져오기
sourceField?: string; // 소스 테이블의 컬럼명 (예: "item_name")
-
+
// type: "reference" - 외부 테이블 참조 (조인)
referenceTable?: string; // 참조 테이블명 (예: "customer_item_mapping")
referenceField?: string; // 참조 테이블에서 가져올 컬럼 (예: "basic_price")
joinCondition?: JoinCondition[]; // 조인 조건
-
+
// type: "manual" - 사용자가 직접 입력
}
diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx
index 3fa8f623..7598d3a8 100644
--- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx
+++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx
@@ -144,6 +144,55 @@ export function UniversalFormModalComponent({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
+ // 🆕 beforeFormSave 이벤트 리스너 - ButtonPrimary 저장 시 formData를 전달
+ // 설정된 필드(columnName)만 병합하여 의도치 않은 덮어쓰기 방지
+ useEffect(() => {
+ const handleBeforeFormSave = (event: Event) => {
+ if (!(event instanceof CustomEvent) || !event.detail?.formData) return;
+
+ // 설정에 정의된 필드 columnName 목록 수집
+ const configuredFields = new Set();
+ config.sections.forEach((section) => {
+ section.fields.forEach((field) => {
+ if (field.columnName) {
+ configuredFields.add(field.columnName);
+ }
+ });
+ });
+
+ console.log("[UniversalFormModal] beforeFormSave 이벤트 수신");
+ console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields));
+
+ // UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함)
+ // 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀
+ // (UniversalFormModal이 해당 필드의 주인이므로)
+ for (const [key, value] of Object.entries(formData)) {
+ // 설정에 정의된 필드만 병합
+ if (configuredFields.has(key)) {
+ if (value !== undefined && value !== null && value !== "") {
+ event.detail.formData[key] = value;
+ console.log(`[UniversalFormModal] 필드 병합: ${key} =`, value);
+ }
+ }
+ }
+
+ // 반복 섹션 데이터도 병합 (필요한 경우)
+ if (Object.keys(repeatSections).length > 0) {
+ for (const [sectionId, items] of Object.entries(repeatSections)) {
+ const sectionKey = `_repeatSection_${sectionId}`;
+ event.detail.formData[sectionKey] = items;
+ console.log(`[UniversalFormModal] 반복 섹션 병합: ${sectionKey}`, items);
+ }
+ }
+ };
+
+ window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
+
+ return () => {
+ window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
+ };
+ }, [formData, repeatSections, config.sections]);
+
// 필드 레벨 linkedFieldGroup 데이터 로드
useEffect(() => {
const loadData = async () => {
diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx
index 3ce7477a..48542342 100644
--- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx
+++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx
@@ -413,14 +413,14 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
ButtonPrimary 컴포넌트로 저장 버튼을 별도 구성할 경우 끄세요
{config.modal.showSaveButton !== false && (
-
-
- updateModalConfig({ saveButtonText: e.target.value })}
- className="h-7 text-xs mt-1"
- />
-
+
+
+ updateModalConfig({ saveButtonText: e.target.value })}
+ className="h-7 text-xs mt-1"
+ />
+
)}
|