Merge branch 'feature/v2-unified-renewal' of http://39.117.244.52:3000/kjs/ERP-node into feature/v2-renewal
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
commit
6350fd6592
|
|
@ -773,18 +773,81 @@ export default function TableManagementPage() {
|
|||
|
||||
// 2. 모든 컬럼 설정 저장
|
||||
if (columns.length > 0) {
|
||||
const columnSettings = columns.map((column) => ({
|
||||
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
||||
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
||||
inputType: column.inputType || "text",
|
||||
detailSettings: column.detailSettings || "",
|
||||
description: column.description || "",
|
||||
codeCategory: column.codeCategory || "",
|
||||
codeValue: column.codeValue || "",
|
||||
referenceTable: column.referenceTable || "",
|
||||
referenceColumn: column.referenceColumn || "",
|
||||
displayColumn: column.displayColumn || "", // 🎯 Entity 조인에서 표시할 컬럼명
|
||||
}));
|
||||
const columnSettings = columns.map((column) => {
|
||||
// detailSettings 계산
|
||||
let finalDetailSettings = column.detailSettings || "";
|
||||
|
||||
// 🆕 Numbering 타입인 경우 numberingRuleId를 detailSettings에 포함
|
||||
if (column.inputType === "numbering" && column.numberingRuleId) {
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
||||
try {
|
||||
existingSettings = JSON.parse(finalDetailSettings);
|
||||
} catch {
|
||||
existingSettings = {};
|
||||
}
|
||||
}
|
||||
const numberingSettings = {
|
||||
...existingSettings,
|
||||
numberingRuleId: column.numberingRuleId,
|
||||
};
|
||||
finalDetailSettings = JSON.stringify(numberingSettings);
|
||||
console.log("🔧 전체저장 - Numbering 설정 JSON 생성:", {
|
||||
columnName: column.columnName,
|
||||
numberingRuleId: column.numberingRuleId,
|
||||
finalDetailSettings,
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 Entity 타입인 경우 detailSettings에 엔티티 설정 포함
|
||||
if (column.inputType === "entity" && column.referenceTable) {
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
||||
try {
|
||||
existingSettings = JSON.parse(finalDetailSettings);
|
||||
} catch {
|
||||
existingSettings = {};
|
||||
}
|
||||
}
|
||||
const entitySettings = {
|
||||
...existingSettings,
|
||||
entityTable: column.referenceTable,
|
||||
entityCodeColumn: column.referenceColumn || "id",
|
||||
entityLabelColumn: column.displayColumn || "name",
|
||||
};
|
||||
finalDetailSettings = JSON.stringify(entitySettings);
|
||||
}
|
||||
|
||||
// 🆕 Code 타입인 경우 hierarchyRole을 detailSettings에 포함
|
||||
if (column.inputType === "code" && column.hierarchyRole) {
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
|
||||
try {
|
||||
existingSettings = JSON.parse(finalDetailSettings);
|
||||
} catch {
|
||||
existingSettings = {};
|
||||
}
|
||||
}
|
||||
const codeSettings = {
|
||||
...existingSettings,
|
||||
hierarchyRole: column.hierarchyRole,
|
||||
};
|
||||
finalDetailSettings = JSON.stringify(codeSettings);
|
||||
}
|
||||
|
||||
return {
|
||||
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
||||
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
||||
inputType: column.inputType || "text",
|
||||
detailSettings: finalDetailSettings,
|
||||
description: column.description || "",
|
||||
codeCategory: column.codeCategory || "",
|
||||
codeValue: column.codeValue || "",
|
||||
referenceTable: column.referenceTable || "",
|
||||
referenceColumn: column.referenceColumn || "",
|
||||
displayColumn: column.displayColumn || "", // 🎯 Entity 조인에서 표시할 컬럼명
|
||||
};
|
||||
});
|
||||
|
||||
// console.log("저장할 전체 설정:", { tableLabel, tableDescription, columnSettings });
|
||||
|
||||
|
|
|
|||
|
|
@ -9,15 +9,17 @@ import { WidgetComponent } from "@/types/screen";
|
|||
import { toast } from "sonner";
|
||||
import { apiClient, getFullImageUrl } from "@/lib/api/client";
|
||||
|
||||
export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
||||
export const ImageWidget: React.FC<WebTypeComponentProps & { size?: { width?: number; height?: number }; style?: React.CSSProperties }> = ({
|
||||
component,
|
||||
value,
|
||||
onChange,
|
||||
readonly = false,
|
||||
isDesignMode = false // 디자인 모드 여부
|
||||
isDesignMode = false, // 디자인 모드 여부
|
||||
size, // props로 전달된 size
|
||||
style: propStyle, // props로 전달된 style
|
||||
}) => {
|
||||
const widget = component as WidgetComponent;
|
||||
const { required, style } = widget;
|
||||
const { required, style: widgetStyle } = widget;
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
|
|
@ -25,8 +27,16 @@ export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
|||
const rawImageUrl = value || widget.value || "";
|
||||
const imageUrl = rawImageUrl ? getFullImageUrl(rawImageUrl) : "";
|
||||
|
||||
// style에서 width, height 제거 (부모 컨테이너 크기 사용)
|
||||
const filteredStyle = style ? { ...style, width: undefined, height: undefined } : {};
|
||||
// 🔧 컴포넌트 크기를 명시적으로 적용 (props.size 우선, 없으면 style에서 가져옴)
|
||||
const effectiveSize = size || (widget as any).size || {};
|
||||
const effectiveStyle = propStyle || widgetStyle || {};
|
||||
const containerStyle: React.CSSProperties = {
|
||||
width: effectiveSize.width ? `${effectiveSize.width}px` : effectiveStyle?.width || "100%",
|
||||
height: effectiveSize.height ? `${effectiveSize.height}px` : effectiveStyle?.height || "100%",
|
||||
};
|
||||
|
||||
// style에서 width, height 제거 (내부 요소용)
|
||||
const filteredStyle = effectiveStyle ? { ...effectiveStyle, width: undefined, height: undefined } : {};
|
||||
|
||||
// 파일 선택 처리
|
||||
const handleFileSelect = () => {
|
||||
|
|
@ -120,11 +130,11 @@ export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col" style={containerStyle}>
|
||||
{imageUrl ? (
|
||||
// 이미지 표시 모드
|
||||
<div
|
||||
className="group relative h-full w-full overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm transition-all hover:shadow-md"
|
||||
className="group relative flex-1 w-full overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm transition-all hover:shadow-md"
|
||||
style={filteredStyle}
|
||||
>
|
||||
<img
|
||||
|
|
@ -154,7 +164,7 @@ export const ImageWidget: React.FC<WebTypeComponentProps> = ({
|
|||
) : (
|
||||
// 업로드 영역
|
||||
<div
|
||||
className={`group relative flex h-full w-full flex-col items-center justify-center rounded-lg border-2 border-dashed p-3 text-center shadow-sm transition-all duration-300 ${
|
||||
className={`group relative flex flex-1 w-full flex-col items-center justify-center rounded-lg border-2 border-dashed p-3 text-center shadow-sm transition-all duration-300 ${
|
||||
isDesignMode
|
||||
? "cursor-default border-gray-200 bg-gray-50"
|
||||
: "cursor-pointer border-gray-300 bg-white hover:border-blue-400 hover:bg-blue-50/50 hover:shadow-md"
|
||||
|
|
|
|||
|
|
@ -334,6 +334,8 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
|||
// formData 추출 (채번규칙 날짜 컬럼 기준 생성 시 사용)
|
||||
const formData = (props as any).formData || {};
|
||||
const columnName = (props as any).columnName;
|
||||
// onFormDataChange 추출 (채번 규칙 ID를 formData에 저장하기 위함)
|
||||
const onFormDataChange = (props as any).onFormDataChange;
|
||||
|
||||
// config가 없으면 기본값 사용
|
||||
const config = (configProp || { type: "text" }) as V2InputConfig & {
|
||||
|
|
@ -526,6 +528,15 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
|||
try {
|
||||
const parsed = JSON.parse(targetColumn.detailSettings);
|
||||
numberingRuleIdRef.current = parsed.numberingRuleId || null;
|
||||
|
||||
// 🆕 채번 규칙 ID를 formData에 저장 (저장 시 allocateCode 호출을 위해)
|
||||
if (parsed.numberingRuleId && onFormDataChange && columnName) {
|
||||
onFormDataChange(`${columnName}_numberingRuleId`, parsed.numberingRuleId);
|
||||
console.log("🔧 채번 규칙 ID를 formData에 저장:", {
|
||||
key: `${columnName}_numberingRuleId`,
|
||||
value: parsed.numberingRuleId,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// JSON 파싱 실패
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,19 +305,19 @@ const ImageUploader = forwardRef<HTMLDivElement, {
|
|||
}, [images, multiple, onChange]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("space-y-3", className)}>
|
||||
<div ref={ref} className={cn("flex h-full w-full flex-col", className)}>
|
||||
{/* 이미지 미리보기 */}
|
||||
{preview && images.length > 0 && (
|
||||
<div className={cn(
|
||||
"grid gap-2",
|
||||
"grid gap-2 flex-1",
|
||||
multiple ? "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4" : "grid-cols-1"
|
||||
)}>
|
||||
{images.map((src, index) => (
|
||||
<div key={index} className="relative group aspect-square rounded-lg overflow-hidden border">
|
||||
<div key={index} className="relative group rounded-lg overflow-hidden border h-full">
|
||||
<img
|
||||
src={src}
|
||||
alt={`이미지 ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button
|
||||
|
|
@ -347,7 +347,7 @@ const ImageUploader = forwardRef<HTMLDivElement, {
|
|||
{(!images.length || multiple) && (
|
||||
<div
|
||||
className={cn(
|
||||
"border-2 border-dashed rounded-lg p-4 text-center transition-colors",
|
||||
"flex flex-1 flex-col items-center justify-center border-2 border-dashed rounded-lg p-4 text-center transition-colors",
|
||||
isDragging && "border-primary bg-primary/5",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
!disabled && "cursor-pointer hover:border-primary/50"
|
||||
|
|
@ -540,7 +540,7 @@ export const V2Media = forwardRef<HTMLDivElement, V2MediaProps>(
|
|||
<div
|
||||
ref={ref}
|
||||
id={id}
|
||||
className="flex flex-col"
|
||||
className="flex h-full w-full flex-col"
|
||||
style={{
|
||||
width: componentWidth,
|
||||
height: componentHeight,
|
||||
|
|
@ -561,7 +561,7 @@ export const V2Media = forwardRef<HTMLDivElement, V2MediaProps>(
|
|||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex-1 min-h-0 h-full">
|
||||
{renderMedia()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,15 +30,21 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
|
|||
}
|
||||
};
|
||||
|
||||
// 라벨: style.labelText 우선, 없으면 component.label 사용
|
||||
// style.labelDisplay가 false면 라벨 숨김
|
||||
const style = component.style || {};
|
||||
const effectiveLabel = style.labelDisplay === false ? undefined : (style.labelText || component.label);
|
||||
|
||||
return (
|
||||
<V2Input
|
||||
id={component.id}
|
||||
label={component.label}
|
||||
label={effectiveLabel}
|
||||
required={component.required}
|
||||
readonly={config.readonly || component.readonly}
|
||||
disabled={config.disabled || component.disabled}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
onFormDataChange={onFormDataChange}
|
||||
config={{
|
||||
type: config.inputType || config.webType || "text",
|
||||
inputType: config.inputType || config.webType || "text",
|
||||
|
|
|
|||
|
|
@ -260,6 +260,10 @@ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 {
|
|||
return {
|
||||
version: "2.0",
|
||||
components,
|
||||
// 레이아웃 메타데이터 포함
|
||||
gridSettings: legacyLayout.gridSettings,
|
||||
screenResolution: legacyLayout.screenResolution,
|
||||
metadata: legacyLayout.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue