fix: 탭 컴포넌트 menuObjid 전달, 카테고리 필터 복원, 설정 초기화 문제 해결
주요 수정사항: 1. 탭 컴포넌트 내 자식 화면에 menuObjid와 tableName 전달 - TabsWidget에 menuObjid prop 추가 - InteractiveScreenViewerDynamic를 통해 자식 화면에 전달 - 채번 규칙 생성 시 올바른 메뉴 스코프 및 테이블명 적용 2. 백엔드: 화면 레이아웃 API에 tableName 추가 - screenManagementService.getLayout()에서 테이블명 반환 - LayoutData 타입에 tableName 필드 추가 - 채번 규칙 생성 시 tableName 검증 강화 3. 카테고리 필터링 기능 복원 - DataFilterConfigPanel에 menuObjid 전달 - getCategoryValues API 사용으로 메뉴 스코프 적용 - 새로고침 후 카테고리 값 자동 재로드 - SplitPanelLayoutConfigPanel에 menuObjid 전달 4. 선택항목 상세입력 설정 패널 포커스 문제 해결 - 로컬 입력 상태 추가로 실시간 속성 편집 패턴 적용 - 텍스트 및 라벨 입력 시 포커스 유지 5. 테이블 리스트 설정 초기화 문제 해결 - handleChange 함수에서 기존 config와 병합하여 전달 - 다른 속성 손실 방지 (columns, dataFilter 등) 버그 수정: - 채번 규칙 생성 시 빈 문자열 대신 null 전달 - 필터 설정 변경 시 컬럼 설정 초기화 방지 - 카테고리 컬럼 선택 시 셀렉트박스 표시
This commit is contained in:
parent
a0180d66a2
commit
a1819e749c
|
|
@ -132,6 +132,16 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo
|
|||
return res.status(400).json({ success: false, error: "최소 1개 이상의 규칙 파트가 필요합니다" });
|
||||
}
|
||||
|
||||
// 🆕 scopeType이 'table'인 경우 tableName 필수 체크
|
||||
if (ruleConfig.scopeType === "table") {
|
||||
if (!ruleConfig.tableName || ruleConfig.tableName.trim() === "") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "테이블 범위 규칙은 테이블명(tableName)이 필수입니다",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId);
|
||||
|
||||
logger.info("✅ [POST /numbering-rules] 채번 규칙 생성 성공:", {
|
||||
|
|
|
|||
|
|
@ -1418,9 +1418,9 @@ export class ScreenManagementService {
|
|||
console.log(`=== 레이아웃 로드 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}`);
|
||||
|
||||
// 권한 확인
|
||||
const screens = await query<{ company_code: string | null }>(
|
||||
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
// 권한 확인 및 테이블명 조회
|
||||
const screens = await query<{ company_code: string | null; table_name: string | null }>(
|
||||
`SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
|
||||
[screenId]
|
||||
);
|
||||
|
||||
|
|
@ -1512,11 +1512,13 @@ export class ScreenManagementService {
|
|||
console.log(`반환할 컴포넌트 수: ${components.length}`);
|
||||
console.log(`최종 격자 설정:`, gridSettings);
|
||||
console.log(`최종 해상도 설정:`, screenResolution);
|
||||
console.log(`테이블명:`, existingScreen.table_name);
|
||||
|
||||
return {
|
||||
components,
|
||||
gridSettings,
|
||||
screenResolution,
|
||||
tableName: existingScreen.table_name, // 🆕 테이블명 추가
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export interface LayoutData {
|
|||
components: ComponentData[];
|
||||
gridSettings?: GridSettings;
|
||||
screenResolution?: ScreenResolution;
|
||||
tableName?: string; // 🆕 화면에 연결된 테이블명
|
||||
}
|
||||
|
||||
// 그리드 설정
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
|||
const ruleToSave = {
|
||||
...currentRule,
|
||||
scopeType: "table" as const, // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
||||
tableName: currentTableName || currentRule.tableName || "", // 현재 테이블명 자동 설정
|
||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 자동 설정 (빈 값은 null)
|
||||
menuObjid: menuObjid || currentRule.menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -433,7 +433,10 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
|||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<TabsWidget component={tabsComponent as any} />
|
||||
<TabsWidget
|
||||
component={tabsComponent as any}
|
||||
menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ interface InteractiveScreenViewerProps {
|
|||
id: number;
|
||||
tableName?: string;
|
||||
};
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
|
||||
onSave?: () => Promise<void>;
|
||||
onRefresh?: () => void;
|
||||
onFlowRefresh?: () => void;
|
||||
|
|
@ -57,6 +58,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
onFormDataChange,
|
||||
hideLabel = false,
|
||||
screenInfo,
|
||||
menuObjid,
|
||||
onSave,
|
||||
onRefresh,
|
||||
onFlowRefresh,
|
||||
|
|
@ -326,6 +328,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
onFormDataChange={handleFormDataChange}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ import { Switch } from "@/components/ui/switch";
|
|||
import { Trash2, Plus } from "lucide-react";
|
||||
import { ColumnFilter, DataFilterConfig } from "@/types/screen-management";
|
||||
import { UnifiedColumnInfo } from "@/types/table-management";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||
|
||||
interface DataFilterConfigPanelProps {
|
||||
tableName?: string;
|
||||
columns?: UnifiedColumnInfo[];
|
||||
config?: DataFilterConfig;
|
||||
onConfigChange: (config: DataFilterConfig) => void;
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,7 +28,15 @@ export function DataFilterConfigPanel({
|
|||
columns = [],
|
||||
config,
|
||||
onConfigChange,
|
||||
menuObjid, // 🆕 메뉴 OBJID
|
||||
}: DataFilterConfigPanelProps) {
|
||||
console.log("🔍 [DataFilterConfigPanel] 초기화:", {
|
||||
tableName,
|
||||
columnsCount: columns.length,
|
||||
menuObjid,
|
||||
sampleColumns: columns.slice(0, 3),
|
||||
});
|
||||
|
||||
const [localConfig, setLocalConfig] = useState<DataFilterConfig>(
|
||||
config || {
|
||||
enabled: false,
|
||||
|
|
@ -43,6 +52,14 @@ export function DataFilterConfigPanel({
|
|||
useEffect(() => {
|
||||
if (config) {
|
||||
setLocalConfig(config);
|
||||
|
||||
// 🆕 기존 필터 중 카테고리 타입인 것들의 값을 로드
|
||||
config.filters?.forEach((filter) => {
|
||||
if (filter.valueType === "category" && filter.columnName) {
|
||||
console.log("🔄 기존 카테고리 필터 감지, 값 로딩:", filter.columnName);
|
||||
loadCategoryValues(filter.columnName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
|
|
@ -55,20 +72,34 @@ export function DataFilterConfigPanel({
|
|||
setLoadingCategories(prev => ({ ...prev, [columnName]: true }));
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/table-categories/${tableName}/${columnName}/values`
|
||||
console.log("🔍 카테고리 값 로드 시작:", {
|
||||
tableName,
|
||||
columnName,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
const response = await getCategoryValues(
|
||||
tableName,
|
||||
columnName,
|
||||
false, // includeInactive
|
||||
menuObjid // 🆕 메뉴 OBJID 전달
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const values = response.data.data.map((item: any) => ({
|
||||
console.log("📦 카테고리 값 로드 응답:", response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const values = response.data.map((item: any) => ({
|
||||
value: item.valueCode,
|
||||
label: item.valueLabel,
|
||||
}));
|
||||
|
||||
console.log("✅ 카테고리 값 설정:", { columnName, valuesCount: values.length });
|
||||
setCategoryValues(prev => ({ ...prev, [columnName]: values }));
|
||||
} else {
|
||||
console.warn("⚠️ 카테고리 값 로드 실패 또는 데이터 없음:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`카테고리 값 로드 실패 (${columnName}):`, error);
|
||||
console.error(`❌ 카테고리 값 로드 실패 (${columnName}):`, error);
|
||||
} finally {
|
||||
setLoadingCategories(prev => ({ ...prev, [columnName]: false }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ interface TabsWidgetProps {
|
|||
component: TabsComponent;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
menuObjid?: number; // 🆕 부모 화면의 메뉴 OBJID
|
||||
}
|
||||
|
||||
export function TabsWidget({ component, className, style }: TabsWidgetProps) {
|
||||
export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) {
|
||||
const {
|
||||
tabs = [],
|
||||
defaultTab,
|
||||
|
|
@ -233,6 +234,11 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) {
|
|||
key={component.id}
|
||||
component={component}
|
||||
allComponents={components}
|
||||
screenInfo={{
|
||||
id: tab.screenId,
|
||||
tableName: layoutData.tableName,
|
||||
}}
|
||||
menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
// 🆕 필드 그룹 상태
|
||||
const [localFieldGroups, setLocalFieldGroups] = useState<FieldGroup[]>(config.fieldGroups || []);
|
||||
|
||||
// 🆕 표시 항목의 입력값을 위한 로컬 상태 (포커스 유지용)
|
||||
const [localDisplayItemInputs, setLocalDisplayItemInputs] = useState<Record<string, Record<number, { label?: string; value?: string }>>>({});
|
||||
|
||||
|
||||
// 🆕 그룹별 펼침/접힘 상태
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
|
|
@ -140,6 +143,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
loadColumns();
|
||||
}, [config.targetTable]);
|
||||
|
||||
// 🆕 필드 그룹 변경 시 로컬 입력 상태 동기화
|
||||
useEffect(() => {
|
||||
setLocalFieldGroups(config.fieldGroups || []);
|
||||
// 로컬 입력 상태는 기존 값 보존 (사용자가 입력 중인 값 유지)
|
||||
}, [config.fieldGroups]);
|
||||
|
||||
// 🆕 초기 렌더링 시 기존 필드들의 autoFillFromTable 컬럼 로드
|
||||
useEffect(() => {
|
||||
if (!localFields || localFields.length === 0) return;
|
||||
|
|
@ -1177,8 +1186,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
{/* 텍스트 설정 */}
|
||||
{item.type === "text" && (
|
||||
<Input
|
||||
value={item.value || ""}
|
||||
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { value: e.target.value })}
|
||||
value={
|
||||
localDisplayItemInputs[group.id]?.[itemIndex]?.value !== undefined
|
||||
? localDisplayItemInputs[group.id][itemIndex].value
|
||||
: item.value || ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
// 로컬 상태 즉시 업데이트 (포커스 유지)
|
||||
setLocalDisplayItemInputs(prev => ({
|
||||
...prev,
|
||||
[group.id]: {
|
||||
...prev[group.id],
|
||||
[itemIndex]: {
|
||||
...prev[group.id]?.[itemIndex],
|
||||
value: newValue
|
||||
}
|
||||
}
|
||||
}));
|
||||
// 실제 상태 업데이트
|
||||
updateDisplayItemInGroup(group.id, itemIndex, { value: newValue });
|
||||
}}
|
||||
placeholder="| , / , -"
|
||||
className="h-6 text-[9px] sm:text-[10px]"
|
||||
/>
|
||||
|
|
@ -1206,8 +1234,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
|
||||
{/* 라벨 */}
|
||||
<Input
|
||||
value={item.label || ""}
|
||||
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { label: e.target.value })}
|
||||
value={
|
||||
localDisplayItemInputs[group.id]?.[itemIndex]?.label !== undefined
|
||||
? localDisplayItemInputs[group.id][itemIndex].label
|
||||
: item.label || ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
// 로컬 상태 즉시 업데이트 (포커스 유지)
|
||||
setLocalDisplayItemInputs(prev => ({
|
||||
...prev,
|
||||
[group.id]: {
|
||||
...prev[group.id],
|
||||
[itemIndex]: {
|
||||
...prev[group.id]?.[itemIndex],
|
||||
label: newValue
|
||||
}
|
||||
}
|
||||
}));
|
||||
// 실제 상태 업데이트
|
||||
updateDisplayItemInGroup(group.id, itemIndex, { label: newValue });
|
||||
}}
|
||||
placeholder="라벨 (예: 거래처:)"
|
||||
className="h-6 w-full text-[9px] sm:text-[10px]"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface SplitPanelLayoutConfigPanelProps {
|
|||
onChange: (config: SplitPanelLayoutConfig) => void;
|
||||
tables?: TableInfo[]; // 전체 테이블 목록 (선택적)
|
||||
screenTableName?: string; // 현재 화면의 테이블명 (좌측 패널에서 사용)
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -201,6 +202,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
onChange,
|
||||
tables = [], // 기본값 빈 배열 (현재 화면 테이블만)
|
||||
screenTableName, // 현재 화면의 테이블명
|
||||
menuObjid, // 🆕 메뉴 OBJID
|
||||
}) => {
|
||||
const [rightTableOpen, setRightTableOpen] = useState(false);
|
||||
const [leftColumnOpen, setLeftColumnOpen] = useState(false);
|
||||
|
|
@ -211,9 +213,26 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
// 엔티티 참조 테이블 컬럼
|
||||
type EntityRefTable = { tableName: string; columns: ColumnInfo[] };
|
||||
const [entityReferenceTables, setEntityReferenceTables] = useState<Record<string, EntityRefTable[]>>({});
|
||||
|
||||
// 🆕 입력 필드용 로컬 상태
|
||||
const [isUserEditing, setIsUserEditing] = useState(false);
|
||||
const [localTitles, setLocalTitles] = useState({
|
||||
left: config.leftPanel?.title || "",
|
||||
right: config.rightPanel?.title || "",
|
||||
});
|
||||
|
||||
// 관계 타입
|
||||
const relationshipType = config.rightPanel?.relation?.type || "detail";
|
||||
|
||||
// config 변경 시 로컬 타이틀 동기화 (사용자가 입력 중이 아닐 때만)
|
||||
useEffect(() => {
|
||||
if (!isUserEditing) {
|
||||
setLocalTitles({
|
||||
left: config.leftPanel?.title || "",
|
||||
right: config.rightPanel?.title || "",
|
||||
});
|
||||
}
|
||||
}, [config.leftPanel?.title, config.rightPanel?.title, isUserEditing]);
|
||||
|
||||
// 조인 모드일 때만 전체 테이블 목록 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -568,8 +587,15 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<div className="space-y-2">
|
||||
<Label>패널 제목</Label>
|
||||
<Input
|
||||
value={config.leftPanel?.title || ""}
|
||||
onChange={(e) => updateLeftPanel({ title: e.target.value })}
|
||||
value={localTitles.left}
|
||||
onChange={(e) => {
|
||||
setIsUserEditing(true);
|
||||
setLocalTitles(prev => ({ ...prev, left: e.target.value }));
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsUserEditing(false);
|
||||
updateLeftPanel({ title: localTitles.left });
|
||||
}}
|
||||
placeholder="좌측 패널 제목"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1345,6 +1371,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
} as any))}
|
||||
config={config.leftPanel?.dataFilter}
|
||||
onConfigChange={(dataFilter) => updateLeftPanel({ dataFilter })}
|
||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -1355,8 +1382,15 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<div className="space-y-2">
|
||||
<Label>패널 제목</Label>
|
||||
<Input
|
||||
value={config.rightPanel?.title || ""}
|
||||
onChange={(e) => updateRightPanel({ title: e.target.value })}
|
||||
value={localTitles.right}
|
||||
onChange={(e) => {
|
||||
setIsUserEditing(true);
|
||||
setLocalTitles(prev => ({ ...prev, right: e.target.value }));
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsUserEditing(false);
|
||||
updateRightPanel({ title: localTitles.right });
|
||||
}}
|
||||
placeholder="우측 패널 제목"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -2270,6 +2304,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
} as any))}
|
||||
config={config.rightPanel?.dataFilter}
|
||||
onConfigChange={(dataFilter) => updateRightPanel({ dataFilter })}
|
||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -255,7 +255,8 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
}, [config.columns]);
|
||||
|
||||
const handleChange = (key: keyof TableListConfig, value: any) => {
|
||||
onChange({ [key]: value });
|
||||
// 기존 config와 병합하여 전달 (다른 속성 손실 방지)
|
||||
onChange({ ...config, [key]: value });
|
||||
};
|
||||
|
||||
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue