fix: 화면 복사 코드 생성 로직 개선 및 UniversalFormModal beforeFormSave 이벤트 연동
- screenManagementService: PostgreSQL regexp_replace로 정확한 최대 번호 조회 - CopyScreenModal: linkedScreens 의존성 추가로 모달 코드 생성 보장 - UniversalFormModal: beforeFormSave 이벤트 리스너로 ButtonPrimary 연동 - 설정된 필드만 병합하여 의도치 않은 덮어쓰기 방지
This commit is contained in:
parent
d550959cb7
commit
5e97a3a5e9
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -1438,7 +1438,7 @@ export function ModalRepeaterTableConfigPanel({
|
|||
checked={col.dynamicDataSource?.enabled || false}
|
||||
onCheckedChange={(checked) => toggleDynamicDataSource(index, checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
컬럼 헤더 클릭으로 데이터 소스 전환 (예: 거래처별 단가, 품목별 단가)
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -161,11 +161,11 @@ export function RepeaterTable({
|
|||
: null;
|
||||
|
||||
return (
|
||||
<th
|
||||
key={col.field}
|
||||
className="px-4 py-2 text-left font-medium text-muted-foreground"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
<th
|
||||
key={col.field}
|
||||
className="px-4 py-2 text-left font-medium text-muted-foreground"
|
||||
style={{ width: col.width }}
|
||||
>
|
||||
{hasDynamicSource ? (
|
||||
<Popover
|
||||
open={openPopover === col.field}
|
||||
|
|
@ -219,11 +219,11 @@ export function RepeaterTable({
|
|||
</Popover>
|
||||
) : (
|
||||
<>
|
||||
{col.label}
|
||||
{col.required && <span className="text-destructive ml-1">*</span>}
|
||||
{col.label}
|
||||
{col.required && <span className="text-destructive ml-1">*</span>}
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-20">
|
||||
|
|
|
|||
|
|
@ -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" - 사용자가 직접 입력
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
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 () => {
|
||||
|
|
|
|||
|
|
@ -413,14 +413,14 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
<HelpText>ButtonPrimary 컴포넌트로 저장 버튼을 별도 구성할 경우 끄세요</HelpText>
|
||||
|
||||
{config.modal.showSaveButton !== false && (
|
||||
<div>
|
||||
<Label className="text-[10px]">저장 버튼 텍스트</Label>
|
||||
<Input
|
||||
value={config.modal.saveButtonText || "저장"}
|
||||
onChange={(e) => updateModalConfig({ saveButtonText: e.target.value })}
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">저장 버튼 텍스트</Label>
|
||||
<Input
|
||||
value={config.modal.saveButtonText || "저장"}
|
||||
onChange={(e) => updateModalConfig({ saveButtonText: e.target.value })}
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
|
|
|
|||
Loading…
Reference in New Issue