synapse-sdk 1.0.0b24__py3-none-any.whl → 2025.9.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of synapse-sdk might be problematic. Click here for more details.
- synapse_sdk/clients/agent/ray.py +50 -0
- synapse_sdk/devtools/docs/docs/api/clients/annotation-mixin.md +378 -0
- synapse_sdk/devtools/docs/docs/api/clients/backend.md +368 -1
- synapse_sdk/devtools/docs/docs/api/clients/core-mixin.md +477 -0
- synapse_sdk/devtools/docs/docs/api/clients/data-collection-mixin.md +422 -0
- synapse_sdk/devtools/docs/docs/api/clients/hitl-mixin.md +554 -0
- synapse_sdk/devtools/docs/docs/api/clients/index.md +391 -0
- synapse_sdk/devtools/docs/docs/api/clients/integration-mixin.md +571 -0
- synapse_sdk/devtools/docs/docs/api/clients/ml-mixin.md +578 -0
- synapse_sdk/devtools/docs/docs/api/clients/ray.md +23 -2
- synapse_sdk/devtools/docs/docs/plugins/developing-upload-template.md +1463 -0
- synapse_sdk/devtools/docs/docs/plugins/export-plugins.md +161 -34
- synapse_sdk/devtools/docs/docs/plugins/upload-plugins.md +1497 -213
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/annotation-mixin.md +289 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/backend.md +378 -11
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/core-mixin.md +417 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/data-collection-mixin.md +356 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/hitl-mixin.md +192 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/index.md +391 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/integration-mixin.md +479 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/ml-mixin.md +284 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/api/clients/ray.md +23 -2
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/developing-upload-template.md +1463 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/export-plugins.md +161 -34
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md +1752 -572
- synapse_sdk/devtools/docs/sidebars.ts +7 -0
- synapse_sdk/plugins/README.md +1 -2
- synapse_sdk/plugins/categories/base.py +23 -0
- synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
- synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
- synapse_sdk/plugins/categories/export/actions/export/action.py +160 -0
- synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
- synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
- synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
- synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
- synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
- synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +1 -1
- synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +1 -2
- synapse_sdk/plugins/categories/upload/actions/upload/action.py +154 -531
- synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
- synapse_sdk/plugins/categories/upload/actions/upload/factory.py +143 -0
- synapse_sdk/plugins/categories/upload/actions/upload/models.py +66 -29
- synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +182 -0
- synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +106 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +62 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +80 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +66 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +101 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +89 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +96 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +61 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +86 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +34 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +233 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +238 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/async_upload.py +109 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +43 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +45 -0
- synapse_sdk/plugins/categories/upload/actions/upload/utils.py +194 -83
- synapse_sdk/plugins/categories/upload/templates/config.yaml +4 -0
- synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +269 -0
- synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +71 -27
- synapse_sdk/plugins/models.py +5 -0
- {synapse_sdk-1.0.0b24.dist-info → synapse_sdk-2025.9.3.dist-info}/METADATA +3 -2
- {synapse_sdk-1.0.0b24.dist-info → synapse_sdk-2025.9.3.dist-info}/RECORD +81 -30
- synapse_sdk/plugins/categories/export/actions/export.py +0 -385
- synapse_sdk/plugins/categories/export/enums.py +0 -7
- {synapse_sdk-1.0.0b24.dist-info → synapse_sdk-2025.9.3.dist-info}/WHEEL +0 -0
- {synapse_sdk-1.0.0b24.dist-info → synapse_sdk-2025.9.3.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-1.0.0b24.dist-info → synapse_sdk-2025.9.3.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-1.0.0b24.dist-info → synapse_sdk-2025.9.3.dist-info}/top_level.txt +0 -0
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/upload-plugins.md
CHANGED
|
@@ -1,240 +1,802 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: upload-plugins
|
|
3
|
+
title: 업로드 플러그인
|
|
4
|
+
sidebar_position: 3
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# 업로드 플러그인
|
|
2
8
|
|
|
3
|
-
업로드 플러그인은
|
|
9
|
+
업로드 플러그인은 포괄적인 메타데이터 지원, 보안 검증 및 체계적인 데이터 단위 생성을 통해 파일을 Synapse 플랫폼으로 처리하기 위한 파일 업로드 및 데이터 수집 작업을 제공합니다.
|
|
4
10
|
|
|
5
11
|
## 개요
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
**사용 가능한 액션:**
|
|
14
|
+
|
|
15
|
+
- `upload` - 선택적 Excel 메타데이터 처리를 통한 파일 및 디렉토리 스토리지 업로드
|
|
16
|
+
|
|
17
|
+
**사용 사례:**
|
|
18
|
+
|
|
19
|
+
- 메타데이터 주석을 포함한 대량 파일 업로드
|
|
20
|
+
- Excel 기반 메타데이터 매핑 및 검증
|
|
21
|
+
- 재귀적 디렉토리 처리
|
|
22
|
+
- 타입 기반 파일 구성
|
|
23
|
+
- 배치 데이터 단위 생성
|
|
24
|
+
- 크기 및 내용 검증을 통한 안전한 파일 처리
|
|
25
|
+
|
|
26
|
+
**지원되는 업로드 소스:**
|
|
27
|
+
|
|
28
|
+
- 로컬 파일 시스템 경로 (파일 및 디렉토리)
|
|
29
|
+
- 재귀적 디렉토리 스캔
|
|
30
|
+
- 향상된 파일 주석을 위한 Excel 메타데이터 파일
|
|
31
|
+
- 자동 구성을 통한 혼합 파일 타입
|
|
8
32
|
|
|
9
|
-
|
|
33
|
+
## 업로드 액션 아키텍처
|
|
34
|
+
|
|
35
|
+
업로드 시스템은 검증된 디자인 패턴을 기반으로 구축된 현대적이고 확장 가능한 아키텍처를 사용합니다. 리팩토링된 구현은 이전의 모놀리식 접근 방식을 관심사의 명확한 분리가 있는 모듈식 전략 기반 시스템으로 변환합니다.
|
|
36
|
+
|
|
37
|
+
### 디자인 패턴
|
|
10
38
|
|
|
11
|
-
|
|
12
|
-
- **메타데이터 추출**: Excel 파일에서 자동 메타데이터 수집
|
|
13
|
-
- **보안 검증**: 파일 액세스 제어 및 권한 검사
|
|
14
|
-
- **진행률 추적**: 실시간 업로드 진행률 모니터링
|
|
15
|
-
- **배치 처리**: 여러 파일의 효율적인 배치 업로드
|
|
16
|
-
- **분산 실행**: Ray를 사용한 확장 가능한 처리
|
|
39
|
+
아키텍처는 여러 핵심 디자인 패턴을 활용합니다:
|
|
17
40
|
|
|
18
|
-
|
|
41
|
+
- **전략 패턴**: 검증, 파일 발견, 메타데이터 처리, 업로드 작업 및 데이터 단위 생성을 위한 플러그형 동작
|
|
42
|
+
- **파사드 패턴**: UploadOrchestrator는 복잡한 워크플로우를 조정하기 위한 단순화된 인터페이스를 제공
|
|
43
|
+
- **팩토리 패턴**: StrategyFactory는 런타임 매개변수를 기반으로 적절한 전략 구현을 생성
|
|
44
|
+
- **컨텍스트 패턴**: UploadContext는 워크플로우 구성 요소 간의 공유 상태 및 통신을 유지
|
|
19
45
|
|
|
20
|
-
|
|
46
|
+
### 컴포넌트 아키텍처
|
|
21
47
|
|
|
22
48
|
```mermaid
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
classDiagram
|
|
50
|
+
%% Light/Dark mode compatible colors
|
|
51
|
+
classDef coreClass fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000000
|
|
52
|
+
classDef strategyClass fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#000000
|
|
53
|
+
classDef stepClass fill:#fff9c4,stroke:#f57c00,stroke-width:2px,color:#000000
|
|
54
|
+
classDef contextClass fill:#ffebee,stroke:#d32f2f,stroke-width:2px,color:#000000
|
|
55
|
+
|
|
56
|
+
class UploadAction {
|
|
57
|
+
+name: str = "upload"
|
|
58
|
+
+category: PluginCategory.UPLOAD
|
|
59
|
+
+method: RunMethod.JOB
|
|
60
|
+
+run_class: UploadRun
|
|
61
|
+
+params_model: UploadParams
|
|
62
|
+
+strategy_factory: StrategyFactory
|
|
63
|
+
+step_registry: StepRegistry
|
|
64
|
+
|
|
65
|
+
+start() dict
|
|
66
|
+
+get_workflow_summary() dict
|
|
67
|
+
+_configure_workflow() None
|
|
68
|
+
+_configure_strategies() dict
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class UploadOrchestrator {
|
|
72
|
+
+context: UploadContext
|
|
73
|
+
+step_registry: StepRegistry
|
|
74
|
+
+strategies: dict
|
|
75
|
+
+executed_steps: list
|
|
76
|
+
+current_step_index: int
|
|
77
|
+
+rollback_executed: bool
|
|
78
|
+
|
|
79
|
+
+execute() dict
|
|
80
|
+
+get_workflow_summary() dict
|
|
81
|
+
+get_executed_steps() list
|
|
82
|
+
+is_rollback_executed() bool
|
|
83
|
+
+_execute_step(step) StepResult
|
|
84
|
+
+_handle_step_failure(step, error) None
|
|
85
|
+
+_rollback_executed_steps() None
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class UploadContext {
|
|
89
|
+
+params: dict
|
|
90
|
+
+run: UploadRun
|
|
91
|
+
+client: Any
|
|
92
|
+
+storage: Any
|
|
93
|
+
+pathlib_cwd: Path
|
|
94
|
+
+metadata: dict
|
|
95
|
+
+file_specifications: dict
|
|
96
|
+
+organized_files: list
|
|
97
|
+
+uploaded_files: list
|
|
98
|
+
+data_units: list
|
|
99
|
+
+metrics: dict
|
|
100
|
+
+errors: list
|
|
101
|
+
+strategies: dict
|
|
102
|
+
+rollback_data: dict
|
|
103
|
+
|
|
104
|
+
+update(result: StepResult) None
|
|
105
|
+
+get_result() dict
|
|
106
|
+
+has_errors() bool
|
|
107
|
+
+update_metrics(category, metrics) None
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class StepRegistry {
|
|
111
|
+
+_steps: list
|
|
112
|
+
+register(step: BaseStep) None
|
|
113
|
+
+get_steps() list
|
|
114
|
+
+get_total_progress_weight() float
|
|
115
|
+
+clear() None
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class StrategyFactory {
|
|
119
|
+
+create_validation_strategy(params, context) BaseValidationStrategy
|
|
120
|
+
+create_file_discovery_strategy(params, context) BaseFileDiscoveryStrategy
|
|
121
|
+
+create_metadata_strategy(params, context) BaseMetadataStrategy
|
|
122
|
+
+create_upload_strategy(params, context) BaseUploadStrategy
|
|
123
|
+
+create_data_unit_strategy(params, context) BaseDataUnitStrategy
|
|
124
|
+
+get_available_strategies() dict
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class BaseStep {
|
|
128
|
+
<<abstract>>
|
|
129
|
+
+name: str
|
|
130
|
+
+progress_weight: float
|
|
131
|
+
+execute(context: UploadContext) StepResult
|
|
132
|
+
+can_skip(context: UploadContext) bool
|
|
133
|
+
+rollback(context: UploadContext) None
|
|
134
|
+
+create_success_result(data) StepResult
|
|
135
|
+
+create_error_result(error) StepResult
|
|
136
|
+
+create_skip_result() StepResult
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class StepResult {
|
|
140
|
+
+success: bool
|
|
141
|
+
+data: dict
|
|
142
|
+
+error: str
|
|
143
|
+
+rollback_data: dict
|
|
144
|
+
+skipped: bool
|
|
145
|
+
+original_exception: Exception
|
|
146
|
+
+timestamp: datetime
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
%% Strategy Base Classes
|
|
150
|
+
class BaseValidationStrategy {
|
|
151
|
+
<<abstract>>
|
|
152
|
+
+validate_files(files, context) bool
|
|
153
|
+
+validate_security(file_path) bool
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class BaseFileDiscoveryStrategy {
|
|
157
|
+
<<abstract>>
|
|
158
|
+
+discover_files(path, context) list
|
|
159
|
+
+organize_files(files, specs, context) list
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class BaseMetadataStrategy {
|
|
163
|
+
<<abstract>>
|
|
164
|
+
+process_metadata(context) dict
|
|
165
|
+
+extract_metadata(file_path) dict
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class BaseUploadStrategy {
|
|
169
|
+
<<abstract>>
|
|
170
|
+
+upload_files(files, context) list
|
|
171
|
+
+upload_batch(batch, context) list
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class BaseDataUnitStrategy {
|
|
175
|
+
<<abstract>>
|
|
176
|
+
+generate_data_units(files, context) list
|
|
177
|
+
+create_data_unit_batch(batch, context) list
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
%% Workflow Steps
|
|
181
|
+
class InitializeStep {
|
|
182
|
+
+name = "initialize"
|
|
183
|
+
+progress_weight = 0.05
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class ProcessMetadataStep {
|
|
187
|
+
+name = "process_metadata"
|
|
188
|
+
+progress_weight = 0.05
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class AnalyzeCollectionStep {
|
|
192
|
+
+name = "analyze_collection"
|
|
193
|
+
+progress_weight = 0.05
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class OrganizeFilesStep {
|
|
197
|
+
+name = "organize_files"
|
|
198
|
+
+progress_weight = 0.10
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class ValidateFilesStep {
|
|
202
|
+
+name = "validate_files"
|
|
203
|
+
+progress_weight = 0.05
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class UploadFilesStep {
|
|
207
|
+
+name = "upload_files"
|
|
208
|
+
+progress_weight = 0.30
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class GenerateDataUnitsStep {
|
|
212
|
+
+name = "generate_data_units"
|
|
213
|
+
+progress_weight = 0.35
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
class CleanupStep {
|
|
217
|
+
+name = "cleanup"
|
|
218
|
+
+progress_weight = 0.05
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
%% Relationships
|
|
222
|
+
UploadAction --> UploadOrchestrator : creates and executes
|
|
223
|
+
UploadAction --> StrategyFactory : configures strategies
|
|
224
|
+
UploadAction --> StepRegistry : manages workflow steps
|
|
225
|
+
UploadOrchestrator --> UploadContext : coordinates state
|
|
226
|
+
UploadOrchestrator --> StepRegistry : executes steps from
|
|
227
|
+
UploadOrchestrator --> BaseStep : executes
|
|
228
|
+
BaseStep --> StepResult : returns
|
|
229
|
+
UploadContext --> StepResult : updates with
|
|
230
|
+
StrategyFactory --> BaseValidationStrategy : creates
|
|
231
|
+
StrategyFactory --> BaseFileDiscoveryStrategy : creates
|
|
232
|
+
StrategyFactory --> BaseMetadataStrategy : creates
|
|
233
|
+
StrategyFactory --> BaseUploadStrategy : creates
|
|
234
|
+
StrategyFactory --> BaseDataUnitStrategy : creates
|
|
235
|
+
StepRegistry --> BaseStep : contains
|
|
236
|
+
|
|
237
|
+
%% Step inheritance
|
|
238
|
+
InitializeStep --|> BaseStep : extends
|
|
239
|
+
ProcessMetadataStep --|> BaseStep : extends
|
|
240
|
+
AnalyzeCollectionStep --|> BaseStep : extends
|
|
241
|
+
OrganizeFilesStep --|> BaseStep : extends
|
|
242
|
+
ValidateFilesStep --|> BaseStep : extends
|
|
243
|
+
UploadFilesStep --|> BaseStep : extends
|
|
244
|
+
GenerateDataUnitsStep --|> BaseStep : extends
|
|
245
|
+
CleanupStep --|> BaseStep : extends
|
|
246
|
+
|
|
247
|
+
%% Note: Class styling defined above - Mermaid will apply based on classDef definitions
|
|
56
248
|
```
|
|
57
249
|
|
|
58
|
-
|
|
250
|
+
### 단계 기반 워크플로우 실행
|
|
251
|
+
|
|
252
|
+
리팩토링된 아키텍처는 UploadOrchestrator에 의해 조정되는 단계 기반 워크플로우를 사용합니다. 각 단계는 정의된 책임과 진행률 가중치를 가집니다.
|
|
59
253
|
|
|
60
|
-
|
|
254
|
+
#### 워크플로우 단계 개요
|
|
61
255
|
|
|
62
|
-
|
|
256
|
+
| 단계 | 이름 | 가중치 | 책임 |
|
|
257
|
+
| ---- | ------------------- | ------ | ---------------------------------- |
|
|
258
|
+
| 1 | Initialize | 5% | 스토리지, pathlib, 기본 검증 설정 |
|
|
259
|
+
| 2 | Process Metadata | 5% | 제공된 Excel 메타데이터 처리 |
|
|
260
|
+
| 3 | Analyze Collection | 5% | 데이터 컬렉션 사양 검색 및 검증 |
|
|
261
|
+
| 4 | Organize Files | 10% | 타입별 파일 발견 및 구성 |
|
|
262
|
+
| 5 | Validate Files | 5% | 보안 및 내용 검증 |
|
|
263
|
+
| 6 | Upload Files | 30% | 스토리지에 파일 업로드 |
|
|
264
|
+
| 7 | Generate Data Units | 35% | 업로드된 파일에서 데이터 단위 생성 |
|
|
265
|
+
| 8 | Cleanup | 5% | 임시 리소스 정리 |
|
|
266
|
+
|
|
267
|
+
#### 실행 플로우
|
|
63
268
|
|
|
64
269
|
```mermaid
|
|
65
270
|
flowchart TD
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
D
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
271
|
+
%% Start
|
|
272
|
+
A["🚀 업로드 액션 시작"] --> B["📋 UploadContext 생성"]
|
|
273
|
+
B --> C["⚙️ 전략 구성"]
|
|
274
|
+
C --> D["📝 워크플로우 단계 등록"]
|
|
275
|
+
D --> E["🎯 UploadOrchestrator 생성"]
|
|
276
|
+
|
|
277
|
+
%% Strategy Injection
|
|
278
|
+
E --> F["💉 컨텍스트에 전략 주입"]
|
|
279
|
+
F --> G["📊 진행률 추적 초기화"]
|
|
280
|
+
|
|
281
|
+
%% Step Execution Loop
|
|
282
|
+
G --> H["🔄 단계 실행 루프 시작"]
|
|
283
|
+
H --> I["📍 다음 단계 가져오기"]
|
|
284
|
+
I --> J{"🤔 단계를 건너뛸 수 있는가?"}
|
|
285
|
+
J -->|Yes| K["⏭️ 단계 건너뛰기"]
|
|
286
|
+
J -->|No| L["▶️ 단계 실행"]
|
|
287
|
+
|
|
288
|
+
%% Step Execution
|
|
289
|
+
L --> M{"✅ 단계 성공?"}
|
|
290
|
+
M -->|Yes| N["📈 진행률 업데이트"]
|
|
291
|
+
M -->|No| O["❌ 단계 실패 처리"]
|
|
292
|
+
|
|
293
|
+
%% Success Path
|
|
294
|
+
N --> P["💾 단계 결과 저장"]
|
|
295
|
+
P --> Q["📝 실행된 단계에 추가"]
|
|
296
|
+
Q --> R{"🏁 더 많은 단계?"}
|
|
297
|
+
R -->|Yes| I
|
|
298
|
+
R -->|No| S["🎉 워크플로우 완료"]
|
|
299
|
+
|
|
300
|
+
%% Skip Path
|
|
301
|
+
K --> T["📊 진행률 업데이트 (건너뛰기)"]
|
|
302
|
+
T --> R
|
|
303
|
+
|
|
304
|
+
%% Error Handling
|
|
305
|
+
O --> U["🔙 롤백 프로세스 시작"]
|
|
306
|
+
U --> V["⏪ 실행된 단계 롤백"]
|
|
307
|
+
V --> W["📝 롤백 결과 로그"]
|
|
308
|
+
W --> X["💥 예외 전파"]
|
|
309
|
+
|
|
310
|
+
%% Final Results
|
|
311
|
+
S --> Y["📊 최종 메트릭 수집"]
|
|
312
|
+
Y --> Z["📋 결과 요약 생성"]
|
|
313
|
+
Z --> AA["🔄 UploadAction으로 반환"]
|
|
314
|
+
|
|
315
|
+
%% Apply styles - Light/Dark mode compatible
|
|
316
|
+
classDef startNode fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000000
|
|
317
|
+
classDef processNode fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#000000
|
|
318
|
+
classDef decisionNode fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000000
|
|
319
|
+
classDef successNode fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#000000
|
|
320
|
+
classDef errorNode fill:#ffebee,stroke:#d32f2f,stroke-width:2px,color:#000000
|
|
321
|
+
classDef stepNode fill:#f0f4c3,stroke:#689f38,stroke-width:1px,color:#000000
|
|
322
|
+
|
|
323
|
+
class A,B,E startNode
|
|
324
|
+
class C,D,F,G,H,I,L,N,P,Q,T,Y,Z,AA processNode
|
|
325
|
+
class J,M,R decisionNode
|
|
326
|
+
class K,S successNode
|
|
327
|
+
class O,U,V,W,X errorNode
|
|
328
|
+
```
|
|
107
329
|
|
|
108
|
-
|
|
109
|
-
action = upload.UploadAction(params=params)
|
|
110
|
-
result = action.run()
|
|
330
|
+
#### 단계 실행 세부사항
|
|
111
331
|
|
|
112
|
-
|
|
332
|
+
```mermaid
|
|
333
|
+
flowchart TD
|
|
334
|
+
%% Individual Step Details
|
|
335
|
+
A["🚀 워크플로우 단계 개요"]
|
|
336
|
+
|
|
337
|
+
A --> B1["1. 🏗️ 초기화 단계<br/>가중치: 5%<br/>• 스토리지 연결 설정<br/>• pathlib 액세스 검증<br/>• 컨텍스트 상태 초기화"]
|
|
338
|
+
A --> B2["2. 📋 메타데이터 처리 단계<br/>가중치: 5%<br/>• Excel 메타데이터 파싱<br/>• 보안 제약 검증<br/>• 파일명 매핑 생성"]
|
|
339
|
+
A --> B3["3. 🔍 컬렉션 분석 단계<br/>가중치: 5%<br/>• 파일 사양 검색<br/>• 컬렉션 접근 검증<br/>• 구성 규칙 설정"]
|
|
340
|
+
A --> B4["4. 🗂️ 파일 구성 단계<br/>가중치: 10%<br/>• 파일 발견 (재귀/평면)<br/>• 파일 타입별 그룹화<br/>• 구성된 구조 생성"]
|
|
341
|
+
A --> B5["5. ✅ 파일 검증 단계<br/>가중치: 5%<br/>• 보안 검증<br/>• 크기 및 내용 검사<br/>• 검증 전략 적용"]
|
|
342
|
+
A --> B6["6. ⬆️ 파일 업로드 단계<br/>가중치: 30%<br/>• 배치 파일 업로드<br/>• 진행률 추적<br/>• 실패한 업로드 재시도"]
|
|
343
|
+
A --> B7["7. 🏗️ 데이터 단위 생성 단계<br/>가중치: 35%<br/>• 데이터 단위 생성<br/>• 배치 처리<br/>• 파일을 단위에 연결"]
|
|
344
|
+
A --> B8["8. 🧹 정리 단계<br/>가중치: 5%<br/>• 임시 파일 정리<br/>• 리소스 해제<br/>• 최종 검증"]
|
|
345
|
+
|
|
346
|
+
%% Step Details
|
|
347
|
+
B1 --> C1["책임사항:<br/>• 클라이언트에서 스토리지 가져오기<br/>• pathlib 작업 디렉터리 초기화<br/>• 기본 업로드 전제조건 검증"]
|
|
348
|
+
B2 --> C2["책임사항:<br/>• Excel 메타데이터 파일 로드 및 파싱<br/>• 보안 검증 적용<br/>• 파일명-메타데이터 매핑 생성"]
|
|
349
|
+
B3 --> C3["책임사항:<br/>• 데이터 컬렉션 사양 가져오기<br/>• 파일 타입 요구사항 검증<br/>• 구성 규칙 설정"]
|
|
350
|
+
B4 --> C4["책임사항:<br/>• 파일 발견 전략 사용<br/>• 파일 필터링 및 분류<br/>• 구성된 파일 구조 생성"]
|
|
351
|
+
B5 --> C5["책임사항:<br/>• 검증 전략 사용<br/>• 파일 보안 및 내용 검사<br/>• 비즈니스 검증 규칙 적용"]
|
|
352
|
+
B6 --> C6["책임사항:<br/>• 업로드 전략 사용<br/>• 구성 가능한 배치로 파일 처리<br/>• 진행률 추적 및 실패 처리"]
|
|
353
|
+
B7 --> C7["책임사항:<br/>• 데이터 단위 전략 사용<br/>• 업로드된 파일에서 데이터 단위 생성<br/>• 파일을 적절한 단위에 연결"]
|
|
354
|
+
B8 --> C8["책임사항:<br/>• 임시 파일 및 리소스 정리<br/>• 최종 시스템 검증 수행<br/>• 깨끗한 종료 보장"]
|
|
355
|
+
|
|
356
|
+
%% Apply styles - Light/Dark mode compatible
|
|
357
|
+
classDef overviewNode fill:#e3f2fd,stroke:#1976d2,stroke-width:3px,color:#000000
|
|
358
|
+
classDef stepNode fill:#fff9c4,stroke:#f57c00,stroke-width:2px,color:#000000
|
|
359
|
+
classDef detailNode fill:#f0f4c3,stroke:#689f38,stroke-width:1px,color:#000000
|
|
360
|
+
|
|
361
|
+
class A overviewNode
|
|
362
|
+
class B1,B2,B3,B4,B5,B6,B7,B8 stepNode
|
|
363
|
+
class C1,C2,C3,C4,C5,C6,C7,C8 detailNode
|
|
113
364
|
```
|
|
114
365
|
|
|
115
|
-
|
|
366
|
+
#### 전략 통합 지점
|
|
116
367
|
|
|
117
|
-
|
|
118
|
-
from synapse_sdk.plugins.categories.upload.actions import upload
|
|
368
|
+
전략은 워크플로우의 특정 지점에서 주입됩니다:
|
|
119
369
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"check_macros": True,
|
|
128
|
-
"check_external_links": True,
|
|
129
|
-
"max_file_size": 50 # MB
|
|
130
|
-
}
|
|
131
|
-
)
|
|
370
|
+
- **검증 전략**: ValidateFilesStep에서 사용
|
|
371
|
+
- **파일 발견 전략**: OrganizeFilesStep에서 사용
|
|
372
|
+
- **메타데이터 전략**: ProcessMetadataStep에서 사용
|
|
373
|
+
- **업로드 전략**: UploadFilesStep에서 사용
|
|
374
|
+
- **데이터 단위 전략**: GenerateDataUnitsStep에서 사용
|
|
375
|
+
|
|
376
|
+
#### 오류 처리 및 롤백
|
|
132
377
|
|
|
133
|
-
|
|
134
|
-
|
|
378
|
+
오케스트레이터는 자동 롤백 기능을 제공합니다:
|
|
379
|
+
|
|
380
|
+
1. **예외 캡처**: 디버깅을 위해 원본 예외 보존
|
|
381
|
+
2. **롤백 실행**: 성공적으로 실행된 모든 단계에서 역순으로 rollback() 호출
|
|
382
|
+
3. **우아한 저하**: 개별 단계 롤백이 실패해도 롤백 계속 진행
|
|
383
|
+
4. **상태 보존**: 실패 후 분석을 위한 실행 상태 유지
|
|
384
|
+
|
|
385
|
+
## 개발 가이드
|
|
386
|
+
|
|
387
|
+
이 섹션은 사용자 정의 전략과 워크플로우 단계로 업로드 액션을 확장하기 위한 포괄적인 가이드를 제공합니다.
|
|
388
|
+
|
|
389
|
+
### 사용자 정의 전략 생성
|
|
390
|
+
|
|
391
|
+
전략은 업로드 프로세스의 다양한 측면에 대한 특정 동작을 구현합니다. 각 전략 타입은 잘 정의된 인터페이스를 가지고 있습니다.
|
|
392
|
+
|
|
393
|
+
#### 사용자 정의 검증 전략
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.strategies.validation.base import BaseValidationStrategy
|
|
397
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.context import UploadContext
|
|
398
|
+
|
|
399
|
+
class CustomValidationStrategy(BaseValidationStrategy):
|
|
400
|
+
"""고급 보안 검사를 포함한 사용자 정의 검증 전략."""
|
|
401
|
+
|
|
402
|
+
def validate_files(self, files: List[Path], context: UploadContext) -> bool:
|
|
403
|
+
"""사용자 정의 비즈니스 규칙을 사용하여 파일 검증."""
|
|
404
|
+
for file_path in files:
|
|
405
|
+
# 사용자 정의 검증 로직
|
|
406
|
+
if not self._validate_custom_rules(file_path):
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
# 보안 검증 호출
|
|
410
|
+
if not self.validate_security(file_path):
|
|
411
|
+
return False
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
def validate_security(self, file_path: Path) -> bool:
|
|
415
|
+
"""사용자 정의 보안 검증."""
|
|
416
|
+
# 사용자 정의 보안 검사 구현
|
|
417
|
+
if file_path.suffix in ['.exe', '.bat', '.sh']:
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
# 파일 크기 검사
|
|
421
|
+
if file_path.stat().st_size > 100 * 1024 * 1024: # 100MB
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
def _validate_custom_rules(self, file_path: Path) -> bool:
|
|
427
|
+
"""도메인별 검증 규칙 구현."""
|
|
428
|
+
# 사용자 정의 비즈니스 로직
|
|
429
|
+
return True
|
|
135
430
|
```
|
|
136
431
|
|
|
137
|
-
|
|
432
|
+
#### 사용자 정의 파일 발견 전략
|
|
138
433
|
|
|
139
434
|
```python
|
|
140
|
-
import
|
|
141
|
-
from
|
|
435
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.strategies.file_discovery.base import BaseFileDiscoveryStrategy
|
|
436
|
+
from pathlib import Path
|
|
437
|
+
from typing import List, Dict, Any
|
|
438
|
+
|
|
439
|
+
class CustomFileDiscoveryStrategy(BaseFileDiscoveryStrategy):
|
|
440
|
+
"""고급 필터링을 포함한 사용자 정의 파일 발견."""
|
|
441
|
+
|
|
442
|
+
def discover_files(self, path: Path, context: UploadContext) -> List[Path]:
|
|
443
|
+
"""사용자 정의 필터링 규칙으로 파일 발견."""
|
|
444
|
+
files = []
|
|
445
|
+
|
|
446
|
+
if context.get_param('is_recursive', False):
|
|
447
|
+
files = list(path.rglob('*'))
|
|
448
|
+
else:
|
|
449
|
+
files = list(path.iterdir())
|
|
450
|
+
|
|
451
|
+
# 사용자 정의 필터링 적용
|
|
452
|
+
return self._apply_custom_filters(files, context)
|
|
453
|
+
|
|
454
|
+
def organize_files(self, files: List[Path], specs: Dict[str, Any], context: UploadContext) -> List[Dict[str, Any]]:
|
|
455
|
+
"""사용자 정의 분류를 사용하여 파일 구성."""
|
|
456
|
+
organized = []
|
|
457
|
+
|
|
458
|
+
for file_path in files:
|
|
459
|
+
if file_path.is_file():
|
|
460
|
+
category = self._determine_category(file_path)
|
|
461
|
+
organized.append({
|
|
462
|
+
'file_path': file_path,
|
|
463
|
+
'category': category,
|
|
464
|
+
'metadata': self._extract_file_metadata(file_path)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
return organized
|
|
468
|
+
|
|
469
|
+
def _apply_custom_filters(self, files: List[Path], context: UploadContext) -> List[Path]:
|
|
470
|
+
"""도메인별 파일 필터 적용."""
|
|
471
|
+
filtered = []
|
|
472
|
+
for file_path in files:
|
|
473
|
+
if self._should_include_file(file_path):
|
|
474
|
+
filtered.append(file_path)
|
|
475
|
+
return filtered
|
|
476
|
+
|
|
477
|
+
def _determine_category(self, file_path: Path) -> str:
|
|
478
|
+
"""사용자 정의 로직을 사용하여 파일 카테고리 결정."""
|
|
479
|
+
# 사용자 정의 분류 로직
|
|
480
|
+
ext = file_path.suffix.lower()
|
|
481
|
+
if ext in ['.jpg', '.png', '.gif']:
|
|
482
|
+
return 'images'
|
|
483
|
+
elif ext in ['.pdf', '.doc', '.docx']:
|
|
484
|
+
return 'documents'
|
|
485
|
+
else:
|
|
486
|
+
return 'other'
|
|
487
|
+
```
|
|
142
488
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
489
|
+
#### 사용자 정의 업로드 전략
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.strategies.upload.base import BaseUploadStrategy
|
|
493
|
+
from typing import List, Dict, Any
|
|
494
|
+
|
|
495
|
+
class CustomUploadStrategy(BaseUploadStrategy):
|
|
496
|
+
"""고급 재시도 로직을 포함한 사용자 정의 업로드 전략."""
|
|
497
|
+
|
|
498
|
+
def upload_files(self, files: List[Dict[str, Any]], context: UploadContext) -> List[Dict[str, Any]]:
|
|
499
|
+
"""사용자 정의 배치 및 재시도 로직으로 파일 업로드."""
|
|
500
|
+
uploaded_files = []
|
|
501
|
+
batch_size = context.get_param('upload_batch_size', 10)
|
|
502
|
+
|
|
503
|
+
# 사용자 정의 배치로 처리
|
|
504
|
+
for i in range(0, len(files), batch_size):
|
|
505
|
+
batch = files[i:i + batch_size]
|
|
506
|
+
batch_results = self.upload_batch(batch, context)
|
|
507
|
+
uploaded_files.extend(batch_results)
|
|
508
|
+
|
|
509
|
+
return uploaded_files
|
|
510
|
+
|
|
511
|
+
def upload_batch(self, batch: List[Dict[str, Any]], context: UploadContext) -> List[Dict[str, Any]]:
|
|
512
|
+
"""재시도 로직으로 파일 배치 업로드."""
|
|
513
|
+
results = []
|
|
514
|
+
|
|
515
|
+
for file_info in batch:
|
|
516
|
+
max_retries = 3
|
|
517
|
+
for attempt in range(max_retries):
|
|
518
|
+
try:
|
|
519
|
+
result = self._upload_single_file(file_info, context)
|
|
520
|
+
results.append(result)
|
|
521
|
+
break
|
|
522
|
+
except Exception as e:
|
|
523
|
+
if attempt == max_retries - 1:
|
|
524
|
+
# 최종 시도 실패
|
|
525
|
+
context.add_error(f"Failed to upload {file_info['file_path']}: {e}")
|
|
526
|
+
else:
|
|
527
|
+
# 재시도 전 대기
|
|
528
|
+
time.sleep(2 ** attempt)
|
|
529
|
+
|
|
530
|
+
return results
|
|
531
|
+
|
|
532
|
+
def _upload_single_file(self, file_info: Dict[str, Any], context: UploadContext) -> Dict[str, Any]:
|
|
533
|
+
"""사용자 정의 로직으로 단일 파일 업로드."""
|
|
534
|
+
# 사용자 정의 업로드 구현
|
|
535
|
+
file_path = file_info['file_path']
|
|
536
|
+
|
|
537
|
+
# 컨텍스트에서 스토리지 사용
|
|
538
|
+
storage = context.storage
|
|
539
|
+
|
|
540
|
+
# 여기에 사용자 정의 업로드 로직
|
|
541
|
+
uploaded_file = {
|
|
542
|
+
'file_path': str(file_path),
|
|
543
|
+
'storage_path': f"uploads/{file_path.name}",
|
|
544
|
+
'size': file_path.stat().st_size,
|
|
545
|
+
'checksum': self._calculate_checksum(file_path)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return uploaded_file
|
|
549
|
+
```
|
|
150
550
|
|
|
151
|
-
|
|
551
|
+
### 사용자 정의 워크플로우 단계 생성
|
|
152
552
|
|
|
153
|
-
|
|
154
|
-
result = await action.run_async(
|
|
155
|
-
progress_callback=lambda progress: print(f"진행률: {progress}%")
|
|
156
|
-
)
|
|
553
|
+
사용자 정의 워크플로우 단계는 기본 단계 클래스를 확장하고 필수 인터페이스를 구현합니다.
|
|
157
554
|
|
|
158
|
-
|
|
555
|
+
#### 사용자 정의 처리 단계
|
|
159
556
|
|
|
160
|
-
|
|
161
|
-
|
|
557
|
+
```python
|
|
558
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.steps.base import BaseStep
|
|
559
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.context import UploadContext, StepResult
|
|
560
|
+
from pathlib import Path
|
|
561
|
+
|
|
562
|
+
class CustomProcessingStep(BaseStep):
|
|
563
|
+
"""특수 파일 처리를 위한 사용자 정의 처리 단계."""
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def name(self) -> str:
|
|
567
|
+
return 'custom_processing'
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def progress_weight(self) -> float:
|
|
571
|
+
return 0.15 # 전체 워크플로우의 15%
|
|
572
|
+
|
|
573
|
+
def execute(self, context: UploadContext) -> StepResult:
|
|
574
|
+
"""사용자 정의 처리 로직 실행."""
|
|
575
|
+
try:
|
|
576
|
+
# 사용자 정의 처리 로직
|
|
577
|
+
processed_files = self._process_files(context)
|
|
578
|
+
|
|
579
|
+
# 결과로 컨텍스트 업데이트
|
|
580
|
+
return self.create_success_result({
|
|
581
|
+
'processed_files': processed_files,
|
|
582
|
+
'processing_stats': self._get_processing_stats()
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
except Exception as e:
|
|
586
|
+
return self.create_error_result(f'Custom processing failed: {str(e)}')
|
|
587
|
+
|
|
588
|
+
def can_skip(self, context: UploadContext) -> bool:
|
|
589
|
+
"""단계를 건너뛸 수 있는지 결정."""
|
|
590
|
+
# 처리할 파일이 없으면 건너뛰기
|
|
591
|
+
return len(context.organized_files) == 0
|
|
592
|
+
|
|
593
|
+
def rollback(self, context: UploadContext) -> None:
|
|
594
|
+
"""사용자 정의 처리 작업 롤백."""
|
|
595
|
+
# 처리 중에 생성된 리소스 정리
|
|
596
|
+
self._cleanup_processing_resources(context)
|
|
597
|
+
|
|
598
|
+
def _process_files(self, context: UploadContext) -> List[Dict]:
|
|
599
|
+
"""사용자 정의 파일 처리 구현."""
|
|
600
|
+
processed = []
|
|
601
|
+
|
|
602
|
+
for file_info in context.organized_files:
|
|
603
|
+
# 사용자 정의 처리 로직
|
|
604
|
+
result = self._process_single_file(file_info)
|
|
605
|
+
processed.append(result)
|
|
606
|
+
|
|
607
|
+
return processed
|
|
608
|
+
|
|
609
|
+
def _process_single_file(self, file_info: Dict) -> Dict:
|
|
610
|
+
"""단일 파일 처리."""
|
|
611
|
+
# 사용자 정의 처리 구현
|
|
612
|
+
return {
|
|
613
|
+
'original': file_info,
|
|
614
|
+
'processed': True,
|
|
615
|
+
'timestamp': datetime.now()
|
|
616
|
+
}
|
|
162
617
|
```
|
|
163
618
|
|
|
164
|
-
|
|
619
|
+
### 전략 팩토리 확장
|
|
620
|
+
|
|
621
|
+
사용자 정의 전략을 사용 가능하게 하려면 StrategyFactory를 확장하세요:
|
|
622
|
+
|
|
623
|
+
```python
|
|
624
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.factory import StrategyFactory
|
|
625
|
+
|
|
626
|
+
class CustomStrategyFactory(StrategyFactory):
|
|
627
|
+
"""사용자 정의 전략을 포함한 확장 팩토리."""
|
|
628
|
+
|
|
629
|
+
def create_validation_strategy(self, params: Dict, context=None):
|
|
630
|
+
"""사용자 정의 옵션으로 검증 전략 생성."""
|
|
631
|
+
validation_type = params.get('custom_validation_type', 'default')
|
|
165
632
|
|
|
166
|
-
|
|
633
|
+
if validation_type == 'strict':
|
|
634
|
+
return CustomValidationStrategy()
|
|
635
|
+
else:
|
|
636
|
+
return super().create_validation_strategy(params, context)
|
|
167
637
|
|
|
168
|
-
|
|
638
|
+
def create_file_discovery_strategy(self, params: Dict, context=None):
|
|
639
|
+
"""사용자 정의 옵션으로 파일 발견 전략 생성."""
|
|
640
|
+
discovery_mode = params.get('discovery_mode', 'default')
|
|
641
|
+
|
|
642
|
+
if discovery_mode == 'advanced':
|
|
643
|
+
return CustomFileDiscoveryStrategy()
|
|
644
|
+
else:
|
|
645
|
+
return super().create_file_discovery_strategy(params, context)
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### 사용자 정의 업로드 액션
|
|
649
|
+
|
|
650
|
+
포괄적인 커스터마이제이션을 위해서는 UploadAction 자체를 확장하세요:
|
|
169
651
|
|
|
170
652
|
```python
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
653
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.action import UploadAction
|
|
654
|
+
from synapse_sdk.plugins.categories.decorators import register_action
|
|
655
|
+
|
|
656
|
+
@register_action
|
|
657
|
+
class CustomUploadAction(UploadAction):
|
|
658
|
+
"""확장 워크플로우를 포함한 사용자 정의 업로드 액션."""
|
|
659
|
+
|
|
660
|
+
name = 'custom_upload'
|
|
175
661
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
662
|
+
def __init__(self, *args, **kwargs):
|
|
663
|
+
super().__init__(*args, **kwargs)
|
|
664
|
+
# 사용자 정의 전략 팩토리 사용
|
|
665
|
+
self.strategy_factory = CustomStrategyFactory()
|
|
666
|
+
|
|
667
|
+
def _configure_workflow(self) -> None:
|
|
668
|
+
"""추가 단계로 사용자 정의 워크플로우 구성."""
|
|
669
|
+
# 표준 단계 등록
|
|
670
|
+
super()._configure_workflow()
|
|
671
|
+
|
|
672
|
+
# 사용자 정의 처리 단계 추가
|
|
673
|
+
self.step_registry.register(CustomProcessingStep())
|
|
674
|
+
|
|
675
|
+
def _configure_strategies(self, context=None) -> Dict[str, Any]:
|
|
676
|
+
"""사용자 정의 매개변수로 전략 구성."""
|
|
677
|
+
strategies = super()._configure_strategies(context)
|
|
678
|
+
|
|
679
|
+
# 사용자 정의 전략 추가
|
|
680
|
+
strategies['custom_processing'] = self._create_custom_processing_strategy()
|
|
681
|
+
|
|
682
|
+
return strategies
|
|
683
|
+
|
|
684
|
+
def _create_custom_processing_strategy(self):
|
|
685
|
+
"""사용자 정의 처리 전략 생성."""
|
|
686
|
+
return CustomProcessingStrategy(self.params)
|
|
180
687
|
```
|
|
181
688
|
|
|
182
|
-
|
|
689
|
+
### 사용자 정의 컴포넌트 테스트
|
|
183
690
|
|
|
184
|
-
|
|
185
|
-
- `run_async() -> UploadResult`: 비동기적으로 업로드를 실행합니다
|
|
186
|
-
- `validate_params() -> bool`: 업로드 매개변수를 검증합니다
|
|
187
|
-
- `discover_files() -> List[str]`: 업로드할 파일을 발견합니다
|
|
188
|
-
- `process_excel_metadata() -> Dict`: Excel 파일에서 메타데이터를 추출합니다
|
|
691
|
+
#### 사용자 정의 전략 테스트
|
|
189
692
|
|
|
190
|
-
|
|
693
|
+
```python
|
|
694
|
+
import pytest
|
|
695
|
+
from unittest.mock import Mock
|
|
696
|
+
from pathlib import Path
|
|
697
|
+
|
|
698
|
+
class TestCustomValidationStrategy:
|
|
699
|
+
|
|
700
|
+
def setup_method(self):
|
|
701
|
+
self.strategy = CustomValidationStrategy()
|
|
702
|
+
self.context = Mock()
|
|
703
|
+
|
|
704
|
+
def test_validate_files_success(self):
|
|
705
|
+
"""성공적인 파일 검증 테스트."""
|
|
706
|
+
files = [Path('/test/file1.txt'), Path('/test/file2.jpg')]
|
|
707
|
+
result = self.strategy.validate_files(files, self.context)
|
|
708
|
+
assert result is True
|
|
709
|
+
|
|
710
|
+
def test_validate_files_security_failure(self):
|
|
711
|
+
"""보안상 이유로 검증 실패 테스트."""
|
|
712
|
+
files = [Path('/test/malware.exe')]
|
|
713
|
+
result = self.strategy.validate_files(files, self.context)
|
|
714
|
+
assert result is False
|
|
715
|
+
|
|
716
|
+
def test_validate_large_file_failure(self):
|
|
717
|
+
"""큰 파일에 대한 검증 실패 테스트."""
|
|
718
|
+
# 큰 크기를 반환하도록 파일 stat 모킹
|
|
719
|
+
large_file = Mock(spec=Path)
|
|
720
|
+
large_file.suffix = '.txt'
|
|
721
|
+
large_file.stat.return_value.st_size = 200 * 1024 * 1024 # 200MB
|
|
722
|
+
|
|
723
|
+
result = self.strategy.validate_security(large_file)
|
|
724
|
+
assert result is False
|
|
725
|
+
```
|
|
191
726
|
|
|
192
|
-
|
|
727
|
+
#### 사용자 정의 단계 테스트
|
|
193
728
|
|
|
194
729
|
```python
|
|
195
|
-
class
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
730
|
+
class TestCustomProcessingStep:
|
|
731
|
+
|
|
732
|
+
def setup_method(self):
|
|
733
|
+
self.step = CustomProcessingStep()
|
|
734
|
+
self.context = Mock()
|
|
735
|
+
self.context.organized_files = [
|
|
736
|
+
{'file_path': '/test/file1.txt'},
|
|
737
|
+
{'file_path': '/test/file2.jpg'}
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
def test_execute_success(self):
|
|
741
|
+
"""성공적인 단계 실행 테스트."""
|
|
742
|
+
result = self.step.execute(self.context)
|
|
743
|
+
|
|
744
|
+
assert result.success is True
|
|
745
|
+
assert 'processed_files' in result.data
|
|
746
|
+
assert len(result.data['processed_files']) == 2
|
|
747
|
+
|
|
748
|
+
def test_can_skip_with_no_files(self):
|
|
749
|
+
"""단계 건너뛰기 로직 테스트."""
|
|
750
|
+
self.context.organized_files = []
|
|
751
|
+
assert self.step.can_skip(self.context) is True
|
|
752
|
+
|
|
753
|
+
def test_rollback_cleanup(self):
|
|
754
|
+
"""롤백 정리 테스트."""
|
|
755
|
+
# 이것은 예외를 발생시키지 않아야 함
|
|
756
|
+
self.step.rollback(self.context)
|
|
205
757
|
```
|
|
206
758
|
|
|
207
|
-
|
|
759
|
+
## 업로드 매개변수
|
|
760
|
+
|
|
761
|
+
업로드 액션은 포괄적인 매개변수 검증을 위해 `UploadParams`를 사용합니다:
|
|
762
|
+
|
|
763
|
+
### 필수 매개변수
|
|
764
|
+
|
|
765
|
+
| 매개변수 | 타입 | 설명 | 검증 |
|
|
766
|
+
| ----------------- | ----- | ----------------------- | ---------------------- |
|
|
767
|
+
| `name` | `str` | 읽기 쉬운 업로드 이름 | 빈 값이 아니어야 함 |
|
|
768
|
+
| `path` | `str` | 소스 파일/디렉토리 경로 | 유효한 경로여야 함 |
|
|
769
|
+
| `storage` | `int` | 대상 스토리지 ID | API를 통해 존재해야 함 |
|
|
770
|
+
| `data_collection` | `int` | 데이터 컬렉션 ID | API를 통해 존재해야 함 |
|
|
208
771
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
772
|
+
### 선택적 매개변수
|
|
773
|
+
|
|
774
|
+
| 매개변수 | 타입 | 기본값 | 설명 |
|
|
775
|
+
| ------------------------------- | ------------- | ------- | --------------------------- |
|
|
776
|
+
| `description` | `str \| None` | `None` | 업로드 설명 |
|
|
777
|
+
| `project` | `int \| None` | `None` | 프로젝트 ID (제공시 검증됨) |
|
|
778
|
+
| `excel_metadata_path` | `str \| None` | `None` | Excel 메타데이터 파일 경로 |
|
|
779
|
+
| `is_recursive` | `bool` | `False` | 디렉토리를 재귀적으로 스캔 |
|
|
780
|
+
| `max_file_size_mb` | `int` | `50` | 최대 파일 크기 (MB) |
|
|
781
|
+
| `creating_data_unit_batch_size` | `int` | `100` | 데이터 단위 배치 크기 |
|
|
782
|
+
| `use_async_upload` | `bool` | `True` | 비동기 처리 사용 |
|
|
218
783
|
|
|
219
784
|
### 매개변수 검증
|
|
220
785
|
|
|
221
|
-
|
|
786
|
+
시스템은 실시간 검증을 수행합니다:
|
|
222
787
|
|
|
223
788
|
```python
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
result = action.run()
|
|
236
|
-
except ValidationError as e:
|
|
237
|
-
print(f"매개변수 검증 실패: {e}")
|
|
789
|
+
# 스토리지 검증
|
|
790
|
+
@field_validator('storage', mode='before')
|
|
791
|
+
@classmethod
|
|
792
|
+
def check_storage_exists(cls, value: str, info) -> str:
|
|
793
|
+
action = info.context['action']
|
|
794
|
+
client = action.client
|
|
795
|
+
try:
|
|
796
|
+
client.get_storage(value)
|
|
797
|
+
except ClientError:
|
|
798
|
+
raise PydanticCustomError('client_error', 'Storage not found')
|
|
799
|
+
return value
|
|
238
800
|
```
|
|
239
801
|
|
|
240
802
|
**검증 규칙:**
|
|
@@ -296,25 +858,13 @@ Excel 파일 보안 설정:
|
|
|
296
858
|
|
|
297
859
|
```python
|
|
298
860
|
class ExcelSecurityConfig:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
allowed_extensions: List[str] = [".xlsx", ".xls"]
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
#### ExcelMetadataUtils
|
|
306
|
-
|
|
307
|
-
Excel 메타데이터 추출을 위한 유틸리티:
|
|
308
|
-
|
|
309
|
-
```python
|
|
310
|
-
class ExcelMetadataUtils:
|
|
311
|
-
@staticmethod
|
|
312
|
-
def extract_metadata(file_path: str) -> Dict:
|
|
313
|
-
"""Excel 파일에서 메타데이터를 추출합니다."""
|
|
861
|
+
max_file_size_mb: int = 10 # 파일 크기 제한 (MB)
|
|
862
|
+
max_rows: int = 100000 # 행 수 제한
|
|
863
|
+
max_columns: int = 50 # 열 수 제한
|
|
314
864
|
|
|
315
|
-
@
|
|
316
|
-
def
|
|
317
|
-
"""
|
|
865
|
+
@classmethod
|
|
866
|
+
def from_action_config(cls, action_config) -> 'ExcelSecurityConfig':
|
|
867
|
+
"""config.yaml에서 설정을 로드합니다."""
|
|
318
868
|
```
|
|
319
869
|
|
|
320
870
|
#### PathAwareJSONEncoder
|
|
@@ -343,555 +893,1185 @@ class PathAwareJSONEncoder:
|
|
|
343
893
|
|
|
344
894
|
## Excel 메타데이터 처리
|
|
345
895
|
|
|
896
|
+
업로드 플러그인은 포괄적인 파일명 매칭, 유연한 헤더 지원 및 최적화된 성능을 통한 고급 Excel 메타데이터 처리를 제공합니다:
|
|
897
|
+
|
|
346
898
|
### Excel 파일 형식
|
|
347
899
|
|
|
348
|
-
|
|
900
|
+
Excel 파일은 유연한 헤더 형식과 포괄적인 파일명 매칭을 지원합니다:
|
|
901
|
+
|
|
902
|
+
#### 지원되는 헤더 형식
|
|
903
|
+
|
|
904
|
+
대소문자를 구분하지 않는 매칭으로 두 헤더 형식 모두 지원됩니다:
|
|
905
|
+
|
|
906
|
+
**옵션 1: "filename" 헤더**
|
|
907
|
+
| filename | category | description | custom_field |
|
|
908
|
+
| ---------- | -------- | ------------------ | ------------ |
|
|
909
|
+
| image1.jpg | nature | Mountain landscape | high_res |
|
|
910
|
+
| image2.png | urban | City skyline | processed |
|
|
911
|
+
|
|
912
|
+
**옵션 2: "file_name" 헤더**
|
|
913
|
+
| file_name | category | description | custom_field |
|
|
914
|
+
| ---------- | -------- | ------------------ | ------------ |
|
|
915
|
+
| image1.jpg | nature | Mountain landscape | high_res |
|
|
916
|
+
| image2.png | urban | City skyline | processed |
|
|
349
917
|
|
|
350
|
-
|
|
351
|
-
|
|
918
|
+
#### 파일명 매칭 전략
|
|
919
|
+
|
|
920
|
+
시스템은 파일과 메타데이터를 연결하기 위해 포괄적인 5단계 우선순위 매칭 알고리즘을 사용합니다:
|
|
921
|
+
|
|
922
|
+
1. **정확한 stem 매칭** (최우선): `image1`이 `image1.jpg`와 매칭
|
|
923
|
+
2. **정확한 파일명 매칭**: `image1.jpg`가 `image1.jpg`와 매칭
|
|
924
|
+
3. **메타데이터 키 stem 매칭**: `path/image1.ext` stem이 `image1`과 매칭
|
|
925
|
+
4. **부분 경로 매칭**: `/uploads/image1.jpg`에 `image1` 포함
|
|
926
|
+
5. **전체 경로 매칭**: 복잡한 구조에 대한 완전한 경로 매칭
|
|
927
|
+
|
|
928
|
+
이 강력한 매칭은 파일 구성이나 명명 규칙에 관계없이 메타데이터가 올바르게 연결되도록 보장합니다.
|
|
352
929
|
|
|
353
930
|
### 보안 검증
|
|
354
931
|
|
|
355
|
-
Excel
|
|
932
|
+
Excel 파일은 포괄적인 보안 검증을 받습니다:
|
|
356
933
|
|
|
357
934
|
```python
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"max_file_size": 50, # 최대 파일 크기 (MB)
|
|
363
|
-
"max_memory_usage": 100, # 최대 메모리 사용량 (MB)
|
|
364
|
-
"max_rows": 1000000, # 최대 행 수
|
|
365
|
-
"max_columns": 16384, # 최대 열 수
|
|
366
|
-
}
|
|
935
|
+
class ExcelSecurityConfig:
|
|
936
|
+
max_file_size_mb: int = 10 # 파일 크기 제한 (MB)
|
|
937
|
+
max_rows: int = 100000 # 행 수 제한
|
|
938
|
+
max_columns: int = 50 # 열 수 제한
|
|
367
939
|
```
|
|
368
940
|
|
|
369
|
-
|
|
941
|
+
#### 고급 보안 기능
|
|
370
942
|
|
|
371
|
-
|
|
943
|
+
- **파일 형식 검증**: Excel 파일 시그니처 확인 (.xlsx의 경우 PK, .xls의 경우 복합 문서)
|
|
944
|
+
- **메모리 추정**: 대용량 스프레드시트로 인한 메모리 고갈 방지
|
|
945
|
+
- **내용 정화**: 지나치게 긴 값의 자동 잘라내기
|
|
946
|
+
- **오류 복원력**: 손상되거나 접근할 수 없는 파일의 우아한 처리
|
|
372
947
|
|
|
373
|
-
|
|
374
|
-
# 메모리 제한 설정
|
|
375
|
-
export EXCEL_MAX_MEMORY_MB=200
|
|
948
|
+
### config.yaml을 통한 구성
|
|
376
949
|
|
|
377
|
-
|
|
378
|
-
export EXCEL_MAX_FILE_SIZE_MB=100
|
|
950
|
+
보안 제한 및 처리 옵션을 구성할 수 있습니다:
|
|
379
951
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
952
|
+
```yaml
|
|
953
|
+
actions:
|
|
954
|
+
upload:
|
|
955
|
+
excel_config:
|
|
956
|
+
max_file_size_mb: 10 # Excel 파일 최대 크기 (MB)
|
|
957
|
+
max_rows: 100000 # 허용되는 최대 행 수
|
|
958
|
+
max_columns: 50 # 허용되는 최대 열 수
|
|
383
959
|
```
|
|
384
960
|
|
|
385
|
-
###
|
|
961
|
+
### 성능 최적화
|
|
962
|
+
|
|
963
|
+
Excel 메타데이터 처리에는 여러 성능 향상 기능이 포함되어 있습니다:
|
|
386
964
|
|
|
387
|
-
|
|
388
|
-
flowchart TD
|
|
389
|
-
A[Excel 파일 발견] --> B[보안 검증]
|
|
390
|
-
B --> C{검증 통과?}
|
|
391
|
-
C -->|실패| D[보안 오류 발생]
|
|
392
|
-
C -->|통과| E[시트 정보 추출]
|
|
393
|
-
E --> F[행/열 수 확인]
|
|
394
|
-
F --> G[메타데이터 수집]
|
|
395
|
-
G --> H[JSON 직렬화]
|
|
396
|
-
H --> I[데이터 단위 생성]
|
|
397
|
-
|
|
398
|
-
style A fill:#e3f2fd80,stroke:#1976d2,stroke-width:2px
|
|
399
|
-
style B fill:#ffebee80,stroke:#d32f2f,stroke-width:2px
|
|
400
|
-
style C fill:#fff3e080,stroke:#f57c00,stroke-width:2px
|
|
401
|
-
style D fill:#ffcdd280,stroke:#d32f2f,stroke-width:2px
|
|
402
|
-
style E fill:#e8f5e880,stroke:#388e3c,stroke-width:2px
|
|
403
|
-
style F fill:#f3e5f580,stroke:#7b1fa2,stroke-width:2px
|
|
404
|
-
style G fill:#e3f2fd80,stroke:#1976d2,stroke-width:2px
|
|
405
|
-
style H fill:#fff3e080,stroke:#f57c00,stroke-width:2px
|
|
406
|
-
style I fill:#e8f5e880,stroke:#388e3c,stroke-width:2px
|
|
407
|
-
```
|
|
965
|
+
#### 메타데이터 인덱싱
|
|
408
966
|
|
|
409
|
-
|
|
967
|
+
- **O(1) 해시 검색**: 정확한 stem 및 파일명 매칭용
|
|
968
|
+
- **사전 구축된 인덱스**: 일반적인 매칭 패턴용
|
|
969
|
+
- **대체 알고리즘**: 복잡한 경로 매칭 시나리오용
|
|
970
|
+
|
|
971
|
+
#### 효율적인 처리
|
|
410
972
|
|
|
411
|
-
|
|
973
|
+
- **최적화된 행 처리**: 빈 행을 조기에 건너뛰기
|
|
974
|
+
- **메모리 인식 작업**: 배치로 파일 처리
|
|
975
|
+
- **스마트 파일 발견**: 반복되는 변환을 피하기 위한 경로 문자열 캐시
|
|
412
976
|
|
|
413
|
-
|
|
977
|
+
### 메타데이터 처리 플로우
|
|
978
|
+
|
|
979
|
+
1. **보안 검증**: 파일 크기, 형식 및 내용 제한
|
|
980
|
+
2. **헤더 검증**: 대소문자를 구분하지 않는 매칭으로 "filename" 및 "file_name" 모두 지원
|
|
981
|
+
3. **인덱스 구축**: 성능을 위한 O(1) 검색 구조 생성
|
|
982
|
+
4. **내용 처리**: 최적화를 통한 행별 메타데이터 추출
|
|
983
|
+
5. **데이터 정화**: 자동 잘라내기 및 검증
|
|
984
|
+
6. **패턴 매칭**: 5단계 파일명 연결 알고리즘
|
|
985
|
+
7. **매핑 생성**: 최적화된 파일명에서 메타데이터로의 매핑
|
|
986
|
+
|
|
987
|
+
### Excel 메타데이터 매개변수
|
|
988
|
+
|
|
989
|
+
사용자 정의 Excel 메타데이터 파일 경로를 지정할 수 있습니다:
|
|
414
990
|
|
|
415
991
|
```python
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
'.json': 'JSON 데이터',
|
|
423
|
-
'.jpg': 'JPEG 이미지',
|
|
424
|
-
'.png': 'PNG 이미지',
|
|
425
|
-
'.pdf': 'PDF 문서'
|
|
992
|
+
params = {
|
|
993
|
+
"name": "Excel 메타데이터 업로드",
|
|
994
|
+
"path": "/data/files",
|
|
995
|
+
"storage": 1,
|
|
996
|
+
"data_collection": 5,
|
|
997
|
+
"excel_metadata_path": "/data/custom_metadata.xlsx" # 사용자 정의 Excel 파일
|
|
426
998
|
}
|
|
427
999
|
```
|
|
428
1000
|
|
|
429
|
-
|
|
1001
|
+
#### 경로 해결
|
|
1002
|
+
|
|
1003
|
+
- **절대 경로**: 존재하고 접근 가능한 경우 직접 사용
|
|
1004
|
+
- **상대 경로**: 업로드 경로에 상대적으로 해결
|
|
1005
|
+
- **기본 발견**: 경로가 지정되지 않은 경우 자동으로 `meta.xlsx` 또는 `meta.xls` 검색
|
|
1006
|
+
- **스토리지 통합**: 적절한 경로 해결을 위해 스토리지 구성 사용
|
|
1007
|
+
|
|
1008
|
+
### 오류 처리
|
|
1009
|
+
|
|
1010
|
+
포괄적인 오류 처리로 강력한 작업을 보장합니다:
|
|
1011
|
+
|
|
1012
|
+
```python
|
|
1013
|
+
# Excel 처리 오류는 우아하게 처리됩니다
|
|
1014
|
+
try:
|
|
1015
|
+
metadata = process_excel_metadata(excel_path)
|
|
1016
|
+
except ExcelSecurityError as e:
|
|
1017
|
+
# 보안 위반 - 파일이 너무 크거나 행이 너무 많음 등
|
|
1018
|
+
log_security_violation(e)
|
|
1019
|
+
except ExcelParsingError as e:
|
|
1020
|
+
# 파싱 실패 - 손상된 파일, 잘못된 형식 등
|
|
1021
|
+
log_parsing_error(e)
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
#### 오류 복구
|
|
1025
|
+
|
|
1026
|
+
- **우아한 성능 저하**: Excel이 실패하면 빈 메타데이터로 처리 계속
|
|
1027
|
+
- **상세 로깅**: 다양한 실패 유형에 대한 특정 오류 코드
|
|
1028
|
+
- **경로 검증**: 매개변수 처리 중 포괄적인 검증
|
|
1029
|
+
- **대체 동작**: 메타데이터를 처리할 수 없을 때 스마트 기본값
|
|
1030
|
+
|
|
1031
|
+
## 파일 구성
|
|
1032
|
+
|
|
1033
|
+
업로드 시스템은 파일을 타입에 따라 자동으로 구성합니다:
|
|
1034
|
+
|
|
1035
|
+
### 타입 감지
|
|
1036
|
+
|
|
1037
|
+
파일은 다음을 기반으로 분류됩니다:
|
|
430
1038
|
|
|
431
|
-
|
|
1039
|
+
- 파일 확장자 패턴
|
|
1040
|
+
- MIME 타입 감지
|
|
1041
|
+
- 내용 분석
|
|
1042
|
+
- 사용자 정의 타입 규칙
|
|
1043
|
+
|
|
1044
|
+
### 디렉토리 구조
|
|
432
1045
|
|
|
433
1046
|
```
|
|
434
|
-
|
|
435
|
-
├── excel_files/
|
|
436
|
-
│ ├── data1.xlsx
|
|
437
|
-
│ └── data2.xlsx
|
|
1047
|
+
upload_output/
|
|
438
1048
|
├── images/
|
|
439
|
-
│ ├──
|
|
440
|
-
│ └──
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1049
|
+
│ ├── image1.jpg
|
|
1050
|
+
│ └── image2.png
|
|
1051
|
+
├── documents/
|
|
1052
|
+
│ ├── report.pdf
|
|
1053
|
+
│ └── data.xlsx
|
|
1054
|
+
└── videos/
|
|
1055
|
+
└── presentation.mp4
|
|
444
1056
|
```
|
|
445
1057
|
|
|
446
1058
|
### 배치 처리
|
|
447
1059
|
|
|
448
|
-
|
|
1060
|
+
파일은 구성 가능한 배치로 처리됩니다:
|
|
449
1061
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1062
|
+
```python
|
|
1063
|
+
# 배치 크기 구성
|
|
1064
|
+
params = {
|
|
1065
|
+
"creating_data_unit_batch_size": 100,
|
|
1066
|
+
"use_async_upload": True
|
|
1067
|
+
}
|
|
1068
|
+
```
|
|
453
1069
|
|
|
454
1070
|
## 진행률 추적 및 메트릭
|
|
455
1071
|
|
|
456
1072
|
### 진행률 카테고리
|
|
457
1073
|
|
|
458
|
-
업로드
|
|
1074
|
+
업로드 액션은 세 가지 주요 단계에서 진행률을 추적합니다:
|
|
459
1075
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
'excel_processing': 'Excel 처리',
|
|
466
|
-
'metadata_extraction': '메타데이터 추출',
|
|
467
|
-
'upload_preparation': '업로드 준비',
|
|
468
|
-
'data_upload': '데이터 업로드',
|
|
469
|
-
'finalization': '완료 처리'
|
|
470
|
-
}
|
|
471
|
-
```
|
|
1076
|
+
| 카테고리 | 비율 | 설명 |
|
|
1077
|
+
| --------------------- | ---- | ------------------------ |
|
|
1078
|
+
| `analyze_collection` | 2% | 매개변수 검증 및 설정 |
|
|
1079
|
+
| `upload_data_files` | 38% | 파일 업로드 처리 |
|
|
1080
|
+
| `generate_data_units` | 60% | 데이터 단위 생성 및 완료 |
|
|
472
1081
|
|
|
473
1082
|
### 메트릭 수집
|
|
474
1083
|
|
|
475
|
-
|
|
1084
|
+
모니터링을 위해 실시간 메트릭이 수집됩니다:
|
|
476
1085
|
|
|
477
1086
|
```python
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
"upload": 45.8
|
|
1087
|
+
metrics_categories = {
|
|
1088
|
+
'data_files': {
|
|
1089
|
+
'stand_by': 0, # 처리 대기 중인 파일
|
|
1090
|
+
'failed': 0, # 업로드 실패한 파일
|
|
1091
|
+
'success': 0, # 성공적으로 업로드된 파일
|
|
484
1092
|
},
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
"other_files": 25
|
|
1093
|
+
'data_units': {
|
|
1094
|
+
'stand_by': 0, # 생성 대기 중인 단위
|
|
1095
|
+
'failed': 0, # 생성 실패한 단위
|
|
1096
|
+
'success': 0, # 성공적으로 생성된 단위
|
|
490
1097
|
},
|
|
491
|
-
"data_volume": {
|
|
492
|
-
"total_size_mb": 2048,
|
|
493
|
-
"excel_data_mb": 512,
|
|
494
|
-
"media_data_mb": 1536
|
|
495
|
-
}
|
|
496
1098
|
}
|
|
497
1099
|
```
|
|
498
1100
|
|
|
499
1101
|
## 타입 안전 로깅
|
|
500
1102
|
|
|
1103
|
+
업로드 시스템은 일관성을 위해 열거형 기반 로깅을 사용합니다:
|
|
1104
|
+
|
|
501
1105
|
### 로그 코드
|
|
502
1106
|
|
|
503
|
-
|
|
1107
|
+
```python
|
|
1108
|
+
class LogCode(str, Enum):
|
|
1109
|
+
VALIDATION_FAILED = 'VALIDATION_FAILED'
|
|
1110
|
+
NO_FILES_FOUND = 'NO_FILES_FOUND'
|
|
1111
|
+
EXCEL_SECURITY_VIOLATION = 'EXCEL_SECURITY_VIOLATION'
|
|
1112
|
+
EXCEL_PARSING_ERROR = 'EXCEL_PARSING_ERROR'
|
|
1113
|
+
FILES_DISCOVERED = 'FILES_DISCOVERED'
|
|
1114
|
+
UPLOADING_DATA_FILES = 'UPLOADING_DATA_FILES'
|
|
1115
|
+
GENERATING_DATA_UNITS = 'GENERATING_DATA_UNITS'
|
|
1116
|
+
IMPORT_COMPLETED = 'IMPORT_COMPLETED'
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
### 로깅 사용법
|
|
504
1120
|
|
|
505
1121
|
```python
|
|
506
|
-
#
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1122
|
+
# 기본 로깅
|
|
1123
|
+
run.log_message_with_code(LogCode.FILES_DISCOVERED, file_count)
|
|
1124
|
+
|
|
1125
|
+
# 사용자 정의 레벨로
|
|
1126
|
+
run.log_message_with_code(
|
|
1127
|
+
LogCode.EXCEL_SECURITY_VIOLATION,
|
|
1128
|
+
filename,
|
|
1129
|
+
level=Context.DANGER
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
# 업로드 특정 이벤트
|
|
1133
|
+
run.log_upload_event(LogCode.UPLOADING_DATA_FILES, batch_size)
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
## 마이그레이션 가이드
|
|
1137
|
+
|
|
1138
|
+
### 레거시에서 리팩토링된 아키텍처로
|
|
1139
|
+
|
|
1140
|
+
업로드 액션은 **100% 하위 호환성**을 유지하면서 현대적인 디자인 패턴을 사용하여 리팩토링되었습니다. 기존 코드는 변경 없이 계속 작동합니다.
|
|
1141
|
+
|
|
1142
|
+
#### 주요 변경 사항
|
|
1143
|
+
|
|
1144
|
+
**이전 (레거시 모놀리식):**
|
|
1145
|
+
|
|
1146
|
+
- 모든 로직을 포함한 단일 900+ 줄 액션 클래스
|
|
1147
|
+
- 검증, 파일 발견 등에 대한 하드코딩된 동작
|
|
1148
|
+
- 확장성이나 커스터마이제이션 옵션 없음
|
|
1149
|
+
- 전반에 걸친 수동 오류 처리
|
|
510
1150
|
|
|
511
|
-
|
|
512
|
-
NO_FILES_FOUND = "UPLOAD_004"
|
|
513
|
-
FILES_DISCOVERED = "UPLOAD_005"
|
|
514
|
-
FILE_FILTERED = "UPLOAD_006"
|
|
1151
|
+
**이후 (전략/파사드 패턴):**
|
|
515
1152
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1153
|
+
- 8개 워크플로우 단계로 명확한 관심사 분리
|
|
1154
|
+
- 다양한 동작을 위한 플러그형 전략
|
|
1155
|
+
- 사용자 정의 구현을 위한 확장 가능한 아키텍처
|
|
1156
|
+
- 자동 롤백 및 포괄적인 오류 처리
|
|
520
1157
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1158
|
+
#### 하위 호환성
|
|
1159
|
+
|
|
1160
|
+
```python
|
|
1161
|
+
# 이 레거시 사용법은 동일하게 작동합니다
|
|
1162
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.action import UploadAction
|
|
1163
|
+
|
|
1164
|
+
params = {
|
|
1165
|
+
"name": "My Upload",
|
|
1166
|
+
"path": "/data/files",
|
|
1167
|
+
"storage": 1,
|
|
1168
|
+
"data_collection": 5 # 'collection'에서 'data_collection'으로 변경
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
action = UploadAction(params=params, plugin_config=config)
|
|
1172
|
+
result = action.start() # 이전과 동일하게 작동
|
|
525
1173
|
```
|
|
526
1174
|
|
|
527
|
-
|
|
1175
|
+
#### 향상된 기능
|
|
528
1176
|
|
|
529
|
-
|
|
1177
|
+
리팩토링된 아키텍처는 새로운 기능을 제공합니다:
|
|
530
1178
|
|
|
531
1179
|
```python
|
|
532
|
-
#
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
#
|
|
1180
|
+
# 자세한 워크플로우 정보 가져오기
|
|
1181
|
+
action = UploadAction(params=params, plugin_config=config)
|
|
1182
|
+
workflow_info = action.get_workflow_summary()
|
|
1183
|
+
print(f"Configured with {workflow_info['step_count']} steps")
|
|
1184
|
+
print(f"Available strategies: {workflow_info['available_strategies']}")
|
|
1185
|
+
|
|
1186
|
+
# 실행하고 자세한 결과 가져오기
|
|
1187
|
+
result = action.start()
|
|
1188
|
+
print(f"Success: {result['success']}")
|
|
1189
|
+
print(f"Uploaded files: {result['uploaded_files_count']}")
|
|
1190
|
+
print(f"Generated data units: {result['generated_data_units_count']}")
|
|
1191
|
+
print(f"Errors: {result['errors']}")
|
|
1192
|
+
print(f"Metrics: {result['metrics']}")
|
|
539
1193
|
```
|
|
540
1194
|
|
|
541
|
-
|
|
1195
|
+
#### 매개변수 변경
|
|
1196
|
+
|
|
1197
|
+
하나의 매개변수 이름만 변경되었습니다:
|
|
1198
|
+
|
|
1199
|
+
| 레거시 | 리팩토링 | 상태 |
|
|
1200
|
+
| ------------------ | ----------------- | ------------- |
|
|
1201
|
+
| `collection` | `data_collection` | **필수 변경** |
|
|
1202
|
+
| 기타 모든 매개변수 | 변경 없음 | 완전 호환 |
|
|
1203
|
+
|
|
1204
|
+
#### 마이그레이션의 이점
|
|
1205
|
+
|
|
1206
|
+
- **더 나은 오류 처리**: 실패 시 자동 롤백
|
|
1207
|
+
- **진행률 추적**: 워크플로우 단계 전반의 자세한 진행률 메트릭
|
|
1208
|
+
- **확장성**: 사용자 정의 전략 및 단계 추가
|
|
1209
|
+
- **테스트**: 모킹 친화적인 아키텍처로 더 나은 테스트 가능성
|
|
1210
|
+
- **유지보수성**: 명확한 관심사 분리
|
|
1211
|
+
- **성능**: 더 효율적인 리소스 관리
|
|
1212
|
+
|
|
1213
|
+
## 사용 예제
|
|
542
1214
|
|
|
543
|
-
###
|
|
1215
|
+
### 기본 파일 업로드 (리팩토링된 아키텍처)
|
|
544
1216
|
|
|
545
1217
|
```python
|
|
546
|
-
from synapse_sdk.plugins.categories.upload.actions import
|
|
1218
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.action import UploadAction
|
|
1219
|
+
|
|
1220
|
+
# 새 아키텍처로 기본 업로드 구성
|
|
1221
|
+
params = {
|
|
1222
|
+
"name": "Dataset Upload",
|
|
1223
|
+
"description": "Training dataset for ML model",
|
|
1224
|
+
"path": "/data/training_images",
|
|
1225
|
+
"storage": 1,
|
|
1226
|
+
"data_collection": 5, # 참고: 'collection' 대신 'data_collection'
|
|
1227
|
+
"is_recursive": True,
|
|
1228
|
+
"max_file_size_mb": 100
|
|
1229
|
+
}
|
|
547
1230
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
collection_id="collection456",
|
|
552
|
-
project_id="project789",
|
|
553
|
-
include_patterns=["*.xlsx", "*.jpg", "*.png"],
|
|
554
|
-
exclude_patterns=["*temp*", "*backup*"]
|
|
1231
|
+
action = UploadAction(
|
|
1232
|
+
params=params,
|
|
1233
|
+
plugin_config=plugin_config
|
|
555
1234
|
)
|
|
556
1235
|
|
|
557
|
-
|
|
558
|
-
result = action.
|
|
1236
|
+
# 자동 단계 기반 워크플로우 및 롤백으로 실행
|
|
1237
|
+
result = action.start()
|
|
1238
|
+
|
|
1239
|
+
# 향상된 결과 정보
|
|
1240
|
+
print(f"Upload successful: {result['success']}")
|
|
1241
|
+
print(f"Uploaded {result['uploaded_files_count']} files")
|
|
1242
|
+
print(f"Generated {result['generated_data_units_count']} data units")
|
|
1243
|
+
print(f"Workflow errors: {result['errors']}")
|
|
1244
|
+
|
|
1245
|
+
# 자세한 메트릭에 액세스
|
|
1246
|
+
workflow_metrics = result['metrics'].get('workflow', {})
|
|
1247
|
+
print(f"Total steps executed: {workflow_metrics.get('current_step', 0)}")
|
|
1248
|
+
print(f"Progress completed: {workflow_metrics.get('progress_percentage', 0)}%")
|
|
559
1249
|
```
|
|
560
1250
|
|
|
561
|
-
###
|
|
1251
|
+
### 진행률 추적이 포함된 Excel 메타데이터 업로드
|
|
562
1252
|
|
|
563
1253
|
```python
|
|
564
|
-
#
|
|
565
|
-
params =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1254
|
+
# Excel 메타데이터 및 진행률 모니터링으로 업로드
|
|
1255
|
+
params = {
|
|
1256
|
+
"name": "Annotated Dataset Upload",
|
|
1257
|
+
"path": "/data/images",
|
|
1258
|
+
"storage": 1,
|
|
1259
|
+
"data_collection": 5,
|
|
1260
|
+
"excel_metadata_path": "/data/metadata.xlsx",
|
|
1261
|
+
"is_recursive": False,
|
|
1262
|
+
"creating_data_unit_batch_size": 50
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
action = UploadAction(
|
|
1266
|
+
params=params,
|
|
1267
|
+
plugin_config=plugin_config
|
|
572
1268
|
)
|
|
1269
|
+
|
|
1270
|
+
# 실행 전 워크플로우 요약 가져오기
|
|
1271
|
+
workflow_info = action.get_workflow_summary()
|
|
1272
|
+
print(f"Workflow configured with {workflow_info['step_count']} steps")
|
|
1273
|
+
print(f"Total progress weight: {workflow_info['total_progress_weight']}")
|
|
1274
|
+
print(f"Steps: {workflow_info['steps']}")
|
|
1275
|
+
|
|
1276
|
+
# 향상된 오류 처리로 실행
|
|
1277
|
+
try:
|
|
1278
|
+
result = action.start()
|
|
1279
|
+
if result['success']:
|
|
1280
|
+
print("Upload completed successfully!")
|
|
1281
|
+
print(f"Files: {result['uploaded_files_count']}")
|
|
1282
|
+
print(f"Data units: {result['generated_data_units_count']}")
|
|
1283
|
+
else:
|
|
1284
|
+
print("Upload failed with errors:")
|
|
1285
|
+
for error in result['errors']:
|
|
1286
|
+
print(f" - {error}")
|
|
1287
|
+
except Exception as e:
|
|
1288
|
+
print(f"Upload action failed: {e}")
|
|
573
1289
|
```
|
|
574
1290
|
|
|
575
|
-
###
|
|
1291
|
+
### 사용자 정의 전략 업로드
|
|
576
1292
|
|
|
577
1293
|
```python
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
)
|
|
1294
|
+
from synapse_sdk.plugins.categories.upload.actions.upload.action import UploadAction
|
|
1295
|
+
from my_custom_strategies import CustomValidationStrategy
|
|
1296
|
+
|
|
1297
|
+
# 사용자 정의 팩토리로 액션 생성
|
|
1298
|
+
class CustomUploadAction(UploadAction):
|
|
1299
|
+
def _configure_strategies(self, context=None):
|
|
1300
|
+
strategies = super()._configure_strategies(context)
|
|
1301
|
+
|
|
1302
|
+
# 사용자 정의 검증으로 오버라이드
|
|
1303
|
+
if self.params.get('use_strict_validation'):
|
|
1304
|
+
strategies['validation'] = CustomValidationStrategy()
|
|
1305
|
+
|
|
1306
|
+
return strategies
|
|
1307
|
+
|
|
1308
|
+
# 사용자 정의 액션 사용
|
|
1309
|
+
params = {
|
|
1310
|
+
"name": "Strict Validation Upload",
|
|
1311
|
+
"path": "/data/sensitive_files",
|
|
1312
|
+
"storage": 1,
|
|
1313
|
+
"data_collection": 5,
|
|
1314
|
+
"use_strict_validation": True,
|
|
1315
|
+
"max_file_size_mb": 10 # 더 엄격한 제한
|
|
1316
|
+
}
|
|
585
1317
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
collection_id="collection456",
|
|
590
|
-
project_id="project789",
|
|
591
|
-
excel_security_config=excel_config
|
|
1318
|
+
action = CustomUploadAction(
|
|
1319
|
+
params=params,
|
|
1320
|
+
plugin_config=plugin_config
|
|
592
1321
|
)
|
|
1322
|
+
|
|
1323
|
+
result = action.start()
|
|
593
1324
|
```
|
|
594
1325
|
|
|
595
|
-
|
|
1326
|
+
### 사용자 정의 구성을 포함한 배치 처리
|
|
596
1327
|
|
|
597
|
-
|
|
1328
|
+
```python
|
|
1329
|
+
import os
|
|
1330
|
+
|
|
1331
|
+
# Excel 처리 제한 구성
|
|
1332
|
+
os.environ['EXCEL_MAX_FILE_SIZE_MB'] = '20'
|
|
1333
|
+
os.environ['EXCEL_MAX_ROWS'] = '20000'
|
|
1334
|
+
|
|
1335
|
+
# 사용자 정의 설정을 포함한 대량 배치 업로드
|
|
1336
|
+
params = {
|
|
1337
|
+
"name": "Large Batch Upload",
|
|
1338
|
+
"path": "/data/large_dataset",
|
|
1339
|
+
"storage": 2,
|
|
1340
|
+
"data_collection": 10,
|
|
1341
|
+
"is_recursive": True,
|
|
1342
|
+
"max_file_size_mb": 500,
|
|
1343
|
+
"creating_data_unit_batch_size": 200,
|
|
1344
|
+
"use_async_upload": True
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
action = UploadAction(
|
|
1348
|
+
params=params,
|
|
1349
|
+
plugin_config=plugin_config
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
# 진행률 모니터링으로 실행
|
|
1353
|
+
result = action.start()
|
|
1354
|
+
|
|
1355
|
+
# 결과 분석
|
|
1356
|
+
print(f"Batch upload summary:")
|
|
1357
|
+
print(f" Success: {result['success']}")
|
|
1358
|
+
print(f" Files processed: {result['uploaded_files_count']}")
|
|
1359
|
+
print(f" Data units created: {result['generated_data_units_count']}")
|
|
1360
|
+
|
|
1361
|
+
# 카테고리별 메트릭 확인
|
|
1362
|
+
metrics = result['metrics']
|
|
1363
|
+
if 'data_files' in metrics:
|
|
1364
|
+
files_metrics = metrics['data_files']
|
|
1365
|
+
print(f" Files - Success: {files_metrics.get('success', 0)}")
|
|
1366
|
+
print(f" Files - Failed: {files_metrics.get('failed', 0)}")
|
|
1367
|
+
|
|
1368
|
+
if 'data_units' in metrics:
|
|
1369
|
+
units_metrics = metrics['data_units']
|
|
1370
|
+
print(f" Units - Success: {units_metrics.get('success', 0)}")
|
|
1371
|
+
print(f" Units - Failed: {units_metrics.get('failed', 0)}")
|
|
1372
|
+
```
|
|
598
1373
|
|
|
599
|
-
###
|
|
1374
|
+
### 오류 처리 및 롤백
|
|
600
1375
|
|
|
601
1376
|
```python
|
|
602
|
-
|
|
1377
|
+
# 자동 롤백을 포함한 향상된 오류 처리 시연
|
|
1378
|
+
params = {
|
|
1379
|
+
"name": "Error Recovery Example",
|
|
1380
|
+
"path": "/data/problematic_files",
|
|
1381
|
+
"storage": 1,
|
|
1382
|
+
"data_collection": 5,
|
|
1383
|
+
"is_recursive": True
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
action = UploadAction(
|
|
1387
|
+
params=params,
|
|
1388
|
+
plugin_config=plugin_config
|
|
1389
|
+
)
|
|
603
1390
|
|
|
604
1391
|
try:
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
except
|
|
620
|
-
print(f"
|
|
621
|
-
|
|
622
|
-
print(f"Excel 파싱 오류: {e}")
|
|
1392
|
+
result = action.start()
|
|
1393
|
+
|
|
1394
|
+
if not result['success']:
|
|
1395
|
+
print("Upload failed, but cleanup was automatic:")
|
|
1396
|
+
print(f"Errors encountered: {len(result['errors'])}")
|
|
1397
|
+
for i, error in enumerate(result['errors'], 1):
|
|
1398
|
+
print(f" {i}. {error}")
|
|
1399
|
+
|
|
1400
|
+
# 롤백이 수행되었는지 확인 (오케스트레이터 내부를 통해)
|
|
1401
|
+
workflow_metrics = result['metrics'].get('workflow', {})
|
|
1402
|
+
current_step = workflow_metrics.get('current_step', 0)
|
|
1403
|
+
total_steps = workflow_metrics.get('total_steps', 0)
|
|
1404
|
+
print(f"Workflow stopped at step {current_step} of {total_steps}")
|
|
1405
|
+
|
|
1406
|
+
except Exception as e:
|
|
1407
|
+
print(f"Critical upload failure: {e}")
|
|
1408
|
+
# 예외 전파 전에 롤백이 자동으로 수행됨
|
|
623
1409
|
```
|
|
624
1410
|
|
|
1411
|
+
## 오류 처리
|
|
1412
|
+
|
|
625
1413
|
### 예외 타입
|
|
626
1414
|
|
|
627
|
-
|
|
1415
|
+
업로드 시스템은 특정 예외를 정의합니다:
|
|
628
1416
|
|
|
629
|
-
|
|
1417
|
+
```python
|
|
1418
|
+
# 보안 위반
|
|
1419
|
+
try:
|
|
1420
|
+
action.start()
|
|
1421
|
+
except ExcelSecurityError as e:
|
|
1422
|
+
print(f"Excel security violation: {e}")
|
|
630
1423
|
|
|
631
|
-
|
|
1424
|
+
# 파싱 오류
|
|
1425
|
+
except ExcelParsingError as e:
|
|
1426
|
+
print(f"Excel parsing failed: {e}")
|
|
632
1427
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1428
|
+
# 일반 업로드 오류
|
|
1429
|
+
except ActionError as e:
|
|
1430
|
+
print(f"Upload action failed: {e}")
|
|
1431
|
+
```
|
|
636
1432
|
|
|
637
|
-
|
|
1433
|
+
### 검증 오류
|
|
638
1434
|
|
|
639
|
-
|
|
1435
|
+
매개변수 검증은 자세한 오류 메시지를 제공합니다:
|
|
640
1436
|
|
|
641
|
-
|
|
1437
|
+
```python
|
|
1438
|
+
from pydantic import ValidationError
|
|
642
1439
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1440
|
+
try:
|
|
1441
|
+
params = UploadParams(**invalid_params)
|
|
1442
|
+
except ValidationError as e:
|
|
1443
|
+
for error in e.errors():
|
|
1444
|
+
print(f"Field {error['loc']}: {error['msg']}")
|
|
1445
|
+
```
|
|
647
1446
|
|
|
648
|
-
|
|
1447
|
+
## API 레퍼런스
|
|
649
1448
|
|
|
650
|
-
|
|
651
|
-
|
|
1449
|
+
### 핵심 컴포넌트
|
|
1450
|
+
|
|
1451
|
+
#### UploadAction
|
|
1452
|
+
|
|
1453
|
+
파일 처리 작업을 위한 전략 및 파사드 패턴을 구현하는 메인 업로드 액션 클래스입니다.
|
|
1454
|
+
|
|
1455
|
+
**클래스 속성:**
|
|
1456
|
+
|
|
1457
|
+
- `name = 'upload'` - 액션 식별자
|
|
1458
|
+
- `category = PluginCategory.UPLOAD` - 플러그인 카테고리
|
|
1459
|
+
- `method = RunMethod.JOB` - 실행 방법
|
|
1460
|
+
- `run_class = UploadRun` - 전문 실행 관리
|
|
1461
|
+
- `params_model = UploadParams` - 매개변수 검증 모델
|
|
1462
|
+
- `strategy_factory: StrategyFactory` - 전략 구현 생성
|
|
1463
|
+
- `step_registry: StepRegistry` - 워크플로우 단계 관리
|
|
1464
|
+
|
|
1465
|
+
**주요 메서드:**
|
|
652
1466
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1467
|
+
- `start() -> Dict[str, Any]` - 오케스트레이션된 업로드 워크플로우 실행
|
|
1468
|
+
- `get_workflow_summary() -> Dict[str, Any]` - 구성된 워크플로우 요약 가져오기
|
|
1469
|
+
- `_configure_workflow() -> None` - 실행 순서로 워크플로우 단계 등록
|
|
1470
|
+
- `_configure_strategies(context=None) -> Dict[str, Any]` - 전략 인스턴스 생성
|
|
656
1471
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1472
|
+
**진행률 카테고리:**
|
|
1473
|
+
|
|
1474
|
+
```python
|
|
1475
|
+
progress_categories = {
|
|
1476
|
+
'analyze_collection': {'proportion': 2},
|
|
1477
|
+
'upload_data_files': {'proportion': 38},
|
|
1478
|
+
'generate_data_units': {'proportion': 60},
|
|
1479
|
+
}
|
|
664
1480
|
```
|
|
665
1481
|
|
|
666
|
-
|
|
1482
|
+
#### UploadOrchestrator
|
|
1483
|
+
|
|
1484
|
+
자동 롤백을 포함한 완전한 업로드 워크플로우를 조정하는 파사드 컴포넌트입니다.
|
|
1485
|
+
|
|
1486
|
+
**클래스 속성:**
|
|
1487
|
+
|
|
1488
|
+
- `context: UploadContext` - 워크플로우 전반의 공유 상태
|
|
1489
|
+
- `step_registry: StepRegistry` - 워크플로우 단계 레지스트리
|
|
1490
|
+
- `strategies: Dict[str, Any]` - 전략 구현
|
|
1491
|
+
- `executed_steps: List[BaseStep]` - 성공적으로 실행된 단계
|
|
1492
|
+
- `current_step_index: int` - 워크플로우의 현재 위치
|
|
1493
|
+
- `rollback_executed: bool` - 롤백이 수행되었는지 여부
|
|
1494
|
+
|
|
1495
|
+
**주요 메서드:**
|
|
1496
|
+
|
|
1497
|
+
- `execute() -> Dict[str, Any]` - 오류 처리를 포함한 완전한 워크플로우 실행
|
|
1498
|
+
- `get_workflow_summary() -> Dict[str, Any]` - 실행 요약 및 메트릭 가져오기
|
|
1499
|
+
- `get_executed_steps() -> List[BaseStep]` - 성공적으로 실행된 단계 목록 가져오기
|
|
1500
|
+
- `is_rollback_executed() -> bool` - 롤백이 수행되었는지 확인
|
|
1501
|
+
- `_execute_step(step: BaseStep) -> StepResult` - 개별 워크플로우 단계 실행
|
|
1502
|
+
- `_handle_step_failure(step: BaseStep, error: Exception) -> None` - 단계 실패 처리
|
|
1503
|
+
- `_rollback_executed_steps() -> None` - 실행된 단계를 역순으로 롤백
|
|
1504
|
+
|
|
1505
|
+
#### UploadContext
|
|
1506
|
+
|
|
1507
|
+
워크플로우 컴포넌트 간의 공유 상태 및 통신을 유지하는 컨텍스트 객체입니다.
|
|
1508
|
+
|
|
1509
|
+
**상태 속성:**
|
|
1510
|
+
|
|
1511
|
+
- `params: Dict` - 업로드 매개변수
|
|
1512
|
+
- `run: UploadRun` - 실행 관리 인스턴스
|
|
1513
|
+
- `client: Any` - 외부 작업을 위한 API 클라이언트
|
|
1514
|
+
- `storage: Any` - 스토리지 구성 객체
|
|
1515
|
+
- `pathlib_cwd: Path` - 현재 작업 디렉토리 경로
|
|
1516
|
+
- `metadata: Dict[str, Dict[str, Any]]` - 파일 메타데이터 매핑
|
|
1517
|
+
- `file_specifications: Dict[str, Any]` - 데이터 컬렉션 파일 사양
|
|
1518
|
+
- `organized_files: List[Dict[str, Any]]` - 구성된 파일 정보
|
|
1519
|
+
- `uploaded_files: List[Dict[str, Any]]` - 성공적으로 업로드된 파일
|
|
1520
|
+
- `data_units: List[Dict[str, Any]]` - 생성된 데이터 단위
|
|
1521
|
+
|
|
1522
|
+
**진행률 및 메트릭:**
|
|
1523
|
+
|
|
1524
|
+
- `metrics: Dict[str, Any]` - 워크플로우 메트릭 및 통계
|
|
1525
|
+
- `errors: List[str]` - 축적된 오류 메시지
|
|
1526
|
+
- `step_results: List[StepResult]` - 실행된 단계의 결과
|
|
1527
|
+
|
|
1528
|
+
**전략 및 롤백:**
|
|
1529
|
+
|
|
1530
|
+
- `strategies: Dict[str, Any]` - 주입된 전략 구현
|
|
1531
|
+
- `rollback_data: Dict[str, Any]` - 롤백 작업을 위한 데이터
|
|
1532
|
+
|
|
1533
|
+
**주요 메서드:**
|
|
1534
|
+
|
|
1535
|
+
- `update(result: StepResult) -> None` - 단계 결과로 컨텍스트 업데이트
|
|
1536
|
+
- `get_result() -> Dict[str, Any]` - 최종 결과 딕셔너리 생성
|
|
1537
|
+
- `has_errors() -> bool` - 축적된 오류 확인
|
|
1538
|
+
- `get_last_step_result() -> Optional[StepResult]` - 가장 최근 단계 결과 가져오기
|
|
1539
|
+
- `update_metrics(category: str, metrics: Dict[str, Any]) -> None` - 메트릭 업데이트
|
|
1540
|
+
- `add_error(error: str) -> None` - 컨텍스트에 오류 추가
|
|
1541
|
+
- `get_param(key: str, default: Any = None) -> Any` - 기본값이 있는 매개변수 가져오기
|
|
667
1542
|
|
|
668
|
-
|
|
1543
|
+
#### StepRegistry
|
|
669
1544
|
|
|
670
|
-
|
|
671
|
-
- 메모리 집약적인 작업을 위해 `max_workers` 제한
|
|
672
|
-
- 스트리밍 처리를 위해 청크 업로드 사용
|
|
1545
|
+
워크플로우 단계의 컬렉션 및 실행 순서를 관리하는 레지스트리입니다.
|
|
673
1546
|
|
|
674
|
-
|
|
1547
|
+
**속성:**
|
|
675
1548
|
|
|
676
|
-
-
|
|
677
|
-
- 신뢰할 수 없는 연결에 대해 재시도 메커니즘 구현
|
|
678
|
-
- 대용량 파일에 대해 체크섬 검증 사용
|
|
1549
|
+
- `_steps: List[BaseStep]` - 실행 순서로 등록된 워크플로우 단계
|
|
679
1550
|
|
|
680
|
-
|
|
1551
|
+
**주요 메서드:**
|
|
1552
|
+
|
|
1553
|
+
- `register(step: BaseStep) -> None` - 워크플로우 단계 등록
|
|
1554
|
+
- `get_steps() -> List[BaseStep]` - 순서대로 모든 등록된 단계 가져오기
|
|
1555
|
+
- `get_total_progress_weight() -> float` - 총 진행률 가중치 계산
|
|
1556
|
+
- `clear() -> None` - 모든 등록된 단계 지우기
|
|
1557
|
+
- `__len__() -> int` - 등록된 단계 수 가져오기
|
|
1558
|
+
|
|
1559
|
+
#### StrategyFactory
|
|
1560
|
+
|
|
1561
|
+
매개변수를 기반으로 적절한 전략 구현을 생성하는 팩토리 컴포넌트입니다.
|
|
1562
|
+
|
|
1563
|
+
**주요 메서드:**
|
|
1564
|
+
|
|
1565
|
+
- `create_validation_strategy(params: Dict, context=None) -> BaseValidationStrategy` - 검증 전략 생성
|
|
1566
|
+
- `create_file_discovery_strategy(params: Dict, context=None) -> BaseFileDiscoveryStrategy` - 파일 발견 전략 생성
|
|
1567
|
+
- `create_metadata_strategy(params: Dict, context=None) -> BaseMetadataStrategy` - 메타데이터 처리 전략 생성
|
|
1568
|
+
- `create_upload_strategy(params: Dict, context: UploadContext) -> BaseUploadStrategy` - 업로드 전략 생성 (컨텍스트 필요)
|
|
1569
|
+
- `create_data_unit_strategy(params: Dict, context: UploadContext) -> BaseDataUnitStrategy` - 데이터 단위 전략 생성 (컨텍스트 필요)
|
|
1570
|
+
- `get_available_strategies() -> Dict[str, List[str]]` - 사용 가능한 전략 타입 및 구현 가져오기
|
|
1571
|
+
|
|
1572
|
+
### 워크플로우 단계
|
|
1573
|
+
|
|
1574
|
+
#### BaseStep (추상)
|
|
1575
|
+
|
|
1576
|
+
공통 인터페이스 및 유틸리티를 제공하는 모든 워크플로우 단계의 기본 클래스입니다.
|
|
1577
|
+
|
|
1578
|
+
**추상 속성:**
|
|
1579
|
+
|
|
1580
|
+
- `name: str` - 고유한 단계 식별자
|
|
1581
|
+
- `progress_weight: float` - 진행률 계산을 위한 가중치 (합은 1.0이어야 함)
|
|
1582
|
+
|
|
1583
|
+
**추상 메서드:**
|
|
1584
|
+
|
|
1585
|
+
- `execute(context: UploadContext) -> StepResult` - 단계 로직 실행
|
|
1586
|
+
- `can_skip(context: UploadContext) -> bool` - 단계를 건너뛸 수 있는지 결정
|
|
1587
|
+
- `rollback(context: UploadContext) -> None` - 단계 작업 롤백
|
|
1588
|
+
|
|
1589
|
+
**유틸리티 메서드:**
|
|
1590
|
+
|
|
1591
|
+
- `create_success_result(data: Dict = None) -> StepResult` - 성공 결과 생성
|
|
1592
|
+
- `create_error_result(error: str, original_exception: Exception = None) -> StepResult` - 오류 결과 생성
|
|
1593
|
+
- `create_skip_result() -> StepResult` - 건너뛰기 결과 생성
|
|
1594
|
+
|
|
1595
|
+
#### StepResult
|
|
1596
|
+
|
|
1597
|
+
워크플로우 단계 실행에서 반환되는 결과 객체입니다.
|
|
1598
|
+
|
|
1599
|
+
**속성:**
|
|
1600
|
+
|
|
1601
|
+
- `success: bool` - 단계가 성공적으로 실행되었는지 여부
|
|
1602
|
+
- `data: Dict[str, Any]` - 단계 결과 데이터
|
|
1603
|
+
- `error: str` - 단계가 실패한 경우 오류 메시지
|
|
1604
|
+
- `rollback_data: Dict[str, Any]` - 롤백에 필요한 데이터
|
|
1605
|
+
- `skipped: bool` - 단계가 건너뛰어졌는지 여부
|
|
1606
|
+
- `original_exception: Optional[Exception]` - 디버깅을 위한 원본 예외
|
|
1607
|
+
- `timestamp: datetime` - 실행 타임스탬프
|
|
1608
|
+
|
|
1609
|
+
**사용법:**
|
|
681
1610
|
|
|
682
1611
|
```python
|
|
683
|
-
#
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
collection_id="collection456",
|
|
688
|
-
project_id="project789",
|
|
689
|
-
execution_method="ray", # Ray 클러스터 사용
|
|
690
|
-
max_workers=10
|
|
691
|
-
)
|
|
1612
|
+
# 불린 평가
|
|
1613
|
+
if step_result:
|
|
1614
|
+
# 단계가 성공함
|
|
1615
|
+
process_success(step_result.data)
|
|
692
1616
|
```
|
|
693
1617
|
|
|
694
|
-
|
|
1618
|
+
#### 구체적인 단계
|
|
695
1619
|
|
|
696
|
-
|
|
1620
|
+
**InitializeStep** (`name: "initialize"`, `weight: 0.05`)
|
|
697
1621
|
|
|
698
|
-
|
|
1622
|
+
- 스토리지 연결 및 pathlib 작업 디렉토리 설정
|
|
1623
|
+
- 기본 업로드 전제조건 검증
|
|
699
1624
|
|
|
700
|
-
|
|
701
|
-
from synapse_sdk.plugins.categories.upload.actions import upload
|
|
1625
|
+
**ProcessMetadataStep** (`name: "process_metadata"`, `weight: 0.05`)
|
|
702
1626
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
print(f"[{log_code.value}] {level}: {message}")
|
|
1627
|
+
- 제공된 Excel 메타데이터 처리
|
|
1628
|
+
- 메타데이터 보안 및 형식 검증
|
|
706
1629
|
|
|
707
|
-
|
|
708
|
-
action = upload.UploadAction(params=params)
|
|
709
|
-
action.run.add_log_handler(custom_log_handler)
|
|
710
|
-
result = action.run()
|
|
711
|
-
```
|
|
1630
|
+
**AnalyzeCollectionStep** (`name: "analyze_collection"`, `weight: 0.05`)
|
|
712
1631
|
|
|
713
|
-
|
|
1632
|
+
- 데이터 컬렉션 파일 사양 검색 및 검증
|
|
1633
|
+
- 파일 구성 규칙 설정
|
|
714
1634
|
|
|
715
|
-
|
|
716
|
-
# 업로드 메트릭 수집
|
|
717
|
-
result = action.run()
|
|
1635
|
+
**OrganizeFilesStep** (`name: "organize_files"`, `weight: 0.10`)
|
|
718
1636
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
"successful_files": len(result.successful_files),
|
|
722
|
-
"failed_files": len(result.failed_files),
|
|
723
|
-
"total_size": result.total_size_bytes,
|
|
724
|
-
"duration": result.duration_seconds,
|
|
725
|
-
"throughput": result.total_size_bytes / result.duration_seconds
|
|
726
|
-
}
|
|
1637
|
+
- 파일 발견 전략을 사용한 파일 발견
|
|
1638
|
+
- 타입 및 사양별 파일 구성
|
|
727
1639
|
|
|
728
|
-
|
|
729
|
-
```
|
|
1640
|
+
**ValidateFilesStep** (`name: "validate_files"`, `weight: 0.05`)
|
|
730
1641
|
|
|
731
|
-
|
|
1642
|
+
- 검증 전략을 사용한 파일 검증
|
|
1643
|
+
- 보안 및 내용 검사 수행
|
|
732
1644
|
|
|
733
|
-
|
|
1645
|
+
**UploadFilesStep** (`name: "upload_files"`, `weight: 0.30`)
|
|
734
1646
|
|
|
735
|
-
|
|
1647
|
+
- 업로드 전략을 사용한 파일 업로드
|
|
1648
|
+
- 배치 및 진행률 추적 처리
|
|
736
1649
|
|
|
737
|
-
|
|
738
|
-
# 모든 업로드 테스트 실행
|
|
739
|
-
python -m pytest tests/plugins/upload/ -v
|
|
1650
|
+
**GenerateDataUnitsStep** (`name: "generate_data_units"`, `weight: 0.35`)
|
|
740
1651
|
|
|
741
|
-
|
|
742
|
-
|
|
1652
|
+
- 데이터 단위 전략을 사용한 데이터 단위 생성
|
|
1653
|
+
- 업로드된 파일을 데이터 단위에 연결
|
|
743
1654
|
|
|
744
|
-
|
|
745
|
-
python -m pytest tests/plugins/upload/ --cov=synapse_sdk.plugins.categories.upload
|
|
746
|
-
```
|
|
1655
|
+
**CleanupStep** (`name: "cleanup"`, `weight: 0.05`)
|
|
747
1656
|
|
|
748
|
-
|
|
1657
|
+
- 임시 리소스 및 파일 정리
|
|
1658
|
+
- 최종 검증 수행
|
|
749
1659
|
|
|
750
|
-
|
|
751
|
-
import pytest
|
|
752
|
-
from synapse_sdk.plugins.categories.upload.actions import upload
|
|
1660
|
+
### 전략 기본 클래스
|
|
753
1661
|
|
|
754
|
-
|
|
755
|
-
"""업로드 매개변수 검증을 테스트합니다."""
|
|
756
|
-
# 유효한 매개변수
|
|
757
|
-
params = upload.UploadParams(
|
|
758
|
-
source_path="/valid/path",
|
|
759
|
-
storage_id="storage123",
|
|
760
|
-
collection_id="collection456",
|
|
761
|
-
project_id="project789"
|
|
762
|
-
)
|
|
763
|
-
assert params.source_path == "/valid/path"
|
|
1662
|
+
#### BaseValidationStrategy (추상)
|
|
764
1663
|
|
|
765
|
-
|
|
766
|
-
with pytest.raises(ValidationError):
|
|
767
|
-
upload.UploadParams(
|
|
768
|
-
source_path="", # 빈 경로
|
|
769
|
-
storage_id="storage123",
|
|
770
|
-
collection_id="collection456",
|
|
771
|
-
project_id="project789"
|
|
772
|
-
)
|
|
773
|
-
```
|
|
1664
|
+
파일 검증 전략의 기본 클래스입니다.
|
|
774
1665
|
|
|
775
|
-
|
|
1666
|
+
**추상 메서드:**
|
|
776
1667
|
|
|
777
|
-
|
|
1668
|
+
- `validate_files(files: List[Path], context: UploadContext) -> bool` - 파일 컬렉션 검증
|
|
1669
|
+
- `validate_security(file_path: Path) -> bool` - 개별 파일 보안 검증
|
|
778
1670
|
|
|
779
|
-
####
|
|
1671
|
+
#### BaseFileDiscoveryStrategy (추상)
|
|
780
1672
|
|
|
781
|
-
|
|
782
|
-
UploadSecurityError: Access denied to source path
|
|
783
|
-
```
|
|
1673
|
+
파일 발견 및 구성 전략의 기본 클래스입니다.
|
|
784
1674
|
|
|
785
|
-
|
|
1675
|
+
**추상 메서드:**
|
|
786
1676
|
|
|
787
|
-
|
|
1677
|
+
- `discover_files(path: Path, context: UploadContext) -> List[Path]` - 경로에서 파일 발견
|
|
1678
|
+
- `organize_files(files: List[Path], specs: Dict[str, Any], context: UploadContext) -> List[Dict[str, Any]]` - 발견된 파일 구성
|
|
788
1679
|
|
|
789
|
-
|
|
790
|
-
UploadExecutionError: Failed to process Excel file - corrupted or protected
|
|
791
|
-
```
|
|
1680
|
+
#### BaseMetadataStrategy (추상)
|
|
792
1681
|
|
|
793
|
-
|
|
1682
|
+
메타데이터 처리 전략의 기본 클래스입니다.
|
|
794
1683
|
|
|
795
|
-
|
|
796
|
-
- 암호로 보호된 파일에 대한 올바른 자격 증명 제공
|
|
797
|
-
- Excel 보안 구성 검토
|
|
1684
|
+
**추상 메서드:**
|
|
798
1685
|
|
|
799
|
-
|
|
1686
|
+
- `process_metadata(context: UploadContext) -> Dict[str, Any]` - 컨텍스트에서 메타데이터 처리
|
|
1687
|
+
- `extract_metadata(file_path: Path) -> Dict[str, Any]` - 파일에서 메타데이터 추출
|
|
800
1688
|
|
|
801
|
-
|
|
802
|
-
MemoryError: Unable to allocate array
|
|
803
|
-
```
|
|
1689
|
+
#### BaseUploadStrategy (추상)
|
|
804
1690
|
|
|
805
|
-
|
|
1691
|
+
파일 업로드 전략의 기본 클래스입니다.
|
|
806
1692
|
|
|
807
|
-
|
|
808
|
-
- 최대 작업자 수 제한 (`max_workers` 매개변수)
|
|
809
|
-
- 대용량 파일에 대해 스트리밍 업로드 사용
|
|
1693
|
+
**추상 메서드:**
|
|
810
1694
|
|
|
811
|
-
|
|
1695
|
+
- `upload_files(files: List[Dict[str, Any]], context: UploadContext) -> List[Dict[str, Any]]` - 파일 컬렉션 업로드
|
|
1696
|
+
- `upload_batch(batch: List[Dict[str, Any]], context: UploadContext) -> List[Dict[str, Any]]` - 파일 배치 업로드
|
|
812
1697
|
|
|
813
|
-
|
|
814
|
-
import logging
|
|
1698
|
+
#### BaseDataUnitStrategy (추상)
|
|
815
1699
|
|
|
816
|
-
|
|
817
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
1700
|
+
데이터 단위 생성 전략의 기본 클래스입니다.
|
|
818
1701
|
|
|
819
|
-
|
|
820
|
-
params = upload.UploadParams(
|
|
821
|
-
source_path="/path/to/files",
|
|
822
|
-
storage_id="storage123",
|
|
823
|
-
collection_id="collection456",
|
|
824
|
-
project_id="project789",
|
|
825
|
-
debug_mode=True # 상세한 로깅 활성화
|
|
826
|
-
)
|
|
1702
|
+
**추상 메서드:**
|
|
827
1703
|
|
|
828
|
-
|
|
829
|
-
|
|
1704
|
+
- `generate_data_units(files: List[Dict[str, Any]], context: UploadContext) -> List[Dict[str, Any]]` - 데이터 단위 생성
|
|
1705
|
+
- `create_data_unit_batch(batch: List[Dict[str, Any]], context: UploadContext) -> List[Dict[str, Any]]` - 데이터 단위 배치 생성
|
|
830
1706
|
|
|
831
|
-
|
|
1707
|
+
### 레거시 컴포넌트
|
|
832
1708
|
|
|
833
|
-
|
|
1709
|
+
#### UploadRun
|
|
834
1710
|
|
|
835
|
-
|
|
1711
|
+
업로드 작업을 위한 전문 실행 관리 (레거시에서 변경 없음).
|
|
836
1712
|
|
|
837
|
-
|
|
838
|
-
git clone <repository-url>
|
|
839
|
-
cd synapse-sdk
|
|
840
|
-
pip install -e ".[dev]"
|
|
841
|
-
```
|
|
1713
|
+
**로깅 메서드:**
|
|
842
1714
|
|
|
843
|
-
|
|
1715
|
+
- `log_message_with_code(code, *args, level=None)` - 타입 안전 로깅
|
|
1716
|
+
- `log_upload_event(code, *args, level=None)` - 업로드 특정 이벤트
|
|
844
1717
|
|
|
845
|
-
|
|
846
|
-
python -m pytest tests/plugins/upload/ -v
|
|
847
|
-
```
|
|
1718
|
+
**중첩 모델:**
|
|
848
1719
|
|
|
849
|
-
|
|
1720
|
+
- `UploadEventLog` - 업로드 이벤트 로깅
|
|
1721
|
+
- `DataFileLog` - 데이터 파일 처리 로그
|
|
1722
|
+
- `DataUnitLog` - 데이터 단위 생성 로그
|
|
1723
|
+
- `TaskLog` - 작업 실행 로그
|
|
1724
|
+
- `MetricsRecord` - 메트릭 추적
|
|
850
1725
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1726
|
+
#### UploadParams
|
|
1727
|
+
|
|
1728
|
+
Pydantic 통합을 포함한 매개변수 검증 모델 (레거시에서 변경 없음).
|
|
1729
|
+
|
|
1730
|
+
**필수 매개변수:**
|
|
1731
|
+
|
|
1732
|
+
- `name: str` - 업로드 이름
|
|
1733
|
+
- `path: str` - 소스 경로
|
|
1734
|
+
- `storage: int` - 스토리지 ID
|
|
1735
|
+
- `data_collection: int` - 데이터 컬렉션 ID
|
|
1736
|
+
|
|
1737
|
+
**선택적 매개변수:**
|
|
1738
|
+
|
|
1739
|
+
- `description: str | None = None` - 업로드 설명
|
|
1740
|
+
- `project: int | None = None` - 프로젝트 ID
|
|
1741
|
+
- `excel_metadata_path: str | None = None` - Excel 메타데이터 파일 경로
|
|
1742
|
+
- `is_recursive: bool = False` - 재귀적 파일 발견
|
|
1743
|
+
- `max_file_size_mb: int = 50` - 최대 파일 크기
|
|
1744
|
+
- `creating_data_unit_batch_size: int = 100` - 데이터 단위 배치 크기
|
|
1745
|
+
- `use_async_upload: bool = True` - 비동기 업로드 처리
|
|
1746
|
+
|
|
1747
|
+
**검증 기능:**
|
|
855
1748
|
|
|
856
|
-
|
|
1749
|
+
- storage/data_collection/project에 대한 실시간 API 검증
|
|
1750
|
+
- 문자열 정화 및 길이 검증
|
|
1751
|
+
- 타입 검사 및 변환
|
|
1752
|
+
- 사용자 정의 검증자 메서드
|
|
857
1753
|
|
|
858
|
-
###
|
|
1754
|
+
### 유틸리티 클래스
|
|
1755
|
+
|
|
1756
|
+
#### ExcelSecurityConfig
|
|
1757
|
+
|
|
1758
|
+
Excel 파일 처리를 위한 보안 구성입니다.
|
|
1759
|
+
|
|
1760
|
+
**구성 속성:**
|
|
1761
|
+
|
|
1762
|
+
- 파일 크기 및 메모리 제한
|
|
1763
|
+
- 행 및 열 개수 제한
|
|
1764
|
+
- 문자열 길이 제한
|
|
1765
|
+
- 환경 변수 오버라이드
|
|
1766
|
+
|
|
1767
|
+
#### ExcelMetadataUtils
|
|
1768
|
+
|
|
1769
|
+
Excel 메타데이터 처리를 위한 유틸리티 메서드입니다.
|
|
1770
|
+
|
|
1771
|
+
**주요 메서드:**
|
|
1772
|
+
|
|
1773
|
+
- `validate_and_truncate_string()` - 문자열 정화
|
|
1774
|
+
- `is_valid_filename_length()` - 파일명 검증
|
|
1775
|
+
|
|
1776
|
+
#### PathAwareJSONEncoder
|
|
1777
|
+
|
|
1778
|
+
Path 및 datetime 객체를 위한 사용자 정의 JSON 인코더입니다.
|
|
1779
|
+
|
|
1780
|
+
**지원되는 타입:**
|
|
1781
|
+
|
|
1782
|
+
- Path 객체 (문자열로 변환)
|
|
1783
|
+
- Datetime 객체 (ISO 형식)
|
|
1784
|
+
- 표준 JSON 직렬화 가능한 타입
|
|
1785
|
+
|
|
1786
|
+
### 열거형
|
|
1787
|
+
|
|
1788
|
+
#### LogCode
|
|
1789
|
+
|
|
1790
|
+
업로드 작업을 위한 타입 안전 로깅 코드입니다.
|
|
1791
|
+
|
|
1792
|
+
**카테고리:**
|
|
859
1793
|
|
|
860
|
-
-
|
|
861
|
-
-
|
|
862
|
-
-
|
|
863
|
-
-
|
|
1794
|
+
- 검증 코드 (VALIDATION_FAILED, STORAGE_VALIDATION_FAILED)
|
|
1795
|
+
- 파일 처리 코드 (NO_FILES_FOUND, FILES_DISCOVERED)
|
|
1796
|
+
- Excel 처리 코드 (EXCEL_SECURITY_VIOLATION, EXCEL_PARSING_ERROR)
|
|
1797
|
+
- 진행률 코드 (UPLOADING_DATA_FILES, GENERATING_DATA_UNITS)
|
|
1798
|
+
|
|
1799
|
+
#### UploadStatus
|
|
1800
|
+
|
|
1801
|
+
업로드 처리 상태 열거형입니다.
|
|
1802
|
+
|
|
1803
|
+
**값:**
|
|
1804
|
+
|
|
1805
|
+
- `SUCCESS = 'success'` - 작업이 성공적으로 완료됨
|
|
1806
|
+
- `FAILED = 'failed'` - 작업이 오류로 실패함
|
|
1807
|
+
|
|
1808
|
+
### 예외
|
|
1809
|
+
|
|
1810
|
+
#### ExcelSecurityError
|
|
1811
|
+
|
|
1812
|
+
Excel 파일이 보안 제약을 위반할 때 발생합니다.
|
|
1813
|
+
|
|
1814
|
+
**일반적인 원인:**
|
|
1815
|
+
|
|
1816
|
+
- 파일 크기가 제한을 초과함
|
|
1817
|
+
- 메모리 사용량 추정이 너무 높음
|
|
1818
|
+
- 내용 보안 위반
|
|
1819
|
+
|
|
1820
|
+
#### ExcelParsingError
|
|
1821
|
+
|
|
1822
|
+
Excel 파일을 파싱할 수 없을 때 발생합니다.
|
|
1823
|
+
|
|
1824
|
+
**일반적인 원인:**
|
|
1825
|
+
|
|
1826
|
+
- 파일 형식 손상
|
|
1827
|
+
- 유효하지 않은 Excel 구조
|
|
1828
|
+
- 필요한 열 누락
|
|
1829
|
+
- 내용 파싱 실패
|
|
864
1830
|
|
|
865
1831
|
## 모범 사례
|
|
866
1832
|
|
|
1833
|
+
### 아키텍처 패턴
|
|
1834
|
+
|
|
1835
|
+
1. **전략 선택**: 사용 사례 요구사항에 따라 적절한 전략을 선택하세요
|
|
1836
|
+
|
|
1837
|
+
- 깊은 디렉토리 구조에는 `RecursiveFileDiscoveryStrategy` 사용
|
|
1838
|
+
- 표준 파일 검증에는 `BasicValidationStrategy` 사용
|
|
1839
|
+
- 큰 파일 세트에는 `AsyncUploadStrategy` 사용
|
|
1840
|
+
|
|
1841
|
+
2. **단계 순서**: 논리적 단계 종속성을 유지하세요
|
|
1842
|
+
|
|
1843
|
+
- Initialize → Process Metadata → Analyze Collection → Organize Files → Validate → Upload → Generate Data Units → Cleanup
|
|
1844
|
+
- 사용자 정의 단계는 워크플로우의 적절한 지점에 삽입해야 함
|
|
1845
|
+
|
|
1846
|
+
3. **컨텍스트 관리**: 상태 공유를 위해 UploadContext를 활용하세요
|
|
1847
|
+
- 다운스트림 단계를 위해 컨텍스트에 중간 결과 저장
|
|
1848
|
+
- 단계 간 통신에 컨텍스트 사용
|
|
1849
|
+
- 정리 작업을 위해 롤백 데이터 보존
|
|
1850
|
+
|
|
867
1851
|
### 성능 최적화
|
|
868
1852
|
|
|
869
|
-
1. **배치 처리**:
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1853
|
+
1. **배치 처리**: 시스템 리소스를 기반으로 최적의 배치 크기를 구성하세요
|
|
1854
|
+
|
|
1855
|
+
```python
|
|
1856
|
+
params = {
|
|
1857
|
+
"creating_data_unit_batch_size": 200, # 메모리에 따라 조정
|
|
1858
|
+
"upload_batch_size": 10, # 업로드 전략을 위한 사용자 정의 매개변수
|
|
1859
|
+
}
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
2. **비동기 작업**: I/O 바인딩 작업에 비동기 처리를 활성화하세요
|
|
1863
|
+
|
|
1864
|
+
```python
|
|
1865
|
+
params = {
|
|
1866
|
+
"use_async_upload": True, # 네트워크 작업의 더 나은 처리량
|
|
1867
|
+
}
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
3. **메모리 관리**: 사용자 정의 전략에서 메모리 사용량을 모니터링하세요
|
|
1871
|
+
|
|
1872
|
+
- 모든 파일을 메모리에 로드하지 말고 청크 단위로 처리
|
|
1873
|
+
- 큰 파일 컬렉션에 제너레이터 사용
|
|
1874
|
+
- Excel 보안 제한을 적절히 구성
|
|
1875
|
+
|
|
1876
|
+
4. **진행률 모니터링**: 자세한 진행률 추적을 구현하세요
|
|
1877
|
+
```python
|
|
1878
|
+
# 진행률 업데이트가 포함된 사용자 정의 단계
|
|
1879
|
+
def execute(self, context):
|
|
1880
|
+
total_files = len(context.organized_files)
|
|
1881
|
+
for i, file_info in enumerate(context.organized_files):
|
|
1882
|
+
# 파일 처리
|
|
1883
|
+
progress = (i + 1) / total_files * 100
|
|
1884
|
+
context.update_metrics('custom_step', {'progress': progress})
|
|
1885
|
+
```
|
|
873
1886
|
|
|
874
1887
|
### 보안 고려사항
|
|
875
1888
|
|
|
876
|
-
1.
|
|
877
|
-
2. **Excel 보안**: 적절한 보안 제한 구성
|
|
878
|
-
3. **경로 삭제**: 파일 경로 검증 및 삭제
|
|
879
|
-
4. **콘텐츠 필터링**: 콘텐츠 기반 보안 검사 구현
|
|
1889
|
+
1. **입력 검증**: 모든 입력 매개변수 및 파일 경로를 검증하세요
|
|
880
1890
|
|
|
881
|
-
|
|
1891
|
+
```python
|
|
1892
|
+
# 전략에서 사용자 정의 검증
|
|
1893
|
+
def validate_files(self, files, context):
|
|
1894
|
+
for file_path in files:
|
|
1895
|
+
if not self._is_safe_path(file_path):
|
|
1896
|
+
return False
|
|
1897
|
+
return True
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
2. **파일 내용 보안**: 내용 기반 보안 검사를 구현하세요
|
|
1901
|
+
|
|
1902
|
+
- 악성 파일 서명 스캔
|
|
1903
|
+
- 파일 헤더가 확장자와 일치하는지 검증
|
|
1904
|
+
- 임베디드 실행 파일 검사
|
|
1905
|
+
|
|
1906
|
+
3. **Excel 보안**: 적절한 보안 제한을 구성하세요
|
|
1907
|
+
|
|
1908
|
+
```python
|
|
1909
|
+
import os
|
|
1910
|
+
os.environ['EXCEL_MAX_FILE_SIZE_MB'] = '10'
|
|
1911
|
+
os.environ['EXCEL_MAX_MEMORY_MB'] = '30'
|
|
1912
|
+
```
|
|
1913
|
+
|
|
1914
|
+
4. **경로 정화**: 모든 파일 경로를 검증하고 정화하세요
|
|
1915
|
+
- 경로 순회 공격 방지
|
|
1916
|
+
- 파일 확장자 검증
|
|
1917
|
+
- 파일 권한 확인
|
|
1918
|
+
|
|
1919
|
+
### 오류 처리 및 복구
|
|
1920
|
+
|
|
1921
|
+
1. **우아한 저하**: 부분 실패 시나리오를 위해 설계하세요
|
|
1922
|
+
|
|
1923
|
+
```python
|
|
1924
|
+
class RobustUploadStrategy(BaseUploadStrategy):
|
|
1925
|
+
def upload_files(self, files, context):
|
|
1926
|
+
successful_uploads = []
|
|
1927
|
+
failed_uploads = []
|
|
1928
|
+
|
|
1929
|
+
for file_info in files:
|
|
1930
|
+
try:
|
|
1931
|
+
result = self._upload_file(file_info)
|
|
1932
|
+
successful_uploads.append(result)
|
|
1933
|
+
except Exception as e:
|
|
1934
|
+
failed_uploads.append({'file': file_info, 'error': str(e)})
|
|
1935
|
+
# 완전히 실패하지 말고 다른 파일로 계속 진행
|
|
1936
|
+
|
|
1937
|
+
# 부분 결과로 컨텍스트 업데이트
|
|
1938
|
+
context.add_uploaded_files(successful_uploads)
|
|
1939
|
+
if failed_uploads:
|
|
1940
|
+
context.add_error(f"Failed to upload {len(failed_uploads)} files")
|
|
1941
|
+
|
|
1942
|
+
return successful_uploads
|
|
1943
|
+
```
|
|
1944
|
+
|
|
1945
|
+
2. **롤백 설계**: 포괄적인 롤백 전략을 구현하세요
|
|
1946
|
+
|
|
1947
|
+
```python
|
|
1948
|
+
def rollback(self, context):
|
|
1949
|
+
# 작업의 역순으로 정리
|
|
1950
|
+
if hasattr(self, '_created_temp_files'):
|
|
1951
|
+
for temp_file in self._created_temp_files:
|
|
1952
|
+
try:
|
|
1953
|
+
temp_file.unlink()
|
|
1954
|
+
except Exception:
|
|
1955
|
+
pass # 정리 문제로 인한 롤백 실패 방지
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
3. **자세한 로깅**: 디버깅을 위한 구조화된 로깅을 사용하세요
|
|
1959
|
+
|
|
1960
|
+
```python
|
|
1961
|
+
def execute(self, context):
|
|
1962
|
+
try:
|
|
1963
|
+
context.run.log_message_with_code(
|
|
1964
|
+
'CUSTOM_STEP_STARTED',
|
|
1965
|
+
{'step': self.name, 'file_count': len(context.organized_files)}
|
|
1966
|
+
)
|
|
1967
|
+
# 여기에 단계 로직
|
|
1968
|
+
except Exception as e:
|
|
1969
|
+
context.run.log_message_with_code(
|
|
1970
|
+
'CUSTOM_STEP_FAILED',
|
|
1971
|
+
{'step': self.name, 'error': str(e)},
|
|
1972
|
+
level=Context.DANGER
|
|
1973
|
+
)
|
|
1974
|
+
raise
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
### 개발 가이드라인
|
|
1978
|
+
|
|
1979
|
+
1. **사용자 정의 전략 개발**: 확립된 패턴을 따르세요
|
|
1980
|
+
|
|
1981
|
+
```python
|
|
1982
|
+
# 항상 적절한 기본 클래스를 확장
|
|
1983
|
+
class MyCustomStrategy(BaseValidationStrategy):
|
|
1984
|
+
def __init__(self, config=None):
|
|
1985
|
+
self.config = config or {}
|
|
1986
|
+
|
|
1987
|
+
def validate_files(self, files, context):
|
|
1988
|
+
# 검증 로직 구현
|
|
1989
|
+
return True
|
|
1990
|
+
|
|
1991
|
+
def validate_security(self, file_path):
|
|
1992
|
+
# 보안 검증 구현
|
|
1993
|
+
return True
|
|
1994
|
+
```
|
|
1995
|
+
|
|
1996
|
+
2. **테스트 전략**: 포괄적인 테스트 커버리지
|
|
1997
|
+
|
|
1998
|
+
```python
|
|
1999
|
+
# 성공 및 실패 시나리오 모두 테스트
|
|
2000
|
+
class TestCustomStrategy:
|
|
2001
|
+
def test_success_case(self):
|
|
2002
|
+
strategy = MyCustomStrategy()
|
|
2003
|
+
result = strategy.validate_files([Path('valid_file.txt')], mock_context)
|
|
2004
|
+
assert result is True
|
|
2005
|
+
|
|
2006
|
+
def test_security_failure(self):
|
|
2007
|
+
strategy = MyCustomStrategy()
|
|
2008
|
+
result = strategy.validate_security(Path('malware.exe'))
|
|
2009
|
+
assert result is False
|
|
2010
|
+
|
|
2011
|
+
def test_rollback_cleanup(self):
|
|
2012
|
+
step = MyCustomStep()
|
|
2013
|
+
step.rollback(mock_context)
|
|
2014
|
+
# 정리가 수행되었는지 확인
|
|
2015
|
+
```
|
|
2016
|
+
|
|
2017
|
+
3. **확장 지점**: 확장성을 위해 팩토리 패턴 사용
|
|
2018
|
+
|
|
2019
|
+
```python
|
|
2020
|
+
class CustomStrategyFactory(StrategyFactory):
|
|
2021
|
+
def create_validation_strategy(self, params, context=None):
|
|
2022
|
+
validation_type = params.get('validation_type', 'basic')
|
|
2023
|
+
|
|
2024
|
+
strategy_map = {
|
|
2025
|
+
'basic': BasicValidationStrategy,
|
|
2026
|
+
'strict': StrictValidationStrategy,
|
|
2027
|
+
'custom': MyCustomValidationStrategy,
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
strategy_class = strategy_map.get(validation_type, BasicValidationStrategy)
|
|
2031
|
+
return strategy_class(params)
|
|
2032
|
+
```
|
|
2033
|
+
|
|
2034
|
+
4. **구성 관리**: 환경 변수 및 매개변수 사용
|
|
2035
|
+
|
|
2036
|
+
```python
|
|
2037
|
+
class ConfigurableStep(BaseStep):
|
|
2038
|
+
def __init__(self):
|
|
2039
|
+
# 런타임 구성 허용
|
|
2040
|
+
self.batch_size = int(os.getenv('STEP_BATCH_SIZE', '50'))
|
|
2041
|
+
self.timeout = int(os.getenv('STEP_TIMEOUT_SECONDS', '300'))
|
|
2042
|
+
|
|
2043
|
+
def execute(self, context):
|
|
2044
|
+
# 구성된 값 사용
|
|
2045
|
+
batch_size = context.get_param('step_batch_size', self.batch_size)
|
|
2046
|
+
timeout = context.get_param('step_timeout', self.timeout)
|
|
2047
|
+
```
|
|
2048
|
+
|
|
2049
|
+
### 피해야 할 안티패턴
|
|
2050
|
+
|
|
2051
|
+
1. **강한 결합**: 전략을 특정 구현에 결합하지 마세요
|
|
2052
|
+
2. **상태 변형**: update() 메서드 외부에서 컨텍스트 상태를 직접 수정하지 마세요
|
|
2053
|
+
3. **예외 삼킴**: 적절한 처리 없이 예외를 잡아서 무시하지 마세요
|
|
2054
|
+
4. **블로킹 작업**: 진행률 업데이트 없이 장시간 실행되는 동기 작업을 수행하지 마세요
|
|
2055
|
+
5. **메모리 누수**: 단계 인스턴스에서 큰 객체에 대한 참조를 보유하지 마세요
|
|
2056
|
+
|
|
2057
|
+
### 문제 해결 가이드
|
|
882
2058
|
|
|
883
|
-
1.
|
|
884
|
-
2.
|
|
885
|
-
3.
|
|
886
|
-
4.
|
|
2059
|
+
1. **단계 실패**: 단계 실행 순서 및 종속성 확인
|
|
2060
|
+
2. **전략 문제**: 전략 팩토리 구성 및 매개변수 전달 확인
|
|
2061
|
+
3. **컨텍스트 문제**: 적절한 컨텍스트 업데이트 및 상태 관리 확인
|
|
2062
|
+
4. **롤백 실패**: 멱등적 롤백 작업 설계
|
|
2063
|
+
5. **성능 문제**: 배치 크기 및 비동기 작업 사용량 프로파일링
|
|
887
2064
|
|
|
888
|
-
###
|
|
2065
|
+
### 마이그레이션 체크리스트
|
|
889
2066
|
|
|
890
|
-
|
|
891
|
-
2. **타입 안전성**: Pydantic 모델 및 열거형 로깅 사용
|
|
892
|
-
3. **테스팅**: 포괄적인 단위 테스트 커버리지
|
|
893
|
-
4. **문서화**: 사용자 정의 검증자와 메서드 문서화
|
|
2067
|
+
레거시 구현에서 업그레이드할 때:
|
|
894
2068
|
|
|
895
|
-
|
|
2069
|
+
- [ ] 매개변수 이름을 `collection`에서 `data_collection`으로 업데이트
|
|
2070
|
+
- [ ] 호환성을 위한 기존 워크플로우 테스트
|
|
2071
|
+
- [ ] 새 아키텍처 기회에 대한 사용자 정의 확장 검토
|
|
2072
|
+
- [ ] 새 롤백 기능을 활용하도록 오류 처리 업데이트
|
|
2073
|
+
- [ ] 특수 요구사항에 대한 사용자 정의 전략 구현 고려
|
|
2074
|
+
- [ ] 새 워크플로우 단계를 검증하도록 테스트 케이스 업데이트
|
|
2075
|
+
- [ ] 향상된 정보에 대한 로깅 및 메트릭 수집 검토
|
|
896
2076
|
|
|
897
|
-
|
|
2077
|
+
BaseUploader 템플릿을 사용한 커스텀 업로드 플러그인 개발에 대한 자세한 정보는 [업로드 템플릿 개발하기](./developing-upload-template.md) 가이드를 참조하세요.
|