Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node
This commit is contained in:
commit
7b773f57b4
|
|
@ -335,9 +335,222 @@ ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.colu
|
|||
|
||||
---
|
||||
|
||||
## 7. 체크리스트
|
||||
## 7. 로그 테이블 생성 (선택사항)
|
||||
|
||||
테이블 생성/수정 시 반드시 확인할 사항:
|
||||
변경 이력 추적이 필요한 테이블에는 로그 테이블을 생성할 수 있습니다.
|
||||
|
||||
### 7.1 로그 테이블 DDL 템플릿
|
||||
|
||||
```sql
|
||||
-- 로그 테이블 생성
|
||||
CREATE TABLE 테이블명_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
operation_type VARCHAR(10) NOT NULL, -- INSERT/UPDATE/DELETE
|
||||
original_id VARCHAR(100), -- 원본 테이블 PK 값
|
||||
changed_column VARCHAR(100), -- 변경된 컬럼명
|
||||
old_value TEXT, -- 변경 전 값
|
||||
new_value TEXT, -- 변경 후 값
|
||||
changed_by VARCHAR(50), -- 변경자 ID
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 변경 시각
|
||||
ip_address VARCHAR(50), -- 변경 요청 IP
|
||||
user_agent TEXT, -- User Agent
|
||||
full_row_before JSONB, -- 변경 전 전체 행
|
||||
full_row_after JSONB -- 변경 후 전체 행
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX idx_테이블명_log_original_id ON 테이블명_log(original_id);
|
||||
CREATE INDEX idx_테이블명_log_changed_at ON 테이블명_log(changed_at);
|
||||
CREATE INDEX idx_테이블명_log_operation ON 테이블명_log(operation_type);
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON TABLE 테이블명_log IS '테이블명 테이블 변경 이력';
|
||||
```
|
||||
|
||||
### 7.2 트리거 함수 DDL 템플릿
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION 테이블명_log_trigger_func()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_column_name TEXT;
|
||||
v_old_value TEXT;
|
||||
v_new_value TEXT;
|
||||
v_user_id VARCHAR(50);
|
||||
v_ip_address VARCHAR(50);
|
||||
BEGIN
|
||||
v_user_id := current_setting('app.user_id', TRUE);
|
||||
v_ip_address := current_setting('app.ip_address', TRUE);
|
||||
|
||||
IF (TG_OP = 'INSERT') THEN
|
||||
INSERT INTO 테이블명_log (operation_type, original_id, changed_by, ip_address, full_row_after)
|
||||
VALUES ('INSERT', NEW.id, v_user_id, v_ip_address, row_to_json(NEW)::jsonb);
|
||||
RETURN NEW;
|
||||
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
FOR v_column_name IN
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = '테이블명'
|
||||
AND table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
|
||||
INTO v_old_value, v_new_value
|
||||
USING OLD, NEW;
|
||||
|
||||
IF v_old_value IS DISTINCT FROM v_new_value THEN
|
||||
INSERT INTO 테이블명_log (
|
||||
operation_type, original_id, changed_column, old_value, new_value,
|
||||
changed_by, ip_address, full_row_before, full_row_after
|
||||
)
|
||||
VALUES (
|
||||
'UPDATE', NEW.id, v_column_name, v_old_value, v_new_value,
|
||||
v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
RETURN NEW;
|
||||
|
||||
ELSIF (TG_OP = 'DELETE') THEN
|
||||
INSERT INTO 테이블명_log (operation_type, original_id, changed_by, ip_address, full_row_before)
|
||||
VALUES ('DELETE', OLD.id, v_user_id, v_ip_address, row_to_json(OLD)::jsonb);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
### 7.3 트리거 DDL 템플릿
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER 테이블명_audit_trigger
|
||||
AFTER INSERT OR UPDATE OR DELETE ON 테이블명
|
||||
FOR EACH ROW EXECUTE FUNCTION 테이블명_log_trigger_func();
|
||||
```
|
||||
|
||||
### 7.4 로그 설정 등록
|
||||
|
||||
```sql
|
||||
INSERT INTO table_log_config (
|
||||
original_table_name, log_table_name, trigger_name,
|
||||
trigger_function_name, is_active, created_by, created_at
|
||||
) VALUES (
|
||||
'테이블명', '테이블명_log', '테이블명_audit_trigger',
|
||||
'테이블명_log_trigger_func', 'Y', '생성자ID', now()
|
||||
);
|
||||
```
|
||||
|
||||
### 7.5 table_labels에 use_log_table 플래그 설정
|
||||
|
||||
```sql
|
||||
UPDATE table_labels
|
||||
SET use_log_table = 'Y', updated_date = now()
|
||||
WHERE table_name = '테이블명';
|
||||
```
|
||||
|
||||
### 7.6 전체 예시: order_info 로그 테이블 생성
|
||||
|
||||
```sql
|
||||
-- Step 1: 로그 테이블 생성
|
||||
CREATE TABLE order_info_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
operation_type VARCHAR(10) NOT NULL,
|
||||
original_id VARCHAR(100),
|
||||
changed_column VARCHAR(100),
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by VARCHAR(50),
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
full_row_before JSONB,
|
||||
full_row_after JSONB
|
||||
);
|
||||
|
||||
CREATE INDEX idx_order_info_log_original_id ON order_info_log(original_id);
|
||||
CREATE INDEX idx_order_info_log_changed_at ON order_info_log(changed_at);
|
||||
CREATE INDEX idx_order_info_log_operation ON order_info_log(operation_type);
|
||||
|
||||
COMMENT ON TABLE order_info_log IS 'order_info 테이블 변경 이력';
|
||||
|
||||
-- Step 2: 트리거 함수 생성
|
||||
CREATE OR REPLACE FUNCTION order_info_log_trigger_func()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_column_name TEXT;
|
||||
v_old_value TEXT;
|
||||
v_new_value TEXT;
|
||||
v_user_id VARCHAR(50);
|
||||
v_ip_address VARCHAR(50);
|
||||
BEGIN
|
||||
v_user_id := current_setting('app.user_id', TRUE);
|
||||
v_ip_address := current_setting('app.ip_address', TRUE);
|
||||
|
||||
IF (TG_OP = 'INSERT') THEN
|
||||
INSERT INTO order_info_log (operation_type, original_id, changed_by, ip_address, full_row_after)
|
||||
VALUES ('INSERT', NEW.id, v_user_id, v_ip_address, row_to_json(NEW)::jsonb);
|
||||
RETURN NEW;
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
FOR v_column_name IN
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'order_info' AND table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
|
||||
INTO v_old_value, v_new_value USING OLD, NEW;
|
||||
IF v_old_value IS DISTINCT FROM v_new_value THEN
|
||||
INSERT INTO order_info_log (operation_type, original_id, changed_column, old_value, new_value, changed_by, ip_address, full_row_before, full_row_after)
|
||||
VALUES ('UPDATE', NEW.id, v_column_name, v_old_value, v_new_value, v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb);
|
||||
END IF;
|
||||
END LOOP;
|
||||
RETURN NEW;
|
||||
ELSIF (TG_OP = 'DELETE') THEN
|
||||
INSERT INTO order_info_log (operation_type, original_id, changed_by, ip_address, full_row_before)
|
||||
VALUES ('DELETE', OLD.id, v_user_id, v_ip_address, row_to_json(OLD)::jsonb);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 3: 트리거 생성
|
||||
CREATE TRIGGER order_info_audit_trigger
|
||||
AFTER INSERT OR UPDATE OR DELETE ON order_info
|
||||
FOR EACH ROW EXECUTE FUNCTION order_info_log_trigger_func();
|
||||
|
||||
-- Step 4: 로그 설정 등록
|
||||
INSERT INTO table_log_config (original_table_name, log_table_name, trigger_name, trigger_function_name, is_active, created_by, created_at)
|
||||
VALUES ('order_info', 'order_info_log', 'order_info_audit_trigger', 'order_info_log_trigger_func', 'Y', 'system', now());
|
||||
|
||||
-- Step 5: table_labels 플래그 업데이트
|
||||
UPDATE table_labels SET use_log_table = 'Y', updated_date = now() WHERE table_name = 'order_info';
|
||||
```
|
||||
|
||||
### 7.7 로그 테이블 삭제
|
||||
|
||||
```sql
|
||||
-- 트리거 삭제
|
||||
DROP TRIGGER IF EXISTS 테이블명_audit_trigger ON 테이블명;
|
||||
|
||||
-- 트리거 함수 삭제
|
||||
DROP FUNCTION IF EXISTS 테이블명_log_trigger_func();
|
||||
|
||||
-- 로그 테이블 삭제
|
||||
DROP TABLE IF EXISTS 테이블명_log;
|
||||
|
||||
-- 로그 설정 삭제
|
||||
DELETE FROM table_log_config WHERE original_table_name = '테이블명';
|
||||
|
||||
-- table_labels 플래그 업데이트
|
||||
UPDATE table_labels SET use_log_table = 'N', updated_date = now() WHERE table_name = '테이블명';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 체크리스트
|
||||
|
||||
### 테이블 생성/수정 시 반드시 확인할 사항:
|
||||
|
||||
- [ ] DDL에 기본 5개 컬럼 포함 (id, created_date, updated_date, writer, company_code)
|
||||
- [ ] 모든 비즈니스 컬럼은 `VARCHAR(500)` 타입 사용
|
||||
|
|
@ -349,9 +562,18 @@ ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.colu
|
|||
- [ ] code/entity 타입은 detail_settings에 참조 정보 포함
|
||||
- [ ] ON CONFLICT 절로 중복 시 UPDATE 처리
|
||||
|
||||
### 로그 테이블 생성 시 확인할 사항 (선택):
|
||||
|
||||
- [ ] 로그 테이블 생성 (`테이블명_log`)
|
||||
- [ ] 인덱스 3개 생성 (original_id, changed_at, operation_type)
|
||||
- [ ] 트리거 함수 생성 (`테이블명_log_trigger_func`)
|
||||
- [ ] 트리거 생성 (`테이블명_audit_trigger`)
|
||||
- [ ] `table_log_config`에 로그 설정 등록
|
||||
- [ ] `table_labels.use_log_table = 'Y'` 업데이트
|
||||
|
||||
---
|
||||
|
||||
## 8. 금지 사항
|
||||
## 9. 금지 사항
|
||||
|
||||
1. **DB 타입 직접 지정 금지**: NUMBER, INTEGER, DATE 등 DB 타입 직접 사용 금지
|
||||
2. **VARCHAR 길이 변경 금지**: 반드시 `VARCHAR(500)` 사용
|
||||
|
|
@ -364,5 +586,7 @@ ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.colu
|
|||
## 참조 파일
|
||||
|
||||
- `backend-node/src/services/ddlExecutionService.ts`: DDL 실행 서비스
|
||||
- `backend-node/src/services/tableManagementService.ts`: 로그 테이블 생성 서비스
|
||||
- `backend-node/src/types/ddl.ts`: DDL 타입 정의
|
||||
- `backend-node/src/controllers/ddlController.ts`: DDL API 컨트롤러
|
||||
- `backend-node/src/controllers/tableManagementController.ts`: 로그 테이블 API 컨트롤러
|
||||
|
|
|
|||
|
|
@ -3081,6 +3081,79 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* 🆕 행 선택 시에만 활성화 설정 */}
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
|
||||
<h4 className="text-sm font-medium text-foreground">행 선택 활성화 조건</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
테이블 리스트나 분할 패널에서 데이터가 선택되었을 때만 버튼을 활성화합니다.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>행 선택 시에만 활성화</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
체크하면 테이블에서 행을 선택해야만 버튼이 활성화됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={component.componentConfig?.action?.requireRowSelection || false}
|
||||
onCheckedChange={(checked) => {
|
||||
onUpdateProperty("componentConfig.action.requireRowSelection", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{component.componentConfig?.action?.requireRowSelection && (
|
||||
<div className="space-y-3 pl-4 border-l-2 border-primary/20">
|
||||
<div>
|
||||
<Label htmlFor="row-selection-source">선택 데이터 소스</Label>
|
||||
<Select
|
||||
value={component.componentConfig?.action?.rowSelectionSource || "auto"}
|
||||
onValueChange={(value) => {
|
||||
onUpdateProperty("componentConfig.action.rowSelectionSource", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="데이터 소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto">자동 감지 (권장)</SelectItem>
|
||||
<SelectItem value="tableList">테이블 리스트 선택</SelectItem>
|
||||
<SelectItem value="splitPanelLeft">분할 패널 좌측 선택</SelectItem>
|
||||
<SelectItem value="flowWidget">플로우 위젯 선택</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
자동 감지: 테이블, 분할 패널, 플로우 위젯 중 선택된 항목이 있으면 활성화
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>다중 선택 허용</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
여러 행이 선택되어도 활성화 (기본: 1개 이상 선택 시 활성화)
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={component.componentConfig?.action?.allowMultiRowSelection ?? true}
|
||||
onCheckedChange={(checked) => {
|
||||
onUpdateProperty("componentConfig.action.allowMultiRowSelection", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!(component.componentConfig?.action?.allowMultiRowSelection ?? true) && (
|
||||
<div className="rounded-md bg-yellow-50 p-2 dark:bg-yellow-950/20">
|
||||
<p className="text-xs text-yellow-800 dark:text-yellow-200">
|
||||
정확히 1개의 행만 선택되어야 버튼이 활성화됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 제어 기능 섹션 */}
|
||||
<div className="mt-8 border-t border-border pt-6">
|
||||
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
|
||||
|
|
|
|||
|
|
@ -296,6 +296,145 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
return false;
|
||||
}, [component.componentConfig?.action, formData, vehicleStatus, statusLoading, component.label]);
|
||||
|
||||
// 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨)
|
||||
const [modalStoreData, setModalStoreData] = useState<Record<string, any[]>>({});
|
||||
|
||||
// modalDataStore 상태 구독 (실시간 업데이트)
|
||||
useEffect(() => {
|
||||
const actionConfig = component.componentConfig?.action;
|
||||
if (!actionConfig?.requireRowSelection) return;
|
||||
|
||||
// 동적 import로 modalDataStore 구독
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||
// 초기값 설정
|
||||
setModalStoreData(useModalDataStore.getState().dataRegistry);
|
||||
|
||||
// 상태 변경 구독
|
||||
unsubscribe = useModalDataStore.subscribe((state) => {
|
||||
setModalStoreData(state.dataRegistry);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [component.componentConfig?.action?.requireRowSelection]);
|
||||
|
||||
// 🆕 행 선택 기반 비활성화 조건 계산
|
||||
const isRowSelectionDisabled = useMemo(() => {
|
||||
const actionConfig = component.componentConfig?.action;
|
||||
|
||||
// requireRowSelection이 활성화되어 있지 않으면 비활성화하지 않음
|
||||
if (!actionConfig?.requireRowSelection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rowSelectionSource = actionConfig.rowSelectionSource || "auto";
|
||||
const allowMultiRowSelection = actionConfig.allowMultiRowSelection ?? true;
|
||||
|
||||
// 선택된 데이터 확인
|
||||
let hasSelection = false;
|
||||
let selectionCount = 0;
|
||||
let selectionSource = "";
|
||||
|
||||
// 1. 자동 감지 모드 또는 테이블 리스트 모드
|
||||
if (rowSelectionSource === "auto" || rowSelectionSource === "tableList") {
|
||||
// TableList에서 선택된 행 확인 (props로 전달됨)
|
||||
if (selectedRowsData && selectedRowsData.length > 0) {
|
||||
hasSelection = true;
|
||||
selectionCount = selectedRowsData.length;
|
||||
selectionSource = "tableList (selectedRowsData)";
|
||||
}
|
||||
// 또는 selectedRows prop 확인
|
||||
else if (selectedRows && selectedRows.length > 0) {
|
||||
hasSelection = true;
|
||||
selectionCount = selectedRows.length;
|
||||
selectionSource = "tableList (selectedRows)";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 분할 패널 좌측 선택 데이터 확인
|
||||
if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") {
|
||||
// SplitPanelContext에서 확인
|
||||
if (splitPanelContext?.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0) {
|
||||
if (!hasSelection) {
|
||||
hasSelection = true;
|
||||
selectionCount = 1;
|
||||
selectionSource = "splitPanelLeft (context)";
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 modalDataStore에서도 확인 (SplitPanelLayoutComponent에서 저장)
|
||||
if (!hasSelection && Object.keys(modalStoreData).length > 0) {
|
||||
// modalDataStore에서 데이터가 있는지 확인
|
||||
for (const [sourceId, items] of Object.entries(modalStoreData)) {
|
||||
if (items && items.length > 0) {
|
||||
hasSelection = true;
|
||||
selectionCount = items.length;
|
||||
selectionSource = `modalDataStore (${sourceId})`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 플로우 위젯 선택 데이터 확인
|
||||
if (rowSelectionSource === "auto" || rowSelectionSource === "flowWidget") {
|
||||
// 플로우 위젯 선택 데이터 확인
|
||||
if (!hasSelection && flowSelectedData && flowSelectedData.length > 0) {
|
||||
hasSelection = true;
|
||||
selectionCount = flowSelectedData.length;
|
||||
selectionSource = "flowWidget";
|
||||
}
|
||||
}
|
||||
|
||||
// 디버깅 로그
|
||||
console.log("🔍 [ButtonPrimary] 행 선택 체크:", component.label, {
|
||||
rowSelectionSource,
|
||||
hasSelection,
|
||||
selectionCount,
|
||||
selectionSource,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||
selectedRowsData: selectedRowsData?.length,
|
||||
selectedRows: selectedRows?.length,
|
||||
flowSelectedData: flowSelectedData?.length,
|
||||
modalStoreDataKeys: Object.keys(modalStoreData),
|
||||
});
|
||||
|
||||
// 선택된 데이터가 없으면 비활성화
|
||||
if (!hasSelection) {
|
||||
console.log("🚫 [ButtonPrimary] 행 선택 필요 → 비활성화:", component.label);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 다중 선택 허용하지 않는 경우, 정확히 1개만 선택되어야 함
|
||||
if (!allowMultiRowSelection && selectionCount !== 1) {
|
||||
console.log("🚫 [ButtonPrimary] 정확히 1개 행 선택 필요 → 비활성화:", component.label, {
|
||||
selectionCount,
|
||||
allowMultiRowSelection,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log("✅ [ButtonPrimary] 행 선택 조건 충족:", component.label, {
|
||||
selectionCount,
|
||||
selectionSource,
|
||||
});
|
||||
return false;
|
||||
}, [
|
||||
component.componentConfig?.action,
|
||||
component.label,
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
splitPanelContext?.selectedLeftData,
|
||||
flowSelectedData,
|
||||
splitPanelContext,
|
||||
modalStoreData,
|
||||
]);
|
||||
|
||||
// 확인 다이얼로그 상태
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<{
|
||||
|
|
@ -832,7 +971,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
}
|
||||
|
||||
// modalDataStore에서 선택된 데이터 가져오기 (분할 패널 등에서 선택한 데이터)
|
||||
if ((!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) && effectiveTableName) {
|
||||
// 단, 모달(modal) 액션은 신규 등록이므로 modalDataStore 데이터를 가져오지 않음
|
||||
// (다른 화면에서 선택한 데이터가 남아있을 수 있으므로)
|
||||
const shouldFetchFromModalDataStore =
|
||||
processedConfig.action.type !== "modal" &&
|
||||
(!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) &&
|
||||
effectiveTableName;
|
||||
|
||||
if (shouldFetchFromModalDataStore) {
|
||||
try {
|
||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
||||
|
|
@ -860,12 +1006,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 모달 액션인데 선택된 데이터가 있으면 경고 메시지 표시하고 중단
|
||||
// (신규 등록 모달에서 선택된 데이터가 초기값으로 전달되는 것을 방지)
|
||||
if (processedConfig.action.type === "modal" && effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) {
|
||||
toast.warning("신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요.");
|
||||
return;
|
||||
}
|
||||
// 🔧 모달 액션 시 선택 데이터 경고 제거
|
||||
// 이전에는 "신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요" 경고를 표시했으나,
|
||||
// 다른 화면에서 선택한 데이터가 남아있는 경우 오탐이 발생하여 제거함.
|
||||
// 모달 화면 내부에서 필요 시 자체적으로 선택 데이터를 무시하도록 처리하면 됨.
|
||||
|
||||
// 수정(edit) 액션 검증
|
||||
if (processedConfig.action.type === "edit") {
|
||||
|
|
@ -1088,17 +1232,26 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화)
|
||||
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || statusLoading;
|
||||
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화 + 행 선택 필수)
|
||||
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
|
||||
|
||||
// 공통 버튼 스타일
|
||||
// 🔧 component.style에서 background/backgroundColor 충돌 방지
|
||||
const userStyle = component.style
|
||||
? Object.fromEntries(
|
||||
Object.entries(component.style).filter(
|
||||
([key]) => !["width", "height", "background", "backgroundColor"].includes(key)
|
||||
)
|
||||
)
|
||||
: {};
|
||||
|
||||
const buttonElementStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: "40px",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
background: finalDisabled ? "#e5e7eb" : buttonColor,
|
||||
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor, // 🔧 background → backgroundColor로 변경
|
||||
color: finalDisabled ? "#9ca3af" : "white",
|
||||
// 🔧 크기 설정 적용 (sm/md/lg)
|
||||
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
||||
|
|
@ -1114,10 +1267,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
margin: "0",
|
||||
lineHeight: "1.25",
|
||||
boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height 제외)
|
||||
...(component.style
|
||||
? Object.fromEntries(Object.entries(component.style).filter(([key]) => key !== "width" && key !== "height"))
|
||||
: {}),
|
||||
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height/background 제외)
|
||||
...userStyle,
|
||||
};
|
||||
|
||||
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||
|
|
|
|||
|
|
@ -2030,14 +2030,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
className="border-border flex flex-shrink-0 flex-col border-r"
|
||||
>
|
||||
<Card className="flex flex-col border-0 shadow-none" style={{ height: "100%" }}>
|
||||
<CardHeader
|
||||
<CardHeader
|
||||
className="flex-shrink-0 border-b"
|
||||
style={{
|
||||
style={{
|
||||
height: componentConfig.leftPanel?.panelHeaderHeight || 48,
|
||||
minHeight: componentConfig.leftPanel?.panelHeaderHeight || 48,
|
||||
padding: '0 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
padding: "0 1rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
|
|
@ -2521,14 +2521,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
className="flex flex-shrink-0 flex-col"
|
||||
>
|
||||
<Card className="flex flex-col border-0 shadow-none" style={{ height: "100%" }}>
|
||||
<CardHeader
|
||||
<CardHeader
|
||||
className="flex-shrink-0 border-b"
|
||||
style={{
|
||||
style={{
|
||||
height: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
||||
minHeight: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
||||
padding: '0 1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
padding: "0 1rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -963,6 +963,13 @@ export function UniversalFormModalComponent({
|
|||
}
|
||||
}
|
||||
|
||||
// 별도 테이블에 저장해야 하는 테이블 섹션 목록
|
||||
const tableSectionsForSeparateTable = config.sections.filter(
|
||||
(s) => s.type === "table" &&
|
||||
s.tableConfig?.saveConfig?.targetTable &&
|
||||
s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName
|
||||
);
|
||||
|
||||
// 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장)
|
||||
// targetTable이 없거나 메인 테이블과 같은 경우
|
||||
const tableSectionsForMainTable = config.sections.filter(
|
||||
|
|
@ -971,6 +978,12 @@ export function UniversalFormModalComponent({
|
|||
s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName)
|
||||
);
|
||||
|
||||
console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName);
|
||||
console.log("[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", tableSectionsForMainTable.map(s => s.id));
|
||||
console.log("[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", tableSectionsForSeparateTable.map(s => s.id));
|
||||
console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData));
|
||||
console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave));
|
||||
|
||||
if (tableSectionsForMainTable.length > 0) {
|
||||
// 공통 저장 필드 수집 (sectionSaveModes 설정에 따라)
|
||||
const commonFieldsData: Record<string, any> = {};
|
||||
|
|
@ -1050,35 +1063,51 @@ export function UniversalFormModalComponent({
|
|||
// 메인 레코드 ID가 필요한 경우 (response.data에서 가져오기)
|
||||
const mainRecordId = response.data?.data?.id;
|
||||
|
||||
// 공통 저장 필드 수집 (sectionSaveModes 설정에 따라)
|
||||
// 공통 저장 필드 수집: 다른 섹션(필드 타입)에서 공통 저장으로 설정된 필드 값
|
||||
// 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual'
|
||||
const commonFieldsData: Record<string, any> = {};
|
||||
const { sectionSaveModes } = config.saveConfig;
|
||||
|
||||
if (sectionSaveModes && sectionSaveModes.length > 0) {
|
||||
// 다른 섹션에서 공통 저장으로 설정된 필드 값 수집
|
||||
for (const otherSection of config.sections) {
|
||||
if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기
|
||||
|
||||
const sectionMode = sectionSaveModes.find((s) => s.sectionId === otherSection.id);
|
||||
const defaultMode = otherSection.type === "table" ? "individual" : "common";
|
||||
const sectionSaveMode = sectionMode?.saveMode || defaultMode;
|
||||
|
||||
// 필드 타입 섹션의 필드들 처리
|
||||
if (otherSection.type !== "table" && otherSection.fields) {
|
||||
for (const field of otherSection.fields) {
|
||||
// 필드별 오버라이드 확인
|
||||
const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName);
|
||||
const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode;
|
||||
|
||||
// 공통 저장이면 formData에서 값을 가져와 모든 품목에 적용
|
||||
if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) {
|
||||
commonFieldsData[field.columnName] = formData[field.columnName];
|
||||
// 다른 섹션에서 공통 저장으로 설정된 필드 값 수집
|
||||
for (const otherSection of config.sections) {
|
||||
if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기
|
||||
|
||||
const sectionMode = sectionSaveModes?.find((s) => s.sectionId === otherSection.id);
|
||||
// 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual'
|
||||
const defaultMode = otherSection.type === "table" ? "individual" : "common";
|
||||
const sectionSaveMode = sectionMode?.saveMode || defaultMode;
|
||||
|
||||
// 필드 타입 섹션의 필드들 처리
|
||||
if (otherSection.type !== "table" && otherSection.fields) {
|
||||
for (const field of otherSection.fields) {
|
||||
// 필드별 오버라이드 확인
|
||||
const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName);
|
||||
const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode;
|
||||
|
||||
// 공통 저장이면 formData에서 값을 가져와 모든 품목에 적용
|
||||
if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) {
|
||||
commonFieldsData[field.columnName] = formData[field.columnName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 선택적 필드 그룹 (optionalFieldGroups)도 처리
|
||||
if (otherSection.optionalFieldGroups && otherSection.optionalFieldGroups.length > 0) {
|
||||
for (const optGroup of otherSection.optionalFieldGroups) {
|
||||
if (optGroup.fields) {
|
||||
for (const field of optGroup.fields) {
|
||||
// 선택적 필드 그룹은 기본적으로 common 저장
|
||||
if (formData[field.columnName] !== undefined) {
|
||||
commonFieldsData[field.columnName] = formData[field.columnName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[saveSingleRow] 별도 테이블 저장 - 공통 필드:", Object.keys(commonFieldsData));
|
||||
|
||||
for (const item of sectionData) {
|
||||
// 공통 필드 병합 + 개별 품목 데이터
|
||||
const itemToSave = { ...commonFieldsData, ...item };
|
||||
|
|
@ -1091,15 +1120,26 @@ export function UniversalFormModalComponent({
|
|||
}
|
||||
}
|
||||
|
||||
// _sourceData 등 내부 메타데이터 제거
|
||||
Object.keys(itemToSave).forEach((key) => {
|
||||
if (key.startsWith("_")) {
|
||||
delete itemToSave[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 메인 레코드와 연결이 필요한 경우
|
||||
if (mainRecordId && config.saveConfig.primaryKeyColumn) {
|
||||
itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId;
|
||||
}
|
||||
|
||||
await apiClient.post(
|
||||
const saveResponse = await apiClient.post(
|
||||
`/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`,
|
||||
itemToSave
|
||||
);
|
||||
|
||||
if (!saveResponse.data?.success) {
|
||||
throw new Error(saveResponse.data?.message || `${section.title || "테이블 섹션"} 저장 실패`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2928,54 +2928,74 @@ export function TableSectionSettingsModal({
|
|||
{/* UI 설정 */}
|
||||
<div className="space-y-3 border rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium">UI 설정</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs">추가 버튼 동작</Label>
|
||||
<Select
|
||||
value={tableConfig.uiConfig?.addButtonType || "search"}
|
||||
onValueChange={(value) => updateUiConfig({ addButtonType: value as "search" | "addRow" })}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs mt-1">
|
||||
<SelectValue placeholder="버튼 동작 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="search">
|
||||
<div className="flex flex-col">
|
||||
<span>검색 모달 열기</span>
|
||||
<span className="text-[10px] text-muted-foreground">기존 데이터에서 선택</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="addRow">
|
||||
<div className="flex flex-col">
|
||||
<span>빈 행 추가</span>
|
||||
<span className="text-[10px] text-muted-foreground">새 데이터 직접 입력</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 버튼 표시 설정 */}
|
||||
<div className="space-y-2 p-3 bg-muted/30 rounded-lg">
|
||||
<Label className="text-xs font-medium">표시할 버튼 선택</Label>
|
||||
<p className="text-[10px] text-muted-foreground mb-2">
|
||||
두 버튼을 동시에 표시할 수 있습니다.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={tableConfig.uiConfig?.showSearchButton ?? true}
|
||||
onCheckedChange={(checked) => updateUiConfig({ showSearchButton: checked })}
|
||||
className="scale-75"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-medium">검색 버튼</span>
|
||||
<p className="text-[10px] text-muted-foreground">기존 데이터에서 선택</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={tableConfig.uiConfig?.showAddRowButton ?? false}
|
||||
onCheckedChange={(checked) => updateUiConfig({ showAddRowButton: checked })}
|
||||
className="scale-75"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-medium">행 추가 버튼</span>
|
||||
<p className="text-[10px] text-muted-foreground">빈 행 직접 입력</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 검색 버튼 텍스트 */}
|
||||
<div>
|
||||
<Label className="text-xs">추가 버튼 텍스트</Label>
|
||||
<Label className="text-xs">검색 버튼 텍스트</Label>
|
||||
<Input
|
||||
value={tableConfig.uiConfig?.addButtonText || ""}
|
||||
onChange={(e) => updateUiConfig({ addButtonText: e.target.value })}
|
||||
placeholder={tableConfig.uiConfig?.addButtonType === "addRow" ? "항목 추가" : "항목 검색"}
|
||||
value={tableConfig.uiConfig?.searchButtonText || ""}
|
||||
onChange={(e) => updateUiConfig({ searchButtonText: e.target.value })}
|
||||
placeholder="품목 검색"
|
||||
className="h-8 text-xs mt-1"
|
||||
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
||||
/>
|
||||
</div>
|
||||
{/* 행 추가 버튼 텍스트 */}
|
||||
<div>
|
||||
<Label className="text-xs">모달 제목</Label>
|
||||
<Label className="text-xs">행 추가 버튼 텍스트</Label>
|
||||
<Input
|
||||
value={tableConfig.uiConfig?.addRowButtonText || ""}
|
||||
onChange={(e) => updateUiConfig({ addRowButtonText: e.target.value })}
|
||||
placeholder="직접 입력"
|
||||
className="h-8 text-xs mt-1"
|
||||
disabled={!tableConfig.uiConfig?.showAddRowButton}
|
||||
/>
|
||||
</div>
|
||||
{/* 모달 제목 */}
|
||||
<div>
|
||||
<Label className="text-xs">검색 모달 제목</Label>
|
||||
<Input
|
||||
value={tableConfig.uiConfig?.modalTitle || ""}
|
||||
onChange={(e) => updateUiConfig({ modalTitle: e.target.value })}
|
||||
placeholder="항목 검색 및 선택"
|
||||
className="h-8 text-xs mt-1"
|
||||
disabled={tableConfig.uiConfig?.addButtonType === "addRow"}
|
||||
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
||||
/>
|
||||
{tableConfig.uiConfig?.addButtonType === "addRow" && (
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">빈 행 추가 모드에서는 모달이 열리지 않습니다</p>
|
||||
)}
|
||||
</div>
|
||||
{/* 테이블 최대 높이 */}
|
||||
<div>
|
||||
<Label className="text-xs">테이블 최대 높이</Label>
|
||||
<Input
|
||||
|
|
@ -2985,13 +3005,14 @@ export function TableSectionSettingsModal({
|
|||
className="h-8 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
{/* 다중 선택 허용 */}
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<Switch
|
||||
checked={tableConfig.uiConfig?.multiSelect ?? true}
|
||||
onCheckedChange={(checked) => updateUiConfig({ multiSelect: checked })}
|
||||
className="scale-75"
|
||||
disabled={tableConfig.uiConfig?.addButtonType === "addRow"}
|
||||
disabled={!(tableConfig.uiConfig?.showSearchButton ?? true)}
|
||||
/>
|
||||
<span>다중 선택 허용</span>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -253,15 +253,19 @@ export interface TableSectionConfig {
|
|||
|
||||
// 6. UI 설정
|
||||
uiConfig?: {
|
||||
addButtonText?: string; // 추가 버튼 텍스트 (기본: "품목 검색")
|
||||
modalTitle?: string; // 모달 제목 (기본: "항목 검색 및 선택")
|
||||
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
||||
maxHeight?: string; // 테이블 최대 높이 (기본: "400px")
|
||||
|
||||
// 추가 버튼 타입
|
||||
// - search: 검색 모달 열기 (기본값) - 기존 데이터에서 선택
|
||||
// - addRow: 빈 행 직접 추가 - 새 데이터 직접 입력
|
||||
// 버튼 표시 설정 (동시 표시 가능)
|
||||
showSearchButton?: boolean; // 검색 버튼 표시 (기본: true)
|
||||
showAddRowButton?: boolean; // 행 추가 버튼 표시 (기본: false)
|
||||
searchButtonText?: string; // 검색 버튼 텍스트 (기본: "품목 검색")
|
||||
addRowButtonText?: string; // 행 추가 버튼 텍스트 (기본: "직접 입력")
|
||||
|
||||
// 레거시 호환용 (deprecated)
|
||||
addButtonType?: "search" | "addRow";
|
||||
addButtonText?: string;
|
||||
};
|
||||
|
||||
// 7. 조건부 테이블 설정 (고급)
|
||||
|
|
|
|||
|
|
@ -1491,6 +1491,7 @@ export class ButtonActionExecutor {
|
|||
* 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리
|
||||
* 범용_폼_모달 내부의 공통 필드 + _tableSection_ 데이터를 병합하여 품목별로 저장
|
||||
* 수정 모드: INSERT/UPDATE/DELETE 지원
|
||||
* 🆕 섹션별 저장 테이블(targetTable) 지원 추가
|
||||
*/
|
||||
private static async handleUniversalFormModalTableSectionSave(
|
||||
config: ButtonActionConfig,
|
||||
|
|
@ -1514,7 +1515,66 @@ export class ButtonActionExecutor {
|
|||
console.log("🎯 [handleUniversalFormModalTableSectionSave] Universal Form Modal 감지:", universalFormModalKey);
|
||||
|
||||
const modalData = formData[universalFormModalKey];
|
||||
|
||||
|
||||
// 🆕 universal-form-modal 컴포넌트 설정 가져오기
|
||||
// 1. componentConfigs에서 컴포넌트 ID로 찾기
|
||||
// 2. allComponents에서 columnName으로 찾기
|
||||
// 3. 화면 레이아웃 API에서 가져오기
|
||||
let modalComponentConfig = context.componentConfigs?.[universalFormModalKey];
|
||||
|
||||
// componentConfigs에서 직접 찾지 못한 경우, allComponents에서 columnName으로 찾기
|
||||
if (!modalComponentConfig && context.allComponents) {
|
||||
const modalComponent = context.allComponents.find(
|
||||
(comp: any) =>
|
||||
comp.columnName === universalFormModalKey || comp.properties?.columnName === universalFormModalKey,
|
||||
);
|
||||
if (modalComponent) {
|
||||
modalComponentConfig = modalComponent.componentConfig || modalComponent.properties?.componentConfig;
|
||||
console.log("🎯 [handleUniversalFormModalTableSectionSave] allComponents에서 설정 찾음:", modalComponent.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 아직도 설정을 찾지 못했으면 화면 레이아웃 API에서 가져오기
|
||||
if (!modalComponentConfig && screenId) {
|
||||
try {
|
||||
console.log("🔍 [handleUniversalFormModalTableSectionSave] 화면 레이아웃 API에서 설정 조회:", screenId);
|
||||
const { screenApi } = await import("@/lib/api/screen");
|
||||
const layoutData = await screenApi.getLayout(screenId);
|
||||
|
||||
if (layoutData && layoutData.components) {
|
||||
// 레이아웃에서 universal-form-modal 컴포넌트 찾기
|
||||
const modalLayout = (layoutData.components as any[]).find(
|
||||
(comp) =>
|
||||
comp.properties?.columnName === universalFormModalKey || comp.columnName === universalFormModalKey,
|
||||
);
|
||||
if (modalLayout) {
|
||||
modalComponentConfig = modalLayout.properties?.componentConfig || modalLayout.componentConfig;
|
||||
console.log(
|
||||
"🎯 [handleUniversalFormModalTableSectionSave] 화면 레이아웃에서 설정 찾음:",
|
||||
modalLayout.componentId,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ [handleUniversalFormModalTableSectionSave] 화면 레이아웃 조회 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const sections: any[] = modalComponentConfig?.sections || [];
|
||||
const saveConfig = modalComponentConfig?.saveConfig || {};
|
||||
|
||||
console.log("🎯 [handleUniversalFormModalTableSectionSave] 컴포넌트 설정:", {
|
||||
hasComponentConfig: !!modalComponentConfig,
|
||||
sectionsCount: sections.length,
|
||||
mainTableName: saveConfig.tableName || tableName,
|
||||
sectionSaveModes: saveConfig.sectionSaveModes,
|
||||
sectionDetails: sections.map((s: any) => ({
|
||||
id: s.id,
|
||||
type: s.type,
|
||||
targetTable: s.tableConfig?.saveConfig?.targetTable,
|
||||
})),
|
||||
});
|
||||
|
||||
// _tableSection_ 데이터 추출
|
||||
const tableSectionData: Record<string, any[]> = {};
|
||||
const commonFieldsData: Record<string, any> = {};
|
||||
|
|
@ -1564,10 +1624,64 @@ export class ButtonActionExecutor {
|
|||
let insertedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let deletedCount = 0;
|
||||
let mainRecordId: number | null = null;
|
||||
|
||||
// 🆕 먼저 메인 테이블에 공통 데이터 저장 (별도 테이블이 있는 경우에만)
|
||||
const hasSeparateTargetTable = sections.some(
|
||||
(s) =>
|
||||
s.type === "table" &&
|
||||
s.tableConfig?.saveConfig?.targetTable &&
|
||||
s.tableConfig.saveConfig.targetTable !== tableName,
|
||||
);
|
||||
|
||||
if (hasSeparateTargetTable && Object.keys(commonFieldsData).length > 0) {
|
||||
console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블에 공통 데이터 저장:", tableName);
|
||||
|
||||
const mainRowToSave = { ...commonFieldsData, ...userInfo };
|
||||
|
||||
// 메타데이터 제거
|
||||
Object.keys(mainRowToSave).forEach((key) => {
|
||||
if (key.startsWith("_")) {
|
||||
delete mainRowToSave[key];
|
||||
}
|
||||
});
|
||||
|
||||
console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave);
|
||||
|
||||
const mainSaveResult = await DynamicFormApi.saveFormData({
|
||||
screenId: screenId!,
|
||||
tableName: tableName!,
|
||||
data: mainRowToSave,
|
||||
});
|
||||
|
||||
if (!mainSaveResult.success) {
|
||||
throw new Error(mainSaveResult.message || "메인 데이터 저장 실패");
|
||||
}
|
||||
|
||||
mainRecordId = mainSaveResult.data?.id || null;
|
||||
console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId);
|
||||
}
|
||||
|
||||
// 각 테이블 섹션 처리
|
||||
for (const [sectionId, currentItems] of Object.entries(tableSectionData)) {
|
||||
console.log(`🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 처리 시작: ${currentItems.length}개 품목`);
|
||||
console.log(
|
||||
`🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 처리 시작: ${currentItems.length}개 품목`,
|
||||
);
|
||||
|
||||
// 🆕 해당 섹션의 설정 찾기
|
||||
const sectionConfig = sections.find((s) => s.id === sectionId);
|
||||
const targetTableName = sectionConfig?.tableConfig?.saveConfig?.targetTable;
|
||||
|
||||
// 🆕 실제 저장할 테이블 결정
|
||||
// - targetTable이 있으면 해당 테이블에 저장
|
||||
// - targetTable이 없으면 메인 테이블에 저장
|
||||
const saveTableName = targetTableName || tableName!;
|
||||
|
||||
console.log(`📊 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 저장 테이블:`, {
|
||||
targetTableName,
|
||||
saveTableName,
|
||||
isMainTable: saveTableName === tableName,
|
||||
});
|
||||
|
||||
// 1️⃣ 신규 품목 INSERT (id가 없는 항목)
|
||||
const newItems = currentItems.filter((item) => !item.id);
|
||||
|
|
@ -1581,11 +1695,16 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
});
|
||||
|
||||
console.log("➕ [INSERT] 신규 품목:", rowToSave);
|
||||
// 🆕 메인 레코드 ID 연결 (별도 테이블에 저장하는 경우)
|
||||
if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
|
||||
rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
|
||||
}
|
||||
|
||||
console.log("➕ [INSERT] 신규 품목:", { tableName: saveTableName, data: rowToSave });
|
||||
|
||||
const saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId: screenId!,
|
||||
tableName: tableName!,
|
||||
tableName: saveTableName,
|
||||
data: rowToSave,
|
||||
});
|
||||
|
||||
|
|
@ -1612,9 +1731,14 @@ export class ButtonActionExecutor {
|
|||
});
|
||||
delete rowToSave.id; // id 제거하여 INSERT
|
||||
|
||||
// 🆕 메인 레코드 ID 연결 (별도 테이블에 저장하는 경우)
|
||||
if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
|
||||
rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
|
||||
}
|
||||
|
||||
const saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId: screenId!,
|
||||
tableName: tableName!,
|
||||
tableName: saveTableName,
|
||||
data: rowToSave,
|
||||
});
|
||||
|
||||
|
|
@ -1631,14 +1755,14 @@ export class ButtonActionExecutor {
|
|||
const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon);
|
||||
|
||||
if (hasChanges) {
|
||||
console.log(`🔄 [UPDATE] 품목 수정: id=${item.id}`);
|
||||
console.log(`🔄 [UPDATE] 품목 수정: id=${item.id}, tableName=${saveTableName}`);
|
||||
|
||||
// 변경된 필드만 추출하여 부분 업데이트
|
||||
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
||||
item.id,
|
||||
originalItem,
|
||||
currentDataWithCommon,
|
||||
tableName!,
|
||||
saveTableName,
|
||||
);
|
||||
|
||||
if (!updateResult.success) {
|
||||
|
|
@ -1656,9 +1780,9 @@ export class ButtonActionExecutor {
|
|||
const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id));
|
||||
|
||||
for (const deletedItem of deletedItems) {
|
||||
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}`);
|
||||
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`);
|
||||
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(tableName!, deletedItem.id);
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(saveTableName, deletedItem.id);
|
||||
|
||||
if (!deleteResult.success) {
|
||||
throw new Error(deleteResult.message || "품목 삭제 실패");
|
||||
|
|
@ -1670,6 +1794,7 @@ export class ButtonActionExecutor {
|
|||
|
||||
// 결과 메시지 생성
|
||||
const resultParts: string[] = [];
|
||||
if (mainRecordId) resultParts.push("메인 데이터 저장");
|
||||
if (insertedCount > 0) resultParts.push(`${insertedCount}개 추가`);
|
||||
if (updatedCount > 0) resultParts.push(`${updatedCount}개 수정`);
|
||||
if (deletedCount > 0) resultParts.push(`${deletedCount}개 삭제`);
|
||||
|
|
@ -2145,17 +2270,20 @@ export class ButtonActionExecutor {
|
|||
* 연관 데이터 버튼의 선택 데이터로 모달 열기
|
||||
* RelatedDataButtons 컴포넌트에서 선택된 버튼 데이터를 모달로 전달
|
||||
*/
|
||||
private static async handleOpenRelatedModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
private static async handleOpenRelatedModal(
|
||||
config: ButtonActionConfig,
|
||||
context: ButtonActionContext,
|
||||
): Promise<boolean> {
|
||||
// 버튼 설정에서 targetScreenId 가져오기 (여러 위치에서 확인)
|
||||
const targetScreenId = config.relatedModalConfig?.targetScreenId || config.targetScreenId;
|
||||
|
||||
|
||||
console.log("🔍 [openRelatedModal] 설정 확인:", {
|
||||
config,
|
||||
relatedModalConfig: config.relatedModalConfig,
|
||||
targetScreenId: config.targetScreenId,
|
||||
finalTargetScreenId: targetScreenId,
|
||||
});
|
||||
|
||||
|
||||
if (!targetScreenId) {
|
||||
console.error("❌ [openRelatedModal] targetScreenId가 설정되지 않았습니다.");
|
||||
toast.error("모달 화면 ID가 설정되지 않았습니다.");
|
||||
|
|
@ -2164,13 +2292,13 @@ export class ButtonActionExecutor {
|
|||
|
||||
// RelatedDataButtons에서 선택된 데이터 가져오기
|
||||
const relatedData = window.__relatedButtonsSelectedData;
|
||||
|
||||
|
||||
console.log("🔍 [openRelatedModal] RelatedDataButtons 데이터:", {
|
||||
relatedData,
|
||||
selectedItem: relatedData?.selectedItem,
|
||||
config: relatedData?.config,
|
||||
});
|
||||
|
||||
|
||||
if (!relatedData?.selectedItem) {
|
||||
console.warn("⚠️ [openRelatedModal] 선택된 버튼이 없습니다.");
|
||||
toast.warning("먼저 버튼을 선택해주세요.");
|
||||
|
|
@ -2181,14 +2309,14 @@ export class ButtonActionExecutor {
|
|||
|
||||
// 데이터 매핑 적용
|
||||
const initialData: Record<string, any> = {};
|
||||
|
||||
|
||||
console.log("🔍 [openRelatedModal] 매핑 설정:", {
|
||||
modalLink: relatedConfig?.modalLink,
|
||||
dataMapping: relatedConfig?.modalLink?.dataMapping,
|
||||
});
|
||||
|
||||
|
||||
if (relatedConfig?.modalLink?.dataMapping && relatedConfig.modalLink.dataMapping.length > 0) {
|
||||
relatedConfig.modalLink.dataMapping.forEach(mapping => {
|
||||
relatedConfig.modalLink.dataMapping.forEach((mapping) => {
|
||||
console.log("🔍 [openRelatedModal] 매핑 처리:", {
|
||||
mapping,
|
||||
sourceField: mapping.sourceField,
|
||||
|
|
@ -2197,7 +2325,7 @@ export class ButtonActionExecutor {
|
|||
selectedItemId: selectedItem.id,
|
||||
rawDataValue: selectedItem.rawData[mapping.sourceField],
|
||||
});
|
||||
|
||||
|
||||
if (mapping.sourceField === "value") {
|
||||
initialData[mapping.targetField] = selectedItem.value;
|
||||
} else if (mapping.sourceField === "id") {
|
||||
|
|
@ -2219,18 +2347,20 @@ export class ButtonActionExecutor {
|
|||
});
|
||||
|
||||
// 모달 열기 이벤트 발생 (ScreenModal은 editData를 사용)
|
||||
window.dispatchEvent(new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
screenId: targetScreenId,
|
||||
title: config.modalTitle,
|
||||
description: config.modalDescription,
|
||||
editData: initialData, // ScreenModal은 editData로 폼 데이터를 받음
|
||||
onSuccess: () => {
|
||||
// 성공 후 데이터 새로고침
|
||||
window.dispatchEvent(new CustomEvent("refreshTableData"));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
screenId: targetScreenId,
|
||||
title: config.modalTitle,
|
||||
description: config.modalDescription,
|
||||
editData: initialData, // ScreenModal은 editData로 폼 데이터를 받음
|
||||
onSuccess: () => {
|
||||
// 성공 후 데이터 새로고침
|
||||
window.dispatchEvent(new CustomEvent("refreshTableData"));
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -3296,10 +3426,7 @@ export class ButtonActionExecutor {
|
|||
* EditModal 등 외부에서도 호출 가능하도록 public으로 변경
|
||||
* 다중 제어 순차 실행 지원
|
||||
*/
|
||||
public static async executeAfterSaveControl(
|
||||
config: ButtonActionConfig,
|
||||
context: ButtonActionContext,
|
||||
): Promise<void> {
|
||||
public static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<void> {
|
||||
console.log("🎯 저장 후 제어 실행:", {
|
||||
enableDataflowControl: config.enableDataflowControl,
|
||||
dataflowConfig: config.dataflowConfig,
|
||||
|
|
@ -4742,7 +4869,7 @@ export class ButtonActionExecutor {
|
|||
|
||||
// 추적 중인지 확인 (새로고침 후에도 DB 상태 기반 종료 가능하도록 수정)
|
||||
const isTrackingActive = !!this.trackingIntervalId;
|
||||
|
||||
|
||||
if (!isTrackingActive) {
|
||||
// 추적 중이 아니어도 DB 상태 변경은 진행 (새로고침 후 종료 지원)
|
||||
console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행");
|
||||
|
|
@ -4758,25 +4885,26 @@ export class ButtonActionExecutor {
|
|||
let dbDeparture: string | null = null;
|
||||
let dbArrival: string | null = null;
|
||||
let dbVehicleId: string | null = null;
|
||||
|
||||
|
||||
const userId = context.userId || this.trackingUserId;
|
||||
if (userId) {
|
||||
try {
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const statusTableName = config.trackingStatusTableName || this.trackingConfig?.trackingStatusTableName || context.tableName || "vehicles";
|
||||
const statusTableName =
|
||||
config.trackingStatusTableName ||
|
||||
this.trackingConfig?.trackingStatusTableName ||
|
||||
context.tableName ||
|
||||
"vehicles";
|
||||
const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id";
|
||||
|
||||
|
||||
// DB에서 현재 차량 정보 조회
|
||||
const vehicleResponse = await apiClient.post(
|
||||
`/table-management/tables/${statusTableName}/data`,
|
||||
{
|
||||
page: 1,
|
||||
size: 1,
|
||||
search: { [keyField]: userId },
|
||||
autoFilter: true,
|
||||
},
|
||||
);
|
||||
|
||||
const vehicleResponse = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
|
||||
page: 1,
|
||||
size: 1,
|
||||
search: { [keyField]: userId },
|
||||
autoFilter: true,
|
||||
});
|
||||
|
||||
const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0];
|
||||
if (vehicleData) {
|
||||
dbDeparture = vehicleData.departure || null;
|
||||
|
|
@ -4792,14 +4920,18 @@ export class ButtonActionExecutor {
|
|||
// 마지막 위치 저장 (추적 중이었던 경우에만)
|
||||
if (isTrackingActive) {
|
||||
// DB 값 우선, 없으면 formData 사용
|
||||
const departure = dbDeparture ||
|
||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null;
|
||||
const arrival = dbArrival ||
|
||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null;
|
||||
const departure =
|
||||
dbDeparture ||
|
||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] ||
|
||||
null;
|
||||
const arrival =
|
||||
dbArrival || this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null;
|
||||
const departureName = this.trackingContext?.formData?.["departure_name"] || null;
|
||||
const destinationName = this.trackingContext?.formData?.["destination_name"] || null;
|
||||
const vehicleId = dbVehicleId ||
|
||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null;
|
||||
const vehicleId =
|
||||
dbVehicleId ||
|
||||
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] ||
|
||||
null;
|
||||
|
||||
await this.saveLocationToHistory(
|
||||
tripId,
|
||||
|
|
@ -5681,10 +5813,10 @@ export class ButtonActionExecutor {
|
|||
const columnMappings = quickInsertConfig.columnMappings || [];
|
||||
|
||||
for (const mapping of columnMappings) {
|
||||
console.log(`📍 매핑 처리 시작:`, mapping);
|
||||
|
||||
console.log("📍 매핑 처리 시작:", mapping);
|
||||
|
||||
if (!mapping.targetColumn) {
|
||||
console.log(`📍 targetColumn 없음, 스킵`);
|
||||
console.log("📍 targetColumn 없음, 스킵");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -5692,12 +5824,12 @@ export class ButtonActionExecutor {
|
|||
|
||||
switch (mapping.sourceType) {
|
||||
case "component":
|
||||
console.log(`📍 component 타입 처리:`, {
|
||||
console.log("📍 component 타입 처리:", {
|
||||
sourceComponentId: mapping.sourceComponentId,
|
||||
sourceColumnName: mapping.sourceColumnName,
|
||||
targetColumn: mapping.targetColumn,
|
||||
});
|
||||
|
||||
|
||||
// 컴포넌트의 현재 값
|
||||
if (mapping.sourceComponentId) {
|
||||
// 1. sourceColumnName이 있으면 직접 사용 (가장 확실한 방법)
|
||||
|
|
@ -5705,34 +5837,34 @@ export class ButtonActionExecutor {
|
|||
value = formData?.[mapping.sourceColumnName];
|
||||
console.log(`📍 방법1 (sourceColumnName): ${mapping.sourceColumnName} = ${value}`);
|
||||
}
|
||||
|
||||
|
||||
// 2. 없으면 컴포넌트 ID로 직접 찾기
|
||||
if (value === undefined) {
|
||||
value = formData?.[mapping.sourceComponentId];
|
||||
console.log(`📍 방법2 (sourceComponentId): ${mapping.sourceComponentId} = ${value}`);
|
||||
}
|
||||
|
||||
|
||||
// 3. 없으면 allComponents에서 컴포넌트를 찾아 columnName으로 시도
|
||||
if (value === undefined && context.allComponents) {
|
||||
const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
|
||||
console.log(`📍 방법3 찾은 컴포넌트:`, comp);
|
||||
console.log("📍 방법3 찾은 컴포넌트:", comp);
|
||||
if (comp?.columnName) {
|
||||
value = formData?.[comp.columnName];
|
||||
console.log(`📍 방법3 (allComponents): ${mapping.sourceComponentId} → ${comp.columnName} = ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. targetColumn과 같은 이름의 키가 formData에 있으면 사용 (폴백)
|
||||
if (value === undefined && mapping.targetColumn && formData?.[mapping.targetColumn] !== undefined) {
|
||||
value = formData[mapping.targetColumn];
|
||||
console.log(`📍 방법4 (targetColumn 폴백): ${mapping.targetColumn} = ${value}`);
|
||||
}
|
||||
|
||||
|
||||
// 5. 그래도 없으면 formData의 모든 키를 확인하고 로깅
|
||||
if (value === undefined) {
|
||||
console.log("📍 방법5: formData에서 값을 찾지 못함. formData 키들:", Object.keys(formData || {}));
|
||||
}
|
||||
|
||||
|
||||
// sourceColumn이 지정된 경우 해당 속성 추출
|
||||
if (mapping.sourceColumn && value && typeof value === "object") {
|
||||
value = value[mapping.sourceColumn];
|
||||
|
|
@ -5742,7 +5874,7 @@ export class ButtonActionExecutor {
|
|||
break;
|
||||
|
||||
case "leftPanel":
|
||||
console.log(`📍 leftPanel 타입 처리:`, {
|
||||
console.log("📍 leftPanel 타입 처리:", {
|
||||
sourceColumn: mapping.sourceColumn,
|
||||
selectedLeftData: splitPanelContext?.selectedLeftData,
|
||||
});
|
||||
|
|
@ -5775,18 +5907,18 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
console.log(`📍 currentUser 값: ${value}`);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
console.log(`📍 알 수 없는 sourceType: ${mapping.sourceType}`);
|
||||
}
|
||||
|
||||
console.log(`📍 매핑 결과: targetColumn=${mapping.targetColumn}, value=${value}, type=${typeof value}`);
|
||||
|
||||
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
insertData[mapping.targetColumn] = value;
|
||||
console.log(`📍 insertData에 추가됨: ${mapping.targetColumn} = ${value}`);
|
||||
} else {
|
||||
console.log(`📍 값이 비어있어서 insertData에 추가 안됨`);
|
||||
console.log("📍 값이 비어있어서 insertData에 추가 안됨");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5794,12 +5926,12 @@ export class ButtonActionExecutor {
|
|||
if (splitPanelContext?.selectedLeftData) {
|
||||
const leftData = splitPanelContext.selectedLeftData;
|
||||
console.log("📍 좌측 패널 자동 매핑 시작:", leftData);
|
||||
|
||||
|
||||
// 대상 테이블의 컬럼 목록 조회
|
||||
let targetTableColumns: string[] = [];
|
||||
try {
|
||||
const columnsResponse = await apiClient.get(
|
||||
`/table-management/tables/${quickInsertConfig.targetTable}/columns`
|
||||
`/table-management/tables/${quickInsertConfig.targetTable}/columns`,
|
||||
);
|
||||
if (columnsResponse.data?.success && columnsResponse.data?.data) {
|
||||
const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
|
||||
|
|
@ -5809,35 +5941,35 @@ export class ButtonActionExecutor {
|
|||
} catch (error) {
|
||||
console.error("대상 테이블 컬럼 조회 실패:", error);
|
||||
}
|
||||
|
||||
|
||||
for (const [key, val] of Object.entries(leftData)) {
|
||||
// 이미 매핑된 컬럼은 스킵
|
||||
if (insertData[key] !== undefined) {
|
||||
console.log(`📍 자동 매핑 스킵 (이미 존재): ${key}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 대상 테이블에 해당 컬럼이 없으면 스킵
|
||||
if (targetTableColumns.length > 0 && !targetTableColumns.includes(key)) {
|
||||
console.log(`📍 자동 매핑 스킵 (대상 테이블에 없는 컬럼): ${key}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 시스템 컬럼 제외 (id, created_date, updated_date, writer 등)
|
||||
const systemColumns = ['id', 'created_date', 'updated_date', 'writer', 'writer_name'];
|
||||
const systemColumns = ["id", "created_date", "updated_date", "writer", "writer_name"];
|
||||
if (systemColumns.includes(key)) {
|
||||
console.log(`📍 자동 매핑 스킵 (시스템 컬럼): ${key}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// _label, _name 으로 끝나는 표시용 컬럼 제외
|
||||
if (key.endsWith('_label') || key.endsWith('_name')) {
|
||||
if (key.endsWith("_label") || key.endsWith("_name")) {
|
||||
console.log(`📍 자동 매핑 스킵 (표시용 컬럼): ${key}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 값이 있으면 자동 추가
|
||||
if (val !== undefined && val !== null && val !== '') {
|
||||
if (val !== undefined && val !== null && val !== "") {
|
||||
insertData[key] = val;
|
||||
console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
|
||||
}
|
||||
|
|
@ -5857,7 +5989,7 @@ export class ButtonActionExecutor {
|
|||
enabled: quickInsertConfig.duplicateCheck?.enabled,
|
||||
columns: quickInsertConfig.duplicateCheck?.columns,
|
||||
});
|
||||
|
||||
|
||||
if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) {
|
||||
const duplicateCheckData: Record<string, any> = {};
|
||||
for (const col of quickInsertConfig.duplicateCheck.columns) {
|
||||
|
|
@ -5877,15 +6009,20 @@ export class ButtonActionExecutor {
|
|||
page: 1,
|
||||
pageSize: 1,
|
||||
search: duplicateCheckData,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
console.log("📍 중복 체크 응답:", checkResponse.data);
|
||||
|
||||
// 응답 구조: { success: true, data: { data: [...], total: N } } 또는 { success: true, data: [...] }
|
||||
const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || [];
|
||||
console.log("📍 기존 데이터:", existingData, "길이:", Array.isArray(existingData) ? existingData.length : 0);
|
||||
|
||||
console.log(
|
||||
"📍 기존 데이터:",
|
||||
existingData,
|
||||
"길이:",
|
||||
Array.isArray(existingData) ? existingData.length : 0,
|
||||
);
|
||||
|
||||
if (Array.isArray(existingData) && existingData.length > 0) {
|
||||
toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
|
||||
return false;
|
||||
|
|
@ -5902,20 +6039,20 @@ export class ButtonActionExecutor {
|
|||
// 데이터 저장
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
|
||||
insertData
|
||||
insertData,
|
||||
);
|
||||
|
||||
if (response.data?.success) {
|
||||
console.log("✅ Quick Insert 저장 성공");
|
||||
|
||||
|
||||
// 저장 후 동작 설정 로그
|
||||
console.log("📍 afterInsert 설정:", quickInsertConfig.afterInsert);
|
||||
|
||||
|
||||
// 🆕 데이터 새로고침 (테이블리스트, 카드 디스플레이 컴포넌트 새로고침)
|
||||
// refreshData가 명시적으로 false가 아니면 기본적으로 새로고침 실행
|
||||
const shouldRefresh = quickInsertConfig.afterInsert?.refreshData !== false;
|
||||
console.log("📍 데이터 새로고침 여부:", shouldRefresh);
|
||||
|
||||
|
||||
if (shouldRefresh) {
|
||||
console.log("📍 데이터 새로고침 이벤트 발송");
|
||||
// 전역 이벤트로 테이블/카드 컴포넌트들에게 새로고침 알림
|
||||
|
|
|
|||
Loading…
Reference in New Issue