하드코딩된 부분 삭제
This commit is contained in:
parent
f19db38973
commit
ae23cfdc2b
|
|
@ -8,13 +8,7 @@ import { reportApi } from "@/lib/api/reportApi";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
const SYSTEM_TEMPLATES = [
|
interface Template {
|
||||||
{ id: "order", name: "발주서", icon: "📋" },
|
|
||||||
{ id: "invoice", name: "청구서", icon: "💰" },
|
|
||||||
{ id: "basic", name: "기본", icon: "📄" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface CustomTemplate {
|
|
||||||
template_id: string;
|
template_id: string;
|
||||||
template_name_kor: string;
|
template_name_kor: string;
|
||||||
template_name_eng: string | null;
|
template_name_eng: string | null;
|
||||||
|
|
@ -23,27 +17,35 @@ interface CustomTemplate {
|
||||||
|
|
||||||
export function TemplatePalette() {
|
export function TemplatePalette() {
|
||||||
const { applyTemplate } = useReportDesigner();
|
const { applyTemplate } = useReportDesigner();
|
||||||
const [customTemplates, setCustomTemplates] = useState<CustomTemplate[]>([]);
|
const [systemTemplates, setSystemTemplates] = useState<Template[]>([]);
|
||||||
|
const [customTemplates, setCustomTemplates] = useState<Template[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const fetchCustomTemplates = async () => {
|
const fetchTemplates = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await reportApi.getTemplates();
|
const response = await reportApi.getTemplates();
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setCustomTemplates(response.data.custom || []);
|
setSystemTemplates(Array.isArray(response.data.system) ? response.data.system : []);
|
||||||
|
setCustomTemplates(Array.isArray(response.data.custom) ? response.data.custom : []);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("템플릿 조회 실패:", error);
|
console.error("템플릿 조회 실패:", error);
|
||||||
|
toast({
|
||||||
|
title: "오류",
|
||||||
|
description: "템플릿 목록을 불러올 수 없습니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomTemplates();
|
fetchTemplates();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleApplyTemplate = async (templateId: string) => {
|
const handleApplyTemplate = async (templateId: string) => {
|
||||||
|
|
@ -63,7 +65,7 @@ export function TemplatePalette() {
|
||||||
title: "성공",
|
title: "성공",
|
||||||
description: "템플릿이 삭제되었습니다.",
|
description: "템플릿이 삭제되었습니다.",
|
||||||
});
|
});
|
||||||
fetchCustomTemplates();
|
fetchTemplates();
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -78,30 +80,31 @@ export function TemplatePalette() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 시스템 템플릿 */}
|
{/* 시스템 템플릿 (DB에서 조회) */}
|
||||||
<div className="space-y-2">
|
{systemTemplates.length > 0 && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-semibold text-gray-600">시스템 템플릿</p>
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold text-gray-600">시스템 템플릿</p>
|
||||||
|
</div>
|
||||||
|
{systemTemplates.map((template) => (
|
||||||
|
<Button
|
||||||
|
key={template.template_id}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start gap-2 text-sm"
|
||||||
|
onClick={() => handleApplyTemplate(template.template_id)}
|
||||||
|
>
|
||||||
|
<span>{template.template_name_kor}</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{SYSTEM_TEMPLATES.map((template) => (
|
)}
|
||||||
<Button
|
|
||||||
key={template.id}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-full justify-start gap-2 text-sm"
|
|
||||||
onClick={() => handleApplyTemplate(template.id)}
|
|
||||||
>
|
|
||||||
<span>{template.icon}</span>
|
|
||||||
<span>{template.name}</span>
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 사용자 정의 템플릿 */}
|
{/* 사용자 정의 템플릿 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-semibold text-gray-600">사용자 정의 템플릿</p>
|
<p className="text-xs font-semibold text-gray-600">사용자 정의 템플릿</p>
|
||||||
<Button variant="ghost" size="sm" onClick={fetchCustomTemplates} disabled={isLoading} className="h-6 w-6 p-0">
|
<Button variant="ghost" size="sm" onClick={fetchTemplates} disabled={isLoading} className="h-6 w-6 p-0">
|
||||||
<RefreshCw className={`h-3 w-3 ${isLoading ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-3 w-3 ${isLoading ? "animate-spin" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,212 +14,7 @@ export interface ReportQuery {
|
||||||
externalConnectionId?: number | null; // 외부 DB 연결 ID
|
externalConnectionId?: number | null; // 외부 DB 연결 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 템플릿 레이아웃 정의
|
// 하드코딩된 템플릿 제거 - 모든 템플릿은 DB에서 관리
|
||||||
interface TemplateLayout {
|
|
||||||
components: ComponentConfig[];
|
|
||||||
queries: ReportQuery[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTemplateLayout(templateId: string): TemplateLayout | null {
|
|
||||||
switch (templateId) {
|
|
||||||
case "order":
|
|
||||||
return {
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-1`,
|
|
||||||
type: "label",
|
|
||||||
x: 50,
|
|
||||||
y: 30,
|
|
||||||
width: 200,
|
|
||||||
height: 40,
|
|
||||||
fontSize: 24,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
defaultValue: "발주서",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-2`,
|
|
||||||
type: "text",
|
|
||||||
x: 50,
|
|
||||||
y: 80,
|
|
||||||
width: 150,
|
|
||||||
height: 30,
|
|
||||||
fontSize: 14,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-3`,
|
|
||||||
type: "text",
|
|
||||||
x: 220,
|
|
||||||
y: 80,
|
|
||||||
width: 150,
|
|
||||||
height: 30,
|
|
||||||
fontSize: 14,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-4`,
|
|
||||||
type: "text",
|
|
||||||
x: 390,
|
|
||||||
y: 80,
|
|
||||||
width: 150,
|
|
||||||
height: 30,
|
|
||||||
fontSize: 14,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-5`,
|
|
||||||
type: "table",
|
|
||||||
x: 50,
|
|
||||||
y: 130,
|
|
||||||
width: 500,
|
|
||||||
height: 200,
|
|
||||||
fontSize: 12,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
queries: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
case "invoice":
|
|
||||||
return {
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-1`,
|
|
||||||
type: "label",
|
|
||||||
x: 50,
|
|
||||||
y: 30,
|
|
||||||
width: 200,
|
|
||||||
height: 40,
|
|
||||||
fontSize: 24,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
defaultValue: "청구서",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-2`,
|
|
||||||
type: "text",
|
|
||||||
x: 50,
|
|
||||||
y: 80,
|
|
||||||
width: 150,
|
|
||||||
height: 30,
|
|
||||||
fontSize: 14,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-3`,
|
|
||||||
type: "text",
|
|
||||||
x: 220,
|
|
||||||
y: 80,
|
|
||||||
width: 150,
|
|
||||||
height: 30,
|
|
||||||
fontSize: 14,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-4`,
|
|
||||||
type: "table",
|
|
||||||
x: 50,
|
|
||||||
y: 130,
|
|
||||||
width: 500,
|
|
||||||
height: 200,
|
|
||||||
fontSize: 12,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-5`,
|
|
||||||
type: "label",
|
|
||||||
x: 400,
|
|
||||||
y: 350,
|
|
||||||
width: 150,
|
|
||||||
height: 30,
|
|
||||||
fontSize: 16,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "#ffffcc",
|
|
||||||
borderColor: "#000000",
|
|
||||||
borderWidth: 1,
|
|
||||||
zIndex: 1,
|
|
||||||
defaultValue: "합계: 0원",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
queries: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
case "basic":
|
|
||||||
return {
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-1`,
|
|
||||||
type: "label",
|
|
||||||
x: 50,
|
|
||||||
y: 30,
|
|
||||||
width: 300,
|
|
||||||
height: 40,
|
|
||||||
fontSize: 20,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
defaultValue: "리포트 제목",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `comp-${Date.now()}-2`,
|
|
||||||
type: "text",
|
|
||||||
x: 50,
|
|
||||||
y: 80,
|
|
||||||
width: 500,
|
|
||||||
height: 100,
|
|
||||||
fontSize: 14,
|
|
||||||
fontColor: "#000000",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
borderColor: "#cccccc",
|
|
||||||
borderWidth: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
defaultValue: "내용을 입력하세요",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
queries: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryResult {
|
export interface QueryResult {
|
||||||
queryId: string;
|
queryId: string;
|
||||||
|
|
@ -1243,75 +1038,93 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 먼저 하드코딩된 시스템 템플릿 확인 (order, invoice, basic)
|
// DB에서 템플릿 조회 (시스템 템플릿 또는 사용자 정의 템플릿)
|
||||||
const systemTemplate = getTemplateLayout(templateId);
|
|
||||||
|
|
||||||
if (systemTemplate) {
|
|
||||||
// 시스템 템플릿 적용
|
|
||||||
setComponents(systemTemplate.components);
|
|
||||||
setQueries(systemTemplate.queries);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "성공",
|
|
||||||
description: "템플릿이 적용되었습니다.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 사용자 정의 템플릿은 백엔드에서 조회
|
|
||||||
const response = await reportApi.getTemplates();
|
const response = await reportApi.getTemplates();
|
||||||
|
|
||||||
if (!response.success || !response.data) {
|
if (!response.success || !response.data) {
|
||||||
throw new Error("템플릿 목록을 불러올 수 없습니다.");
|
throw new Error("템플릿 목록을 불러올 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 커스텀 템플릿 찾기
|
// 시스템 템플릿과 커스텀 템플릿 모두에서 찾기
|
||||||
const customTemplates = response.data.custom || [];
|
const allTemplates = [...(response.data.system || []), ...(response.data.custom || [])];
|
||||||
const template = customTemplates.find((t: { template_id: string }) => t.template_id === templateId);
|
const template = allTemplates.find((t: { template_id: string }) => t.template_id === templateId);
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new Error("템플릿을 찾을 수 없습니다.");
|
throw new Error("템플릿을 찾을 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 템플릿 데이터 파싱 및 적용
|
// 템플릿 데이터 확인용 로그
|
||||||
const layoutConfig =
|
console.log("===== 선택된 템플릿 =====");
|
||||||
typeof template.layout_config === "string" ? JSON.parse(template.layout_config) : template.layout_config;
|
console.log("Template ID:", template.template_id);
|
||||||
|
console.log("Template Name:", template.template_name_kor);
|
||||||
|
console.log("Layout Config:", template.layout_config);
|
||||||
|
console.log("Default Queries:", template.default_queries);
|
||||||
|
console.log("========================");
|
||||||
|
|
||||||
const defaultQueries =
|
// 템플릿 데이터 파싱 및 적용
|
||||||
typeof template.default_queries === "string"
|
let layoutConfig: { components?: ComponentConfig[] } | null = null;
|
||||||
? JSON.parse(template.default_queries)
|
let defaultQueries: unknown[] = [];
|
||||||
: template.default_queries || [];
|
|
||||||
|
// layout_config 파싱 (안전하게)
|
||||||
|
try {
|
||||||
|
if (template.layout_config) {
|
||||||
|
layoutConfig =
|
||||||
|
typeof template.layout_config === "string" ? JSON.parse(template.layout_config) : template.layout_config;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("layout_config 파싱 오류:", e);
|
||||||
|
layoutConfig = { components: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// default_queries 파싱 (안전하게)
|
||||||
|
try {
|
||||||
|
if (template.default_queries) {
|
||||||
|
defaultQueries =
|
||||||
|
typeof template.default_queries === "string"
|
||||||
|
? JSON.parse(template.default_queries)
|
||||||
|
: template.default_queries;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("default_queries 파싱 오류:", e);
|
||||||
|
defaultQueries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// layoutConfig가 없으면 빈 구조로 초기화
|
||||||
|
if (!layoutConfig || typeof layoutConfig !== "object") {
|
||||||
|
layoutConfig = { components: [] };
|
||||||
|
}
|
||||||
|
|
||||||
// 컴포넌트 적용 (ID 재생성)
|
// 컴포넌트 적용 (ID 재생성)
|
||||||
const newComponents = (layoutConfig.components as ComponentConfig[]).map((comp) => ({
|
const templateComponents = Array.isArray(layoutConfig.components) ? layoutConfig.components : [];
|
||||||
|
const newComponents = templateComponents.map((comp: ComponentConfig) => ({
|
||||||
...comp,
|
...comp,
|
||||||
id: `comp-${Date.now()}-${Math.random()}`,
|
id: `comp-${Date.now()}-${Math.random()}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 쿼리 적용 (ID 재생성)
|
// 쿼리 적용 (ID 재생성)
|
||||||
const newQueries = (
|
const templateQueries = Array.isArray(defaultQueries) ? defaultQueries : [];
|
||||||
defaultQueries as Array<{
|
const newQueries = templateQueries
|
||||||
name: string;
|
.filter((q): q is Record<string, unknown> => typeof q === "object" && q !== null)
|
||||||
type: "MASTER" | "DETAIL";
|
.map((q) => ({
|
||||||
sqlQuery: string;
|
id: `query-${Date.now()}-${Math.random()}`,
|
||||||
parameters: string[];
|
name: (q.name as string) || "",
|
||||||
externalConnectionId?: number | null;
|
type: (q.type as "MASTER" | "DETAIL") || "MASTER",
|
||||||
}>
|
sqlQuery: (q.sqlQuery as string) || "",
|
||||||
).map((q) => ({
|
parameters: Array.isArray(q.parameters) ? (q.parameters as string[]) : [],
|
||||||
id: `query-${Date.now()}-${Math.random()}`,
|
externalConnectionId: (q.externalConnectionId as number | null) || null,
|
||||||
name: q.name,
|
}));
|
||||||
type: q.type,
|
|
||||||
sqlQuery: q.sqlQuery,
|
|
||||||
parameters: q.parameters || [],
|
|
||||||
externalConnectionId: q.externalConnectionId || null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setComponents(newComponents);
|
setComponents(newComponents);
|
||||||
setQueries(newQueries);
|
setQueries(newQueries);
|
||||||
|
|
||||||
|
const message =
|
||||||
|
newComponents.length === 0
|
||||||
|
? "템플릿이 적용되었습니다. (빈 템플릿)"
|
||||||
|
: `템플릿이 적용되었습니다. (컴포넌트 ${newComponents.length}개, 쿼리 ${newQueries.length}개)`;
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "성공",
|
title: "성공",
|
||||||
description: "사용자 정의 템플릿이 적용되었습니다.",
|
description: message,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "템플릿 적용에 실패했습니다.";
|
const errorMessage = error instanceof Error ? error.message : "템플릿 적용에 실패했습니다.";
|
||||||
|
|
@ -1322,7 +1135,8 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[components.length, toast],
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const value: ReportDesignerContextType = {
|
const value: ReportDesignerContextType = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue