하드코딩된 부분 삭제

This commit is contained in:
dohyeons 2025-10-02 09:49:21 +09:00
parent f19db38973
commit ae23cfdc2b
2 changed files with 98 additions and 281 deletions

View File

@ -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에서 조회) */}
{systemTemplates.length > 0 && (
<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>
</div> </div>
{SYSTEM_TEMPLATES.map((template) => ( {systemTemplates.map((template) => (
<Button <Button
key={template.id} key={template.template_id}
variant="outline" variant="outline"
size="sm" size="sm"
className="w-full justify-start gap-2 text-sm" className="w-full justify-start gap-2 text-sm"
onClick={() => handleApplyTemplate(template.id)} onClick={() => handleApplyTemplate(template.template_id)}
> >
<span>{template.icon}</span> <span>{template.template_name_kor}</span>
<span>{template.name}</span>
</Button> </Button>
))} ))}
</div> </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>

View File

@ -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 = // 템플릿 데이터 파싱 및 적용
let layoutConfig: { components?: ComponentConfig[] } | null = null;
let defaultQueries: unknown[] = [];
// 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" typeof template.default_queries === "string"
? JSON.parse(template.default_queries) ? JSON.parse(template.default_queries)
: 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;
parameters: string[];
externalConnectionId?: number | null;
}>
).map((q) => ({
id: `query-${Date.now()}-${Math.random()}`, id: `query-${Date.now()}-${Math.random()}`,
name: q.name, name: (q.name as string) || "",
type: q.type, type: (q.type as "MASTER" | "DETAIL") || "MASTER",
sqlQuery: q.sqlQuery, sqlQuery: (q.sqlQuery as string) || "",
parameters: q.parameters || [], parameters: Array.isArray(q.parameters) ? (q.parameters as string[]) : [],
externalConnectionId: q.externalConnectionId || null, externalConnectionId: (q.externalConnectionId as number | null) || 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 = {