diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 65dbf84c..74b1b8f6 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -120,10 +120,28 @@ export const ScreenModal: React.FC = ({ className }) => { }; }; + // πŸ†• μ„ νƒλœ 데이터 μƒνƒœ μΆ”κ°€ (RepeatScreenModal λ“±μ—μ„œ μ‚¬μš©) + const [selectedData, setSelectedData] = useState[]>([]); + // μ „μ—­ λͺ¨λ‹¬ 이벀트 λ¦¬μŠ€λ„ˆ useEffect(() => { const handleOpenModal = (event: CustomEvent) => { - const { screenId, title, description, size, urlParams } = event.detail; + const { screenId, title, description, size, urlParams, selectedData: eventSelectedData, selectedIds } = event.detail; + + console.log("πŸ“¦ [ScreenModal] λͺ¨λ‹¬ μ—΄κΈ° 이벀트 μˆ˜μ‹ :", { + screenId, + title, + selectedData: eventSelectedData, + selectedIds, + }); + + // πŸ†• μ„ νƒλœ 데이터 μ €μž₯ + if (eventSelectedData && Array.isArray(eventSelectedData)) { + setSelectedData(eventSelectedData); + console.log("πŸ“¦ [ScreenModal] μ„ νƒλœ 데이터 μ €μž₯:", eventSelectedData.length, "건"); + } else { + setSelectedData([]); + } // πŸ†• URL νŒŒλΌλ―Έν„°κ°€ 있으면 ν˜„μž¬ URL에 μΆ”κ°€ if (urlParams && typeof window !== "undefined") { @@ -164,6 +182,7 @@ export const ScreenModal: React.FC = ({ className }) => { }); setScreenData(null); setFormData({}); + setSelectedData([]); // πŸ†• μ„ νƒλœ 데이터 μ΄ˆκΈ°ν™” setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 μ €μž₯ console.log("πŸ”„ 연속 λͺ¨λ“œ μ΄ˆκΈ°ν™”: false"); @@ -605,6 +624,8 @@ export const ScreenModal: React.FC = ({ className }) => { userId={userId} userName={userName} companyCode={user?.companyCode} + // πŸ†• μ„ νƒλœ 데이터 전달 (RepeatScreenModal λ“±μ—μ„œ μ‚¬μš©) + groupedData={selectedData.length > 0 ? selectedData : undefined} /> ); })} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 41e321e5..e351b68c 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -408,6 +408,7 @@ export const InteractiveScreenViewerDynamic: React.FC handleFormDataChange(fieldName, value), onFormDataChange: handleFormDataChange, + formData: formData, // πŸ†• 전체 formData 전달 isInteractive: true, readonly: readonly, required: required, @@ -415,6 +416,7 @@ export const InteractiveScreenViewerDynamic: React.FC { diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index e3e8cbb3..90187838 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -863,27 +863,23 @@ export const DetailSettingsPanel: React.FC = ({ }); // 래퍼 μ»΄ν¬λ„ŒνŠΈ: μƒˆ ConfigPanel μΈν„°νŽ˜μ΄μŠ€λ₯Ό κΈ°μ‘΄ νŒ¨ν„΄μ— 맞좀 - const ConfigPanelWrapper = () => { - // Section Card, Section Paper λ“± μ‹ κ·œ μ»΄ν¬λ„ŒνŠΈλŠ” componentConfig λ°”λ‘œ μ•„λž˜μ— μ„€μ • μ €μž₯ - const config = currentConfig || definition.defaultProps?.componentConfig || {}; - - const handleConfigChange = (newConfig: any) => { - // componentConfig 전체λ₯Ό μ—…λ°μ΄νŠΈ - onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); - }; - - return ( -
-
- -

{definition.name} μ„€μ •

-
- -
- ); + // Section Card, Section Paper λ“± μ‹ κ·œ μ»΄ν¬λ„ŒνŠΈλŠ” componentConfig λ°”λ‘œ μ•„λž˜μ— μ„€μ • μ €μž₯ + const config = currentConfig || definition.defaultProps?.componentConfig || {}; + + const handleConfigChange = (newConfig: any) => { + // componentConfig 전체λ₯Ό μ—…λ°μ΄νŠΈ + onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); }; - return ; + return ( +
+
+ +

{definition.name} μ„€μ •

+
+ +
+ ); } else { console.warn("⚠️ ConfigPanel μ—†μŒ:", { componentId, diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 69b7d092..e3940073 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -326,40 +326,36 @@ export const UnifiedPropertiesPanel: React.FC = ({ }); // 래퍼 μ»΄ν¬λ„ŒνŠΈ: μƒˆ ConfigPanel μΈν„°νŽ˜μ΄μŠ€λ₯Ό κΈ°μ‘΄ νŒ¨ν„΄μ— 맞좀 - const ConfigPanelWrapper = () => { - // Section Card, Section Paper λ“± μ‹ κ·œ μ»΄ν¬λ„ŒνŠΈλŠ” componentConfig λ°”λ‘œ μ•„λž˜μ— μ„€μ • μ €μž₯ - const config = currentConfig || definition.defaultProps?.componentConfig || {}; - - const handleConfigChange = (newConfig: any) => { - // componentConfig 전체λ₯Ό μ—…λ°μ΄νŠΈ - onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); - }; - - return ( -
-
- -

{definition.name} μ„€μ •

-
- -
μ„€μ • νŒ¨λ„ λ‘œλ”© 쀑...
-
- }> - - - - ); + // Section Card, Section Paper λ“± μ‹ κ·œ μ»΄ν¬λ„ŒνŠΈλŠ” componentConfig λ°”λ‘œ μ•„λž˜μ— μ„€μ • μ €μž₯ + const config = currentConfig || definition.defaultProps?.componentConfig || {}; + + const handleConfigChange = (newConfig: any) => { + // componentConfig 전체λ₯Ό μ—…λ°μ΄νŠΈ + onUpdateProperty(selectedComponent.id, "componentConfig", newConfig); }; - return ; + return ( +
+
+ +

{definition.name} μ„€μ •

+
+ +
μ„€μ • νŒ¨λ„ λ‘œλ”© 쀑...
+
+ }> + + + + ); } else { console.warn("⚠️ ComponentRegistryμ—μ„œ ConfigPanel을 찾을 수 μ—†μŒ - switch case둜 이동:", { componentId, diff --git a/frontend/lib/registry/components/repeat-screen-modal/README.md b/frontend/lib/registry/components/repeat-screen-modal/README.md index 87e8114f..6ba2783a 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/README.md +++ b/frontend/lib/registry/components/repeat-screen-modal/README.md @@ -1,71 +1,145 @@ -# RepeatScreenModal μ»΄ν¬λ„ŒνŠΈ v2 +# RepeatScreenModal μ»΄ν¬λ„ŒνŠΈ v3 ## κ°œμš” -`RepeatScreenModal`은 μ„ νƒν•œ 데이터λ₯Ό κ·Έλ£Ήν•‘ν•˜μ—¬ μΉ΄λ“œ ν˜•νƒœλ‘œ ν‘œμ‹œν•˜κ³ , 각 μΉ΄λ“œ λ‚΄μ—μ„œ 데이터λ₯Ό νŽΈμ§‘ν•  수 μžˆλŠ” **만λŠ₯ 폼 μ»΄ν¬λ„ŒνŠΈ**μž…λ‹ˆλ‹€. +`RepeatScreenModal`은 μ„ νƒν•œ 데이터λ₯Ό 기반으둜 μ—¬λŸ¬ 개의 μΉ΄λ“œλ₯Ό μƒμ„±ν•˜κ³ , 각 μΉ΄λ“œμ˜ λ‚΄λΆ€ λ ˆμ΄μ•„μ›ƒμ„ 자유둭게 ꡬ성할 수 μžˆλŠ” μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€. -**이 μ»΄ν¬λ„ŒνŠΈ ν•˜λ‚˜λ‘œ λŒ€λΆ€λΆ„μ˜ ERP 화면을 μ„€μ •λ§ŒμœΌλ‘œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.** +## v3 μ£Όμš” 변경사항 -## 핡심 μ² ν•™ +### 자유 λ ˆμ΄μ•„μ›ƒ μ‹œμŠ€ν…œ + +기쑴의 "simple λͺ¨λ“œ / withTable λͺ¨λ“œ" ꡬ뢄을 μ—†μ• κ³ , **ν–‰(Row)을 μΆ”κ°€ν•˜κ³  각 ν–‰λ§ˆλ‹€ νƒ€μž…μ„ 선택**ν•˜λŠ” λ°©μ‹μœΌλ‘œ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ ERP ν™”λ©΄ κ΅¬μ„±μ˜ 핡심 β”‚ +β”‚ μΉ΄λ“œ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β”‚ -β”‚ 1. μ–΄λ–€ ν…Œμ΄λΈ”μ—μ„œ β†’ μ–΄λ–€ μ»¬λŸΌμ„ β†’ μ–΄λ–»κ²Œ 보여쀄 것인가? β”‚ -β”‚ β”‚ -β”‚ 2. 보기만 ν•  것인가? vs μˆ˜μ • κ°€λŠ₯ν•˜κ²Œ ν•  것인가? β”‚ -β”‚ β”‚ -β”‚ 3. μˆ˜μ •ν•œλ‹€λ©΄ β†’ μ–΄λ–€ ν…Œμ΄λΈ”μ˜ β†’ μ–΄λ–€ μ»¬λŸΌμ— μ €μž₯ν•  것인가? β”‚ -β”‚ β”‚ -β”‚ 4. 데이터λ₯Ό μ–΄λ–»κ²Œ κ·Έλ£Ήν™”ν•΄μ„œ 보여쀄 것인가? β”‚ -β”‚ β”‚ +β”‚ [ν–‰ 1] νƒ€μž…: 헀더 β†’ ν’ˆλͺ©μ½”λ“œ, ν’ˆλͺ©λͺ…, 규격 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [ν–‰ 2] νƒ€μž…: 집계 β†’ μ΄μˆ˜μ£Όμž”λŸ‰, ν˜„μž¬κ³ , κ°€μš©μž¬κ³  β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [ν–‰ 3] νƒ€μž…: ν…Œμ΄λΈ” β†’ 수주번호, 거래처, 납기일, μΆœν•˜κ³„νš β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [ν–‰ 4] νƒ€μž…: ν…Œμ΄λΈ” β†’ 또 λ‹€λ₯Έ ν…Œμ΄λΈ”λ„ κ°€λŠ₯! β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -## μΉ΄λ“œ λͺ¨λ“œ +### ν–‰ νƒ€μž… -### 1. Simple λͺ¨λ“œ (λ‹¨μˆœ) +| νƒ€μž… | μ„€λͺ… | μ‚¬μš© μ‹œλ‚˜λ¦¬μ˜€ | +|------|------|---------------| +| **헀더 (header)** | ν•„λ“œλ“€μ„ κ°€λ‘œ/μ„Έλ‘œλ‘œ λ‚˜μ—΄ | ν’ˆλͺ©μ •보, κ±°λž˜μ²˜μ •λ³΄ ν‘œμ‹œ | +| **ν•„λ“œ (fields)** | 헀더와 동일, νŽΈμ§‘ κ°€λŠ₯ | 폼 μž…λ ₯ μ˜μ—­ | +| **집계 (aggregation)** | κ·Έλ£Ή λ‚΄ 데이터 집계값 ν‘œμ‹œ | μ΄μˆ˜λŸ‰, ν•©κ³„κΈˆμ•‘ λ“± | +| **ν…Œμ΄λΈ” (table)** | κ·Έλ£Ή λ‚΄ 각 행을 ν…Œμ΄λΈ”λ‘œ ν‘œμ‹œ | 수주λͺ©λ‘, ν’ˆλͺ©λͺ©λ‘ λ“± | -- **1ν–‰ = 1μΉ΄λ“œ**: μ„ νƒν•œ 각 행이 독립적인 μΉ΄λ“œλ‘œ ν‘œμ‹œ -- 자유둜운 λ ˆμ΄μ•„μ›ƒ ꡬ성 (ν–‰/컬럼 기반) -- μ ν•©ν•œ 상황: λ‹¨μˆœ 데이터 νŽΈμ§‘, κ°œλ³„ λ ˆμ½”λ“œ μˆ˜μ • - -### 2. WithTable λͺ¨λ“œ (ν…Œμ΄λΈ” 포함) - -- **Nν–‰ = 1μΉ΄λ“œ**: κ·Έλ£Ήν•‘λœ μ—¬λŸ¬ 행이 ν•˜λ‚˜μ˜ μΉ΄λ“œλ‘œ ν‘œμ‹œ -- μΉ΄λ“œ = 헀더 μ˜μ—­ + ν…Œμ΄λΈ” μ˜μ—­ -- 헀더: κ·Έλ£Ή λŒ€ν‘œκ°’, 집계값 ν‘œμ‹œ -- ν…Œμ΄λΈ”: κ·Έλ£Ή λ‚΄ 각 행을 ν…Œμ΄λΈ”λ‘œ ν‘œμ‹œ -- μ ν•©ν•œ 상황: μΆœν•˜κ³„νš, ꡬ맀발주, μƒμ‚°κ³„νš λ“± 일괄 등둝 - -## μ£Όμš” κΈ°λŠ₯ - -| κΈ°λŠ₯ | μ„€λͺ… | -|------|------| -| κ·Έλ£Ήν•‘ | νŠΉμ • ν•„λ“œ κΈ°μ€€μœΌλ‘œ μ—¬λŸ¬ 행을 ν•˜λ‚˜μ˜ μΉ΄λ“œλ‘œ 묢음 | -| 집계 | κ·Έλ£Ή λ‚΄ λ°μ΄ν„°μ˜ 합계/개수/평균/μ΅œμ†Œ/μ΅œλŒ€ μžλ™ 계산 | -| μΉ΄λ“œ λ‚΄ ν…Œμ΄λΈ” | κ·Έλ£Ή λ‚΄ 각 행을 ν…Œμ΄λΈ” ν˜•νƒœλ‘œ ν‘œμ‹œ | -| ν…Œμ΄λΈ” λ‚΄ νŽΈμ§‘ | ν…Œμ΄λΈ”μ˜ νŠΉμ • μ»¬λŸΌμ„ νŽΈμ§‘ κ°€λŠ₯ν•˜κ²Œ μ„€μ • | -| 닀쀑 ν…Œμ΄λΈ” μ €μž₯ | ν•˜λ‚˜μ˜ μΉ΄λ“œμ—μ„œ μ—¬λŸ¬ ν…Œμ΄λΈ” λ™μ‹œ μ €μž₯ | -| μ»¬λŸΌλ³„ μ†ŒμŠ€ μ„€μ • | 직접 쑰회/쑰인 쑰회/μˆ˜λ™ μž…λ ₯ 선택 | -| μ»¬λŸΌλ³„ νƒ€κ²Ÿ μ„€μ • | μ €μž₯ν•  ν…Œμ΄λΈ”κ³Ό 컬럼 μ§€μ • | - -## μ‚¬μš© μ‹œλ‚˜λ¦¬μ˜€ - -### μ‹œλ‚˜λ¦¬μ˜€ 1: μΆœν•˜κ³„νš λ™μ‹œ 등둝 +### 자유둜운 μ‘°ν•© ``` -κ·Έλ£Ήν•‘: part_code (ν’ˆλͺ©μ½”λ“œ) -헀더: ν’ˆλͺ©μ •보 + μ΄μˆ˜μ£Όμž”λŸ‰ + ν˜„μž¬κ³  -ν…Œμ΄λΈ”: μˆ˜μ£Όλ³„ μΆœν•˜κ³„νš μž…λ ₯ +μ˜ˆμ‹œ 1: 헀더 + 집계 + ν…Œμ΄λΈ” (μΆœν•˜κ³„νš) +β”œβ”€β”€ [ν–‰ 1] 헀더: ν’ˆλͺ©μ½”λ“œ, ν’ˆλͺ©λͺ… +β”œβ”€β”€ [ν–‰ 2] 집계: μ΄μˆ˜μ£Όμž”λŸ‰, ν˜„μž¬κ³  +└── [ν–‰ 3] ν…Œμ΄λΈ”: μˆ˜μ£Όλ³„ μΆœν•˜κ³„νš + +μ˜ˆμ‹œ 2: μ§‘κ³„λ§Œ +└── [ν–‰ 1] 집계: 총맀좜, μ΄λΉ„μš©, 순이읡 + +μ˜ˆμ‹œ 3: ν…Œμ΄λΈ”λ§Œ +└── [ν–‰ 1] ν…Œμ΄λΈ”: ν’ˆλͺ© λͺ©λ‘ + +μ˜ˆμ‹œ 4: ν…Œμ΄λΈ” 2개 +β”œβ”€β”€ [ν–‰ 1] ν…Œμ΄λΈ”: μž…κ³  λ‚΄μ—­ +└── [ν–‰ 2] ν…Œμ΄λΈ”: 좜고 λ‚΄μ—­ + +μ˜ˆμ‹œ 5: 헀더 + 헀더 + ν•„λ“œ +β”œβ”€β”€ [ν–‰ 1] 헀더: κΈ°λ³Έ 정보 (μ½κΈ°μ „μš©) +β”œβ”€β”€ [ν–‰ 2] 헀더: 상세 정보 (μ½κΈ°μ „μš©) +└── [ν–‰ 3] ν•„λ“œ: μž…λ ₯ ν•„λ“œ (νŽΈμ§‘κ°€λŠ₯) ``` -**μ„€μ • μ˜ˆμ‹œ:** +## μ„€μ • 방법 + +### 1. κΈ°λ³Έ μ„€μ • νƒ­ + +- **μΉ΄λ“œ 제λͺ© ν‘œμ‹œ**: μΉ΄λ“œ 상단에 제λͺ©μ„ ν‘œμ‹œν• μ§€ μ—¬λΆ€ +- **μΉ΄λ“œ 제λͺ© ν…œν”Œλ¦Ώ**: `{field_name}` ν˜•μ‹μœΌλ‘œ 동적 제λͺ© 생성 +- **μΉ΄λ“œ 간격**: μΉ΄λ“œ μ‚¬μ΄μ˜ 간격 (8px ~ 32px) +- **ν…Œλ‘λ¦¬**: μΉ΄λ“œ ν…Œλ‘λ¦¬ ν‘œμ‹œ μ—¬λΆ€ +- **μ €μž₯ λͺ¨λ“œ**: 전체 μ €μž₯ / κ°œλ³„ μ €μž₯ + +### 2. 데이터 μ†ŒμŠ€ νƒ­ + +- **μ†ŒμŠ€ ν…Œμ΄λΈ”**: 데이터λ₯Ό μ‘°νšŒν•  ν…Œμ΄λΈ” +- **ν•„ν„° ν•„λ“œ**: formDataμ—μ„œ 필터링할 ν•„λ“œ (예: selectedIds) + +### 3. κ·Έλ£Ή νƒ­ + +- **κ·Έλ£Ήν•‘ ν™œμ„±ν™”**: μ—¬λŸ¬ 행을 ν•˜λ‚˜μ˜ μΉ΄λ“œλ‘œ 묢을지 μ—¬λΆ€ +- **κ·Έλ£Ή κΈ°μ€€ ν•„λ“œ**: κ·Έλ£Ήν•‘ν•  ν•„λ“œ (예: part_code) +- **집계 μ„€μ •**: + - 원본 ν•„λ“œ: 합계할 ν•„λ“œ (예: balance_qty) + - 집계 νƒ€μž…: sum, count, avg, min, max + - κ²°κ³Ό ν•„λ“œλͺ…: 집계 κ²°κ³Όλ₯Ό μ €μž₯ν•  ν•„λ“œλͺ… + - 라벨: ν‘œμ‹œλ  라벨 + +### 4. λ ˆμ΄μ•„μ›ƒ νƒ­ + +#### ν–‰ μΆ”κ°€ + +4κ°€μ§€ νƒ€μž…μ˜ 행을 μΆ”κ°€ν•  수 μžˆμŠ΅λ‹ˆλ‹€: +- **헀더**: ν•„λ“œ 정보 ν‘œμ‹œ (μ½κΈ°μ „μš©) +- **집계**: κ·Έλ£Ή 집계값 ν‘œμ‹œ +- **ν…Œμ΄λΈ”**: κ·Έλ£Ή λ‚΄ 행듀을 ν…Œμ΄λΈ”λ‘œ ν‘œμ‹œ +- **ν•„λ“œ**: μž…λ ₯ ν•„λ“œ (νŽΈμ§‘κ°€λŠ₯) + +#### 헀더/ν•„λ“œ ν–‰ μ„€μ • + +- **λ°©ν–₯**: κ°€λ‘œ / μ„Έλ‘œ +- **배경색**: μ—†μŒ, νŒŒλž‘, 초둝, 보라, μ£Όν™© +- **컬럼**: ν•„λ“œλͺ…, 라벨, νƒ€μž…, λ„ˆλΉ„, νŽΈμ§‘ κ°€λŠ₯, ν•„μˆ˜ +- **μ†ŒμŠ€ μ„€μ •**: 직접 / 쑰인 / μˆ˜λ™ +- **μ €μž₯ μ„€μ •**: μ €μž₯ν•  ν…Œμ΄λΈ”κ³Ό 컬럼 + +#### 집계 ν–‰ μ„€μ • + +- **λ ˆμ΄μ•„μ›ƒ**: κ°€λ‘œ λ‚˜μ—΄ / κ·Έλ¦¬λ“œ +- **κ·Έλ¦¬λ“œ 컬럼 수**: 2, 3, 4개 +- **집계 ν•„λ“œ**: κ·Έλ£Ή νƒ­μ—μ„œ μ •μ˜ν•œ 집계 κ²°κ³Ό 선택 +- **μŠ€νƒ€μΌ**: 배경색, 폰트 크기 + +#### ν…Œμ΄λΈ” ν–‰ μ„€μ • + +- **ν…Œμ΄λΈ” 제λͺ©**: 선택사항 +- **헀더 ν‘œμ‹œ**: ν…Œμ΄λΈ” 헀더 ν‘œμ‹œ μ—¬λΆ€ +- **ν…Œμ΄λΈ” 컬럼**: ν•„λ“œλͺ…, 라벨, νƒ€μž…, λ„ˆλΉ„, νŽΈμ§‘ κ°€λŠ₯ +- **μ €μž₯ μ„€μ •**: νŽΈμ§‘ κ°€λŠ₯ν•œ 컬럼의 μ €μž₯ μœ„μΉ˜ + +## 데이터 흐름 + +``` +1. formDataμ—μ„œ selectedIds κ°€μ Έμ˜€κΈ° + ↓ +2. μ†ŒμŠ€ ν…Œμ΄λΈ”μ—μ„œ ν•΄λ‹Ή IDλ“€μ˜ 데이터 쑰회 + ↓ +3. κ·Έλ£Ήν•‘ ν™œμ„±ν™” μ‹œ groupByField κΈ°μ€€μœΌλ‘œ κ·Έλ£Ήν™” + ↓ +4. 각 그룹에 λŒ€ν•΄ 집계값 계산 + ↓ +5. μΉ΄λ“œ λ Œλ”λ§ (contentRows 기반) + ↓ +6. μ‚¬μš©μž νŽΈμ§‘ + ↓ +7. μ €μž₯ μ‹œ targetConfig에 따라 ν…Œμ΄λΈ”λ³„λ‘œ 데이터 λΆ„λ₯˜ ν›„ μ €μž₯ +``` + +## μ‚¬μš© μ˜ˆμ‹œ + +### μΆœν•˜κ³„νš 등둝 + ```typescript { - cardMode: "withTable", + showCardTitle: true, + cardTitle: "{part_code} - {part_name}", dataSource: { sourceTable: "sales_order_mng", filterField: "selectedIds" @@ -78,159 +152,55 @@ { sourceField: "id", type: "count", resultField: "order_count", label: "수주건수" } ] }, - tableLayout: { - headerRows: [ - { - columns: [ - { field: "part_code", label: "ν’ˆλͺ©μ½”λ“œ", type: "text", editable: false }, - { field: "part_name", label: "ν’ˆλͺ©λͺ…", type: "text", editable: false }, - { field: "total_balance", label: "μ΄μˆ˜μ£Όμž”λŸ‰", type: "aggregation", aggregationField: "total_balance" } - ] - } - ], - tableColumns: [ - { field: "order_no", label: "수주번호", type: "text", editable: false }, - { field: "partner_id", label: "거래처", type: "text", editable: false }, - { field: "due_date", label: "납기일", type: "date", editable: false }, - { field: "balance_qty", label: "λ―ΈμΆœν•˜", type: "number", editable: false }, - { - field: "plan_qty", - label: "μΆœν•˜κ³„νš", - type: "number", - editable: true, - targetConfig: { targetTable: "shipment_plan", targetColumn: "plan_qty", saveEnabled: true } - } - ] - } + contentRows: [ + { + id: "row-1", + type: "header", + columns: [ + { id: "c1", field: "part_code", label: "ν’ˆλͺ©μ½”λ“œ", type: "text", editable: false }, + { id: "c2", field: "part_name", label: "ν’ˆλͺ©λͺ…", type: "text", editable: false } + ], + layout: "horizontal" + }, + { + id: "row-2", + type: "aggregation", + aggregationLayout: "horizontal", + aggregationFields: [ + { aggregationResultField: "total_balance", label: "μ΄μˆ˜μ£Όμž”λŸ‰", backgroundColor: "blue" }, + { aggregationResultField: "order_count", label: "수주건수", backgroundColor: "green" } + ] + }, + { + id: "row-3", + type: "table", + tableTitle: "수주 λͺ©λ‘", + showTableHeader: true, + tableColumns: [ + { id: "tc1", field: "order_no", label: "수주번호", type: "text", editable: false }, + { id: "tc2", field: "partner_name", label: "거래처", type: "text", editable: false }, + { id: "tc3", field: "balance_qty", label: "λ―ΈμΆœν•˜", type: "number", editable: false }, + { + id: "tc4", + field: "plan_qty", + label: "μΆœν•˜κ³„νš", + type: "number", + editable: true, + targetConfig: { targetTable: "shipment_plan", targetColumn: "plan_qty", saveEnabled: true } + } + ] + } + ] } ``` -### μ‹œλ‚˜λ¦¬μ˜€ 2: ꡬ맀발주 일괄 등둝 +## λ ˆκ±°μ‹œ ν˜Έν™˜ -``` -κ·Έλ£Ήν•‘: supplier_id (곡급업체) -헀더: 곡급업체정보 + μ΄λ°œμ£ΌκΈˆμ•‘ -ν…Œμ΄λΈ”: ν’ˆλͺ©λ³„ λ°œμ£Όμˆ˜λŸ‰ μž…λ ₯ -``` +v2μ—μ„œ μ‚¬μš©ν•˜λ˜ `cardMode`, `cardLayout`, `tableLayout` 섀정도 계속 μ§€μ›λ©λ‹ˆλ‹€. +μƒˆλ‘œμš΄ ν”„λ‘œμ νŠΈμ—μ„œλŠ” `contentRows`λ₯Ό μ‚¬μš©ν•˜λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€. -### μ‹œλ‚˜λ¦¬μ˜€ 3: μƒμ‚°κ³„νš 일괄 등둝 +## μ£Όμ˜μ‚¬ν•­ -``` -κ·Έλ£Ήν•‘: product_code (μ œν’ˆμ½”λ“œ) -헀더: μ œν’ˆμ •λ³΄ + ν˜„μž¬κ³  + ν•„μš”μˆ˜λŸ‰ -ν…Œμ΄λΈ”: μž‘μ—…μ§€μ‹œλ³„ μƒμ‚°μˆ˜λŸ‰ μž…λ ₯ -``` - -### μ‹œλ‚˜λ¦¬μ˜€ 4: μž…κ³ κ²€μ‚¬ 일괄 처리 - -``` -κ·Έλ£Ήν•‘: po_no (발주번호) -헀더: λ°œμ£Όμ •λ³΄ + 곡급업체 -ν…Œμ΄λΈ”: ν’ˆλͺ©λ³„ 검사결과 μž…λ ₯ -``` - -## ConfigPanel μ‚¬μš©λ²• - -### 1. κΈ°λ³Έ μ„€μ • νƒ­ - -- **μΉ΄λ“œ 제λͺ©**: `{field}` ν˜•μ‹μœΌλ‘œ 동적 제λͺ© μ„€μ • -- **μΉ΄λ“œ 간격**: μΉ΄λ“œ 사이 간격 (8px ~ 32px) -- **ν…Œλ‘λ¦¬**: μΉ΄λ“œ ν…Œλ‘λ¦¬ ν‘œμ‹œ μ—¬λΆ€ -- **μ €μž₯ λͺ¨λ“œ**: 전체 μ €μž₯ / κ°œλ³„ μ €μž₯ -- **μΉ΄λ“œ λͺ¨λ“œ**: λ‹¨μˆœ / ν…Œμ΄λΈ” - -### 2. 데이터 μ†ŒμŠ€ νƒ­ - -- **μ†ŒμŠ€ ν…Œμ΄λΈ”**: 데이터λ₯Ό μ‘°νšŒν•  ν…Œμ΄λΈ” -- **ν•„ν„° ν•„λ“œ**: formDataμ—μ„œ κ°€μ Έμ˜¬ ν•„ν„° ν•„λ“œλͺ… (예: `selectedIds`) - -### 3. κ·Έλ£Ήν•‘ νƒ­ (ν…Œμ΄λΈ” λͺ¨λ“œμ—μ„œ ν™œμ„±ν™”) - -- **κ·Έλ£Ήν•‘ ν™œμ„±ν™”**: ON/OFF -- **κ·Έλ£Ή κΈ°μ€€ ν•„λ“œ**: κ·Έλ£Ήν•‘ν•  ν•„λ“œ 선택 -- **집계 μ„€μ •**: 합계/개수/평균 λ“± 집계 μΆ”κ°€ - -### 4. λ ˆμ΄μ•„μ›ƒ νƒ­ - -**Simple λͺ¨λ“œ:** -- ν–‰ μΆ”κ°€/μ‚­μ œ -- 각 행에 컬럼 μΆ”κ°€/μ‚­μ œ -- μ»¬λŸΌλ³„ ν•„λ“œλͺ…, 라벨, νƒ€μž…, λ„ˆλΉ„, νŽΈμ§‘ κ°€λŠ₯ μ—¬λΆ€ μ„€μ • - -**WithTable λͺ¨λ“œ:** -- 헀더 μ˜μ—­: κ·Έλ£Ή λŒ€ν‘œκ°’, 집계값 ν‘œμ‹œμš© ν–‰/컬럼 μ„€μ • -- ν…Œμ΄λΈ” μ˜μ—­: κ·Έλ£Ή λ‚΄ 각 행을 ν‘œμ‹œν•  ν…Œμ΄λΈ” 컬럼 μ„€μ • - -## 컬럼 μ„€μ • 상세 - -### μ†ŒμŠ€ μ„€μ • (데이터 쑰회) - -| νƒ€μž… | μ„€λͺ… | -|------|------| -| direct | μ†ŒμŠ€ ν…Œμ΄λΈ”μ—μ„œ 직접 쑰회 | -| join | λ‹€λ₯Έ ν…Œμ΄λΈ”κ³Ό μ‘°μΈν•˜μ—¬ 쑰회 | -| manual | μ‚¬μš©μž 직접 μž…λ ₯ | - -### νƒ€κ²Ÿ μ„€μ • (데이터 μ €μž₯) - -- **μ €μž₯ ν…Œμ΄λΈ”**: 데이터λ₯Ό μ €μž₯ν•  ν…Œμ΄λΈ” -- **μ €μž₯ 컬럼**: 데이터λ₯Ό μ €μž₯ν•  컬럼 -- **μ €μž₯ ν™œμ„±ν™”**: μ €μž₯ μ—¬λΆ€ - -## νƒ€μž… μ •μ˜ - -```typescript -interface RepeatScreenModalProps { - // κΈ°λ³Έ μ„€μ • - cardTitle?: string; - cardSpacing?: string; - showCardBorder?: boolean; - saveMode?: "all" | "individual"; - cardMode?: "simple" | "withTable"; - - // 데이터 μ†ŒμŠ€ - dataSource?: DataSourceConfig; - - // κ·Έλ£Ήν•‘ μ„€μ • - grouping?: GroupingConfig; - - // λ ˆμ΄μ•„μ›ƒ - cardLayout?: CardRowConfig[]; // simple λͺ¨λ“œ - tableLayout?: TableLayoutConfig; // withTable λͺ¨λ“œ -} - -interface GroupingConfig { - enabled: boolean; - groupByField: string; - aggregations?: AggregationConfig[]; -} - -interface AggregationConfig { - sourceField: string; - type: "sum" | "count" | "avg" | "min" | "max"; - resultField: string; - label: string; -} - -interface TableLayoutConfig { - headerRows: CardRowConfig[]; - tableColumns: TableColumnConfig[]; -} -``` - -## 파일 ꡬ쑰 - -``` -repeat-screen-modal/ -β”œβ”€β”€ index.ts # μ»΄ν¬λ„ŒνŠΈ μ •μ˜ 및 export -β”œβ”€β”€ types.ts # νƒ€μž… μ •μ˜ -β”œβ”€β”€ RepeatScreenModalComponent.tsx # 메인 μ»΄ν¬λ„ŒνŠΈ -β”œβ”€β”€ RepeatScreenModalConfigPanel.tsx # μ„€μ • νŒ¨λ„ -β”œβ”€β”€ RepeatScreenModalRenderer.tsx # μžλ™ 등둝 -└── README.md # λ¬Έμ„œ -``` - -## 버전 νžˆμŠ€ν† λ¦¬ - -- **v2.0.0**: κ·Έλ£Ήν•‘, 집계, ν…Œμ΄λΈ” λͺ¨λ“œ μΆ”κ°€ -- **v1.0.0**: 초기 버전 (Simple λͺ¨λ“œ) +1. **μ§‘κ³„λŠ” κ·Έλ£Ήν•‘ ν•„μˆ˜**: 집계 행은 그룹핑이 ν™œμ„±ν™”λ˜μ–΄ μžˆμ–΄μ•Ό μ˜λ―Έκ°€ μžˆμŠ΅λ‹ˆλ‹€. +2. **ν…Œμ΄λΈ”μ€ κ·Έλ£Ήν•‘ ν•„μˆ˜**: ν…Œμ΄λΈ” 행도 그룹핑이 ν™œμ„±ν™”λ˜μ–΄ μžˆμ–΄μ•Ό κ·Έλ£Ή λ‚΄ 행듀을 ν‘œμ‹œν•  수 μžˆμŠ΅λ‹ˆλ‹€. +3. **λ‹¨μˆœ λͺ¨λ“œ**: κ·Έλ£Ήν•‘ 없이 μ‚¬μš©ν•˜λ©΄ 1ν–‰ = 1μΉ΄λ“œλ‘œ λ™μž‘ν•©λ‹ˆλ‹€. 이 경우 헀더/ν•„λ“œ νƒ€μž…λ§Œ μ‚¬μš© κ°€λŠ₯ν•©λ‹ˆλ‹€. diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx index 5f2e1690..997b381c 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx @@ -18,6 +18,8 @@ import { CardRowData, AggregationConfig, TableColumnConfig, + CardContentRowConfig, + AggregationDisplayConfig, } from "./types"; import { ComponentRendererProps } from "@/types/component"; import { cn } from "@/lib/utils"; @@ -25,6 +27,7 @@ import { apiClient } from "@/lib/api/client"; export interface RepeatScreenModalComponentProps extends ComponentRendererProps { config?: RepeatScreenModalProps; + groupedData?: Record[]; // EditModalμ—μ„œ μ „λ‹¬ν•˜λŠ” κ·Έλ£Ή 데이터 } export function RepeatScreenModalComponent({ @@ -34,22 +37,31 @@ export function RepeatScreenModalComponent({ onFormDataChange, config, className, + groupedData: propsGroupedData, // EditModalμ—μ„œ μ „λ‹¬λ°›λŠ” κ·Έλ£Ή 데이터 ...props }: RepeatScreenModalComponentProps) { + // propsμ—μ„œλ„ groupedDataλ₯Ό μΆ”μΆœ (DynamicWebTypeRendererμ—μ„œ 전달될 수 있음) + const groupedData = propsGroupedData || (props as any).groupedData; const componentConfig = { ...config, ...component?.config, }; // μ„€μ • κ°’ μΆ”μΆœ - const cardLayout = componentConfig?.cardLayout || []; const dataSource = componentConfig?.dataSource; const saveMode = componentConfig?.saveMode || "all"; const cardSpacing = componentConfig?.cardSpacing || "24px"; const showCardBorder = componentConfig?.showCardBorder ?? true; + const showCardTitle = componentConfig?.showCardTitle ?? true; const cardTitle = componentConfig?.cardTitle || "μΉ΄λ“œ {index}"; - const cardMode = componentConfig?.cardMode || "simple"; const grouping = componentConfig?.grouping; + + // πŸ†• v3: 자유 λ ˆμ΄μ•„μ›ƒ + const contentRows = componentConfig?.contentRows || []; + + // (λ ˆκ±°μ‹œ ν˜Έν™˜) + const cardLayout = componentConfig?.cardLayout || []; + const cardMode = componentConfig?.cardMode || "simple"; const tableLayout = componentConfig?.tableLayout; // μƒνƒœ @@ -63,59 +75,127 @@ export function RepeatScreenModalComponent({ // 초기 데이터 λ‘œλ“œ useEffect(() => { const loadInitialData = async () => { - if (!dataSource || !dataSource.sourceTable) { - return; - } + console.log("[RepeatScreenModal] 데이터 λ‘œλ“œ μ‹œμž‘"); + console.log("[RepeatScreenModal] groupedData (from EditModal):", groupedData); + console.log("[RepeatScreenModal] formData:", formData); + console.log("[RepeatScreenModal] dataSource:", dataSource); setIsLoading(true); setLoadError(null); try { - // ν•„ν„° 쑰건 생성 - const filters: Record = {}; + let loadedData: any[] = []; - if (dataSource.filterField && formData) { - const filterValue = formData[dataSource.filterField]; - if (filterValue) { - // 배열이면 IN 쑰건, μ•„λ‹ˆλ©΄ 단일 쑰건 - if (Array.isArray(filterValue)) { - filters.id = filterValue; - } else { - filters.id = filterValue; + // πŸ†• μš°μ„ μˆœμœ„ 1: EditModalμ—μ„œ 전달받은 groupedData μ‚¬μš© + if (groupedData && groupedData.length > 0) { + console.log("[RepeatScreenModal] groupedData μ‚¬μš©:", groupedData.length, "건"); + loadedData = groupedData; + } + // μš°μ„ μˆœμœ„ 2: API 호좜 + else if (dataSource && dataSource.sourceTable) { + // ν•„ν„° 쑰건 생성 + const filters: Record = {}; + + // formDataμ—μ„œ μ„ νƒλœ ν–‰ ID κ°€μ Έμ˜€κΈ° + let selectedIds: any[] = []; + + if (formData) { + // 1. λͺ…μ‹œμ μœΌλ‘œ μ„€μ •λœ filterField 확인 + if (dataSource.filterField) { + const filterValue = formData[dataSource.filterField]; + if (filterValue) { + selectedIds = Array.isArray(filterValue) ? filterValue : [filterValue]; + } + } + + // 2. 일반적인 선택 ν•„λ“œ 확인 (fallback) + if (selectedIds.length === 0) { + const commonFields = ['selectedRows', 'selectedIds', 'checkedRows', 'checkedIds', 'ids']; + for (const field of commonFields) { + if (formData[field]) { + const value = formData[field]; + selectedIds = Array.isArray(value) ? value : [value]; + console.log(`[RepeatScreenModal] ${field}μ—μ„œ μ„ νƒλœ ID 발견:`, selectedIds); + break; + } + } + } + + // 3. formData에 idκ°€ 있으면 단일 ν–‰ + if (selectedIds.length === 0 && formData.id) { + selectedIds = [formData.id]; + console.log("[RepeatScreenModal] formData.id μ‚¬μš©:", selectedIds); } } - } - // API 호좜 - const response = await apiClient.post(`/table-management/tables/${dataSource.sourceTable}/data`, { - search: filters, - page: 1, - size: 1000, - }); + console.log("[RepeatScreenModal] μ΅œμ’… μ„ νƒλœ ID:", selectedIds); - if (response.data.success && response.data.data?.data) { - const loadedData = response.data.data.data; - setRawData(loadedData); - - // λͺ¨λ“œμ— 따라 데이터 처리 - if (cardMode === "withTable" && grouping?.enabled && grouping.groupByField) { - // κ·Έλ£Ήν•‘ λͺ¨λ“œ - const grouped = processGroupedData(loadedData, grouping); - setGroupedCardsData(grouped); + // μ„ νƒλœ IDκ°€ 있으면 ν•„ν„° 적용 + if (selectedIds.length > 0) { + filters.id = selectedIds; } else { - // λ‹¨μˆœ λͺ¨λ“œ - const initialCards: CardData[] = await Promise.all( - loadedData.map(async (row: any, index: number) => ({ - _cardId: `card-${index}-${Date.now()}`, - _originalData: { ...row }, - _isDirty: false, - ...(await loadCardData(row)), - })) - ); - setCardsData(initialCards); + console.warn("[RepeatScreenModal] μ„ νƒλœ 데이터가 μ—†μŠ΅λ‹ˆλ‹€."); + setRawData([]); + setCardsData([]); + setGroupedCardsData([]); + setIsLoading(false); + return; + } + + console.log("[RepeatScreenModal] API ν•„ν„°:", filters); + + // API 호좜 + const response = await apiClient.post(`/table-management/tables/${dataSource.sourceTable}/data`, { + search: filters, + page: 1, + size: 1000, + }); + + if (response.data.success && response.data.data?.data) { + loadedData = response.data.data.data; } } else { - setLoadError("데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + console.log("[RepeatScreenModal] 데이터 μ†ŒμŠ€ μ—†μŒ"); + setRawData([]); + setCardsData([]); + setGroupedCardsData([]); + setIsLoading(false); + return; + } + + console.log("[RepeatScreenModal] λ‘œλ“œλœ 데이터:", loadedData.length, "건"); + + if (loadedData.length === 0) { + setRawData([]); + setCardsData([]); + setGroupedCardsData([]); + setIsLoading(false); + return; + } + + setRawData(loadedData); + + // πŸ†• v3: contentRowsκ°€ 있으면 μƒˆλ‘œμš΄ 방식 μ‚¬μš© + const useNewLayout = contentRows && contentRows.length > 0; + + // κ·Έλ£Ήν•‘ λͺ¨λ“œ 확인 (groupByFieldκ°€ 없어도 enabledλ©΄ κ·Έλ£Ήν•‘ λͺ¨λ“œλ‘œ 처리) + const useGrouping = grouping?.enabled; + + if (useGrouping) { + // κ·Έλ£Ήν•‘ λͺ¨λ“œ + const grouped = processGroupedData(loadedData, grouping); + setGroupedCardsData(grouped); + } else { + // λ‹¨μˆœ λͺ¨λ“œ: 각 행이 ν•˜λ‚˜μ˜ μΉ΄λ“œ + const initialCards: CardData[] = await Promise.all( + loadedData.map(async (row: any, index: number) => ({ + _cardId: `card-${index}-${Date.now()}`, + _originalData: { ...row }, + _isDirty: false, + ...(await loadCardData(row)), + })) + ); + setCardsData(initialCards); } } catch (error: any) { console.error("데이터 λ‘œλ“œ μ‹€νŒ¨:", error); @@ -126,25 +206,34 @@ export function RepeatScreenModalComponent({ }; loadInitialData(); - }, [dataSource, formData, cardMode, grouping?.enabled, grouping?.groupByField]); + }, [dataSource, formData, groupedData, contentRows, grouping?.enabled, grouping?.groupByField]); // κ·Έλ£Ήν™”λœ 데이터 처리 const processGroupedData = (data: any[], groupingConfig: typeof grouping): GroupedCardData[] => { - if (!groupingConfig?.enabled || !groupingConfig.groupByField) { + if (!groupingConfig?.enabled) { return []; } const groupByField = groupingConfig.groupByField; const groupMap = new Map(); - // κ·Έλ£Ήλ³„λ‘œ 데이터 λΆ„λ₯˜ - data.forEach((row) => { - const groupKey = String(row[groupByField] || ""); - if (!groupMap.has(groupKey)) { - groupMap.set(groupKey, []); - } - groupMap.get(groupKey)!.push(row); - }); + // groupByFieldκ°€ μ—†μœΌλ©΄ 각 행을 κ°œλ³„ 그룹으둜 처리 + if (!groupByField) { + // 각 행이 ν•˜λ‚˜μ˜ μΉ΄λ“œ (κ·Έλ£Ή) + data.forEach((row, index) => { + const groupKey = `row-${index}`; + groupMap.set(groupKey, [row]); + }); + } else { + // κ·Έλ£Ήλ³„λ‘œ 데이터 λΆ„λ₯˜ + data.forEach((row) => { + const groupKey = String(row[groupByField] || ""); + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []); + } + groupMap.get(groupKey)!.push(row); + }); + } // GroupedCardData 생성 const result: GroupedCardData[] = []; @@ -170,7 +259,7 @@ export function RepeatScreenModalComponent({ result.push({ _cardId: `grouped-card-${cardIndex}-${Date.now()}`, _groupKey: groupKey, - _groupField: groupByField, + _groupField: groupByField || "", _aggregations: aggregations, _rows: cardRows, _representativeData: rows[0] || {}, @@ -206,18 +295,49 @@ export function RepeatScreenModalComponent({ const loadCardData = async (originalData: any): Promise> => { const cardData: Record = {}; - for (const row of cardLayout) { - for (const col of row.columns) { - if (col.sourceConfig) { - if (col.sourceConfig.type === "direct") { - cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field]; - } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) { - cardData[col.field] = null; // 쑰인은 λ‚˜μ€‘μ— 일괄 처리 - } else if (col.sourceConfig.type === "manual") { - cardData[col.field] = null; + // πŸ†• v3: contentRows μ‚¬μš© + if (contentRows && contentRows.length > 0) { + for (const contentRow of contentRows) { + // 헀더/ν•„λ“œ νƒ€μž…μ˜ 컬럼 처리 + if ((contentRow.type === "header" || contentRow.type === "fields") && contentRow.columns) { + for (const col of contentRow.columns) { + if (col.sourceConfig) { + if (col.sourceConfig.type === "direct") { + cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field]; + } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) { + cardData[col.field] = null; // 쑰인은 λ‚˜μ€‘μ— 일괄 처리 + } else if (col.sourceConfig.type === "manual") { + cardData[col.field] = null; + } + } else { + // sourceConfigκ°€ μ—†μœΌλ©΄ 원본 λ°μ΄ν„°μ—μ„œ 직접 κ°€μ Έμ˜΄ + cardData[col.field] = originalData[col.field]; + } + } + } + + // ν…Œμ΄λΈ” νƒ€μž…μ˜ 컬럼 처리 + if (contentRow.type === "table" && contentRow.tableColumns) { + for (const col of contentRow.tableColumns) { + cardData[col.field] = originalData[col.field]; + } + } + } + } else { + // λ ˆκ±°μ‹œ: cardLayout μ‚¬μš© + for (const row of cardLayout) { + for (const col of row.columns) { + if (col.sourceConfig) { + if (col.sourceConfig.type === "direct") { + cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field]; + } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) { + cardData[col.field] = null; // 쑰인은 λ‚˜μ€‘μ— 일괄 처리 + } else if (col.sourceConfig.type === "manual") { + cardData[col.field] = null; + } + } else { + cardData[col.field] = originalData[col.field]; } - } else { - cardData[col.field] = originalData[col.field]; } } } @@ -424,6 +544,14 @@ export function RepeatScreenModalComponent({ // λ””μžμΈ λͺ¨λ“œ λ Œλ”λ§ if (isDesignMode) { + // ν–‰ νƒ€μž…λ³„ 개수 계산 + const rowTypeCounts = { + header: contentRows.filter((r) => r.type === "header").length, + aggregation: contentRows.filter((r) => r.type === "aggregation").length, + table: contentRows.filter((r) => r.type === "table").length, + fields: contentRows.filter((r) => r.type === "fields").length, + }; + return (
{/* μ•„μ΄μ½˜ */}
- {cardMode === "withTable" ? : } +
{/* 제λͺ© */}
Repeat Screen Modal
반볡 ν™”λ©΄ λͺ¨λ‹¬
- - {cardMode === "withTable" ? "ν…Œμ΄λΈ” λͺ¨λ“œ" : "λ‹¨μˆœ λͺ¨λ“œ"} - + v3 자유 λ ˆμ΄μ•„μ›ƒ +
+ + {/* ν–‰ ꡬ성 정보 */} +
+ {contentRows.length > 0 ? ( + <> + {rowTypeCounts.header > 0 && ( + + 헀더 {rowTypeCounts.header}개 + + )} + {rowTypeCounts.aggregation > 0 && ( + + 집계 {rowTypeCounts.aggregation}개 + + )} + {rowTypeCounts.table > 0 && ( + + ν…Œμ΄λΈ” {rowTypeCounts.table}개 + + )} + {rowTypeCounts.fields > 0 && ( + + ν•„λ“œ {rowTypeCounts.fields}개 + + )} + + ) : ( + ν–‰ μ—†μŒ + )}
{/* 톡계 정보 */}
- {cardMode === "simple" ? ( - <> -
-
{cardLayout.length}
-
ν–‰ (Rows)
-
-
-
-
- {cardLayout.reduce((sum, row) => sum + row.columns.length, 0)} -
-
컬럼 (Columns)
-
- - ) : ( - <> -
-
{tableLayout?.headerRows?.length || 0}
-
헀더 ν–‰
-
-
-
-
{tableLayout?.tableColumns?.length || 0}
-
ν…Œμ΄λΈ” 컬럼
-
-
-
-
{grouping?.aggregations?.length || 0}
-
집계
-
- - )} +
+
{contentRows.length}
+
ν–‰ (Rows)
+
-
{dataSource?.sourceTable ? "βœ“" : "β—‹"}
+
{grouping?.aggregations?.length || 0}
+
집계 μ„€μ •
+
+
+
+
{dataSource?.sourceTable ? 1 : 0}
데이터 μ†ŒμŠ€
+ {/* 데이터 μ†ŒμŠ€ 정보 */} + {dataSource?.sourceTable && ( +
+ μ†ŒμŠ€ ν…Œμ΄λΈ”: {dataSource.sourceTable} + {dataSource.filterField && (ν•„ν„°: {dataSource.filterField})} +
+ )} + {/* κ·Έλ£Ήν•‘ 정보 */} {grouping?.enabled && (
@@ -494,9 +635,16 @@ export function RepeatScreenModalComponent({
)} + {/* μΉ΄λ“œ 제λͺ© 정보 */} + {showCardTitle && cardTitle && ( +
+ μΉ΄λ“œ 제λͺ©: {cardTitle} +
+ )} + {/* μ„€μ • μ•ˆλ‚΄ */}
- 였λ₯Έμͺ½ νŒ¨λ„μ—μ„œ μΉ΄λ“œ λ ˆμ΄μ•„μ›ƒκ³Ό 데이터 μ†ŒμŠ€λ₯Ό μ„€μ •ν•˜μ„Έμš” + 였λ₯Έμͺ½ νŒ¨λ„μ—μ„œ 행을 μΆ”κ°€ν•˜κ³  νƒ€μž…(헀더/집계/ν…Œμ΄λΈ”/ν•„λ“œ)을 μ„ νƒν•˜μ„Έμš”
@@ -526,8 +674,12 @@ export function RepeatScreenModalComponent({ ); } - // WithTable λͺ¨λ“œ λ Œλ”λ§ - if (cardMode === "withTable" && grouping?.enabled) { + // πŸ†• v3: 자유 λ ˆμ΄μ•„μ›ƒ λ Œλ”λ§ (contentRows μ‚¬μš©) + const useNewLayout = contentRows && contentRows.length > 0; + const useGrouping = grouping?.enabled; + + // κ·Έλ£Ήν•‘ λͺ¨λ“œ λ Œλ”λ§ + if (useGrouping) { return (
@@ -540,77 +692,90 @@ export function RepeatScreenModalComponent({ card._rows.some((r) => r._isDirty) && "border-primary shadow-lg" )} > - - - {getCardTitle(card._representativeData, cardIndex)} - {card._rows.some((r) => r._isDirty) && ( - - μˆ˜μ •λ¨ - - )} - - + {/* μΉ΄λ“œ 제λͺ© (선택사항) */} + {showCardTitle && ( + + + {getCardTitle(card._representativeData, cardIndex)} + {card._rows.some((r) => r._isDirty) && ( + + μˆ˜μ •λ¨ + + )} + + + )} - {/* 헀더 μ˜μ—­ (κ·Έλ£Ή λŒ€ν‘œκ°’, 집계값) */} - {tableLayout?.headerRows && tableLayout.headerRows.length > 0 && ( -
- {tableLayout.headerRows.map((row, rowIndex) => ( -
- {row.columns.map((col, colIndex) => ( -
- {renderHeaderColumn(col, card, grouping?.aggregations || [])} + {/* πŸ†• v3: contentRows 기반 λ Œλ”λ§ */} + {useNewLayout ? ( + contentRows.map((contentRow, rowIndex) => ( +
+ {renderContentRow(contentRow, card, grouping?.aggregations || [], handleRowDataChange)} +
+ )) + ) : ( + // λ ˆκ±°μ‹œ: tableLayout μ‚¬μš© + <> + {tableLayout?.headerRows && tableLayout.headerRows.length > 0 && ( +
+ {tableLayout.headerRows.map((row, rowIndex) => ( +
+ {row.columns.map((col, colIndex) => ( +
+ {renderHeaderColumn(col, card, grouping?.aggregations || [])} +
+ ))}
))}
- ))} -
- )} + )} - {/* ν…Œμ΄λΈ” μ˜μ—­ */} - {tableLayout?.tableColumns && tableLayout.tableColumns.length > 0 && ( -
- - - - {tableLayout.tableColumns.map((col) => ( - - {col.label} - - ))} - - - - {card._rows.map((row) => ( - - {tableLayout.tableColumns.map((col) => ( - - {renderTableCell(col, row, (value) => - handleRowDataChange(card._cardId, row._rowId, col.field, value) - )} - + {tableLayout?.tableColumns && tableLayout.tableColumns.length > 0 && ( +
+
+ + + {tableLayout.tableColumns.map((col) => ( + + {col.label} + + ))} + + + + {card._rows.map((row) => ( + + {tableLayout.tableColumns.map((col) => ( + + {renderTableCell(col, row, (value) => + handleRowDataChange(card._cardId, row._rowId, col.field, value) + )} + + ))} + ))} - - ))} - -
-
+ + +
+ )} + )} @@ -635,7 +800,7 @@ export function RepeatScreenModalComponent({ ); } - // Simple λͺ¨λ“œ λ Œλ”λ§ + // λ‹¨μˆœ λͺ¨λ“œ λ Œλ”λ§ (κ·Έλ£Ήν•‘ μ—†μŒ) return (
@@ -644,29 +809,44 @@ export function RepeatScreenModalComponent({ key={card._cardId} className={cn("transition-shadow", showCardBorder && "border-2", card._isDirty && "border-primary shadow-lg")} > - - - {getCardTitle(card, cardIndex)} - {card._isDirty && (μˆ˜μ •λ¨)} - - + {/* μΉ΄λ“œ 제λͺ© (선택사항) */} + {showCardTitle && ( + + + {getCardTitle(card, cardIndex)} + {card._isDirty && (μˆ˜μ •λ¨)} + + + )} - {cardLayout.map((row, rowIndex) => ( -
- {row.columns.map((col, colIndex) => ( -
- {renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))} -
- ))} -
- ))} + {/* πŸ†• v3: contentRows 기반 λ Œλ”λ§ */} + {useNewLayout ? ( + contentRows.map((contentRow, rowIndex) => ( +
+ {renderSimpleContentRow(contentRow, card, (value, field) => + handleCardDataChange(card._cardId, field, value) + )} +
+ )) + ) : ( + // λ ˆκ±°μ‹œ: cardLayout μ‚¬μš© + cardLayout.map((row, rowIndex) => ( +
+ {row.columns.map((col, colIndex) => ( +
+ {renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))} +
+ ))} +
+ )) + )}
))} @@ -690,6 +870,350 @@ export function RepeatScreenModalComponent({ ); } +// πŸ†• v3: contentRow λ Œλ”λ§ (κ·Έλ£Ήν•‘ λͺ¨λ“œ) +function renderContentRow( + contentRow: CardContentRowConfig, + card: GroupedCardData, + aggregations: AggregationConfig[], + onRowDataChange: (cardId: string, rowId: string, field: string, value: any) => void +) { + switch (contentRow.type) { + case "header": + case "fields": + // contentRowμ—μ„œ 직접 columns κ°€μ Έμ˜€κΈ° (v3 ꡬ쑰) + const headerColumns = contentRow.columns || []; + + if (headerColumns.length === 0) { + return ( +
+ 헀더 컬럼이 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. +
+ ); + } + + return ( +
+ {headerColumns.map((col, colIndex) => ( +
+ {renderHeaderColumn(col, card, aggregations)} +
+ ))} +
+ ); + + case "aggregation": + // contentRowμ—μ„œ 직접 aggregationFields κ°€μ Έμ˜€κΈ° (v3 ꡬ쑰) + const aggFields = contentRow.aggregationFields || []; + + if (aggFields.length === 0) { + return ( +
+ 집계 ν•„λ“œκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. (λ ˆμ΄μ•„μ›ƒ νƒ­μ—μ„œ 집계 ν•„λ“œλ₯Ό μΆ”κ°€ν•˜μ„Έμš”) +
+ ); + } + + return ( +
+ {aggFields.map((aggField, fieldIndex) => { + // 집계 κ²°κ³Όμ—μ„œ κ°’ κ°€μ Έμ˜€κΈ° (aggregationResultField μ‚¬μš©) + const value = card._aggregations?.[aggField.aggregationResultField] || 0; + return ( +
+
{aggField.label || aggField.aggregationResultField}
+
+ {typeof value === "number" ? value.toLocaleString() : value || "-"} +
+
+ ); + })} +
+ ); + + case "table": + // contentRowμ—μ„œ 직접 tableColumns κ°€μ Έμ˜€κΈ° (v3 ꡬ쑰) + const tableColumns = contentRow.tableColumns || []; + + if (tableColumns.length === 0) { + return ( +
+ ν…Œμ΄λΈ” 컬럼이 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. (λ ˆμ΄μ•„μ›ƒ νƒ­μ—μ„œ ν…Œμ΄λΈ” μ»¬λŸΌμ„ μΆ”κ°€ν•˜μ„Έμš”) +
+ ); + } + + return ( +
+ {contentRow.tableTitle && ( +
+ {contentRow.tableTitle} +
+ )} + + {contentRow.showTableHeader !== false && ( + + + {tableColumns.map((col) => ( + + {col.label} + + ))} + + + )} + + {card._rows.map((row) => ( + + {tableColumns.map((col) => ( + + {renderTableCell(col, row, (value) => + onRowDataChange(card._cardId, row._rowId, col.field, value) + )} + + ))} + + ))} + +
+
+ ); + + default: + return null; + } +} + +// πŸ†• v3: contentRow λ Œλ”λ§ (λ‹¨μˆœ λͺ¨λ“œ) +function renderSimpleContentRow( + contentRow: CardContentRowConfig, + card: CardData, + onChange: (value: any, field: string) => void +) { + switch (contentRow.type) { + case "header": + case "fields": + return ( +
+ {(contentRow.columns || []).map((col, colIndex) => ( +
+ {renderColumn(col, card, (value) => onChange(value, col.field))} +
+ ))} +
+ ); + + case "aggregation": + // λ‹¨μˆœ λͺ¨λ“œμ—μ„œλ„ 집계 ν‘œμ‹œ (단일 μΉ΄λ“œ κΈ°μ€€) + // contentRowμ—μ„œ 직접 aggregationFields κ°€μ Έμ˜€κΈ° (v3 ꡬ쑰) + const aggFields = contentRow.aggregationFields || []; + + if (aggFields.length === 0) { + return ( +
+ 집계 ν•„λ“œκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. +
+ ); + } + + return ( +
+ {aggFields.map((aggField, fieldIndex) => { + // λ‹¨μˆœ λͺ¨λ“œμ—μ„œλŠ” μΉ΄λ“œ λ°μ΄ν„°μ—μ„œ 직접 값을 κ°€μ Έμ˜΄ (aggregationResultField μ‚¬μš©) + const value = card[aggField.aggregationResultField] || card._originalData?.[aggField.aggregationResultField]; + return ( +
+
{aggField.label || aggField.aggregationResultField}
+
+ {typeof value === "number" ? value.toLocaleString() : value || "-"} +
+
+ ); + })} +
+ ); + + case "table": + // λ‹¨μˆœ λͺ¨λ“œμ—μ„œλ„ ν…Œμ΄λΈ” ν‘œμ‹œ (단일 ν–‰) + // contentRowμ—μ„œ 직접 tableColumns κ°€μ Έμ˜€κΈ° (v3 ꡬ쑰) + const tableColumns = contentRow.tableColumns || []; + + if (tableColumns.length === 0) { + return ( +
+ ν…Œμ΄λΈ” 컬럼이 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. +
+ ); + } + + return ( +
+ {contentRow.tableTitle && ( +
+ {contentRow.tableTitle} +
+ )} + + {contentRow.showTableHeader !== false && ( + + + {tableColumns.map((col) => ( + + {col.label} + + ))} + + + )} + + {/* λ‹¨μˆœ λͺ¨λ“œ: μΉ΄λ“œ μžμ²΄κ°€ ν•˜λ‚˜μ˜ ν–‰ */} + + {tableColumns.map((col) => ( + + {renderSimpleTableCell(col, card, (value) => onChange(value, col.field))} + + ))} + + +
+
+ ); + + default: + return null; + } +} + +// λ‹¨μˆœ λͺ¨λ“œ ν…Œμ΄λΈ” μ…€ λ Œλ”λ§ +function renderSimpleTableCell( + col: TableColumnConfig, + card: CardData, + onChange: (value: any) => void +) { + const value = card[col.field] || card._originalData?.[col.field]; + + if (!col.editable) { + // 읽기 μ „μš© + if (col.type === "number") { + return typeof value === "number" ? value.toLocaleString() : value || "-"; + } + return value || "-"; + } + + // νŽΈμ§‘ κ°€λŠ₯ + switch (col.type) { + case "number": + return ( + onChange(parseFloat(e.target.value) || 0)} + className="h-8 text-sm" + /> + ); + case "date": + return ( + onChange(e.target.value)} + className="h-8 text-sm" + /> + ); + case "select": + return ( + + ); + default: + return ( + onChange(e.target.value)} + className="h-8 text-sm" + /> + ); + } +} + // 배경색 클래슀 λ³€ν™˜ function getBackgroundClass(color: string): string { const colorMap: Record = { diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx index d55e557c..ab8c962d 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -21,6 +21,8 @@ import { AggregationConfig, TableLayoutConfig, TableColumnConfig, + CardContentRowConfig, + AggregationDisplayConfig, } from "./types"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -119,6 +121,305 @@ function SourceColumnSelector({ ); } +// μΉ΄λ“œ 제λͺ© νŽΈμ§‘κΈ° - 직접 μž…λ ₯ + ν•„λ“œ μ‚½μž… 방식 +function CardTitleEditor({ + sourceTable, + currentValue, + onChange, +}: { + sourceTable: string; + currentValue: string; + onChange: (value: string) => void; +}) { + const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [open, setOpen] = useState(false); + const [localValue, setLocalValue] = useState(currentValue || ""); + const inputRef = React.useRef(null); + + useEffect(() => { + setLocalValue(currentValue || ""); + }, [currentValue]); + + useEffect(() => { + const loadColumns = async () => { + if (!sourceTable) { + setColumns([]); + return; + } + + setIsLoading(true); + try { + const response = await tableManagementApi.getColumnList(sourceTable); + if (response.success && response.data) { + setColumns(response.data.columns || []); + } + } catch (error) { + console.error("컬럼 λ‘œλ“œ μ‹€νŒ¨:", error); + setColumns([]); + } finally { + setIsLoading(false); + } + }; + loadColumns(); + }, [sourceTable]); + + // ν•„λ“œ μ‚½μž… (ν˜„μž¬ μ»€μ„œ μœ„μΉ˜ λ˜λŠ” 끝에) + const insertField = (fieldName: string) => { + const newValue = localValue ? `${localValue} - {${fieldName}}` : `{${fieldName}}`; + setLocalValue(newValue); + onChange(newValue); + setOpen(false); + }; + + // μΆ”μ²œ ν…œν”Œλ¦Ώ + const templateOptions = useMemo(() => { + const options = [ + { value: "μΉ΄λ“œ {index}", label: "μΉ΄λ“œ {index} - 순번만" }, + ]; + + if (columns.length > 0) { + // part_code - part_name νŒ¨ν„΄ μ°ΎκΈ° + const codeCol = columns.find((c) => + c.columnName.toLowerCase().includes("code") || c.columnName.toLowerCase().includes("no") + ); + const nameCol = columns.find((c) => + c.columnName.toLowerCase().includes("name") && !c.columnName.toLowerCase().includes("code") + ); + + if (codeCol && nameCol) { + options.push({ + value: `{${codeCol.columnName}} - {${nameCol.columnName}}`, + label: `{${codeCol.columnName}} - {${nameCol.columnName}} (μΆ”μ²œ)`, + }); + } + + // 첫 번째 컬럼 단일 + const firstCol = columns[0]; + options.push({ + value: `{${firstCol.columnName}}`, + label: `{${firstCol.columnName}}${firstCol.displayName ? ` - ${firstCol.displayName}` : ""}`, + }); + } + + return options; + }, [columns]); + + // μž…λ ₯κ°’ λ³€κ²½ ν•Έλ“€λŸ¬ + const handleInputChange = (e: React.ChangeEvent) => { + setLocalValue(e.target.value); + }; + + // μž…λ ₯ μ™„λ£Œ (blur λ˜λŠ” Enter) + const handleInputBlur = () => { + if (localValue !== currentValue) { + onChange(localValue); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleInputBlur(); + } + }; + + return ( +
+ {/* 직접 μž…λ ₯ ν•„λ“œ */} +
+ + + + + + + + + + μ—†μŒ + + {/* μΆ”μ²œ ν…œν”Œλ¦Ώ */} + + {templateOptions.map((opt) => ( + { + setLocalValue(opt.value); + onChange(opt.value); + setOpen(false); + }} + className="text-[10px] py-1" + > + {opt.label} + + ))} + + + {/* ν•„λ“œ μ‚½μž… */} + {columns.length > 0 && ( + + insertField("index")} + className="text-[10px] py-1" + > + + index - 순번 + + {columns.map((col) => ( + insertField(col.columnName)} + className="text-[10px] py-1" + > + + + {col.columnName} + {col.displayName && ( + - {col.displayName} + )} + + + ))} + + )} + + + + +
+ + {/* μ•ˆλ‚΄ ν…μŠ€νŠΈ */} +

+ 직접 μž…λ ₯ν•˜κ±°λ‚˜ + λ²„νŠΌμœΌλ‘œ ν•„λ“œ μΆ”κ°€. 예: {"{part_code} - {part_name}"} +

+
+ ); +} + +// 집계 μ„€μ • μ•„μ΄ν…œ (둜컬 μƒνƒœ κ΄€λ¦¬λ‘œ μž…λ ₯ μ‹œ λ¦¬λ Œλ”λ§ λ°©μ§€) +function AggregationConfigItem({ + agg, + index, + sourceTable, + onUpdate, + onRemove, +}: { + agg: AggregationConfig; + index: number; + sourceTable: string; + onUpdate: (updates: Partial) => void; + onRemove: () => void; +}) { + const [localLabel, setLocalLabel] = useState(agg.label || ""); + const [localResultField, setLocalResultField] = useState(agg.resultField || ""); + + // agg λ³€κ²½ μ‹œ 둜컬 μƒνƒœ 동기화 + useEffect(() => { + setLocalLabel(agg.label || ""); + setLocalResultField(agg.resultField || ""); + }, [agg.label, agg.resultField]); + + return ( +
+
+ + 집계 {index + 1} + + +
+ +
+ + onUpdate({ sourceField: value })} + placeholder="합계할 ν•„λ“œ" + /> +
+ +
+
+ + +
+ +
+ + setLocalLabel(e.target.value)} + onBlur={() => onUpdate({ label: localLabel })} + onKeyDown={(e) => { + if (e.key === "Enter") { + onUpdate({ label: localLabel }); + } + }} + placeholder="μ΄μˆ˜μ£Όμž”λŸ‰" + className="h-6 text-[10px]" + /> +
+
+ +
+ + setLocalResultField(e.target.value)} + onBlur={() => onUpdate({ resultField: localResultField })} + onKeyDown={(e) => { + if (e.key === "Enter") { + onUpdate({ resultField: localResultField }); + } + }} + placeholder="total_balance_qty" + className="h-6 text-[10px]" + /> +
+
+ ); +} + // ν…Œμ΄λΈ” 선택기 (Combobox) - 240px μ΅œμ ν™” function TableSelector({ value, onChange }: { value: string; onChange: (value: string) => void }) { const [tables, setTables] = useState<{ tableName: string; displayName?: string }[]>([]); @@ -131,7 +432,11 @@ function TableSelector({ value, onChange }: { value: string; onChange: (value: s try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { - setTables(response.data.tables || []); + // API 응닡이 배열인 κ²½μš°μ™€ 객체인 경우 λͺ¨λ‘ 처리 + const tableData = Array.isArray(response.data) + ? response.data + : (response.data as any).tables || response.data || []; + setTables(tableData); } } catch (error) { console.error("ν…Œμ΄λΈ” λ‘œλ“œ μ‹€νŒ¨:", error); @@ -190,21 +495,36 @@ function TableSelector({ value, onChange }: { value: string; onChange: (value: s ); } +// λͺ¨λ“ˆ λ ˆλ²¨μ—μ„œ νƒ­ μƒνƒœ μœ μ§€ (μ»΄ν¬λ„ŒνŠΈ 리마운트 μ‹œμ—λ„ μœ μ§€) +let persistedActiveTab = "basic"; + export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenModalConfigPanelProps) { - const [localConfig, setLocalConfig] = useState>({ - cardLayout: [], + const [localConfig, setLocalConfig] = useState>(() => ({ dataSource: { sourceTable: "" }, saveMode: "all", cardSpacing: "24px", showCardBorder: true, + showCardTitle: true, cardTitle: "μΉ΄λ“œ {index}", - cardMode: "simple", grouping: { enabled: false, groupByField: "", aggregations: [] }, + contentRows: [], // πŸ†• v3: 자유 λ ˆμ΄μ•„μ›ƒ + // λ ˆκ±°μ‹œ ν˜Έν™˜ + cardMode: "simple", + cardLayout: [], tableLayout: { headerRows: [], tableColumns: [] }, ...config, - }); + })); const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]); + + // νƒ­ μƒνƒœ μœ μ§€ (λͺ¨λ“ˆ 레벨 λ³€μˆ˜μ™€ 동기화) + const [activeTab, setActiveTab] = useState(persistedActiveTab); + + // νƒ­ λ³€κ²½ μ‹œ λͺ¨λ“ˆ 레벨 λ³€μˆ˜λ„ μ—…λ°μ΄νŠΈ + const handleTabChange = (tab: string) => { + persistedActiveTab = tab; + setActiveTab(tab); + }; // ν…Œμ΄λΈ” λͺ©λ‘ λ‘œλ“œ useEffect(() => { @@ -212,7 +532,11 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { - setAllTables(response.data.tables || []); + // API 응닡이 배열인 κ²½μš°μ™€ 객체인 경우 λͺ¨λ‘ 처리 + const tableData = Array.isArray(response.data) + ? response.data + : (response.data as any).tables || response.data || []; + setAllTables(tableData); } } catch (error) { console.error("ν…Œμ΄λΈ” λ‘œλ“œ μ‹€νŒ¨:", error); @@ -238,13 +562,17 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM ); // Immediate update for select/switch fields - const updateConfig = (updates: Partial) => { + // requestAnimationFrame을 μ‚¬μš©ν•˜μ—¬ React λ Œλ”λ§ 사이클 이후에 onChange 호좜 + const updateConfig = useCallback((updates: Partial) => { setLocalConfig((prev) => { const newConfig = { ...prev, ...updates }; - onChange(newConfig); + // λΉ„λ™κΈ°λ‘œ onChange ν˜ΈμΆœν•˜μ—¬ ν˜„μž¬ λ Œλ”λ§ 사이클 μ™„λ£Œ ν›„ μ‹€ν–‰ + requestAnimationFrame(() => { + onChange(newConfig); + }); return newConfig; }); - }; + }, [onChange]); // === κ·Έλ£Ήν•‘ κ΄€λ ¨ ν•¨μˆ˜ === const updateGrouping = (updates: Partial) => { @@ -372,7 +700,125 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM updateTableLayout({ headerRows: newRows }); }; - // === Simple λͺ¨λ“œ ν–‰/컬럼 κ΄€λ ¨ ν•¨μˆ˜ === + // === πŸ†• v3: contentRows κ΄€λ ¨ ν•¨μˆ˜ === + const addContentRow = (type: CardContentRowConfig["type"]) => { + const newRow: CardContentRowConfig = { + id: `crow-${Date.now()}`, + type, + // νƒ€μž…λ³„ κΈ°λ³Έκ°’ + ...(type === "header" || type === "fields" + ? { columns: [], layout: "horizontal", gap: "16px" } + : {}), + ...(type === "aggregation" + ? { aggregationFields: [], aggregationLayout: "horizontal" } + : {}), + ...(type === "table" + ? { tableColumns: [], showTableHeader: true } + : {}), + }; + updateConfig({ + contentRows: [...(localConfig.contentRows || []), newRow], + }); + }; + + const removeContentRow = (rowIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + newRows.splice(rowIndex, 1); + updateConfig({ contentRows: newRows }); + }; + + const updateContentRow = (rowIndex: number, updates: Partial) => { + const newRows = [...(localConfig.contentRows || [])]; + newRows[rowIndex] = { ...newRows[rowIndex], ...updates }; + updateConfig({ contentRows: newRows }); + }; + + // contentRow λ‚΄ 컬럼 관리 (header/fields νƒ€μž…) + const addContentRowColumn = (rowIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + const newColumn: CardColumnConfig = { + id: `col-${Date.now()}`, + field: "", + label: "", + type: "text", + width: "auto", + editable: false, + }; + newRows[rowIndex].columns = [...(newRows[rowIndex].columns || []), newColumn]; + updateConfig({ contentRows: newRows }); + }; + + const removeContentRowColumn = (rowIndex: number, colIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + newRows[rowIndex].columns?.splice(colIndex, 1); + updateConfig({ contentRows: newRows }); + }; + + const updateContentRowColumn = (rowIndex: number, colIndex: number, updates: Partial) => { + const newRows = [...(localConfig.contentRows || [])]; + if (newRows[rowIndex].columns) { + newRows[rowIndex].columns![colIndex] = { ...newRows[rowIndex].columns![colIndex], ...updates }; + } + updateConfig({ contentRows: newRows }); + }; + + // contentRow λ‚΄ 집계 ν•„λ“œ 관리 (aggregation νƒ€μž…) + const addContentRowAggField = (rowIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + const newAggField: AggregationDisplayConfig = { + aggregationResultField: "", + label: "", + }; + newRows[rowIndex].aggregationFields = [...(newRows[rowIndex].aggregationFields || []), newAggField]; + updateConfig({ contentRows: newRows }); + }; + + const removeContentRowAggField = (rowIndex: number, fieldIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + newRows[rowIndex].aggregationFields?.splice(fieldIndex, 1); + updateConfig({ contentRows: newRows }); + }; + + const updateContentRowAggField = (rowIndex: number, fieldIndex: number, updates: Partial) => { + const newRows = [...(localConfig.contentRows || [])]; + if (newRows[rowIndex].aggregationFields) { + newRows[rowIndex].aggregationFields![fieldIndex] = { + ...newRows[rowIndex].aggregationFields![fieldIndex], + ...updates, + }; + } + updateConfig({ contentRows: newRows }); + }; + + // contentRow λ‚΄ ν…Œμ΄λΈ” 컬럼 관리 (table νƒ€μž…) + const addContentRowTableColumn = (rowIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + const newCol: TableColumnConfig = { + id: `tcol-${Date.now()}`, + field: "", + label: "", + type: "text", + editable: false, + }; + newRows[rowIndex].tableColumns = [...(newRows[rowIndex].tableColumns || []), newCol]; + updateConfig({ contentRows: newRows }); + }; + + const removeContentRowTableColumn = (rowIndex: number, colIndex: number) => { + const newRows = [...(localConfig.contentRows || [])]; + newRows[rowIndex].tableColumns?.splice(colIndex, 1); + updateConfig({ contentRows: newRows }); + }; + + const updateContentRowTableColumn = (rowIndex: number, colIndex: number, updates: Partial) => { + const newRows = [...(localConfig.contentRows || [])]; + if (newRows[rowIndex].tableColumns) { + newRows[rowIndex].tableColumns![colIndex] = { ...newRows[rowIndex].tableColumns![colIndex], ...updates }; + } + updateConfig({ contentRows: newRows }); + }; + + // === (λ ˆκ±°μ‹œ) Simple λͺ¨λ“œ ν–‰/컬럼 κ΄€λ ¨ ν•¨μˆ˜ === const addRow = () => { const newRow: CardRowConfig = { id: `row-${Date.now()}`, @@ -430,7 +876,7 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM return (
- + κΈ°λ³Έ @@ -451,20 +897,31 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM

μΉ΄λ“œ μ„€μ •

-
- - { - setLocalConfig((prev) => ({ ...prev, cardTitle: e.target.value })); - updateConfigDebounced({ cardTitle: e.target.value }); - }} - placeholder="{part_code} - {part_name}" - className="h-7 text-[10px]" + {/* μΉ΄λ“œ 제λͺ© ν‘œμ‹œ μ—¬λΆ€ */} +
+ + updateConfig({ showCardTitle: checked })} + className="scale-75" /> -

{"{field}"}: ν•„λ“œκ°’ μ‚¬μš©

+ {/* μΉ΄λ“œ 제λͺ© μ„€μ • (ν‘œμ‹œν•  λ•Œλ§Œ) */} + {localConfig.showCardTitle && ( +
+ + { + setLocalConfig((prev) => ({ ...prev, cardTitle: value })); + updateConfig({ cardTitle: value }); + }} + /> +
+ )} +
- - {/* μΉ΄λ“œ λͺ¨λ“œ 선택 */} -
- -
- -