엔티티조인 읽기전용 컬럼 추가
This commit is contained in:
@@ -629,6 +629,10 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const fieldName = columnName || comp.id;
|
||||
const currentValue = formData[fieldName] || "";
|
||||
|
||||
// 🆕 엔티티 조인 컬럼은 읽기 전용으로 처리
|
||||
const isEntityJoin = (comp as any).isEntityJoin === true;
|
||||
const isReadonly = readonly || isEntityJoin;
|
||||
|
||||
// 다국어 라벨 적용 (langKey가 있으면 번역 텍스트 사용)
|
||||
const compLangKey = (comp as any).langKey;
|
||||
const label = compLangKey && translations[compLangKey] ? translations[compLangKey] : originalLabel;
|
||||
@@ -745,7 +749,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
placeholder={isAutoInput ? `자동입력: ${config?.autoValueType}` : finalPlaceholder}
|
||||
value={displayValue}
|
||||
onChange={isAutoInput ? undefined : handleInputChange}
|
||||
disabled={readonly || isAutoInput}
|
||||
disabled={isReadonly || isAutoInput}
|
||||
readOnly={isAutoInput}
|
||||
required={required}
|
||||
minLength={config?.minLength}
|
||||
@@ -786,7 +790,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.valueAsNumber || 0)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
min={config?.min}
|
||||
max={config?.max}
|
||||
@@ -825,7 +829,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
minLength={config?.minLength}
|
||||
maxLength={config?.maxLength}
|
||||
@@ -877,7 +881,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
value={currentValue}
|
||||
onChange={(value) => updateFormData(fieldName, value)}
|
||||
placeholder={finalPlaceholder}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
/>,
|
||||
);
|
||||
@@ -895,7 +899,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
value={currentValue}
|
||||
onChange={(value) => updateFormData(fieldName, value)}
|
||||
placeholder={finalPlaceholder}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
/>,
|
||||
);
|
||||
@@ -912,7 +916,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
<Select
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onValueChange={(value) => updateFormData(fieldName, value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className="h-full w-full">
|
||||
@@ -959,7 +963,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
id={fieldName}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => updateFormData(fieldName, checked)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
/>
|
||||
<label htmlFor={fieldName} className="text-sm">
|
||||
@@ -1005,7 +1009,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
value=""
|
||||
checked={selectedValue === ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
@@ -1023,7 +1027,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
value={option.value}
|
||||
checked={selectedValue === option.value}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly || option.disabled}
|
||||
disabled={isReadonly || option.disabled}
|
||||
required={required}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
@@ -1064,7 +1068,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
min={config?.minDate}
|
||||
max={config?.maxDate}
|
||||
@@ -1081,7 +1085,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-full w-full justify-start text-left font-normal"
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateValue ? format(dateValue, "PPP", { locale: ko }) : config?.defaultValue || finalPlaceholder}
|
||||
@@ -1124,7 +1128,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
min={config?.minDate}
|
||||
max={config?.maxDate}
|
||||
@@ -1301,7 +1305,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
type="file"
|
||||
data-field={fieldName}
|
||||
onChange={handleFileChange}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
accept={config?.accept}
|
||||
@@ -1409,7 +1413,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
<Select
|
||||
value={currentValue || ""}
|
||||
onValueChange={(value) => updateFormData(fieldName, value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className="h-full w-full">
|
||||
@@ -1947,7 +1951,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
return applyStyles(
|
||||
<button
|
||||
onClick={handleButtonClick}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
className={`focus:ring-ring w-full rounded-md px-3 py-2 text-sm font-medium transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none disabled:opacity-50 ${
|
||||
hasCustomColors
|
||||
? ""
|
||||
@@ -1972,7 +1976,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
placeholder={placeholder || "입력하세요..."}
|
||||
value={currentValue}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
className="w-full"
|
||||
style={{ height: "100%" }}
|
||||
|
||||
@@ -2727,14 +2727,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
componentWidth,
|
||||
});
|
||||
|
||||
// 엔티티 조인 컬럼인 경우 읽기 전용으로 설정
|
||||
const isEntityJoinColumn = column.isEntityJoin === true;
|
||||
|
||||
newComponent = {
|
||||
id: generateComponentId(),
|
||||
type: "component", // ✅ Unified 컴포넌트 시스템 사용
|
||||
label: column.columnLabel || column.columnName,
|
||||
tableName: table.tableName,
|
||||
columnName: column.columnName,
|
||||
required: column.required,
|
||||
readonly: false,
|
||||
required: isEntityJoinColumn ? false : column.required, // 조인 컬럼은 필수 아님
|
||||
readonly: isEntityJoinColumn, // 🆕 엔티티 조인 컬럼은 읽기 전용
|
||||
parentId: formContainerId, // 폼 컨테이너의 자식으로 설정
|
||||
componentType: unifiedMapping.componentType, // unified-input, unified-select 등
|
||||
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
||||
@@ -2744,6 +2747,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
column.codeCategory && {
|
||||
codeCategory: column.codeCategory,
|
||||
}),
|
||||
// 엔티티 조인 정보 저장
|
||||
...(isEntityJoinColumn && {
|
||||
isEntityJoin: true,
|
||||
entityJoinTable: column.entityJoinTable,
|
||||
entityJoinColumn: column.entityJoinColumn,
|
||||
}),
|
||||
style: {
|
||||
labelDisplay: false, // 라벨 숨김
|
||||
labelFontSize: "12px",
|
||||
@@ -2754,6 +2763,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
componentConfig: {
|
||||
type: unifiedMapping.componentType, // unified-input, unified-select 등
|
||||
...unifiedMapping.componentConfig, // Unified 컴포넌트 기본 설정
|
||||
...(isEntityJoinColumn && { disabled: true }), // 🆕 조인 컬럼은 비활성화
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -2784,14 +2794,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
componentWidth,
|
||||
});
|
||||
|
||||
// 엔티티 조인 컬럼인 경우 읽기 전용으로 설정
|
||||
const isEntityJoinColumn = column.isEntityJoin === true;
|
||||
|
||||
newComponent = {
|
||||
id: generateComponentId(),
|
||||
type: "component", // ✅ Unified 컴포넌트 시스템 사용
|
||||
label: column.columnLabel || column.columnName, // 컬럼 라벨 우선, 없으면 컬럼명
|
||||
tableName: table.tableName,
|
||||
columnName: column.columnName,
|
||||
required: column.required,
|
||||
readonly: false,
|
||||
required: isEntityJoinColumn ? false : column.required, // 조인 컬럼은 필수 아님
|
||||
readonly: isEntityJoinColumn, // 🆕 엔티티 조인 컬럼은 읽기 전용
|
||||
componentType: unifiedMapping.componentType, // unified-input, unified-select 등
|
||||
position: { x, y, z: 1 } as Position,
|
||||
size: { width: componentWidth, height: getDefaultHeight(column.widgetType) },
|
||||
@@ -2800,6 +2813,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
column.codeCategory && {
|
||||
codeCategory: column.codeCategory,
|
||||
}),
|
||||
// 엔티티 조인 정보 저장
|
||||
...(isEntityJoinColumn && {
|
||||
isEntityJoin: true,
|
||||
entityJoinTable: column.entityJoinTable,
|
||||
entityJoinColumn: column.entityJoinColumn,
|
||||
}),
|
||||
style: {
|
||||
labelDisplay: false, // 라벨 숨김
|
||||
labelFontSize: "14px",
|
||||
@@ -2810,6 +2829,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
componentConfig: {
|
||||
type: unifiedMapping.componentType, // unified-input, unified-select 등
|
||||
...unifiedMapping.componentConfig, // Unified 컴포넌트 기본 설정
|
||||
...(isEntityJoinColumn && { disabled: true }), // 🆕 조인 컬럼은 비활성화
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Database, Type, Hash, Calendar, CheckSquare, List, AlignLeft, Code, Building, File } from "lucide-react";
|
||||
import {
|
||||
Database,
|
||||
Type,
|
||||
Hash,
|
||||
Calendar,
|
||||
CheckSquare,
|
||||
List,
|
||||
AlignLeft,
|
||||
Code,
|
||||
Building,
|
||||
File,
|
||||
Link2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { TableInfo, WebType } from "@/types/screen";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
|
||||
interface EntityJoinColumn {
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
dataType: string;
|
||||
inputType?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface EntityJoinTable {
|
||||
tableName: string;
|
||||
currentDisplayColumn: string;
|
||||
availableColumns: EntityJoinColumn[];
|
||||
}
|
||||
|
||||
interface TablesPanelProps {
|
||||
tables: TableInfo[];
|
||||
@@ -53,15 +82,90 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
onDragStart,
|
||||
placedColumns = new Set(),
|
||||
}) => {
|
||||
// 엔티티 조인 컬럼 상태
|
||||
const [entityJoinTables, setEntityJoinTables] = useState<EntityJoinTable[]>([]);
|
||||
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||
const [expandedJoinTables, setExpandedJoinTables] = useState<Set<string>>(new Set());
|
||||
|
||||
// 시스템 컬럼 목록 (숨김 처리)
|
||||
const systemColumns = new Set([
|
||||
'id',
|
||||
'created_date',
|
||||
'updated_date',
|
||||
'writer',
|
||||
'company_code'
|
||||
"id",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"writer",
|
||||
"company_code",
|
||||
]);
|
||||
|
||||
// 메인 테이블명 추출
|
||||
const mainTableName = tables[0]?.tableName;
|
||||
|
||||
// 엔티티 조인 컬럼 로드
|
||||
useEffect(() => {
|
||||
const fetchEntityJoinColumns = async () => {
|
||||
if (!mainTableName) {
|
||||
setEntityJoinTables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingEntityJoins(true);
|
||||
try {
|
||||
const result = await entityJoinApi.getEntityJoinColumns(mainTableName);
|
||||
setEntityJoinTables(result.joinTables || []);
|
||||
// 기본적으로 모든 조인 테이블 펼치기
|
||||
setExpandedJoinTables(new Set(result.joinTables?.map((t) => t.tableName) || []));
|
||||
} catch (error) {
|
||||
console.error("엔티티 조인 컬럼 조회 오류:", error);
|
||||
setEntityJoinTables([]);
|
||||
} finally {
|
||||
setLoadingEntityJoins(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEntityJoinColumns();
|
||||
}, [mainTableName]);
|
||||
|
||||
// 조인 테이블 펼치기/접기 토글
|
||||
const toggleJoinTable = (tableName: string) => {
|
||||
setExpandedJoinTables((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(tableName)) {
|
||||
newSet.delete(tableName);
|
||||
} else {
|
||||
newSet.add(tableName);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// 엔티티 조인 컬럼 드래그 핸들러
|
||||
const handleEntityJoinDragStart = (
|
||||
e: React.DragEvent,
|
||||
joinTable: EntityJoinTable,
|
||||
column: EntityJoinColumn,
|
||||
) => {
|
||||
// "테이블명.컬럼명" 형식으로 컬럼 정보 생성
|
||||
const fullColumnName = `${joinTable.tableName}.${column.columnName}`;
|
||||
|
||||
const columnData = {
|
||||
columnName: fullColumnName,
|
||||
columnLabel: column.columnLabel || column.columnName,
|
||||
dataType: column.dataType,
|
||||
widgetType: "text" as WebType,
|
||||
isEntityJoin: true,
|
||||
entityJoinTable: joinTable.tableName,
|
||||
entityJoinColumn: column.columnName,
|
||||
};
|
||||
|
||||
// 기존 테이블 정보를 기반으로 가상의 테이블 정보 생성
|
||||
const virtualTable: TableInfo = {
|
||||
tableName: mainTableName || "",
|
||||
tableLabel: tables[0]?.tableLabel || mainTableName || "",
|
||||
columns: [columnData],
|
||||
};
|
||||
|
||||
onDragStart(e, virtualTable, columnData);
|
||||
};
|
||||
|
||||
// 이미 배치된 컬럼과 시스템 컬럼을 제외한 테이블 정보 생성
|
||||
const tablesWithAvailableColumns = tables.map((table) => ({
|
||||
...table,
|
||||
@@ -126,18 +230,19 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
{table.columns.map((column) => (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className="hover:bg-accent/50 flex cursor-grab items-center justify-between rounded-md p-2 transition-colors"
|
||||
className="hover:bg-accent/50 flex cursor-grab items-center gap-2 rounded-md p-2 transition-colors"
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, table, column)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
{getWidgetIcon(column.widgetType)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-medium">{column.columnLabel || column.columnName}</div>
|
||||
<div className="text-muted-foreground truncate text-[10px]">{column.dataType}</div>
|
||||
{getWidgetIcon(column.widgetType)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="text-xs font-medium"
|
||||
title={column.columnLabel || column.columnName}
|
||||
>
|
||||
{column.columnLabel || column.columnName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center gap-1">
|
||||
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
|
||||
{column.widgetType}
|
||||
@@ -153,6 +258,103 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 엔티티 조인 컬럼 섹션 */}
|
||||
{entityJoinTables.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2 px-2 py-1">
|
||||
<Link2 className="h-3.5 w-3.5 text-cyan-600" />
|
||||
<span className="text-muted-foreground text-xs font-medium">엔티티 조인 컬럼</span>
|
||||
<Badge variant="outline" className="h-4 px-1.5 text-[10px]">
|
||||
{entityJoinTables.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{entityJoinTables.map((joinTable) => {
|
||||
const isExpanded = expandedJoinTables.has(joinTable.tableName);
|
||||
// 검색어로 필터링
|
||||
const filteredColumns = searchTerm
|
||||
? joinTable.availableColumns.filter(
|
||||
(col) =>
|
||||
col.columnName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
col.columnLabel.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
: joinTable.availableColumns;
|
||||
|
||||
// 검색 결과가 없으면 표시하지 않음
|
||||
if (searchTerm && filteredColumns.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={joinTable.tableName} className="space-y-1">
|
||||
{/* 조인 테이블 헤더 */}
|
||||
<div
|
||||
className="flex cursor-pointer items-center justify-between rounded-md bg-cyan-50 p-2 hover:bg-cyan-100"
|
||||
onClick={() => toggleJoinTable(joinTable.tableName)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 text-cyan-600" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-cyan-600" />
|
||||
)}
|
||||
<Building className="h-3.5 w-3.5 text-cyan-600" />
|
||||
<span className="text-xs font-semibold text-cyan-800">{joinTable.tableName}</span>
|
||||
<Badge variant="secondary" className="h-4 px-1.5 text-[10px]">
|
||||
{filteredColumns.length}개
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조인 컬럼 목록 */}
|
||||
{isExpanded && (
|
||||
<div className="space-y-1 pl-4">
|
||||
{filteredColumns.map((column) => {
|
||||
const fullColumnName = `${joinTable.tableName}.${column.columnName}`;
|
||||
const isPlaced = placedColumns.has(fullColumnName);
|
||||
|
||||
if (isPlaced) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className="flex cursor-grab items-center gap-2 rounded-md border border-cyan-200 bg-cyan-50/50 p-2 transition-colors hover:bg-cyan-100"
|
||||
draggable
|
||||
onDragStart={(e) => handleEntityJoinDragStart(e, joinTable, column)}
|
||||
title="읽기 전용 - 조인된 테이블에서 참조"
|
||||
>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-cyan-500" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs font-medium" title={column.columnLabel || column.columnName}>
|
||||
{column.columnLabel || column.columnName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-1">
|
||||
<Badge variant="secondary" className="h-4 border-gray-300 bg-gray-100 px-1 text-[9px] text-gray-600">
|
||||
읽기
|
||||
</Badge>
|
||||
<Badge variant="outline" className="h-4 border-cyan-300 px-1.5 text-[10px]">
|
||||
{column.inputType || "text"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 로딩 표시 */}
|
||||
{loadingEntityJoins && (
|
||||
<div className="text-muted-foreground flex items-center justify-center py-4 text-xs">
|
||||
엔티티 조인 컬럼 로드 중...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user