From d64ca5a8c0f62345637e17dc8ef686eec1e6810f Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 4 Nov 2025 11:41:20 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 282 ++++++++++++------ .../screen/InteractiveScreenViewer.tsx | 15 +- .../screen/InteractiveScreenViewerDynamic.tsx | 6 +- .../screen/RealtimePreviewDynamic.tsx | 29 +- frontend/components/screen/ScreenList.tsx | 35 ++- .../screen/widgets/types/ButtonWidget.tsx | 6 +- .../lib/registry/DynamicWebTypeRenderer.tsx | 16 +- 7 files changed, 256 insertions(+), 133 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index dac590d6..a0b12c7f 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -26,7 +26,7 @@ export default function ScreenViewPage() { // ๐Ÿ†• ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด const { user, userName, companyCode } = useAuth(); - + // ๐Ÿ†• ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ ๊ฐ์ง€ const { isMobile } = useResponsive(); @@ -189,10 +189,10 @@ export default function ScreenViewPage() { if (loading) { return ( -
-
- -

ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ +

ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

); @@ -200,13 +200,13 @@ export default function ScreenViewPage() { if (error || !screen) { return ( -
-
-
+
+
+
โš ๏ธ
-

ํ™”๋ฉด์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค

-

{error || "์š”์ฒญํ•˜์‹  ํ™”๋ฉด์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."}

+

ํ™”๋ฉด์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค

+

{error || "์š”์ฒญํ•˜์‹  ํ™”๋ฉด์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."}

@@ -225,7 +225,7 @@ export default function ScreenViewPage() { {/* ์ ˆ๋Œ€ ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง */} {layout && layout.components.length > 0 ? (
!component.parentId); + // ๋ฒ„ํŠผ์€ scale์— ๋งž์ถฐ ์œ„์น˜๋งŒ ์กฐ์ •ํ•˜๋ฉด ๋จ (scale = 1.0์ด๋ฉด ๊ทธ๋Œ€๋กœ, scale < 1.0์ด๋ฉด ์™ผ์ชฝ์œผ๋กœ) + // ํ•˜์ง€๋งŒ x=0 ์ปดํฌ๋„ŒํŠธ๋Š” width: 100%๋กœ ํ™•์žฅ๋˜๋ฏ€๋กœ, ๊ทธ๋งŒํผ ๋ฒ„ํŠผ์„ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ด๋™ + const leftmostComponent = topLevelComponents.find((c) => c.position.x === 0); + let widthOffset = 0; + + if (leftmostComponent && containerWidth > 0) { + const originalWidth = leftmostComponent.size?.width || screenWidth; + const actualWidth = containerWidth / scale; + widthOffset = Math.max(0, actualWidth - originalWidth); + + console.log("๐Ÿ“Š widthOffset ๊ณ„์‚ฐ:", { + containerWidth, + scale, + screenWidth, + originalWidth, + actualWidth, + widthOffset, + leftmostType: leftmostComponent.type, + }); + } + const buttonGroups: Record = {}; const processedButtonIds = new Set(); + // ๐Ÿ” ์ „์ฒด ๋ฒ„ํŠผ ๋ชฉ๋ก ํ™•์ธ + const allButtons = topLevelComponents.filter((component) => { + const isButton = + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)) || + (component.type === "widget" && (component as any).widgetType === "button"); + return isButton; + }); + + console.log( + "๐Ÿ” ๋ฉ”๋‰ด์—์„œ ๋ฐœ๊ฒฌ๋œ ์ „์ฒด ๋ฒ„ํŠผ:", + allButtons.map((b) => ({ + id: b.id, + label: b.label, + positionX: b.position.x, + positionY: b.position.y, + })), + ); topLevelComponents.forEach((component) => { const isButton = - component.type === "button" || (component.type === "component" && - ["button-primary", "button-secondary"].includes((component as any).componentType)); + ["button-primary", "button-secondary"].includes((component as any).componentType)) || + (component.type === "widget" && (component as any).widgetType === "button"); if (isButton) { const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as | FlowVisibilityConfig | undefined; - if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { + // ๐Ÿ”ง ์ž„์‹œ: ๋ฒ„ํŠผ ๊ทธ๋ฃน ๊ธฐ๋Šฅ ์™„์ „ ๋น„ํ™œ์„ฑํ™” + // TODO: ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ทธ๋ฃน์„ ์›ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ™œ์„ฑํ™”ํ•˜๋„๋ก UI ๊ฐœ์„  ํ•„์š” + const DISABLE_BUTTON_GROUPS = true; + + if ( + !DISABLE_BUTTON_GROUPS && + flowConfig?.enabled && + flowConfig.layoutBehavior === "auto-compact" && + flowConfig.groupId + ) { if (!buttonGroups[flowConfig.groupId]) { buttonGroups[flowConfig.groupId] = []; } buttonGroups[flowConfig.groupId].push(component); processedButtonIds.add(component.id); } + // else: ๋ชจ๋“  ๋ฒ„ํŠผ์„ ๊ฐœ๋ณ„ ๋ Œ๋”๋ง } }); @@ -267,92 +316,121 @@ export default function ScreenViewPage() { return ( <> {/* ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋“ค */} - {regularComponents.map((component) => ( - {}} - screenId={screenId} - tableName={screen?.tableName} - userId={user?.userId} - userName={userName} - companyCode={companyCode} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - console.log("๐Ÿ” ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ:", selectedData); - setSelectedRowsData(selectedData); - }} - flowSelectedData={flowSelectedData} - flowSelectedStepId={flowSelectedStepId} - onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { - setFlowSelectedData(selectedData); - setFlowSelectedStepId(stepId); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // ์„ ํƒ ํ•ด์ œ - }} - flowRefreshKey={flowRefreshKey} - onFlowRefresh={() => { - setFlowRefreshKey((prev) => prev + 1); - setFlowSelectedData([]); // ์„ ํƒ ํ•ด์ œ - setFlowSelectedStepId(null); - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - > - {/* ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค */} - {(component.type === "group" || component.type === "container" || component.type === "area") && - layout.components - .filter((child) => child.parentId === component.id) - .map((child) => { - // ์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ ์œ„์น˜๋ฅผ ๋ถ€๋ชจ ๊ธฐ์ค€ ์ƒ๋Œ€ ์ขŒํ‘œ๋กœ ์กฐ์ • - const relativeChildComponent = { - ...child, - position: { - x: child.position.x - component.position.x, - y: child.position.y - component.position.y, - z: child.position.z || 1, - }, - }; + {regularComponents.map((component) => { + // ๋ฒ„ํŠผ์ธ ๊ฒฝ์šฐ ์œ„์น˜ ์กฐ์ • (ํ…Œ์ด๋ธ”์ด ๋Š˜์–ด๋‚œ ๋งŒํผ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ด๋™) + const isButton = + (component.type === "component" && + ["button-primary", "button-secondary"].includes((component as any).componentType)) || + (component.type === "widget" && (component as any).widgetType === "button"); - return ( - {}} - screenId={screenId} - tableName={screen?.tableName} - userId={user?.userId} - userName={userName} - companyCode={companyCode} - selectedRowsData={selectedRowsData} - onSelectedRowsChange={(_, selectedData) => { - console.log("๐Ÿ” ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ (์ž์‹):", selectedData); - setSelectedRowsData(selectedData); - }} - refreshKey={tableRefreshKey} - onRefresh={() => { - console.log("๐Ÿ”„ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ๋จ (์ž์‹)"); - setTableRefreshKey((prev) => prev + 1); - setSelectedRowsData([]); // ์„ ํƒ ํ•ด์ œ - }} - formData={formData} - onFormDataChange={(fieldName, value) => { - setFormData((prev) => ({ ...prev, [fieldName]: value })); - }} - /> - ); - })} - - ))} + const adjustedComponent = + isButton && widthOffset > 0 + ? { + ...component, + position: { + ...component.position, + x: component.position.x + widthOffset, + }, + } + : component; + + // ๋ฒ„ํŠผ์ผ ๊ฒฝ์šฐ ๋กœ๊ทธ ์ถœ๋ ฅ + if (isButton) { + console.log("๐Ÿ”˜ ๋ฒ„ํŠผ ์œ„์น˜ ์กฐ์ •:", { + label: component.label, + originalX: component.position.x, + adjustedX: component.position.x + widthOffset, + widthOffset, + }); + } + + return ( + {}} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("๐Ÿ” ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ:", selectedData); + setSelectedRowsData(selectedData); + }} + flowSelectedData={flowSelectedData} + flowSelectedStepId={flowSelectedStepId} + onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => { + setFlowSelectedData(selectedData); + setFlowSelectedStepId(stepId); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // ์„ ํƒ ํ•ด์ œ + }} + flowRefreshKey={flowRefreshKey} + onFlowRefresh={() => { + setFlowRefreshKey((prev) => prev + 1); + setFlowSelectedData([]); // ์„ ํƒ ํ•ด์ œ + setFlowSelectedStepId(null); + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + > + {/* ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค */} + {(component.type === "group" || component.type === "container" || component.type === "area") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // ์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ ์œ„์น˜๋ฅผ ๋ถ€๋ชจ ๊ธฐ์ค€ ์ƒ๋Œ€ ์ขŒํ‘œ๋กœ ์กฐ์ • + const relativeChildComponent = { + ...child, + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + z: child.position.z || 1, + }, + }; + + return ( + {}} + screenId={screenId} + tableName={screen?.tableName} + userId={user?.userId} + userName={userName} + companyCode={companyCode} + selectedRowsData={selectedRowsData} + onSelectedRowsChange={(_, selectedData) => { + console.log("๐Ÿ” ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ (์ž์‹):", selectedData); + setSelectedRowsData(selectedData); + }} + refreshKey={tableRefreshKey} + onRefresh={() => { + console.log("๐Ÿ”„ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ๋จ (์ž์‹)"); + setTableRefreshKey((prev) => prev + 1); + setSelectedRowsData([]); // ์„ ํƒ ํ•ด์ œ + }} + formData={formData} + onFormDataChange={(fieldName, value) => { + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + /> + ); + })} + + ); + })} {/* ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน๋“ค */} {Object.entries(buttonGroups).map(([groupId, buttons]) => { @@ -372,6 +450,12 @@ export default function ScreenViewPage() { { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 }, ); + // ๋ฒ„ํŠผ ๊ทธ๋ฃน ์œ„์น˜์—๋„ widthOffset ์ ์šฉ (ํ…Œ์ด๋ธ”์ด ๋Š˜์–ด๋‚œ ๋งŒํผ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ด๋™) + const adjustedGroupPosition = { + ...groupPosition, + x: groupPosition.x + widthOffset, + }; + // ๊ทธ๋ฃน์˜ ํฌ๊ธฐ ๊ณ„์‚ฐ: ๋ฒ„ํŠผ๋“ค์˜ ์‹ค์ œ ํฌ๊ธฐ + ๊ฐ„๊ฒฉ์„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ const direction = groupConfig.groupDirection || "horizontal"; const gap = groupConfig.groupGap ?? 8; diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 8a114aa4..45b263d6 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1633,24 +1633,19 @@ export const InteractiveScreenViewer: React.FC = ( } }; - return ( + return applyStyles( +
+ + + +
+ + +
+ +
+ + +
+ + {part.generationMethod === "auto" ? ( + onUpdate({ autoConfig })} + isPreview={isPreview} + /> + ) : ( + onUpdate({ manualConfig })} + isPreview={isPreview} + /> + )} +
+ + ); +}; diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx new file mode 100644 index 00000000..d318feb0 --- /dev/null +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -0,0 +1,407 @@ +"use client"; + +import React, { useState, useCallback, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Plus, Save, Edit2, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { NumberingRuleConfig, NumberingRulePart } from "@/types/numbering-rule"; +import { NumberingRuleCard } from "./NumberingRuleCard"; +import { NumberingRulePreview } from "./NumberingRulePreview"; +import { + getNumberingRules, + createNumberingRule, + updateNumberingRule, + deleteNumberingRule, +} from "@/lib/api/numberingRule"; + +interface NumberingRuleDesignerProps { + initialConfig?: NumberingRuleConfig; + onSave?: (config: NumberingRuleConfig) => void; + onChange?: (config: NumberingRuleConfig) => void; + maxRules?: number; + isPreview?: boolean; + className?: string; +} + +export const NumberingRuleDesigner: React.FC = ({ + initialConfig, + onSave, + onChange, + maxRules = 6, + isPreview = false, + className = "", +}) => { + const [savedRules, setSavedRules] = useState([]); + const [selectedRuleId, setSelectedRuleId] = useState(null); + const [currentRule, setCurrentRule] = useState(null); + const [loading, setLoading] = useState(false); + const [leftTitle, setLeftTitle] = useState("์ €์žฅ๋œ ๊ทœ์น™ ๋ชฉ๋ก"); + const [rightTitle, setRightTitle] = useState("๊ทœ์น™ ํŽธ์ง‘"); + const [editingLeftTitle, setEditingLeftTitle] = useState(false); + const [editingRightTitle, setEditingRightTitle] = useState(false); + + useEffect(() => { + loadRules(); + }, []); + + const loadRules = useCallback(async () => { + setLoading(true); + try { + const response = await getNumberingRules(); + if (response.success && response.data) { + setSavedRules(response.data); + } else { + toast.error(response.error || "๊ทœ์น™ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + } + } catch (error: any) { + toast.error(`๋กœ๋”ฉ ์‹คํŒจ: ${error.message}`); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (currentRule) { + onChange?.(currentRule); + } + }, [currentRule, onChange]); + + const handleAddPart = useCallback(() => { + if (!currentRule) return; + + if (currentRule.parts.length >= maxRules) { + toast.error(`์ตœ๋Œ€ ${maxRules}๊ฐœ๊นŒ์ง€ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค`); + return; + } + + const newPart: NumberingRulePart = { + id: `part-${Date.now()}`, + order: currentRule.parts.length + 1, + partType: "prefix", + generationMethod: "auto", + autoConfig: { prefix: "CODE" }, + }; + + setCurrentRule((prev) => { + if (!prev) return null; + return { ...prev, parts: [...prev.parts, newPart] }; + }); + + toast.success(`๊ทœ์น™ ${newPart.order}๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); + }, [currentRule, maxRules]); + + const handleUpdatePart = useCallback((partId: string, updates: Partial) => { + setCurrentRule((prev) => { + if (!prev) return null; + return { + ...prev, + parts: prev.parts.map((part) => (part.id === partId ? { ...part, ...updates } : part)), + }; + }); + }, []); + + const handleDeletePart = useCallback((partId: string) => { + setCurrentRule((prev) => { + if (!prev) return null; + return { + ...prev, + parts: prev.parts + .filter((part) => part.id !== partId) + .map((part, index) => ({ ...part, order: index + 1 })), + }; + }); + + toast.success("๊ทœ์น™์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + }, []); + + const handleSave = useCallback(async () => { + if (!currentRule) { + toast.error("์ €์žฅํ•  ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค"); + return; + } + + if (currentRule.parts.length === 0) { + toast.error("์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”"); + return; + } + + setLoading(true); + try { + const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId); + + let response; + if (existing) { + response = await updateNumberingRule(currentRule.ruleId, currentRule); + } else { + response = await createNumberingRule(currentRule); + } + + if (response.success && response.data) { + setSavedRules((prev) => { + if (existing) { + return prev.map((r) => (r.ruleId === currentRule.ruleId ? response.data! : r)); + } else { + return [...prev, response.data!]; + } + }); + + setCurrentRule(response.data); + setSelectedRuleId(response.data.ruleId); + + await onSave?.(response.data); + toast.success("์ฑ„๋ฒˆ ๊ทœ์น™์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } else { + toast.error(response.error || "์ €์žฅ ์‹คํŒจ"); + } + } catch (error: any) { + toast.error(`์ €์žฅ ์‹คํŒจ: ${error.message}`); + } finally { + setLoading(false); + } + }, [currentRule, savedRules, onSave]); + + const handleSelectRule = useCallback((rule: NumberingRuleConfig) => { + setSelectedRuleId(rule.ruleId); + setCurrentRule(rule); + toast.info(`"${rule.ruleName}" ๊ทœ์น™์„ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค`); + }, []); + + const handleDeleteSavedRule = useCallback(async (ruleId: string) => { + setLoading(true); + try { + const response = await deleteNumberingRule(ruleId); + + if (response.success) { + setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId)); + + if (selectedRuleId === ruleId) { + setSelectedRuleId(null); + setCurrentRule(null); + } + + toast.success("๊ทœ์น™์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } else { + toast.error(response.error || "์‚ญ์ œ ์‹คํŒจ"); + } + } catch (error: any) { + toast.error(`์‚ญ์ œ ์‹คํŒจ: ${error.message}`); + } finally { + setLoading(false); + } + }, [selectedRuleId]); + + const handleNewRule = useCallback(() => { + const newRule: NumberingRuleConfig = { + ruleId: `rule-${Date.now()}`, + ruleName: "์ƒˆ ์ฑ„๋ฒˆ ๊ทœ์น™", + parts: [], + separator: "-", + resetPeriod: "none", + currentSequence: 1, + }; + + setSelectedRuleId(newRule.ruleId); + setCurrentRule(newRule); + + toast.success("์ƒˆ ๊ทœ์น™์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + }, []); + + return ( +
+ {/* ์ขŒ์ธก: ์ €์žฅ๋œ ๊ทœ์น™ ๋ชฉ๋ก */} +
+
+ {editingLeftTitle ? ( + setLeftTitle(e.target.value)} + onBlur={() => setEditingLeftTitle(false)} + onKeyDown={(e) => e.key === "Enter" && setEditingLeftTitle(false)} + className="h-8 text-sm font-semibold" + autoFocus + /> + ) : ( +

{leftTitle}

+ )} + +
+ + + +
+ {loading ? ( +
+

๋กœ๋”ฉ ์ค‘...

+
+ ) : savedRules.length === 0 ? ( +
+

์ €์žฅ๋œ ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค

+
+ ) : ( + savedRules.map((rule) => ( + handleSelectRule(rule)} + > + +
+
+ {rule.ruleName} +

+ ๊ทœ์น™ {rule.parts.length}๊ฐœ +

+
+ +
+
+ + + +
+ )) + )} +
+
+ + {/* ๊ตฌ๋ถ„์„  */} +
+ + {/* ์šฐ์ธก: ํŽธ์ง‘ ์˜์—ญ */} +
+ {!currentRule ? ( +
+
+

+ ๊ทœ์น™์„ ์„ ํƒํ•ด์ฃผ์„ธ์š” +

+

+ ์ขŒ์ธก์—์„œ ๊ทœ์น™์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์„ธ์š” +

+
+
+ ) : ( + <> +
+ {editingRightTitle ? ( + setRightTitle(e.target.value)} + onBlur={() => setEditingRightTitle(false)} + onKeyDown={(e) => e.key === "Enter" && setEditingRightTitle(false)} + className="h-8 text-sm font-semibold" + autoFocus + /> + ) : ( +

{rightTitle}

+ )} + +
+ +
+ + + setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value })) + } + className="h-9" + placeholder="์˜ˆ: ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ" + /> +
+ + + + ๋ฏธ๋ฆฌ๋ณด๊ธฐ + + + + + + +
+
+

์ฝ”๋“œ ๊ตฌ์„ฑ

+ + {currentRule.parts.length}/{maxRules} + +
+ + {currentRule.parts.length === 0 ? ( +
+

+ ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜์„ธ์š” +

+
+ ) : ( +
+ {currentRule.parts.map((part) => ( + handleUpdatePart(part.id, updates)} + onDelete={() => handleDeletePart(part.id)} + isPreview={isPreview} + /> + ))} +
+ )} +
+ +
+ + +
+ + )} +
+
+ ); +}; diff --git a/frontend/components/numbering-rule/NumberingRulePreview.tsx b/frontend/components/numbering-rule/NumberingRulePreview.tsx new file mode 100644 index 00000000..38e9dbfd --- /dev/null +++ b/frontend/components/numbering-rule/NumberingRulePreview.tsx @@ -0,0 +1,97 @@ +"use client"; + +import React, { useMemo } from "react"; +import { NumberingRuleConfig } from "@/types/numbering-rule"; + +interface NumberingRulePreviewProps { + config: NumberingRuleConfig; + compact?: boolean; +} + +export const NumberingRulePreview: React.FC = ({ + config, + compact = false +}) => { + const generatedCode = useMemo(() => { + if (!config.parts || config.parts.length === 0) { + return "๊ทœ์น™์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”"; + } + + const parts = config.parts + .sort((a, b) => a.order - b.order) + .map((part) => { + if (part.generationMethod === "manual") { + return part.manualConfig?.value || "XXX"; + } + + const autoConfig = part.autoConfig || {}; + + switch (part.partType) { + case "prefix": + return autoConfig.prefix || "PREFIX"; + + case "sequence": { + const length = autoConfig.sequenceLength || 4; + const startFrom = autoConfig.startFrom || 1; + return String(startFrom).padStart(length, "0"); + } + + case "date": { + const format = autoConfig.dateFormat || "YYYYMMDD"; + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + + switch (format) { + case "YYYY": return String(year); + case "YY": return String(year).slice(-2); + case "YYYYMM": return `${year}${month}`; + case "YYMM": return `${String(year).slice(-2)}${month}`; + case "YYYYMMDD": return `${year}${month}${day}`; + case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`; + default: return `${year}${month}${day}`; + } + } + + case "year": { + const now = new Date(); + const format = autoConfig.dateFormat || "YYYY"; + return format === "YY" + ? String(now.getFullYear()).slice(-2) + : String(now.getFullYear()); + } + + case "month": { + const now = new Date(); + return String(now.getMonth() + 1).padStart(2, "0"); + } + + case "custom": + return autoConfig.value || "CUSTOM"; + + default: + return "XXX"; + } + }); + + return parts.join(config.separator || ""); + }, [config]); + + if (compact) { + return ( +
+ {generatedCode} +
+ ); + } + + return ( +
+

์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

+
+ {generatedCode} +
+
+ ); +}; diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts new file mode 100644 index 00000000..7702ea08 --- /dev/null +++ b/frontend/lib/api/numberingRule.ts @@ -0,0 +1,81 @@ +/** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ด€๋ฆฌ API ํด๋ผ์ด์–ธํŠธ + */ + +import { apiClient } from "./client"; +import { NumberingRuleConfig } from "@/types/numbering-rule"; + +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} + +export async function getNumberingRules(): Promise> { + try { + const response = await apiClient.get("/numbering-rules"); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ" }; + } +} + +export async function getNumberingRuleById(ruleId: string): Promise> { + try { + const response = await apiClient.get(`/numbering-rules/${ruleId}`); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ" }; + } +} + +export async function createNumberingRule( + config: NumberingRuleConfig +): Promise> { + try { + const response = await apiClient.post("/numbering-rules", config); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "๊ทœ์น™ ์ƒ์„ฑ ์‹คํŒจ" }; + } +} + +export async function updateNumberingRule( + ruleId: string, + config: Partial +): Promise> { + try { + const response = await apiClient.put(`/numbering-rules/${ruleId}`, config); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ" }; + } +} + +export async function deleteNumberingRule(ruleId: string): Promise> { + try { + const response = await apiClient.delete(`/numbering-rules/${ruleId}`); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "๊ทœ์น™ ์‚ญ์ œ ์‹คํŒจ" }; + } +} + +export async function generateCode(ruleId: string): Promise> { + try { + const response = await apiClient.post(`/numbering-rules/${ruleId}/generate`); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "์ฝ”๋“œ ์ƒ์„ฑ ์‹คํŒจ" }; + } +} + +export async function resetSequence(ruleId: string): Promise> { + try { + const response = await apiClient.post(`/numbering-rules/${ruleId}/reset`); + return response.data; + } catch (error: any) { + return { success: false, error: error.message || "์‹œํ€€์Šค ์ดˆ๊ธฐํ™” ์‹คํŒจ" }; + } +} diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index f2ac68c2..315cf1da 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -39,6 +39,7 @@ import "./split-panel-layout/SplitPanelLayoutRenderer"; import "./map/MapRenderer"; import "./repeater-field-group/RepeaterFieldGroupRenderer"; import "./flow-widget/FlowWidgetRenderer"; +import "./numbering-rule/NumberingRuleRenderer"; /** * ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ diff --git a/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx b/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx new file mode 100644 index 00000000..78c366fd --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/NumberingRuleComponent.tsx @@ -0,0 +1,29 @@ +"use client"; + +import React from "react"; +import { NumberingRuleDesigner } from "@/components/numbering-rule/NumberingRuleDesigner"; +import { NumberingRuleComponentConfig } from "./types"; + +interface NumberingRuleWrapperProps { + config: NumberingRuleComponentConfig; + onChange?: (config: NumberingRuleComponentConfig) => void; + isPreview?: boolean; +} + +export const NumberingRuleWrapper: React.FC = ({ + config, + onChange, + isPreview = false, +}) => { + return ( +
+ +
+ ); +}; + +export const NumberingRuleComponent = NumberingRuleWrapper; diff --git a/frontend/lib/registry/components/numbering-rule/NumberingRuleConfigPanel.tsx b/frontend/lib/registry/components/numbering-rule/NumberingRuleConfigPanel.tsx new file mode 100644 index 00000000..332d4055 --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/NumberingRuleConfigPanel.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { NumberingRuleComponentConfig } from "./types"; + +interface NumberingRuleConfigPanelProps { + config: NumberingRuleComponentConfig; + onChange: (config: NumberingRuleComponentConfig) => void; +} + +export const NumberingRuleConfigPanel: React.FC = ({ + config, + onChange, +}) => { + return ( +
+
+ + + onChange({ ...config, maxRules: parseInt(e.target.value) || 6 }) + } + className="h-9" + /> +

+ ํ•œ ๊ทœ์น™์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ํŒŒํŠธ ๊ฐœ์ˆ˜ (1-10) +

+
+ +
+
+ +

+ ํŽธ์ง‘ ๊ธฐ๋Šฅ์„ ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค +

+
+ + onChange({ ...config, readonly: checked }) + } + /> +
+ +
+
+ +

+ ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ•ญ์ƒ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค +

+
+ + onChange({ ...config, showPreview: checked }) + } + /> +
+ +
+
+ +

+ ์ €์žฅ๋œ ๊ทœ์น™ ๋ชฉ๋ก์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค +

+
+ + onChange({ ...config, showRuleList: checked }) + } + /> +
+ +
+ + +

+ ๊ทœ์น™ ํŒŒํŠธ ์นด๋“œ์˜ ๋ฐฐ์น˜ ๋ฐฉํ–ฅ +

+
+
+ ); +}; diff --git a/frontend/lib/registry/components/numbering-rule/NumberingRuleRenderer.tsx b/frontend/lib/registry/components/numbering-rule/NumberingRuleRenderer.tsx new file mode 100644 index 00000000..29c98b45 --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/NumberingRuleRenderer.tsx @@ -0,0 +1,33 @@ +"use client"; + +import React from "react"; +import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; +import { NumberingRuleDefinition } from "./index"; +import { NumberingRuleComponent } from "./NumberingRuleComponent"; + +/** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ Œ๋”๋Ÿฌ + * ์ž๋™ ๋“ฑ๋ก ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋ก + */ +export class NumberingRuleRenderer extends AutoRegisteringComponentRenderer { + static componentDefinition = NumberingRuleDefinition; + + render(): React.ReactElement { + return ; + } + + /** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ํŠนํ™” ๋ฉ”์„œ๋“œ + */ + protected handleValueChange = (value: any) => { + this.updateComponent({ value }); + }; +} + +// ์ž๋™ ๋“ฑ๋ก ์‹คํ–‰ +NumberingRuleRenderer.registerSelf(); + +// Hot Reload ์ง€์› (๊ฐœ๋ฐœ ๋ชจ๋“œ) +if (process.env.NODE_ENV === "development") { + NumberingRuleRenderer.enableHotReload(); +} diff --git a/frontend/lib/registry/components/numbering-rule/README.md b/frontend/lib/registry/components/numbering-rule/README.md new file mode 100644 index 00000000..5d04d894 --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/README.md @@ -0,0 +1,102 @@ +# ์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ + +## ๊ฐœ์š” + +์‹œ์Šคํ…œ์—์„œ ์ž๋™์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ทœ์น™์„ ์„ค์ •ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ด€๋ฆฌ์ž ์ „์šฉ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. + +## ์ฃผ์š” ๊ธฐ๋Šฅ + +- **์ขŒ์šฐ ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ**: ์ขŒ์ธก์—์„œ ๊ทœ์น™ ๋ชฉ๋ก, ์šฐ์ธก์—์„œ ํŽธ์ง‘ +- **๋™์  ํŒŒํŠธ ์‹œ์Šคํ…œ**: ์ตœ๋Œ€ 6๊ฐœ์˜ ํŒŒํŠธ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์กฐํ•ฉ +- **์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ**: ์„ค์ • ์ฆ‰์‹œ ์ƒ์„ฑ๋  ์ฝ”๋“œ ํ™•์ธ +- **๋‹ค์–‘ํ•œ ํŒŒํŠธ ์œ ํ˜•**: ์ ‘๋‘์‚ฌ, ์ˆœ๋ฒˆ, ๋‚ ์งœ, ์—ฐ๋„, ์›”, ์ปค์Šคํ…€ + +## ์ƒ์„ฑ ์ฝ”๋“œ ์˜ˆ์‹œ + +- ์ œํ’ˆ ์ฝ”๋“œ: `PROD-20251104-0001` +- ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ: `PRJ-2025-001` +- ๊ฑฐ๋ž˜์ฒ˜ ์ฝ”๋“œ: `CUST-A-0001` + +## ํŒŒํŠธ ์œ ํ˜• + +### 1. ์ ‘๋‘์‚ฌ (prefix) +๊ณ ์ •๋œ ๋ฌธ์ž์—ด์„ ์ฝ”๋“œ ์•ž์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. +- ์˜ˆ: `PROD`, `PRJ`, `CUST` + +### 2. ์ˆœ๋ฒˆ (sequence) +์ž๋™์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋Š” ๋ฒˆํ˜ธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. +- ์ž๋ฆฟ์ˆ˜ ์„ค์ • ๊ฐ€๋Šฅ (1-10) +- ์‹œ์ž‘ ๋ฒˆํ˜ธ ์„ค์ • ๊ฐ€๋Šฅ +- ์˜ˆ: `0001`, `00001` + +### 3. ๋‚ ์งœ (date) +ํ˜„์žฌ ๋‚ ์งœ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. +- YYYY: 2025 +- YYYYMMDD: 20251104 +- YYMMDD: 251104 + +### 4. ์—ฐ๋„ (year) +ํ˜„์žฌ ์—ฐ๋„๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. +- YYYY: 2025 +- YY: 25 + +### 5. ์›” (month) +ํ˜„์žฌ ์›”์„ 2์ž๋ฆฌ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. +- ์˜ˆ: 01, 02, ..., 12 + +### 6. ์‚ฌ์šฉ์ž ์ •์˜ (custom) +์›ํ•˜๋Š” ๊ฐ’์„ ์ง์ ‘ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. + +## ์ƒ์„ฑ ๋ฐฉ์‹ + +### ์ž๋™ ์ƒ์„ฑ (auto) +์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ๊ฐ’์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + +### ์ง์ ‘ ์ž…๋ ฅ (manual) +์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ’์„ ์ง์ ‘ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. + +## ์„ค์ • ์˜ต์…˜ + +| ์˜ต์…˜ | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|------|------|--------|------| +| `maxRules` | number | 6 | ์ตœ๋Œ€ ํŒŒํŠธ ๊ฐœ์ˆ˜ | +| `readonly` | boolean | false | ์ฝ๊ธฐ ์ „์šฉ ๋ชจ๋“œ | +| `showPreview` | boolean | true | ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ | +| `showRuleList` | boolean | true | ๊ทœ์น™ ๋ชฉ๋ก ํ‘œ์‹œ | +| `cardLayout` | "vertical" \| "horizontal" | "vertical" | ์นด๋“œ ๋ฐฐ์น˜ ๋ฐฉํ–ฅ | + +## ์‚ฌ์šฉ ์˜ˆ์‹œ + +```typescript + +``` + +## ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ + +### numbering_rules (๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”) +- ๊ทœ์น™ ID, ๊ทœ์น™๋ช…, ๊ตฌ๋ถ„์ž +- ์ดˆ๊ธฐํ™” ์ฃผ๊ธฐ, ํ˜„์žฌ ์‹œํ€€์Šค +- ์ ์šฉ ๋Œ€์ƒ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ + +### numbering_rule_parts (ํŒŒํŠธ ํ…Œ์ด๋ธ”) +- ํŒŒํŠธ ์ˆœ์„œ, ํŒŒํŠธ ์œ ํ˜• +- ์ƒ์„ฑ ๋ฐฉ์‹, ์„ค์ • (JSONB) + +## API ์—”๋“œํฌ์ธํŠธ + +- `GET /api/numbering-rules` - ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ +- `POST /api/numbering-rules` - ๊ทœ์น™ ์ƒ์„ฑ +- `PUT /api/numbering-rules/:ruleId` - ๊ทœ์น™ ์ˆ˜์ • +- `DELETE /api/numbering-rules/:ruleId` - ๊ทœ์น™ ์‚ญ์ œ +- `POST /api/numbering-rules/:ruleId/generate` - ์ฝ”๋“œ ์ƒ์„ฑ + +## ๋ฒ„์ „ ์ •๋ณด + +- **๋ฒ„์ „**: 1.0.0 +- **์ž‘์„ฑ์ผ**: 2025-11-04 +- **์ž‘์„ฑ์ž**: ๊ฐœ๋ฐœํŒ€ + diff --git a/frontend/lib/registry/components/numbering-rule/config.ts b/frontend/lib/registry/components/numbering-rule/config.ts new file mode 100644 index 00000000..87e5c996 --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/config.ts @@ -0,0 +1,15 @@ +/** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ ์„ค์ • + */ + +import { NumberingRuleComponentConfig } from "./types"; + +export const defaultConfig: NumberingRuleComponentConfig = { + maxRules: 6, + readonly: false, + showPreview: true, + showRuleList: true, + enableReorder: false, + cardLayout: "vertical", +}; + diff --git a/frontend/lib/registry/components/numbering-rule/index.ts b/frontend/lib/registry/components/numbering-rule/index.ts new file mode 100644 index 00000000..6399ab2a --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/index.ts @@ -0,0 +1,42 @@ +"use client"; + +import React from "react"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import { NumberingRuleWrapper } from "./NumberingRuleComponent"; +import { NumberingRuleConfigPanel } from "./NumberingRuleConfigPanel"; +import { defaultConfig } from "./config"; + +/** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ์ •์˜ + * ์ฝ”๋“œ ์ž๋™ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์„ค์ •ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ด€๋ฆฌ์ž ์ „์šฉ ์ปดํฌ๋„ŒํŠธ + */ +export const NumberingRuleDefinition = createComponentDefinition({ + id: "numbering-rule", + name: "์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™", + nameEng: "Numbering Rule Component", + description: "์ฝ”๋“œ ์ž๋™ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์„ค์ •ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ", + category: ComponentCategory.DISPLAY, + webType: "component", + component: NumberingRuleWrapper, + defaultConfig: defaultConfig, + defaultSize: { + width: 1200, + height: 800, + gridColumnSpan: "12", + }, + configPanel: NumberingRuleConfigPanel, + icon: "Hash", + tags: ["์ฝ”๋“œ", "์ฑ„๋ฒˆ", "๊ทœ์น™", "ํ‘œ์‹œ", "์ž๋™์ƒ์„ฑ"], + version: "1.0.0", + author: "๊ฐœ๋ฐœํŒ€", + documentation: "์ฝ”๋“œ ์ž๋™ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ ‘๋‘์‚ฌ, ๋‚ ์งœ, ์ˆœ๋ฒˆ ๋“ฑ์„ ์กฐํ•ฉํ•˜์—ฌ ๊ณ ์œ ํ•œ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", +}); + +// ํƒ€์ž… ๋‚ด๋ณด๋‚ด๊ธฐ +export type { NumberingRuleComponentConfig } from "./types"; + +// ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ณด๋‚ด๊ธฐ +export { NumberingRuleComponent } from "./NumberingRuleComponent"; +export { NumberingRuleRenderer } from "./NumberingRuleRenderer"; + diff --git a/frontend/lib/registry/components/numbering-rule/types.ts b/frontend/lib/registry/components/numbering-rule/types.ts new file mode 100644 index 00000000..43def2cb --- /dev/null +++ b/frontend/lib/registry/components/numbering-rule/types.ts @@ -0,0 +1,15 @@ +/** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ •์˜ + */ + +import { NumberingRuleConfig } from "@/types/numbering-rule"; + +export interface NumberingRuleComponentConfig { + ruleConfig?: NumberingRuleConfig; + maxRules?: number; + readonly?: boolean; + showPreview?: boolean; + showRuleList?: boolean; + enableReorder?: boolean; + cardLayout?: "vertical" | "horizontal"; +} diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts new file mode 100644 index 00000000..dbdbf9bd --- /dev/null +++ b/frontend/types/numbering-rule.ts @@ -0,0 +1,117 @@ +/** + * ์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ •์˜ + * Shadcn/ui ๊ฐ€์ด๋“œ๋ผ์ธ ๊ธฐ๋ฐ˜ + */ + +/** + * ์ฝ”๋“œ ํŒŒํŠธ ์œ ํ˜• + */ +export type CodePartType = + | "prefix" // ์ ‘๋‘์‚ฌ (๊ณ ์ • ๋ฌธ์ž์—ด) + | "sequence" // ์ˆœ๋ฒˆ (์ž๋™ ์ฆ๊ฐ€) + | "date" // ๋‚ ์งœ (YYYYMMDD ๋“ฑ) + | "year" // ์—ฐ๋„ (YYYY) + | "month" // ์›” (MM) + | "custom"; // ์‚ฌ์šฉ์ž ์ •์˜ + +/** + * ์ƒ์„ฑ ๋ฐฉ์‹ + */ +export type GenerationMethod = + | "auto" // ์ž๋™ ์ƒ์„ฑ + | "manual"; // ์ง์ ‘ ์ž…๋ ฅ + +/** + * ๋‚ ์งœ ํ˜•์‹ + */ +export type DateFormat = + | "YYYY" // 2025 + | "YY" // 25 + | "YYYYMM" // 202511 + | "YYMM" // 2511 + | "YYYYMMDD" // 20251104 + | "YYMMDD"; // 251104 + +/** + * ๋‹จ์ผ ๊ทœ์น™ ํŒŒํŠธ + */ +export interface NumberingRulePart { + id: string; // ๊ณ ์œ  ID + order: number; // ์ˆœ์„œ (1-6) + partType: CodePartType; // ํŒŒํŠธ ์œ ํ˜• + generationMethod: GenerationMethod; // ์ƒ์„ฑ ๋ฐฉ์‹ + + // ์ž๋™ ์ƒ์„ฑ ์„ค์ • + autoConfig?: { + prefix?: string; // ์ ‘๋‘์‚ฌ + sequenceLength?: number; // ์ˆœ๋ฒˆ ์ž๋ฆฟ์ˆ˜ + startFrom?: number; // ์‹œ์ž‘ ๋ฒˆํ˜ธ + dateFormat?: DateFormat; // ๋‚ ์งœ ํ˜•์‹ + value?: string; // ์ปค์Šคํ…€ ๊ฐ’ + }; + + // ์ง์ ‘ ์ž…๋ ฅ ์„ค์ • + manualConfig?: { + value: string; // ์ž…๋ ฅ๊ฐ’ + placeholder?: string; // ํ”Œ๋ ˆ์ด์Šคํ™€๋” + }; + + // ์ƒ์„ฑ๋œ ๊ฐ’ (๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ) + generatedValue?: string; +} + +/** + * ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ + */ +export interface NumberingRuleConfig { + ruleId: string; // ๊ทœ์น™ ID + ruleName: string; // ๊ทœ์น™๋ช… + description?: string; // ์„ค๋ช… + parts: NumberingRulePart[]; // ๊ทœ์น™ ํŒŒํŠธ ๋ฐฐ์—ด + + // ์„ค์ • + separator?: string; // ๊ตฌ๋ถ„์ž (๊ธฐ๋ณธ: "-") + resetPeriod?: "none" | "daily" | "monthly" | "yearly"; + currentSequence?: number; // ํ˜„์žฌ ์‹œํ€€์Šค + + // ์ ์šฉ ๋Œ€์ƒ + tableName?: string; // ์ ์šฉํ•  ํ…Œ์ด๋ธ”๋ช… + columnName?: string; // ์ ์šฉํ•  ์ปฌ๋Ÿผ๋ช… + + // ๋ฉ”ํƒ€ ์ •๋ณด + companyCode?: string; + createdAt?: string; + updatedAt?: string; + createdBy?: string; +} + +/** + * UI ์˜ต์…˜ ์ƒ์ˆ˜ + */ +export const CODE_PART_TYPE_OPTIONS: Array<{ value: CodePartType; label: string }> = [ + { value: "prefix", label: "์ ‘๋‘์‚ฌ" }, + { value: "sequence", label: "์ˆœ๋ฒˆ" }, + { value: "date", label: "๋‚ ์งœ" }, + { value: "year", label: "์—ฐ๋„" }, + { value: "month", label: "์›”" }, + { value: "custom", label: "์‚ฌ์šฉ์ž ์ •์˜" }, +]; + +export const DATE_FORMAT_OPTIONS: Array<{ value: DateFormat; label: string; example: string }> = [ + { value: "YYYY", label: "์—ฐ๋„ (4์ž๋ฆฌ)", example: "2025" }, + { value: "YY", label: "์—ฐ๋„ (2์ž๋ฆฌ)", example: "25" }, + { value: "YYYYMM", label: "์—ฐ๋„+์›”", example: "202511" }, + { value: "YYMM", label: "์—ฐ๋„(2)+์›”", example: "2511" }, + { value: "YYYYMMDD", label: "์—ฐ์›”์ผ", example: "20251104" }, + { value: "YYMMDD", label: "์—ฐ(2)+์›”์ผ", example: "251104" }, +]; + +export const RESET_PERIOD_OPTIONS: Array<{ + value: "none" | "daily" | "monthly" | "yearly"; + label: string; +}> = [ + { value: "none", label: "์ดˆ๊ธฐํ™” ์•ˆํ•จ" }, + { value: "daily", label: "์ผ๋ณ„ ์ดˆ๊ธฐํ™”" }, + { value: "monthly", label: "์›”๋ณ„ ์ดˆ๊ธฐํ™”" }, + { value: "yearly", label: "์—ฐ๋ณ„ ์ดˆ๊ธฐํ™”" }, +]; From 6901baab8e8fe552eadf9e12e9132c06ce7728b8 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 4 Nov 2025 16:17:19 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat(screen-designer):=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20=EC=BB=AC=EB=9F=BC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=84=88=EB=B9=84=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ: - ๊ฒฉ์ž ์„ค์ •์„ ํŽธ์ง‘ ํƒญ์—์„œ ํ•ญ์ƒ ํ‘œ์‹œ (ํ•ด์ƒ๋„ ์„ค์ • ํ•˜๋‹จ) - ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ๋™์  ์กฐ์ • ๊ฐ€๋Šฅ (1-24) - ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์‹œ ํ˜„์žฌ ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ๊ธฐ๋ฐ˜ ์ž๋™ ๊ณ„์‚ฐ - ์ปดํฌ๋„ŒํŠธ ๋„ˆ๋น„๊ฐ€ ์„ค์ •ํ•œ ์ปฌ๋Ÿผ ์ˆ˜๋Œ€๋กœ ์ •ํ™•ํžˆ ํ‘œ์‹œ๋˜๋„๋ก ์ˆ˜์ • ์ˆ˜์ •๋œ ํŒŒ์ผ: - ScreenDesigner: ์ปดํฌ๋„ŒํŠธ ๋“œ๋กญ ์‹œ gridColumns์™€ style.width ๋™์  ๊ณ„์‚ฐ - UnifiedPropertiesPanel: ๊ฒฉ์ž ์„ค์ • UI ํ†ตํ•ฉ, ์ฐจ์ง€ ์ปฌ๋Ÿผ ์ˆ˜ ์„ค์ • ์‹œ width ์ž๋™ ๊ณ„์‚ฐ - RealtimePreviewDynamic: getWidth ์šฐ์„ ์ˆœ์œ„ ์ˆ˜์ •, DOM ํฌ๊ธฐ ๋””๋ฒ„๊น… ๋กœ๊ทธ ์ถ”๊ฐ€ - 8๊ฐœ ์ปดํฌ๋„ŒํŠธ: componentStyle.width๋ฅผ ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • * ButtonPrimaryComponent * TextInputComponent * NumberInputComponent * TextareaBasicComponent * DateInputComponent * TableListComponent * CardDisplayComponent ๋ฌธ์ œ ํ•ด๊ฒฐ: - ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ component.style.width๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ์ด์ค‘ ์ถ•์†Œ ๋ฐœ์ƒ - ํ•ด๊ฒฐ: ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ(RealtimePreviewDynamic)๊ฐ€ width ์ œ์–ด, ์ปดํฌ๋„ŒํŠธ๋Š” ํ•ญ์ƒ 100% - ๊ฒฐ๊ณผ: ํŒŒ๋ž€ ํ…Œ๋‘๋ฆฌ์™€ ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ ๊ฐ€ ๋™์ผํ•œ ํฌ๊ธฐ๋กœ ์ •ํ™•ํžˆ ํ‘œ์‹œ --- .../src/services/numberingRuleService.ts | 21 +- .../numbering-rule/AutoConfigPanel.tsx | 117 +++-- .../numbering-rule/NumberingRuleCard.tsx | 10 +- .../numbering-rule/NumberingRuleDesigner.tsx | 30 +- .../numbering-rule/NumberingRulePreview.tsx | 32 +- .../components/screen/RealtimePreview.tsx | 17 +- .../screen/RealtimePreviewDynamic.tsx | 125 +++-- frontend/components/screen/ScreenDesigner.tsx | 117 +++-- .../components/screen/panels/GridPanel.tsx | 25 +- .../screen/panels/PropertiesPanel.tsx | 49 +- .../screen/panels/UnifiedPropertiesPanel.tsx | 211 +++++++- .../screen/templates/NumberingRuleTemplate.ts | 77 +++ .../components/screen/widgets/TabsWidget.tsx | 210 ++++++++ .../button-primary/ButtonPrimaryComponent.tsx | 2 + .../card-display/CardDisplayComponent.tsx | 3 + .../checkbox-basic/CheckboxBasicComponent.tsx | 2 + .../date-input/DateInputComponent.tsx | 2 + .../divider-line/DividerLineComponent.tsx | 2 + .../image-display/ImageDisplayComponent.tsx | 2 + .../number-input/NumberInputComponent.tsx | 2 + .../radio-basic/RadioBasicComponent.tsx | 2 + .../slider-basic/SliderBasicComponent.tsx | 2 + .../table-list/TableListComponent.tsx | 6 +- .../text-input/TextInputComponent.tsx | 4 +- .../textarea-basic/TextareaBasicComponent.tsx | 2 + .../toggle-switch/ToggleSwitchComponent.tsx | 2 + frontend/types/numbering-rule.ts | 44 +- ์ฝ”๋“œ_์ฑ„๋ฒˆ_๊ทœ์น™_์ปดํฌ๋„ŒํŠธ_๊ตฌํ˜„_๊ณ„ํš์„œ.md | 494 ++++++++++++++++++ 28 files changed, 1397 insertions(+), 215 deletions(-) create mode 100644 frontend/components/screen/templates/NumberingRuleTemplate.ts create mode 100644 frontend/components/screen/widgets/TabsWidget.tsx create mode 100644 ์ฝ”๋“œ_์ฑ„๋ฒˆ_๊ทœ์น™_์ปดํฌ๋„ŒํŠธ_๊ตฌํ˜„_๊ณ„ํš์„œ.md diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index bd896845..c61fce29 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -51,6 +51,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -104,6 +106,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", + menu_id AS "menuId", + scope_type AS "scopeType", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -153,8 +157,9 @@ class NumberingRuleService { const insertRuleQuery = ` INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, - current_sequence, table_name, column_name, company_code, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + current_sequence, table_name, column_name, company_code, + menu_objid, scope_type, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING rule_id AS "ruleId", rule_name AS "ruleName", @@ -165,6 +170,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -180,6 +187,8 @@ class NumberingRuleService { config.tableName || null, config.columnName || null, companyCode, + config.menuObjid || null, + config.scopeType || "global", userId, ]); @@ -248,8 +257,10 @@ class NumberingRuleService { reset_period = COALESCE($4, reset_period), table_name = COALESCE($5, table_name), column_name = COALESCE($6, column_name), + menu_objid = COALESCE($7, menu_objid), + scope_type = COALESCE($8, scope_type), updated_at = NOW() - WHERE rule_id = $7 AND company_code = $8 + WHERE rule_id = $9 AND company_code = $10 RETURNING rule_id AS "ruleId", rule_name AS "ruleName", @@ -260,6 +271,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -272,6 +285,8 @@ class NumberingRuleService { updates.resetPeriod, updates.tableName, updates.columnName, + updates.menuObjid, + updates.scopeType, ruleId, companyCode, ]); diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx index 1f54cae6..8f05e2e3 100644 --- a/frontend/components/numbering-rule/AutoConfigPanel.tsx +++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx @@ -19,24 +19,7 @@ export const AutoConfigPanel: React.FC = ({ onChange, isPreview = false, }) => { - if (partType === "prefix") { - return ( -
- - onChange({ ...config, prefix: e.target.value })} - placeholder="์˜ˆ: PROD" - disabled={isPreview} - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -

- ์ฝ”๋“œ ์•ž์— ๋ถ™์„ ๊ณ ์ • ๋ฌธ์ž์—ด -

-
- ); - } - + // 1. ์ˆœ๋ฒˆ (์ž๋™ ์ฆ๊ฐ€) if (partType === "sequence") { return (
@@ -46,15 +29,15 @@ export const AutoConfigPanel: React.FC = ({ type="number" min={1} max={10} - value={config.sequenceLength || 4} + value={config.sequenceLength || 3} onChange={(e) => - onChange({ ...config, sequenceLength: parseInt(e.target.value) || 4 }) + onChange({ ...config, sequenceLength: parseInt(e.target.value) || 3 }) } disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" />

- ์˜ˆ: 4 โ†’ 0001, 5 โ†’ 00001 + ์˜ˆ: 3 โ†’ 001, 4 โ†’ 0001

@@ -69,11 +52,56 @@ export const AutoConfigPanel: React.FC = ({ disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" /> +

+ ์ˆœ๋ฒˆ์ด ์‹œ์ž‘๋  ๋ฒˆํ˜ธ +

); } + // 2. ์ˆซ์ž (๊ณ ์ • ์ž๋ฆฟ์ˆ˜) + if (partType === "number") { + return ( +
+
+ + + onChange({ ...config, numberLength: parseInt(e.target.value) || 4 }) + } + disabled={isPreview} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +

+ ์˜ˆ: 4 โ†’ 0001, 5 โ†’ 00001 +

+
+
+ + + onChange({ ...config, numberValue: parseInt(e.target.value) || 0 }) + } + disabled={isPreview} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +

+ ๊ณ ์ •์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆซ์ž +

+
+
+ ); + } + + // 3. ๋‚ ์งœ if (partType === "date") { return (
@@ -94,53 +122,28 @@ export const AutoConfigPanel: React.FC = ({ ))} -
- ); - } - - if (partType === "year") { - return ( -
- - -
- ); - } - - if (partType === "month") { - return ( -
- -

- ํ˜„์žฌ ์›”์ด 2์ž๋ฆฌ ํ˜•์‹(01-12)์œผ๋กœ ์ž๋™ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค +

+ ํ˜„์žฌ ๋‚ ์งœ๊ฐ€ ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค

); } - if (partType === "custom") { + // 4. ๋ฌธ์ž + if (partType === "text") { return (
- + onChange({ ...config, value: e.target.value })} - placeholder="์ž…๋ ฅ๊ฐ’" + value={config.textValue || ""} + onChange={(e) => onChange({ ...config, textValue: e.target.value })} + placeholder="์˜ˆ: PRJ, CODE, PROD" disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" /> +

+ ๊ณ ์ •์œผ๋กœ ์‚ฌ์šฉํ•  ํ…์ŠคํŠธ ๋˜๋Š” ์ฝ”๋“œ +

); } diff --git a/frontend/components/numbering-rule/NumberingRuleCard.tsx b/frontend/components/numbering-rule/NumberingRuleCard.tsx index a6f2cab3..83fcd3a2 100644 --- a/frontend/components/numbering-rule/NumberingRuleCard.tsx +++ b/frontend/components/numbering-rule/NumberingRuleCard.tsx @@ -35,7 +35,7 @@ export const NumberingRuleCard: React.FC = ({ variant="ghost" size="icon" onClick={onDelete} - className="h-7 w-7 text-destructive sm:h-8 sm:w-8" + className="text-destructive h-7 w-7 sm:h-8 sm:w-8" disabled={isPreview} > @@ -75,8 +75,12 @@ export const NumberingRuleCard: React.FC = ({ - ์ž๋™ ์ƒ์„ฑ - ์ง์ ‘ ์ž…๋ ฅ + + ์ž๋™ ์ƒ์„ฑ + + + ์ง์ ‘ ์ž…๋ ฅ +
diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index d318feb0..96c88201 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, Save, Edit2, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { NumberingRuleConfig, NumberingRulePart } from "@/types/numbering-rule"; @@ -80,9 +81,9 @@ export const NumberingRuleDesigner: React.FC = ({ const newPart: NumberingRulePart = { id: `part-${Date.now()}`, order: currentRule.parts.length + 1, - partType: "prefix", + partType: "text", generationMethod: "auto", - autoConfig: { prefix: "CODE" }, + autoConfig: { textValue: "CODE" }, }; setCurrentRule((prev) => { @@ -201,6 +202,7 @@ export const NumberingRuleDesigner: React.FC = ({ separator: "-", resetPeriod: "none", currentSequence: 1, + scopeType: "global", }; setSelectedRuleId(newRule.ruleId); @@ -342,6 +344,30 @@ export const NumberingRuleDesigner: React.FC = ({ />
+
+ + +

+ {currentRule.scopeType === "menu" + ? "์ด ๊ทœ์น™์ด ์„ค์ •๋œ ์ƒ์œ„ ๋ฉ”๋‰ด์˜ ๋ชจ๋“  ํ•˜์œ„ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" + : "ํšŒ์‚ฌ ๋‚ด ๋ชจ๋“  ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ „์—ญ ๊ทœ์น™์ž…๋‹ˆ๋‹ค"} +

+
+ ๋ฏธ๋ฆฌ๋ณด๊ธฐ diff --git a/frontend/components/numbering-rule/NumberingRulePreview.tsx b/frontend/components/numbering-rule/NumberingRulePreview.tsx index 38e9dbfd..e29cd4f4 100644 --- a/frontend/components/numbering-rule/NumberingRulePreview.tsx +++ b/frontend/components/numbering-rule/NumberingRulePreview.tsx @@ -27,15 +27,21 @@ export const NumberingRulePreview: React.FC = ({ const autoConfig = part.autoConfig || {}; switch (part.partType) { - case "prefix": - return autoConfig.prefix || "PREFIX"; - + // 1. ์ˆœ๋ฒˆ (์ž๋™ ์ฆ๊ฐ€) case "sequence": { - const length = autoConfig.sequenceLength || 4; + const length = autoConfig.sequenceLength || 3; const startFrom = autoConfig.startFrom || 1; return String(startFrom).padStart(length, "0"); } + // 2. ์ˆซ์ž (๊ณ ์ • ์ž๋ฆฟ์ˆ˜) + case "number": { + const length = autoConfig.numberLength || 4; + const value = autoConfig.numberValue || 0; + return String(value).padStart(length, "0"); + } + + // 3. ๋‚ ์งœ case "date": { const format = autoConfig.dateFormat || "YYYYMMDD"; const now = new Date(); @@ -54,21 +60,9 @@ export const NumberingRulePreview: React.FC = ({ } } - case "year": { - const now = new Date(); - const format = autoConfig.dateFormat || "YYYY"; - return format === "YY" - ? String(now.getFullYear()).slice(-2) - : String(now.getFullYear()); - } - - case "month": { - const now = new Date(); - return String(now.getMonth() + 1).padStart(2, "0"); - } - - case "custom": - return autoConfig.value || "CUSTOM"; + // 4. ๋ฌธ์ž + case "text": + return autoConfig.textValue || "TEXT"; default: return "XXX"; diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 12c300de..906d5ad6 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -399,13 +399,26 @@ export const RealtimePreviewDynamic: React.FC = ({ willUse100Percent: positionX === 0, }); + // ๋„ˆ๋น„ ๊ฒฐ์ • ๋กœ์ง: style.width (ํผ์„ผํŠธ) > ์กฐ๊ฑด๋ถ€ 100% > size.width (ํ”ฝ์…€) + const getWidth = () => { + // 1์ˆœ์œ„: style.width๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (ํผ์„ผํŠธ ๊ฐ’) + if (style?.width) { + return style.width; + } + // 2์ˆœ์œ„: left๊ฐ€ 0์ด๋ฉด 100% + if (positionX === 0) { + return "100%"; + } + // 3์ˆœ์œ„: size.width ํ”ฝ์…€ ๊ฐ’ + return size?.width || 200; + }; + const componentStyle = { position: "absolute" as const, ...style, // ๋จผ์ € ์ ์šฉํ•˜๊ณ  left: positionX, top: position?.y || 0, - // ๐Ÿ†• left๊ฐ€ 0์ด๋ฉด ๋ถ€๋ชจ ๋„ˆ๋น„๋ฅผ 100% ์ฑ„์šฐ๋„๋ก ์ˆ˜์ • (์šฐ์ธก ์—ฌ๋ฐฑ ์ œ๊ฑฐ) - width: positionX === 0 ? "100%" : (size?.width || 200), + width: getWidth(), // ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ฅธ ๋„ˆ๋น„ height: finalHeight, zIndex: position?.z || 1, // right ์†์„ฑ ๊ฐ•์ œ ์ œ๊ฑฐ diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index e90a83ee..1f11182f 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -200,19 +200,58 @@ export const RealtimePreviewDynamic: React.FC = ({ : {}; // ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ ์Šคํƒ€์ผ - ๋ ˆ์ด์•„์›ƒ์€ ํ•ญ์ƒ ๋งจ ์•„๋ž˜ - // ๋„ˆ๋น„ ์šฐ์„ ์ˆœ์œ„: style.width > size.width (ํ”ฝ์…€๊ฐ’) + // ๋„ˆ๋น„ ์šฐ์„ ์ˆœ์œ„: style.width > ์กฐ๊ฑด๋ถ€ 100% > size.width (ํ”ฝ์…€๊ฐ’) const getWidth = () => { - // 1์ˆœ์œ„: style.width๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + // 1์ˆœ์œ„: style.width๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (ํผ์„ผํŠธ ๊ฐ’) if (componentStyle?.width) { + console.log("โœ… [getWidth] style.width ์‚ฌ์šฉ:", { + componentId: id, + label: component.label, + styleWidth: componentStyle.width, + gridColumns: (component as any).gridColumns, + componentStyle: componentStyle, + baseStyle: { + left: `${position.x}px`, + top: `${position.y}px`, + width: componentStyle.width, + height: getHeight(), + }, + }); return componentStyle.width; } - // 2์ˆœ์œ„: size.width (ํ”ฝ์…€) - if (component.componentConfig?.type === "table-list") { - return `${Math.max(size?.width || 120, 120)}px`; + // 2์ˆœ์œ„: x=0์ธ ์ปดํฌ๋„ŒํŠธ๋Š” ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ (๋ฒ„ํŠผ ์ œ์™ธ) + const isButtonComponent = + (component.type === "widget" && (component as WidgetComponent).widgetType === "button") || + (component.type === "component" && (component as any).componentType?.includes("button")); + + if (position.x === 0 && !isButtonComponent) { + console.log("โš ๏ธ [getWidth] 100% ์‚ฌ์šฉ (x=0):", { + componentId: id, + label: component.label, + }); + return "100%"; } - return `${size?.width || 100}px`; + // 3์ˆœ์œ„: size.width (ํ”ฝ์…€) + if (component.componentConfig?.type === "table-list") { + const width = `${Math.max(size?.width || 120, 120)}px`; + console.log("๐Ÿ“ [getWidth] ํ”ฝ์…€ ์‚ฌ์šฉ (table-list):", { + componentId: id, + label: component.label, + width, + }); + return width; + } + + const width = `${size?.width || 100}px`; + console.log("๐Ÿ“ [getWidth] ํ”ฝ์…€ ์‚ฌ์šฉ (๊ธฐ๋ณธ):", { + componentId: id, + label: component.label, + width, + sizeWidth: size?.width, + }); + return width; }; const getHeight = () => { @@ -235,35 +274,54 @@ export const RealtimePreviewDynamic: React.FC = ({ return `${size?.height || 40}px`; }; - // ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ํ™•์ธ - const isButtonComponent = - (component.type === "widget" && (component as WidgetComponent).widgetType === "button") || - (component.type === "component" && (component as any).componentType?.includes("button")); - - // ๋ฒ„ํŠผ์ผ ๊ฒฝ์šฐ ๋กœ๊ทธ ์ถœ๋ ฅ (ํŽธ์ง‘๊ธฐ) - if (isButtonComponent && isDesignMode) { - console.log("๐ŸŽจ [ํŽธ์ง‘๊ธฐ] ๋ฒ„ํŠผ ์œ„์น˜:", { - label: component.label, - positionX: position.x, - positionY: position.y, - sizeWidth: size?.width, - sizeHeight: size?.height, - }); - } - const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, - // x=0์ธ ์ปดํฌ๋„ŒํŠธ๋Š” ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ (๋ฒ„ํŠผ ์ œ์™ธ) - width: (position.x === 0 && !isButtonComponent) ? "100%" : getWidth(), + width: getWidth(), // getWidth()๊ฐ€ ๋ชจ๋“  ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ฒ˜๋ฆฌ height: getHeight(), zIndex: component.type === "layout" ? 1 : position.z || 2, ...componentStyle, - // x=0์ธ ์ปดํฌ๋„ŒํŠธ๋Š” 100% ๋„ˆ๋น„ ๊ฐ•์ œ (๋ฒ„ํŠผ ์ œ์™ธ) - ...(position.x === 0 && !isButtonComponent && { width: "100%" }), right: undefined, }; + // ๐Ÿ” DOM ๋ Œ๋”๋ง ํ›„ ์‹ค์ œ ํฌ๊ธฐ ์ธก์ • + const innerDivRef = React.useRef(null); + const outerDivRef = React.useRef(null); + + React.useEffect(() => { + if (outerDivRef.current && innerDivRef.current) { + const outerRect = outerDivRef.current.getBoundingClientRect(); + const innerRect = innerDivRef.current.getBoundingClientRect(); + const computedOuter = window.getComputedStyle(outerDivRef.current); + const computedInner = window.getComputedStyle(innerDivRef.current); + + console.log("๐Ÿ“ [DOM ์‹ค์ œ ํฌ๊ธฐ ์ƒ์„ธ]:", { + componentId: id, + label: component.label, + gridColumns: (component as any).gridColumns, + "1. baseStyle.width": baseStyle.width, + "2. ์™ธ๋ถ€ div (ํŒŒ๋ž€ ํ…Œ๋‘๋ฆฌ)": { + width: `${outerRect.width}px`, + height: `${outerRect.height}px`, + computedWidth: computedOuter.width, + computedHeight: computedOuter.height, + }, + "3. ๋‚ด๋ถ€ div (์ปจํ…์ธ  ๋ž˜ํผ)": { + width: `${innerRect.width}px`, + height: `${innerRect.height}px`, + computedWidth: computedInner.width, + computedHeight: computedInner.height, + className: innerDivRef.current.className, + inlineStyle: innerDivRef.current.getAttribute("style"), + }, + "4. ๋„ˆ๋น„ ๋น„๊ต": { + "์™ธ๋ถ€ / ๋‚ด๋ถ€": `${outerRect.width}px / ${innerRect.width}px`, + "๋น„์œจ": `${((innerRect.width / outerRect.width) * 100).toFixed(2)}%`, + }, + }); + } + }, [id, component.label, (component as any).gridColumns, baseStyle.width]); + const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(e); @@ -285,7 +343,9 @@ export const RealtimePreviewDynamic: React.FC = ({ return (
= ({ > {/* ๋™์  ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง */}
{ + // ๋ฉ€ํ‹ฐ ref ์ฒ˜๋ฆฌ + innerDivRef.current = node; + if (component.type === "component" && (component as any).componentType === "flow-widget") { + (contentRef as any).current = node; + } + }} + className={`${component.type === "component" && (component as any).componentType === "flow-widget" ? "h-auto" : "h-full"} overflow-visible`} + style={{ width: "100%", maxWidth: "100%" }} > = { + // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ๋น„์œจ ๋งคํ•‘ (12์ปฌ๋Ÿผ ๊ธฐ์ค€ ๋น„์œจ) + const gridColumnsRatioMap: Record = { // ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ (INPUT ์นดํ…Œ๊ณ ๋ฆฌ) - "text-input": 4, // ํ…์ŠคํŠธ ์ž…๋ ฅ (33%) - "number-input": 2, // ์ˆซ์ž ์ž…๋ ฅ (16.67%) - "email-input": 4, // ์ด๋ฉ”์ผ ์ž…๋ ฅ (33%) - "tel-input": 3, // ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ (25%) - "date-input": 3, // ๋‚ ์งœ ์ž…๋ ฅ (25%) - "datetime-input": 4, // ๋‚ ์งœ์‹œ๊ฐ„ ์ž…๋ ฅ (33%) - "time-input": 2, // ์‹œ๊ฐ„ ์ž…๋ ฅ (16.67%) - "textarea-basic": 6, // ํ…์ŠคํŠธ ์˜์—ญ (50%) - "select-basic": 3, // ์…€๋ ‰ํŠธ (25%) - "checkbox-basic": 2, // ์ฒดํฌ๋ฐ•์Šค (16.67%) - "radio-basic": 3, // ๋ผ๋””์˜ค (25%) - "file-basic": 4, // ํŒŒ์ผ (33%) - "file-upload": 4, // ํŒŒ์ผ ์—…๋กœ๋“œ (33%) - "slider-basic": 3, // ์Šฌ๋ผ์ด๋” (25%) - "toggle-switch": 2, // ํ† ๊ธ€ ์Šค์œ„์น˜ (16.67%) - "repeater-field-group": 6, // ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน (50%) + "text-input": 4 / 12, // ํ…์ŠคํŠธ ์ž…๋ ฅ (33%) + "number-input": 2 / 12, // ์ˆซ์ž ์ž…๋ ฅ (16.67%) + "email-input": 4 / 12, // ์ด๋ฉ”์ผ ์ž…๋ ฅ (33%) + "tel-input": 3 / 12, // ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ (25%) + "date-input": 3 / 12, // ๋‚ ์งœ ์ž…๋ ฅ (25%) + "datetime-input": 4 / 12, // ๋‚ ์งœ์‹œ๊ฐ„ ์ž…๋ ฅ (33%) + "time-input": 2 / 12, // ์‹œ๊ฐ„ ์ž…๋ ฅ (16.67%) + "textarea-basic": 6 / 12, // ํ…์ŠคํŠธ ์˜์—ญ (50%) + "select-basic": 3 / 12, // ์…€๋ ‰ํŠธ (25%) + "checkbox-basic": 2 / 12, // ์ฒดํฌ๋ฐ•์Šค (16.67%) + "radio-basic": 3 / 12, // ๋ผ๋””์˜ค (25%) + "file-basic": 4 / 12, // ํŒŒ์ผ (33%) + "file-upload": 4 / 12, // ํŒŒ์ผ ์—…๋กœ๋“œ (33%) + "slider-basic": 3 / 12, // ์Šฌ๋ผ์ด๋” (25%) + "toggle-switch": 2 / 12, // ํ† ๊ธ€ ์Šค์œ„์น˜ (16.67%) + "repeater-field-group": 6 / 12, // ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน (50%) // ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ (DISPLAY ์นดํ…Œ๊ณ ๋ฆฌ) - "label-basic": 2, // ๋ผ๋ฒจ (16.67%) - "text-display": 3, // ํ…์ŠคํŠธ ํ‘œ์‹œ (25%) - "card-display": 8, // ์นด๋“œ (66.67%) - "badge-basic": 1, // ๋ฐฐ์ง€ (8.33%) - "alert-basic": 6, // ์•Œ๋ฆผ (50%) - "divider-basic": 12, // ๊ตฌ๋ถ„์„  (100%) - "divider-line": 12, // ๊ตฌ๋ถ„์„  (100%) - "accordion-basic": 12, // ์•„์ฝ”๋””์–ธ (100%) - "table-list": 12, // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ (100%) - "image-display": 4, // ์ด๋ฏธ์ง€ ํ‘œ์‹œ (33%) - "split-panel-layout": 6, // ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ (50%) - "flow-widget": 12, // ํ”Œ๋กœ์šฐ ์œ„์ ฏ (100%) + "label-basic": 2 / 12, // ๋ผ๋ฒจ (16.67%) + "text-display": 3 / 12, // ํ…์ŠคํŠธ ํ‘œ์‹œ (25%) + "card-display": 8 / 12, // ์นด๋“œ (66.67%) + "badge-basic": 1 / 12, // ๋ฐฐ์ง€ (8.33%) + "alert-basic": 6 / 12, // ์•Œ๋ฆผ (50%) + "divider-basic": 1, // ๊ตฌ๋ถ„์„  (100%) + "divider-line": 1, // ๊ตฌ๋ถ„์„  (100%) + "accordion-basic": 1, // ์•„์ฝ”๋””์–ธ (100%) + "table-list": 1, // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ (100%) + "image-display": 4 / 12, // ์ด๋ฏธ์ง€ ํ‘œ์‹œ (33%) + "split-panel-layout": 6 / 12, // ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ (50%) + "flow-widget": 1, // ํ”Œ๋กœ์šฐ ์œ„์ ฏ (100%) // ์•ก์…˜ ์ปดํฌ๋„ŒํŠธ (ACTION ์นดํ…Œ๊ณ ๋ฆฌ) - "button-basic": 1, // ๋ฒ„ํŠผ (8.33%) - "button-primary": 1, // ํ”„๋ผ์ด๋จธ๋ฆฌ ๋ฒ„ํŠผ (8.33%) - "button-secondary": 1, // ์„ธ์ปจ๋”๋ฆฌ ๋ฒ„ํŠผ (8.33%) - "icon-button": 1, // ์•„์ด์ฝ˜ ๋ฒ„ํŠผ (8.33%) + "button-basic": 1 / 12, // ๋ฒ„ํŠผ (8.33%) + "button-primary": 1 / 12, // ํ”„๋ผ์ด๋จธ๋ฆฌ ๋ฒ„ํŠผ (8.33%) + "button-secondary": 1 / 12, // ์„ธ์ปจ๋”๋ฆฌ ๋ฒ„ํŠผ (8.33%) + "icon-button": 1 / 12, // ์•„์ด์ฝ˜ ๋ฒ„ํŠผ (8.33%) // ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ - "container-basic": 6, // ์ปจํ…Œ์ด๋„ˆ (50%) - "section-basic": 12, // ์„น์…˜ (100%) - "panel-basic": 6, // ํŒจ๋„ (50%) + "container-basic": 6 / 12, // ์ปจํ…Œ์ด๋„ˆ (50%) + "section-basic": 1, // ์„น์…˜ (100%) + "panel-basic": 6 / 12, // ํŒจ๋„ (50%) // ๊ธฐํƒ€ - "image-basic": 4, // ์ด๋ฏธ์ง€ (33%) - "icon-basic": 1, // ์•„์ด์ฝ˜ (8.33%) - "progress-bar": 4, // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” (33%) - "chart-basic": 6, // ์ฐจํŠธ (50%) + "image-basic": 4 / 12, // ์ด๋ฏธ์ง€ (33%) + "icon-basic": 1 / 12, // ์•„์ด์ฝ˜ (8.33%) + "progress-bar": 4 / 12, // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” (33%) + "chart-basic": 6 / 12, // ์ฐจํŠธ (50%) }; - // defaultSize์— gridColumnSpan์ด "full"์ด๋ฉด 12์ปฌ๋Ÿผ ์‚ฌ์šฉ + // defaultSize์— gridColumnSpan์ด "full"์ด๋ฉด ์ „์ฒด ์ปฌ๋Ÿผ ์‚ฌ์šฉ if (component.defaultSize?.gridColumnSpan === "full") { - gridColumns = 12; + gridColumns = currentGridColumns; } else { - // componentId ๋˜๋Š” webType์œผ๋กœ ๋งคํ•‘, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 3 - gridColumns = gridColumnsMap[componentId] || gridColumnsMap[webType] || 3; + // componentId ๋˜๋Š” webType์œผ๋กœ ๋น„์œจ ์ฐพ๊ธฐ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ 25% + const ratio = gridColumnsRatioMap[componentId] || gridColumnsRatioMap[webType] || 0.25; + // ํ˜„์žฌ ๊ฒฉ์ž ์ปฌ๋Ÿผ ์ˆ˜์— ๋น„์œจ์„ ๊ณฑํ•˜์—ฌ ๊ณ„์‚ฐ (์ตœ์†Œ 1, ์ตœ๋Œ€ currentGridColumns) + gridColumns = Math.max(1, Math.min(currentGridColumns, Math.round(ratio * currentGridColumns))); } console.log("๐ŸŽฏ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„ gridColumns ์„ค์ •:", { @@ -2141,6 +2144,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }; } + // gridColumns์— ๋งž์ถฐ width๋ฅผ ํผ์„ผํŠธ๋กœ ๊ณ„์‚ฐ + const widthPercent = (gridColumns / currentGridColumns) * 100; + + console.log("๐ŸŽจ [์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ] ๋„ˆ๋น„ ๊ณ„์‚ฐ:", { + componentName: component.name, + componentId: component.id, + currentGridColumns, + gridColumns, + widthPercent: `${widthPercent}%`, + calculatedWidth: `${Math.round(widthPercent * 100) / 100}%`, + }); + const newComponent: ComponentData = { id: generateComponentId(), type: "component", // โœ… ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ ์‚ฌ์šฉ @@ -2162,6 +2177,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD labelColor: "#212121", labelFontWeight: "500", labelMarginBottom: "4px", + width: `${widthPercent}%`, // gridColumns์— ๋งž์ถ˜ ํผ์„ผํŠธ ๋„ˆ๋น„ }, }; @@ -4238,7 +4254,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD { + setLayout((prev) => ({ + ...prev, + gridSettings: newSettings, + })); + }} onDeleteComponent={deleteComponent} onCopyComponent={copyComponent} currentTable={tables.length > 0 ? tables[0] : undefined} diff --git a/frontend/components/screen/panels/GridPanel.tsx b/frontend/components/screen/panels/GridPanel.tsx index 16178e57..a38c4cd6 100644 --- a/frontend/components/screen/panels/GridPanel.tsx +++ b/frontend/components/screen/panels/GridPanel.tsx @@ -127,10 +127,27 @@ export const GridPanel: React.FC = ({
+
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value >= 1 && value <= 24) { + updateSetting("columns", value); + } + }} + className="h-8 text-xs" + /> + / 24 +
= ({ className="w-full" />
- 1 - 24 + 1์—ด + 24์—ด
diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index b45bc517..88643c60 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -109,6 +109,12 @@ interface PropertiesPanelProps { draggedComponent: ComponentData | null; currentPosition: { x: number; y: number; z: number }; }; + gridSettings?: { + columns: number; + gap: number; + padding: number; + snapToGrid: boolean; + }; onUpdateProperty: (path: string, value: unknown) => void; onDeleteComponent: () => void; onCopyComponent: () => void; @@ -124,6 +130,7 @@ const PropertiesPanelComponent: React.FC = ({ selectedComponent, tables = [], dragState, + gridSettings, onUpdateProperty, onDeleteComponent, onCopyComponent, @@ -744,9 +751,47 @@ const PropertiesPanelComponent: React.FC = ({ {/* ์นด๋“œ ๋ ˆ์ด์•„์›ƒ์€ ์ž๋™ ํฌ๊ธฐ ๊ณ„์‚ฐ์œผ๋กœ ๋„ˆ๋น„/๋†’์ด ์„ค์ • ์ˆจ๊น€ */} {selectedComponent?.type !== "layout" || (selectedComponent as any)?.layoutType !== "card" ? ( <> - {/* ๐Ÿ†• ์ปฌ๋Ÿผ ์ŠคํŒฌ ์„ ํƒ (width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜) - ๊ธฐ์กด UI ์œ ์ง€ */} + {/* ๐Ÿ†• ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ์ง์ ‘ ์ž…๋ ฅ */}
- + +
+ { + const value = parseInt(e.target.value, 10); + const maxColumns = gridSettings?.columns || 12; + if (!isNaN(value) && value >= 1 && value <= maxColumns) { + // gridColumns ์—…๋ฐ์ดํŠธ + onUpdateProperty("gridColumns", value); + + // width๋ฅผ ํผ์„ผํŠธ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ์—…๋ฐ์ดํŠธ + const widthPercent = (value / maxColumns) * 100; + onUpdateProperty("style.width", `${widthPercent}%`); + + // localWidthSpan๋„ ์—…๋ฐ์ดํŠธ + setLocalWidthSpan(calculateWidthSpan(`${widthPercent}%`, value)); + } + }} + className="h-8 text-xs" + /> + + / {gridSettings?.columns || 12}์—ด + +
+

+ ์ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฐจ์ง€ํ•  ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ (1-{gridSettings?.columns || 12}) +

+
+ + {/* ๊ธฐ์กด ์ปฌ๋Ÿผ ์ŠคํŒฌ ์„ ํƒ (width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜) - ์ฐธ๊ณ ์šฉ */} +
+ { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value >= 1 && value <= 24) { + updateGridSetting("columns", value); + } + }} + className="h-6 px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + /> + / 24 +
+ updateGridSetting("columns", value)} + className="w-full" + /> +
+ + {/* ๊ฐ„๊ฒฉ */} +
+ + updateGridSetting("gap", value)} + className="w-full" + /> +
+ + {/* ์—ฌ๋ฐฑ */} +
+ + updateGridSetting("padding", value)} + className="w-full" + /> +
+
+
+ ); + }; + + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์•˜์„ ๋•Œ๋„ ํ•ด์ƒ๋„ ์„ค์ •๊ณผ ๊ฒฉ์ž ์„ค์ •์€ ํ‘œ์‹œ if (!selectedComponent) { return (
- {/* ํ•ด์ƒ๋„ ์„ค์ •๋งŒ ํ‘œ์‹œ */} + {/* ํ•ด์ƒ๋„ ์„ค์ •๊ณผ ๊ฒฉ์ž ์„ค์ • ํ‘œ์‹œ */}
+ {/* ํ•ด์ƒ๋„ ์„ค์ • */} {currentResolution && onResolutionChange && ( -
-
- -

ํ•ด์ƒ๋„ ์„ค์ •

+ <> +
+
+ +

ํ•ด์ƒ๋„ ์„ค์ •

+
+
- -
+ + )} + {/* ๊ฒฉ์ž ์„ค์ • */} + {renderGridSettings()} + {/* ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ */}
@@ -283,22 +431,31 @@ export const UnifiedPropertiesPanel: React.FC = ({
{(selectedComponent as any).gridColumns !== undefined && (
- - + +
+ { + const value = parseInt(e.target.value, 10); + const maxColumns = gridSettings?.columns || 12; + if (!isNaN(value) && value >= 1 && value <= maxColumns) { + handleUpdate("gridColumns", value); + + // width๋ฅผ ํผ์„ผํŠธ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ์—…๋ฐ์ดํŠธ + const widthPercent = (value / maxColumns) * 100; + handleUpdate("style.width", `${widthPercent}%`); + } + }} + className="h-6 w-full px-2 py-0 text-xs" + style={{ fontSize: "12px" }} + /> + + /{gridSettings?.columns || 12} + +
)}
@@ -896,6 +1053,10 @@ export const UnifiedPropertiesPanel: React.FC = ({ )} + {/* ๊ฒฉ์ž ์„ค์ • - ํ•ด์ƒ๋„ ์„ค์ • ์•„๋ž˜ ํ‘œ์‹œ */} + {renderGridSettings()} + {gridSettings && onGridSettingsChange && } + {/* ๊ธฐ๋ณธ ์„ค์ • */} {renderBasicTab()} diff --git a/frontend/components/screen/templates/NumberingRuleTemplate.ts b/frontend/components/screen/templates/NumberingRuleTemplate.ts new file mode 100644 index 00000000..ee386c4b --- /dev/null +++ b/frontend/components/screen/templates/NumberingRuleTemplate.ts @@ -0,0 +1,77 @@ +/** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ํ…œํ”Œ๋ฆฟ + * ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ๋“ฑ๋กํ•˜์—ฌ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ์‚ฌ์šฉ + */ + +import { Hash } from "lucide-react"; + +export const getDefaultNumberingRuleConfig = () => ({ + template_code: "numbering-rule-designer", + template_name: "์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™", + template_name_eng: "Numbering Rule Designer", + description: "์ฝ”๋“œ ์ž๋™ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์„ค์ •ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ", + category: "admin" as const, + icon_name: "hash", + default_size: { + width: 1200, + height: 800, + }, + layout_config: { + components: [ + { + type: "numbering-rule" as const, + label: "์ฑ„๋ฒˆ ๊ทœ์น™ ์„ค์ •", + position: { x: 0, y: 0 }, + size: { width: 1200, height: 800 }, + ruleConfig: { + ruleId: "new-rule", + ruleName: "์ƒˆ ์ฑ„๋ฒˆ ๊ทœ์น™", + parts: [], + separator: "-", + resetPeriod: "none", + currentSequence: 1, + }, + maxRules: 6, + style: { + padding: "16px", + backgroundColor: "#ffffff", + }, + }, + ], + }, +}); + +/** + * ํ…œํ”Œ๋ฆฟ ํŒจ๋„์—์„œ ์‚ฌ์šฉํ•  ์ปดํฌ๋„ŒํŠธ ์ •๋ณด + */ +export const numberingRuleTemplate = { + id: "numbering-rule", + name: "์ฑ„๋ฒˆ ๊ทœ์น™", + description: "์ฝ”๋“œ ์ž๋™ ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ค์ •", + category: "admin" as const, + icon: Hash, + defaultSize: { width: 1200, height: 800 }, + components: [ + { + type: "numbering-rule" as const, + widgetType: undefined, + label: "์ฑ„๋ฒˆ ๊ทœ์น™ ์„ค์ •", + position: { x: 0, y: 0 }, + size: { width: 1200, height: 800 }, + style: { + padding: "16px", + backgroundColor: "#ffffff", + }, + ruleConfig: { + ruleId: "new-rule", + ruleName: "์ƒˆ ์ฑ„๋ฒˆ ๊ทœ์น™", + parts: [], + separator: "-", + resetPeriod: "none", + currentSequence: 1, + }, + maxRules: 6, + }, + ], +}; + diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx new file mode 100644 index 00000000..03dec3ba --- /dev/null +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -0,0 +1,210 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { TabsComponent, TabItem, ScreenDefinition } from "@/types"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Loader2, FileQuestion } from "lucide-react"; +import { screenApi } from "@/lib/api/screen"; +import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; + +interface TabsWidgetProps { + component: TabsComponent; + isPreview?: boolean; +} + +/** + * ํƒญ ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ + * ๊ฐ ํƒญ์— ๋‹ค๋ฅธ ํ™”๋ฉด์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค + */ +export const TabsWidget: React.FC = ({ component, isPreview = false }) => { + // componentConfig์—์„œ ์„ค์ • ์ฝ๊ธฐ (์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ) + const config = (component as any).componentConfig || component; + const { tabs = [], defaultTab, orientation = "horizontal", variant = "default" } = config; + + // console.log("๐Ÿ” TabsWidget ๋ Œ๋”๋ง:", { + // component, + // componentConfig: (component as any).componentConfig, + // tabs, + // tabsLength: tabs.length + // }); + + const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.id || ""); + const [loadedScreens, setLoadedScreens] = useState>({}); + const [loadingScreens, setLoadingScreens] = useState>({}); + const [screenErrors, setScreenErrors] = useState>({}); + + // ํƒญ ๋ณ€๊ฒฝ ์‹œ ํ™”๋ฉด ๋กœ๋“œ + useEffect(() => { + if (!activeTab) return; + + const currentTab = tabs.find((tab) => tab.id === activeTab); + if (!currentTab || !currentTab.screenId) return; + + // ์ด๋ฏธ ๋กœ๋“œ๋œ ํ™”๋ฉด์ด๋ฉด ์Šคํ‚ต + if (loadedScreens[activeTab]) return; + + // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์Šคํ‚ต + if (loadingScreens[activeTab]) return; + + // ํ™”๋ฉด ๋กœ๋“œ ์‹œ์ž‘ + loadScreen(activeTab, currentTab.screenId); + }, [activeTab, tabs]); + + const loadScreen = async (tabId: string, screenId: number) => { + setLoadingScreens((prev) => ({ ...prev, [tabId]: true })); + setScreenErrors((prev) => ({ ...prev, [tabId]: "" })); + + try { + const layoutData = await screenApi.getLayout(screenId); + + if (layoutData) { + setLoadedScreens((prev) => ({ + ...prev, + [tabId]: { + screenId, + layout: layoutData, + }, + })); + } else { + setScreenErrors((prev) => ({ + ...prev, + [tabId]: "ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", + })); + } + } catch (error: any) { + setScreenErrors((prev) => ({ + ...prev, + [tabId]: error.message || "ํ™”๋ฉด ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค", + })); + } finally { + setLoadingScreens((prev) => ({ ...prev, [tabId]: false })); + } + }; + + // ํƒญ ์ฝ˜ํ…์ธ  ๋ Œ๋”๋ง + const renderTabContent = (tab: TabItem) => { + const isLoading = loadingScreens[tab.id]; + const error = screenErrors[tab.id]; + const screenData = loadedScreens[tab.id]; + + // ๋กœ๋”ฉ ์ค‘ + if (isLoading) { + return ( +
+ +

ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+ ); + } + + // ์—๋Ÿฌ ๋ฐœ์ƒ + if (error) { + return ( +
+ +
+

ํ™”๋ฉด ๋กœ๋“œ ์‹คํŒจ

+

{error}

+
+
+ ); + } + + // ํ™”๋ฉด ID๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ + if (!tab.screenId) { + return ( +
+ +
+

ํ™”๋ฉด์ด ํ• ๋‹น๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค

+

์ƒ์„ธ์„ค์ •์—์„œ ํ™”๋ฉด์„ ์„ ํƒํ•˜์„ธ์š”

+
+
+ ); + } + + // ํ™”๋ฉด ๋ Œ๋”๋ง - ์›๋ณธ ํ™”๋ฉด์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋ง + if (screenData && screenData.layout && screenData.layout.components) { + const components = screenData.layout.components; + const screenResolution = screenData.layout.screenResolution || { width: 1920, height: 1080 }; + + return ( +
+
+ {components.map((comp) => ( + + ))} +
+
+ ); + } + + return ( +
+ +
+

ํ™”๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค

+
+
+ ); + }; + + // ๋นˆ ํƒญ ๋ชฉ๋ก + if (tabs.length === 0) { + return ( + +
+

ํƒญ์ด ์—†์Šต๋‹ˆ๋‹ค

+

์ƒ์„ธ์„ค์ •์—์„œ ํƒญ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”

+
+
+ ); + } + + return ( +
+ + + {tabs.map((tab) => ( + + {tab.label} + {tab.screenName && ( + + {tab.screenName} + + )} + + ))} + + + {tabs.map((tab) => ( + + {renderTabContent(tab)} + + ))} + +
+ ); +}; + diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 93d96ca0..5bf11eec 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -223,6 +223,8 @@ export const ButtonPrimaryComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ (border ์†์„ฑ ๋ถ„๋ฆฌํ•˜์—ฌ ์ถฉ๋Œ ๋ฐฉ์ง€) diff --git a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx index 64bdaac9..0912afd7 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx @@ -185,6 +185,9 @@ export const CardDisplayComponent: React.FC = ({ position: "relative", backgroundColor: "transparent", }; + + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + // ์นด๋“œ ์ปดํฌ๋„ŒํŠธ๋Š” ...style ์Šคํ”„๋ ˆ๋“œ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ • if (isDesignMode) { componentStyle.border = "1px dashed hsl(var(--border))"; diff --git a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx index 0fd31f25..2f2c5622 100644 --- a/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx +++ b/frontend/lib/registry/components/checkbox-basic/CheckboxBasicComponent.tsx @@ -48,6 +48,8 @@ export const CheckboxBasicComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/date-input/DateInputComponent.tsx b/frontend/lib/registry/components/date-input/DateInputComponent.tsx index 29b25029..928df3de 100644 --- a/frontend/lib/registry/components/date-input/DateInputComponent.tsx +++ b/frontend/lib/registry/components/date-input/DateInputComponent.tsx @@ -204,6 +204,8 @@ export const DateInputComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx b/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx index 45d5089f..5cc4fcfd 100644 --- a/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx +++ b/frontend/lib/registry/components/divider-line/DividerLineComponent.tsx @@ -36,6 +36,8 @@ export const DividerLineComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx b/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx index bda6fd8b..13a7ac4f 100644 --- a/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx +++ b/frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx @@ -36,6 +36,8 @@ export const ImageDisplayComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/number-input/NumberInputComponent.tsx b/frontend/lib/registry/components/number-input/NumberInputComponent.tsx index e5d49328..3f41505c 100644 --- a/frontend/lib/registry/components/number-input/NumberInputComponent.tsx +++ b/frontend/lib/registry/components/number-input/NumberInputComponent.tsx @@ -43,6 +43,8 @@ export const NumberInputComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx b/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx index 41c91032..8d196db7 100644 --- a/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx +++ b/frontend/lib/registry/components/radio-basic/RadioBasicComponent.tsx @@ -47,6 +47,8 @@ export const RadioBasicComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx b/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx index c23cf50b..22c364fb 100644 --- a/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx +++ b/frontend/lib/registry/components/slider-basic/SliderBasicComponent.tsx @@ -39,6 +39,8 @@ export const SliderBasicComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 4e5243b6..46c03aef 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -234,6 +234,8 @@ export const TableListComponent: React.FC = ({ backgroundColor: "hsl(var(--background))", overflow: "hidden", ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ======================================== @@ -1167,7 +1169,7 @@ export const TableListComponent: React.FC = ({
)} -
+
= ({ )} {/* ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ */} -
+
{/* ์Šคํฌ๋กค ์˜์—ญ */}
= ({ height: "100%", ...component.style, ...style, - // ์ˆจ๊น€ ๊ธฐ๋Šฅ: ํŽธ์ง‘ ๋ชจ๋“œ์—์„œ๋งŒ ์—ฐํ•˜๊ฒŒ ํ‘œ์‹œ + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", + // ์ˆจ๊น€ ๊ธฐ๋Šฅ: ํŽธ์ง‘ ๋ชจ๋“œ์—์„œ๋งŒ ์—ฐํ•˜๊ฒŒ ํ‘œ์‹œ ...(isHidden && isDesignMode && { opacity: 0.4, diff --git a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx index e842ff34..eea2f113 100644 --- a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx +++ b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx @@ -39,6 +39,8 @@ export const TextareaBasicComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx index a3a3786e..4d9fcbe2 100644 --- a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx +++ b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx @@ -39,6 +39,8 @@ export const ToggleSwitchComponent: React.FC = ({ height: "100%", ...component.style, ...style, + // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + width: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts index dbdbf9bd..9cd81bdb 100644 --- a/frontend/types/numbering-rule.ts +++ b/frontend/types/numbering-rule.ts @@ -4,15 +4,13 @@ */ /** - * ์ฝ”๋“œ ํŒŒํŠธ ์œ ํ˜• + * ์ฝ”๋“œ ํŒŒํŠธ ์œ ํ˜• (4๊ฐ€์ง€) */ export type CodePartType = - | "prefix" // ์ ‘๋‘์‚ฌ (๊ณ ์ • ๋ฌธ์ž์—ด) - | "sequence" // ์ˆœ๋ฒˆ (์ž๋™ ์ฆ๊ฐ€) - | "date" // ๋‚ ์งœ (YYYYMMDD ๋“ฑ) - | "year" // ์—ฐ๋„ (YYYY) - | "month" // ์›” (MM) - | "custom"; // ์‚ฌ์šฉ์ž ์ •์˜ + | "sequence" // ์ˆœ๋ฒˆ (์ž๋™ ์ฆ๊ฐ€ ์ˆซ์ž) + | "number" // ์ˆซ์ž (๊ณ ์ • ์ž๋ฆฟ์ˆ˜) + | "date" // ๋‚ ์งœ (๋‹ค์–‘ํ•œ ๋‚ ์งœ ํ˜•์‹) + | "text"; // ๋ฌธ์ž (ํ…์ŠคํŠธ) /** * ์ƒ์„ฑ ๋ฐฉ์‹ @@ -43,11 +41,19 @@ export interface NumberingRulePart { // ์ž๋™ ์ƒ์„ฑ ์„ค์ • autoConfig?: { - prefix?: string; // ์ ‘๋‘์‚ฌ - sequenceLength?: number; // ์ˆœ๋ฒˆ ์ž๋ฆฟ์ˆ˜ - startFrom?: number; // ์‹œ์ž‘ ๋ฒˆํ˜ธ + // ์ˆœ๋ฒˆ์šฉ + sequenceLength?: number; // ์ˆœ๋ฒˆ ์ž๋ฆฟ์ˆ˜ (์˜ˆ: 3 โ†’ 001) + startFrom?: number; // ์‹œ์ž‘ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ: 1) + + // ์ˆซ์ž์šฉ + numberLength?: number; // ์ˆซ์ž ์ž๋ฆฟ์ˆ˜ (์˜ˆ: 4 โ†’ 0001) + numberValue?: number; // ์ˆซ์ž ๊ฐ’ + + // ๋‚ ์งœ์šฉ dateFormat?: DateFormat; // ๋‚ ์งœ ํ˜•์‹ - value?: string; // ์ปค์Šคํ…€ ๊ฐ’ + + // ๋ฌธ์ž์šฉ + textValue?: string; // ํ…์ŠคํŠธ ๊ฐ’ (์˜ˆ: "PRJ", "CODE") }; // ์ง์ ‘ ์ž…๋ ฅ ์„ค์ • @@ -74,6 +80,10 @@ export interface NumberingRuleConfig { resetPeriod?: "none" | "daily" | "monthly" | "yearly"; currentSequence?: number; // ํ˜„์žฌ ์‹œํ€€์Šค + // ์ ์šฉ ๋ฒ”์œ„ + scopeType?: "global" | "menu"; // ์ ์šฉ ๋ฒ”์œ„ (์ „์—ญ/๋ฉ”๋‰ด๋ณ„) + menuObjid?: number; // ์ ์šฉํ•  ๋ฉ”๋‰ด OBJID (์ƒ์œ„ ๋ฉ”๋‰ด ๊ธฐ์ค€) + // ์ ์šฉ ๋Œ€์ƒ tableName?: string; // ์ ์šฉํ•  ํ…Œ์ด๋ธ”๋ช… columnName?: string; // ์ ์šฉํ•  ์ปฌ๋Ÿผ๋ช… @@ -88,13 +98,11 @@ export interface NumberingRuleConfig { /** * UI ์˜ต์…˜ ์ƒ์ˆ˜ */ -export const CODE_PART_TYPE_OPTIONS: Array<{ value: CodePartType; label: string }> = [ - { value: "prefix", label: "์ ‘๋‘์‚ฌ" }, - { value: "sequence", label: "์ˆœ๋ฒˆ" }, - { value: "date", label: "๋‚ ์งœ" }, - { value: "year", label: "์—ฐ๋„" }, - { value: "month", label: "์›”" }, - { value: "custom", label: "์‚ฌ์šฉ์ž ์ •์˜" }, +export const CODE_PART_TYPE_OPTIONS: Array<{ value: CodePartType; label: string; description: string }> = [ + { value: "sequence", label: "์ˆœ๋ฒˆ", description: "์ž๋™ ์ฆ๊ฐ€ ์ˆœ๋ฒˆ (1, 2, 3...)" }, + { value: "number", label: "์ˆซ์ž", description: "๊ณ ์ • ์ž๋ฆฟ์ˆ˜ ์ˆซ์ž (001, 002...)" }, + { value: "date", label: "๋‚ ์งœ", description: "๋‚ ์งœ ํ˜•์‹ (2025-11-04)" }, + { value: "text", label: "๋ฌธ์ž", description: "ํ…์ŠคํŠธ ๋˜๋Š” ์ฝ”๋“œ" }, ]; export const DATE_FORMAT_OPTIONS: Array<{ value: DateFormat; label: string; example: string }> = [ diff --git a/์ฝ”๋“œ_์ฑ„๋ฒˆ_๊ทœ์น™_์ปดํฌ๋„ŒํŠธ_๊ตฌํ˜„_๊ณ„ํš์„œ.md b/์ฝ”๋“œ_์ฑ„๋ฒˆ_๊ทœ์น™_์ปดํฌ๋„ŒํŠธ_๊ตฌํ˜„_๊ณ„ํš์„œ.md new file mode 100644 index 00000000..69af1e04 --- /dev/null +++ b/์ฝ”๋“œ_์ฑ„๋ฒˆ_๊ทœ์น™_์ปดํฌ๋„ŒํŠธ_๊ตฌํ˜„_๊ณ„ํš์„œ.md @@ -0,0 +1,494 @@ +# ์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ ๊ณ„ํš์„œ + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-11-03 +- **๋ชฉ์ **: Shadcn/ui ๊ฐ€์ด๋“œ๋ผ์ธ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ +- **์šฐ์„ ์ˆœ์œ„**: ์ค‘๊ฐ„ +- **๋””์ž์ธ ์›์น™**: ์‹ฌํ”Œํ•˜๊ณ  ๊น”๋”ํ•œ UI, ์ค‘์ฒฉ ๋ฐ•์Šค ๊ธˆ์ง€, ์ผ๊ด€๋œ ์ปฌ๋Ÿฌ ์‹œ์Šคํ…œ + +--- + +## 1. ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ + +### 1.1 ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ +- ๋™์  ๊ทœ์น™ ํŒŒํŠธ ์ถ”๊ฐ€/์‚ญ์ œ (์ตœ๋Œ€ 6๊ฐœ) +- ์‹ค์‹œ๊ฐ„ ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +- ๊ทœ์น™ ์ˆœ์„œ ์กฐ์ • +- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ ๋ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + +### 1.2 UI ์š”๊ตฌ์‚ฌํ•ญ +- ์ขŒ์ธก: ์ฝ”๋“œ ๋ชฉ๋ก (์„ ํƒ์ ) +- ์šฐ์ธก: ๊ทœ์น™ ์„ค์ • ์˜์—ญ +- ์ƒ๋‹จ: ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + ๊ทœ์น™๋ช… +- ์ค‘์•™: ๊ทœ์น™ ์นด๋“œ ๋ฆฌ์ŠคํŠธ +- ํ•˜๋‹จ: ๊ทœ์น™ ์ถ”๊ฐ€ + ์ €์žฅ ๋ฒ„ํŠผ + +--- + +## 2. ๋””์ž์ธ ์‹œ์Šคํ…œ (Shadcn/ui ๊ธฐ๋ฐ˜) + +### 2.1 ์ƒ‰์ƒ ์‚ฌ์šฉ ๊ทœ์น™ + +```tsx +// ๋ฐฐ๊ฒฝ +bg-background // ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ +bg-card // ์นด๋“œ ๋ฐฐ๊ฒฝ +bg-muted // ์•ฝํ•œ ๋ฐฐ๊ฒฝ (๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋“ฑ) + +// ํ…์ŠคํŠธ +text-foreground // ๊ธฐ๋ณธ ํ…์ŠคํŠธ +text-muted-foreground // ๋ณด์กฐ ํ…์ŠคํŠธ +text-primary // ๊ฐ•์กฐ ํ…์ŠคํŠธ + +// ํ…Œ๋‘๋ฆฌ +border-border // ๊ธฐ๋ณธ ํ…Œ๋‘๋ฆฌ +border-input // ์ž…๋ ฅ ํ•„๋“œ ํ…Œ๋‘๋ฆฌ + +// ๋ฒ„ํŠผ +bg-primary // ์ฃผ์š” ๋ฒ„ํŠผ (์ €์žฅ, ์ถ”๊ฐ€) +bg-destructive // ์‚ญ์ œ ๋ฒ„ํŠผ +variant="outline" // ๋ณด์กฐ ๋ฒ„ํŠผ (์ทจ์†Œ) +variant="ghost" // ์•„์ด์ฝ˜ ๋ฒ„ํŠผ +``` + +### 2.2 ๊ฐ„๊ฒฉ ์‹œ์Šคํ…œ + +```tsx +// ์นด๋“œ ๊ฐ„ ๊ฐ„๊ฒฉ +gap-6 // 24px (์นด๋“œ ์‚ฌ์ด) + +// ์นด๋“œ ๋‚ด๋ถ€ ํŒจ๋”ฉ +p-6 // 24px (CardContent) + +// ํผ ํ•„๋“œ ๊ฐ„๊ฒฉ +space-y-4 // 16px (์ž…๋ ฅ ํ•„๋“œ๋“ค) +space-y-3 // 12px (๋ชจ๋ฐ”์ผ) + +// ์„น์…˜ ๊ฐ„๊ฒฉ +space-y-6 // 24px (ํฐ ์„น์…˜) +``` + +### 2.3 ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ + +```tsx +// ํŽ˜์ด์ง€ ์ œ๋ชฉ +text-2xl font-semibold + +// ์„น์…˜ ์ œ๋ชฉ +text-lg font-semibold + +// ์นด๋“œ ์ œ๋ชฉ +text-base font-semibold + +// ๋ผ๋ฒจ +text-sm font-medium + +// ๋ณธ๋ฌธ ํ…์ŠคํŠธ +text-sm text-muted-foreground + +// ์ž‘์€ ํ…์ŠคํŠธ +text-xs text-muted-foreground +``` + +### 2.4 ๋ฐ˜์‘ํ˜• ์„ค์ • + +```tsx +// ๋ชจ๋ฐ”์ผ ์šฐ์„  + ๋ฐ์Šคํฌํ†ฑ ์ตœ์ ํ™” +className="text-xs sm:text-sm" // ํฐํŠธ ํฌ๊ธฐ +className="h-8 sm:h-10" // ์ž…๋ ฅ ํ•„๋“œ ๋†’์ด +className="flex-col md:flex-row" // ๋ ˆ์ด์•„์›ƒ +className="gap-2 sm:gap-4" // ๊ฐ„๊ฒฉ +``` + +### 2.5 ์ค‘์ฒฉ ๋ฐ•์Šค ๊ธˆ์ง€ ์›์น™ + +**โŒ ์ž˜๋ชป๋œ ์˜ˆ์‹œ**: +```tsx + + +
{/* ์ค‘์ฒฉ ๋ฐ•์Šค! */} +
{/* ๋˜ ์ค‘์ฒฉ! */} + ๋‚ด์šฉ +
+
+
+
+``` + +**โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ**: +```tsx + + + ์ œ๋ชฉ + + + {/* ์ง์ ‘ ์ปจํ…์ธ  ๋ฐฐ์น˜ */} +
๋‚ด์šฉ 1
+
๋‚ด์šฉ 2
+
+
+``` + +--- + +## 3. ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ + +### 3.1 ํƒ€์ž… ์ •์˜ + +```typescript +// frontend/types/numbering-rule.ts + +import { BaseComponent } from "./screen-management"; + +/** + * ์ฝ”๋“œ ํŒŒํŠธ ์œ ํ˜• + */ +export type CodePartType = + | "prefix" // ์ ‘๋‘์‚ฌ (๊ณ ์ • ๋ฌธ์ž์—ด) + | "sequence" // ์ˆœ๋ฒˆ (์ž๋™ ์ฆ๊ฐ€) + | "date" // ๋‚ ์งœ (YYYYMMDD ๋“ฑ) + | "year" // ์—ฐ๋„ (YYYY) + | "month" // ์›” (MM) + | "custom"; // ์‚ฌ์šฉ์ž ์ •์˜ + +/** + * ์ƒ์„ฑ ๋ฐฉ์‹ + */ +export type GenerationMethod = + | "auto" // ์ž๋™ ์ƒ์„ฑ + | "manual"; // ์ง์ ‘ ์ž…๋ ฅ + +/** + * ๋‚ ์งœ ํ˜•์‹ + */ +export type DateFormat = + | "YYYY" // 2025 + | "YY" // 25 + | "YYYYMM" // 202511 + | "YYMM" // 2511 + | "YYYYMMDD" // 20251103 + | "YYMMDD"; // 251103 + +/** + * ๋‹จ์ผ ๊ทœ์น™ ํŒŒํŠธ + */ +export interface NumberingRulePart { + id: string; // ๊ณ ์œ  ID + order: number; // ์ˆœ์„œ (1-6) + partType: CodePartType; // ํŒŒํŠธ ์œ ํ˜• + generationMethod: GenerationMethod; // ์ƒ์„ฑ ๋ฐฉ์‹ + + // ์ž๋™ ์ƒ์„ฑ ์„ค์ • + autoConfig?: { + // ์ ‘๋‘์‚ฌ ์„ค์ • + prefix?: string; // ์˜ˆ: "ITM" + + // ์ˆœ๋ฒˆ ์„ค์ • + sequenceLength?: number; // ์ž๋ฆฟ์ˆ˜ (์˜ˆ: 4 โ†’ 0001) + startFrom?: number; // ์‹œ์ž‘ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ: 1) + + // ๋‚ ์งœ ์„ค์ • + dateFormat?: DateFormat; // ๋‚ ์งœ ํ˜•์‹ + }; + + // ์ง์ ‘ ์ž…๋ ฅ ์„ค์ • + manualConfig?: { + value: string; // ์ž…๋ ฅ๊ฐ’ + placeholder?: string; // ํ”Œ๋ ˆ์ด์Šคํ™€๋” + }; + + // ์ƒ์„ฑ๋œ ๊ฐ’ (๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ) + generatedValue?: string; +} + +/** + * ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ + */ +export interface NumberingRuleConfig { + ruleId: string; // ๊ทœ์น™ ID + ruleName: string; // ๊ทœ์น™๋ช… + description?: string; // ์„ค๋ช… + parts: NumberingRulePart[]; // ๊ทœ์น™ ํŒŒํŠธ ๋ฐฐ์—ด (์ตœ๋Œ€ 6๊ฐœ) + + // ์„ค์ • + separator?: string; // ๊ตฌ๋ถ„์ž (๊ธฐ๋ณธ: "-") + resetPeriod?: "none" | "daily" | "monthly" | "yearly"; // ์ดˆ๊ธฐํ™” ์ฃผ๊ธฐ + currentSequence?: number; // ํ˜„์žฌ ์‹œํ€€์Šค + + // ์ ์šฉ ๋Œ€์ƒ + tableName?: string; // ์ ์šฉํ•  ํ…Œ์ด๋ธ”๋ช… + columnName?: string; // ์ ์šฉํ•  ์ปฌ๋Ÿผ๋ช… + + // ๋ฉ”ํƒ€ ์ •๋ณด + companyCode?: string; + createdAt?: string; + updatedAt?: string; + createdBy?: string; +} + +/** + * ํ™”๋ฉด๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ ์ธํ„ฐํŽ˜์ด์Šค + */ +export interface NumberingRuleComponent extends BaseComponent { + type: "numbering-rule"; + + // ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ค์ • + ruleConfig: NumberingRuleConfig; + + // UI ์„ค์ • + showRuleList?: boolean; // ์ขŒ์ธก ๋ชฉ๋ก ํ‘œ์‹œ ์—ฌ๋ถ€ + maxRules?: number; // ์ตœ๋Œ€ ๊ทœ์น™ ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ: 6) + enableReorder?: boolean; // ์ˆœ์„œ ๋ณ€๊ฒฝ ํ—ˆ์šฉ ์—ฌ๋ถ€ + + // ์Šคํƒ€์ผ + cardLayout?: "vertical" | "horizontal"; // ์นด๋“œ ๋ ˆ์ด์•„์›ƒ +} +``` + +### 3.2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ + +```sql +-- db/migrations/034_create_numbering_rules.sql + +-- ์ฑ„๋ฒˆ ๊ทœ์น™ ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS numbering_rules ( + rule_id VARCHAR(50) PRIMARY KEY, + rule_name VARCHAR(100) NOT NULL, + description TEXT, + separator VARCHAR(10) DEFAULT '-', + reset_period VARCHAR(20) DEFAULT 'none', + current_sequence INTEGER DEFAULT 1, + table_name VARCHAR(100), + column_name VARCHAR(100), + company_code VARCHAR(20) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by VARCHAR(50), + + CONSTRAINT fk_company FOREIGN KEY (company_code) + REFERENCES company_info(company_code) +); + +-- ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ธ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS numbering_rule_parts ( + id SERIAL PRIMARY KEY, + rule_id VARCHAR(50) NOT NULL, + part_order INTEGER NOT NULL, + part_type VARCHAR(50) NOT NULL, + generation_method VARCHAR(20) NOT NULL, + auto_config JSONB, + manual_config JSONB, + company_code VARCHAR(20) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT fk_numbering_rule FOREIGN KEY (rule_id) + REFERENCES numbering_rules(rule_id) ON DELETE CASCADE, + CONSTRAINT fk_company FOREIGN KEY (company_code) + REFERENCES company_info(company_code), + CONSTRAINT unique_rule_order UNIQUE (rule_id, part_order, company_code) +); + +-- ์ธ๋ฑ์Šค +CREATE INDEX idx_numbering_rules_company ON numbering_rules(company_code); +CREATE INDEX idx_numbering_rule_parts_rule ON numbering_rule_parts(rule_id); +CREATE INDEX idx_numbering_rules_table ON numbering_rules(table_name, column_name); + +-- ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ +INSERT INTO numbering_rules (rule_id, rule_name, description, company_code, created_by) +VALUES ('SAMPLE_RULE', '์ƒ˜ํ”Œ ์ฑ„๋ฒˆ ๊ทœ์น™', '์ œํ’ˆ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ', '*', 'system') +ON CONFLICT (rule_id) DO NOTHING; + +INSERT INTO numbering_rule_parts (rule_id, part_order, part_type, generation_method, auto_config, company_code) +VALUES + ('SAMPLE_RULE', 1, 'prefix', 'auto', '{"prefix": "PROD"}', '*'), + ('SAMPLE_RULE', 2, 'date', 'auto', '{"dateFormat": "YYYYMMDD"}', '*'), + ('SAMPLE_RULE', 3, 'sequence', 'auto', '{"sequenceLength": 4, "startFrom": 1}', '*') +ON CONFLICT (rule_id, part_order, company_code) DO NOTHING; +``` + +--- + +## 4. ๊ตฌํ˜„ ์ˆœ์„œ + +### Phase 1: ํƒ€์ž… ์ •์˜ ๋ฐ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ โœ… +1. ํƒ€์ž… ์ •์˜ ํŒŒ์ผ ์ƒ์„ฑ +2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +3. ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… + +### Phase 2: ๋ฐฑ์—”๋“œ API ๊ตฌํ˜„ +1. Controller ์ƒ์„ฑ +2. Service ๋ ˆ์ด์–ด ๊ตฌํ˜„ +3. API ํ…Œ์ŠคํŠธ + +### Phase 3: ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ +1. NumberingRuleDesigner (๋ฉ”์ธ) +2. NumberingRulePreview (๋ฏธ๋ฆฌ๋ณด๊ธฐ) +3. NumberingRuleCard (๋‹จ์ผ ๊ทœ์น™ ์นด๋“œ) + +### Phase 4: ์ƒ์„ธ ์„ค์ • ํŒจ๋„ +1. PartTypeSelector (ํŒŒํŠธ ์œ ํ˜• ์„ ํƒ) +2. AutoConfigPanel (์ž๋™ ์ƒ์„ฑ ์„ค์ •) +3. ManualConfigPanel (์ง์ ‘ ์ž…๋ ฅ ์„ค์ •) + +### Phase 5: ํ™”๋ฉด๊ด€๋ฆฌ ํ†ตํ•ฉ +1. ComponentType์— "numbering-rule" ์ถ”๊ฐ€ +2. RealtimePreview ๋ Œ๋”๋ง ์ถ”๊ฐ€ +3. ํ…œํ”Œ๋ฆฟ ๋“ฑ๋ก +4. ์†์„ฑ ํŒจ๋„ ๊ตฌํ˜„ + +### Phase 6: ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” +1. ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ +2. ๋ฐ˜์‘ํ˜• ํ…Œ์ŠคํŠธ +3. ์„ฑ๋Šฅ ์ตœ์ ํ™” +4. ๋ฌธ์„œํ™” + +--- + +## 5. ๊ตฌํ˜„ ์™„๋ฃŒ โœ… + +### Phase 1: ํƒ€์ž… ์ •์˜ ๋ฐ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ โœ… +- โœ… `frontend/types/numbering-rule.ts` ์ƒ์„ฑ +- โœ… `db/migrations/034_create_numbering_rules.sql` ์ƒ์„ฑ ๋ฐ ์‹คํ–‰ +- โœ… ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์™„๋ฃŒ + +### Phase 2: ๋ฐฑ์—”๋“œ API ๊ตฌํ˜„ โœ… +- โœ… `backend-node/src/services/numberingRuleService.ts` ์ƒ์„ฑ +- โœ… `backend-node/src/controllers/numberingRuleController.ts` ์ƒ์„ฑ +- โœ… `app.ts`์— ๋ผ์šฐํ„ฐ ๋“ฑ๋ก (`/api/numbering-rules`) +- โœ… ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ ์™„๋ฃŒ + +### Phase 3: ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ โœ… +- โœ… `NumberingRulePreview.tsx` - ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +- โœ… `NumberingRuleCard.tsx` - ๋‹จ์ผ ๊ทœ์น™ ์นด๋“œ +- โœ… `AutoConfigPanel.tsx` - ์ž๋™ ์ƒ์„ฑ ์„ค์ • +- โœ… `ManualConfigPanel.tsx` - ์ง์ ‘ ์ž…๋ ฅ ์„ค์ • +- โœ… `NumberingRuleDesigner.tsx` - ๋ฉ”์ธ ๋””์ž์ด๋„ˆ + +### Phase 4: ์ƒ์„ธ ์„ค์ • ํŒจ๋„ โœ… +- โœ… ํŒŒํŠธ ์œ ํ˜•๋ณ„ ์„ค์ • UI (์ ‘๋‘์‚ฌ, ์ˆœ๋ฒˆ, ๋‚ ์งœ, ์—ฐ๋„, ์›”, ์ปค์Šคํ…€) +- โœ… ์ž๋™ ์ƒ์„ฑ / ์ง์ ‘ ์ž…๋ ฅ ๋ชจ๋“œ ์ „ํ™˜ +- โœ… ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์—…๋ฐ์ดํŠธ + +### Phase 5: ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ ํ†ตํ•ฉ โœ… +- โœ… `unified-core.ts`์— "numbering-rule" ComponentType ์ถ”๊ฐ€ +- โœ… `screen-management.ts`์— ComponentData ์œ ๋‹ˆ์˜จ ํƒ€์ž… ์ถ”๊ฐ€ +- โœ… `RealtimePreview.tsx`์— ๋ Œ๋”๋ง ๋กœ์ง ์ถ”๊ฐ€ +- โœ… `TemplatesPanel.tsx`์— "๊ด€๋ฆฌ์ž" ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ ํ…œํ”Œ๋ฆฟ ์ถ”๊ฐ€ +- โœ… `NumberingRuleTemplate.ts` ์ƒ์„ฑ + +### Phase 6: ์™„๋ฃŒ โœ… +๋ชจ๋“  ๋‹จ๊ณ„๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! + +--- + +## 6. ์‚ฌ์šฉ ๋ฐฉ๋ฒ• + +### 6.1 ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ + +1. **ํ™”๋ฉด๊ด€๋ฆฌ** ํŽ˜์ด์ง€๋กœ ์ด๋™ +2. ์ขŒ์ธก **ํ…œํ”Œ๋ฆฟ ํŒจ๋„**์—์„œ **๊ด€๋ฆฌ์ž** ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ +3. **์ฝ”๋“œ ์ฑ„๋ฒˆ ๊ทœ์น™** ํ…œํ”Œ๋ฆฟ์„ ์บ”๋ฒ„์Šค๋กœ ๋“œ๋ž˜๊ทธ +4. ๊ทœ์น™ ํŒŒํŠธ ์ถ”๊ฐ€ ๋ฐ ์„ค์ • +5. ์ €์žฅ + +### 6.2 API ์‚ฌ์šฉํ•˜๊ธฐ + +#### ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ +```bash +GET /api/numbering-rules +``` + +#### ๊ทœ์น™ ์ƒ์„ฑ +```bash +POST /api/numbering-rules +{ + "ruleId": "PROD_CODE", + "ruleName": "์ œํ’ˆ ์ฝ”๋“œ ๊ทœ์น™", + "parts": [ + { + "id": "part-1", + "order": 1, + "partType": "prefix", + "generationMethod": "auto", + "autoConfig": { "prefix": "PROD" } + }, + { + "id": "part-2", + "order": 2, + "partType": "date", + "generationMethod": "auto", + "autoConfig": { "dateFormat": "YYYYMMDD" } + }, + { + "id": "part-3", + "order": 3, + "partType": "sequence", + "generationMethod": "auto", + "autoConfig": { "sequenceLength": 4, "startFrom": 1 } + } + ], + "separator": "-" +} +``` + +#### ์ฝ”๋“œ ์ƒ์„ฑ +```bash +POST /api/numbering-rules/PROD_CODE/generate + +์‘๋‹ต: { "success": true, "data": { "code": "PROD-20251103-0001" } } +``` + +--- + +## 7. ๊ตฌํ˜„๋œ ํŒŒ์ผ ๋ชฉ๋ก + +### ํ”„๋ก ํŠธ์—”๋“œ +``` +frontend/ +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ numbering-rule.ts โœ… +โ”œโ”€โ”€ components/ +โ”‚ โ””โ”€โ”€ numbering-rule/ +โ”‚ โ”œโ”€โ”€ NumberingRuleDesigner.tsx โœ… +โ”‚ โ”œโ”€โ”€ NumberingRuleCard.tsx โœ… +โ”‚ โ”œโ”€โ”€ NumberingRulePreview.tsx โœ… +โ”‚ โ”œโ”€โ”€ AutoConfigPanel.tsx โœ… +โ”‚ โ””โ”€โ”€ ManualConfigPanel.tsx โœ… +โ””โ”€โ”€ components/screen/ + โ”œโ”€โ”€ RealtimePreview.tsx โœ… (์ˆ˜์ •๋จ) + โ”œโ”€โ”€ panels/ + โ”‚ โ””โ”€โ”€ TemplatesPanel.tsx โœ… (์ˆ˜์ •๋จ) + โ””โ”€โ”€ templates/ + โ””โ”€โ”€ NumberingRuleTemplate.ts โœ… +``` + +### ๋ฐฑ์—”๋“œ +``` +backend-node/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ services/ +โ”‚ โ”‚ โ””โ”€โ”€ numberingRuleService.ts โœ… +โ”‚ โ”œโ”€โ”€ controllers/ +โ”‚ โ”‚ โ””โ”€โ”€ numberingRuleController.ts โœ… +โ”‚ โ””โ”€โ”€ app.ts โœ… (์ˆ˜์ •๋จ) +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +``` +db/ +โ””โ”€โ”€ migrations/ + โ””โ”€โ”€ 034_create_numbering_rules.sql โœ… +``` + +--- + +## 8. ๋‹ค์Œ ๊ฐœ์„  ์‚ฌํ•ญ (์„ ํƒ์‚ฌํ•ญ) + +- [ ] ๊ทœ์น™ ์ˆœ์„œ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ๋ณ€๊ฒฝ +- [ ] ๊ทœ์น™ ๋ณต์ œ ๊ธฐ๋Šฅ +- [ ] ๊ทœ์น™ ํ…œํ”Œ๋ฆฟ ์ œ๊ณต (์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด) +- [ ] ์ฝ”๋“œ ๊ฒ€์ฆ ๋กœ์ง +- [ ] ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹œ ์ž๋™ ์ฑ„๋ฒˆ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ํ†ตํ•ฉ +- [ ] ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์ž…๋ ฅ ํผ์— ์ž๋™ ์ฝ”๋“œ ์ƒ์„ฑ ๋ฒ„ํŠผ ์ถ”๊ฐ€ + From 37796ecc9d36bd61902530f97f76abfa45fe341a Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 4 Nov 2025 16:18:12 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20FileComponentConfigPanel=EC=97=90=20?= =?UTF-8?q?cn=20=ED=95=A8=EC=88=98=20import=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/panels/FileComponentConfigPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/screen/panels/FileComponentConfigPanel.tsx b/frontend/components/screen/panels/FileComponentConfigPanel.tsx index 8db01ad0..f14c861f 100644 --- a/frontend/components/screen/panels/FileComponentConfigPanel.tsx +++ b/frontend/components/screen/panels/FileComponentConfigPanel.tsx @@ -12,7 +12,7 @@ import { Plus, X, Upload, File, Image, FileText, Download, Trash2 } from "lucide import { Button } from "@/components/ui/button"; import { FileInfo, FileUploadResponse } from "@/lib/registry/components/file-upload/types"; import { uploadFiles, downloadFile, deleteFile } from "@/lib/api/file"; -import { formatFileSize } from "@/lib/utils"; +import { formatFileSize, cn } from "@/lib/utils"; import { toast } from "sonner"; interface FileComponentConfigPanelProps { From b8e30c9557341227fdb715b8ec89f21622a24857 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 4 Nov 2025 16:21:24 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/panels/UnifiedPropertiesPanel.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 32200450..01716bb0 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -180,27 +180,21 @@ export const UnifiedPropertiesPanel: React.FC = ({ id="columns" type="number" min={1} - max={24} value={gridSettings.columns} onChange={(e) => { const value = parseInt(e.target.value, 10); - if (!isNaN(value) && value >= 1 && value <= 24) { + if (!isNaN(value) && value >= 1) { updateGridSetting("columns", value); } }} className="h-6 px-2 py-0 text-xs" style={{ fontSize: "12px" }} + placeholder="1 ์ด์ƒ์˜ ์ˆซ์ž" /> - / 24
- updateGridSetting("columns", value)} - className="w-full" - /> +

+ 1 ์ด์ƒ์˜ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” +

{/* ๊ฐ„๊ฒฉ */}