feature/v2-unified-renewal #379

Merged
kjs merged 145 commits from feature/v2-unified-renewal into main 2026-02-03 12:11:19 +09:00
3 changed files with 154 additions and 55 deletions
Showing only changes of commit ce85528ddf - Show all commits

View File

@ -10,7 +10,7 @@
| 기능 | 적용 완료 | 미적용 | 해당없음 |
| -------------------------- | --------- | ------ | -------- |
| **다국어 지원** | 3개 | 10개 | 3개 |
| **컴포넌트별 테이블 설정** | 5개 | 5개 | 6개 |
| **컴포넌트별 테이블 설정** | 6개 | 4개 | 6개 |
---
@ -47,8 +47,8 @@
### 레이아웃 (Layout) - 5개
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
| ----------------- | :---------: | :---------: | --------------------------------------------- |
| **분할 패널** | ✅ 적용 | ⚠️ 부분 | 다국어 지원, 테이블 설정은 하위 패널에서 처리 |
| ----------------- | :---------: | :---------: | ----------------------------------------------------- |
| **분할 패널** | ✅ 적용 | ✅ 적용 | 다국어 지원, 좌우 패널 각각 Combobox UI로 테이블 선택 |
| **탭 컴포넌트** | ❌ 미적용 | 해당없음 | 화면 전환용 컨테이너 |
| **Section Card** | ❌ 미적용 | 해당없음 | 그룹화 컨테이너 |
| **Section Paper** | ❌ 미적용 | 해당없음 | 그룹화 컨테이너 |
@ -95,19 +95,19 @@
**완전 적용 (5개)**
| 컴포넌트 | 적용 방식 |
| ------------------ | --------------------------------------------------------------------------------- |
| -------------------- | --------------------------------------------------------------------------------- |
| `table-list` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable`, `isReadOnly` 지원 |
| `unified-repeater` | Combobox UI로 테이블 선택, `mainTableName`, `foreignKeyColumn` 지원, FK 자동 연결 |
| `unified-list` | `TableListConfigPanel` 래핑하여 동일 기능 제공 |
| `card-display` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable` 지원 |
| `split-panel-layout` | 좌우 패널 각각 Combobox UI로 테이블 선택, 다국어 지원 |
**부분 적용 (5개)**
**부분 적용 (4개)**
| 컴포넌트 | 현재 상태 | 필요 작업 |
| ------------------------ | --------------------------- | --------------------- |
| `pivot-grid` | `tableName` 설정 가능 | Combobox UI 추가 필요 |
| `repeat-screen-modal` | `tableName` 설정 가능 | Combobox UI 추가 필요 |
| `split-panel-layout` | 하위 패널에서 처리 | 하위 컴포넌트에 위임 |
| `location-swap-selector` | `customTableName` 지원 | Combobox UI 추가 필요 |
| `table-search-widget` | `screenTableName` 자동 감지 | 현재 방식 유지 가능 |

View File

@ -388,9 +388,9 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
)}
</div>
{/* ===== 4. 컬럼 매핑 (조인 키) ===== */}
{/* ===== 4. 컬럼 매핑 (연결 키) ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> ( )</Label>
<Label className="text-xs font-semibold text-blue-600"> ( )</Label>
<p className="text-[10px] text-gray-500">
</p>
@ -1067,10 +1067,11 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
screenTableName, // 현재 화면의 테이블명
menuObjid, // 🆕 메뉴 OBJID
}) => {
const [leftTableOpen, setLeftTableOpen] = useState(false); // 🆕 좌측 테이블 Combobox 상태
const [rightTableOpen, setRightTableOpen] = useState(false);
const [loadedTableColumns, setLoadedTableColumns] = useState<Record<string, ColumnInfo[]>>({});
const [loadingColumns, setLoadingColumns] = useState<Record<string, boolean>>({});
const [allTables, setAllTables] = useState<any[]>([]); // 조인 모드용 전체 테이블 목록
const [allTables, setAllTables] = useState<any[]>([]); // 테이블 목록
// 엔티티 참조 테이블 컬럼
type EntityRefTable = { tableName: string; columns: ColumnInfo[] };
const [entityReferenceTables, setEntityReferenceTables] = useState<Record<string, EntityRefTable[]>>({});
@ -1095,15 +1096,14 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
}
}, [config.leftPanel?.title, config.rightPanel?.title, isUserEditing]);
// 조인 모드일 때만 전체 테이블 목록 로드
// 전체 테이블 목록 항상 로드 (좌측/우측 모두 사용)
useEffect(() => {
if (relationshipType === "join") {
const loadAllTables = async () => {
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
console.log("✅ 분할패널 조인 모드: 전체 테이블 목록 로드", response.data.length, "개");
console.log("✅ 분할패널: 전체 테이블 목록 로드", response.data.length, "개");
setAllTables(response.data);
}
} catch (error) {
@ -1111,20 +1111,13 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
}
};
loadAllTables();
} else {
// 상세 모드일 때는 기본 테이블만 사용
setAllTables([]);
}
}, [relationshipType]);
}, []);
// screenTableName이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정
// 초기 로드 시 좌측 패널 테이블이 없으면 화면 테이블로 설정
useEffect(() => {
if (screenTableName) {
// 좌측 패널은 항상 현재 화면의 테이블 사용
if (config.leftPanel?.tableName !== screenTableName) {
if (screenTableName && !config.leftPanel?.tableName) {
updateLeftPanel({ tableName: screenTableName });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]);
@ -1345,8 +1338,13 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
return leftTableName ? loadedTableColumns[leftTableName] || [] : [];
}, [loadedTableColumns, leftTableName]);
// 우측 테이블명
const rightTableName = config.rightPanel?.tableName || "";
// 우측 테이블명 (상세 모드에서는 좌측과 동일)
const rightTableName = useMemo(() => {
if (relationshipType === "detail") {
return leftTableName; // 상세 모드에서는 좌측과 동일
}
return config.rightPanel?.tableName || "";
}, [relationshipType, leftTableName, config.rightPanel?.tableName]);
// 우측 테이블 컬럼 (로드된 컬럼 사용)
const rightTableColumns = useMemo(() => {
@ -1406,8 +1404,8 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
</SelectItem>
<SelectItem value="join">
<div className="flex flex-col">
<span className="font-medium">(JOIN)</span>
<span className="text-xs text-gray-500"> ( )</span>
<span className="font-medium"> (FILTERED)</span>
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
</SelectContent>
@ -1418,6 +1416,103 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
<div className="mt-4 space-y-4 border-t pt-4">
<h3 className="text-sm font-semibold"> ()</h3>
{/* 🆕 좌측 패널 테이블 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Popover open={leftTableOpen} onOpenChange={setLeftTableOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={leftTableOpen}
className="h-9 w-full justify-between text-xs"
>
<div className="flex items-center gap-2 truncate">
<Database className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">
{config.leftPanel?.tableName
? allTables.find((t) => (t.tableName || t.table_name) === config.leftPanel?.tableName)?.tableLabel ||
allTables.find((t) => (t.tableName || t.table_name) === config.leftPanel?.tableName)?.displayName ||
config.leftPanel?.tableName
: "테이블을 선택하세요"}
</span>
</div>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-3 text-center text-xs text-muted-foreground">
.
</CommandEmpty>
{/* 화면 기본 테이블 */}
{screenTableName && (
<CommandGroup heading="기본 (화면 테이블)">
<CommandItem
value={screenTableName}
onSelect={() => {
updateLeftPanel({ tableName: screenTableName, columns: [] });
setLeftTableOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.leftPanel?.tableName === screenTableName ? "opacity-100" : "opacity-0"
)}
/>
<Database className="mr-2 h-3.5 w-3.5 text-blue-500" />
{allTables.find((t) => (t.tableName || t.table_name) === screenTableName)?.tableLabel ||
allTables.find((t) => (t.tableName || t.table_name) === screenTableName)?.displayName ||
screenTableName}
</CommandItem>
</CommandGroup>
)}
{/* 전체 테이블 */}
<CommandGroup heading="전체 테이블">
{allTables
.filter((t) => (t.tableName || t.table_name) !== screenTableName)
.map((table) => {
const tableName = table.tableName || table.table_name;
const displayName = table.tableLabel || table.displayName || tableName;
return (
<CommandItem
key={tableName}
value={tableName}
onSelect={() => {
updateLeftPanel({ tableName, columns: [] });
setLeftTableOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.leftPanel?.tableName === tableName ? "opacity-100" : "opacity-0"
)}
/>
<Database className="mr-2 h-3.5 w-3.5 text-muted-foreground" />
<span className="truncate">{displayName}</span>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{config.leftPanel?.tableName && config.leftPanel?.tableName !== screenTableName && (
<p className="text-[10px] text-muted-foreground">
.
</p>
)}
</div>
<div className="space-y-2">
<Label> </Label>
<Input
@ -1661,7 +1756,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
{/* 우측 패널 설정 */}
<div className="mt-4 space-y-4 border-t pt-4">
<h3 className="text-sm font-semibold"> ({relationshipType === "detail" ? "상세" : "조"})</h3>
<h3 className="text-sm font-semibold"> ({relationshipType === "detail" ? "상세" : "조건 필터"})</h3>
<div className="space-y-2">
<Label> </Label>
@ -1706,9 +1801,9 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
</div>
</div>
) : (
// 조 모드: 전체 테이블에서 선택 가능
// 조건 필터 모드: 전체 테이블에서 선택 가능
<div className="space-y-2">
<Label> ( )</Label>
<Label> </Label>
<Popover open={rightTableOpen} onOpenChange={setRightTableOpen}>
<PopoverTrigger asChild>
<Button
@ -1814,12 +1909,12 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
</div>
)}
{/* 엔티티 설정 선택 - 조 모드에서만 표시 */}
{/* 엔티티 설정 선택 - 조건 필터 모드에서만 표시 */}
{relationshipType !== "detail" && (
<div className="space-y-2">
<Label className="text-xs font-medium"> ( )</Label>
<Label className="text-xs font-medium"> </Label>
<p className="text-muted-foreground text-[10px]">
</p>
<Select
value={config.rightPanel?.relation?.foreignKey || ""}

View File

@ -115,6 +115,8 @@ export interface SplitPanelLayoutConfig {
langKey?: string; // 다국어 키
panelHeaderHeight?: number; // 패널 상단 헤더 높이 (px)
tableName?: string; // 데이터베이스 테이블명
useCustomTable?: boolean; // 화면 기본 테이블이 아닌 다른 테이블 사용 여부
customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때)
dataSource?: string; // API 엔드포인트
displayMode?: "list" | "table"; // 표시 모드: 목록 또는 테이블
showSearch?: boolean;
@ -180,6 +182,8 @@ export interface SplitPanelLayoutConfig {
langKey?: string; // 다국어 키
panelHeaderHeight?: number; // 패널 상단 헤더 높이 (px)
tableName?: string;
useCustomTable?: boolean; // 화면 기본 테이블이 아닌 다른 테이블 사용 여부
customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때)
dataSource?: string;
displayMode?: "list" | "table"; // 표시 모드: 목록 또는 테이블
showSearch?: boolean;