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:
kjs 2025-11-25 15:55:05 +09:00
parent a0180d66a2
commit a1819e749c
11 changed files with 160 additions and 21 deletions

View File

@ -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] 채번 규칙 생성 성공:", {

View File

@ -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, // 🆕 테이블명 추가
};
}

View File

@ -101,6 +101,7 @@ export interface LayoutData {
components: ComponentData[];
gridSettings?: GridSettings;
screenResolution?: ScreenResolution;
tableName?: string; // 🆕 화면에 연결된 테이블명
}
// 그리드 설정

View File

@ -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 설정 (필터링용)
};

View File

@ -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>
);
}

View File

@ -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} // ✅ 회사 코드 전달

View File

@ -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 }));
}

View File

@ -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>

View File

@ -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]"
/>

View File

@ -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>

View File

@ -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) => {