pyjallib 0.1.16__py3-none-any.whl → 0.1.19__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.
Files changed (46) hide show
  1. pyjallib/__init__.py +4 -1
  2. pyjallib/exceptions.py +75 -0
  3. pyjallib/logger.py +288 -0
  4. pyjallib/max/__init__.py +8 -0
  5. pyjallib/max/autoClavicle.py +17 -5
  6. pyjallib/max/bip.py +0 -21
  7. pyjallib/max/bone.py +21 -1
  8. pyjallib/max/boneChain.py +2 -0
  9. pyjallib/max/constraint.py +27 -2
  10. pyjallib/max/elbow.py +105 -0
  11. pyjallib/max/groinBone.py +2 -0
  12. pyjallib/max/header.py +121 -113
  13. pyjallib/max/hip.py +2 -0
  14. pyjallib/max/inguinal.py +117 -0
  15. pyjallib/max/kneeBone.py +2 -0
  16. pyjallib/max/macro/jal_macro_bone.py +221 -8
  17. pyjallib/max/macro/jal_macro_constraint.py +30 -0
  18. pyjallib/max/mirror.py +20 -13
  19. pyjallib/max/shoulder.py +173 -0
  20. pyjallib/max/twistBone.py +22 -19
  21. pyjallib/max/volumeBone.py +12 -1
  22. pyjallib/max/wrist.py +113 -0
  23. pyjallib/nameToPath.py +78 -61
  24. pyjallib/naming.py +4 -1
  25. pyjallib/perforce.py +196 -347
  26. pyjallib/progressEvent.py +75 -0
  27. pyjallib/ue5/ConfigFiles/UE5NamingConfig.json +487 -0
  28. pyjallib/ue5/__init__.py +170 -0
  29. pyjallib/ue5/disableInterchangeFrameWork.py +82 -0
  30. pyjallib/ue5/inUnreal/__init__.py +95 -0
  31. pyjallib/ue5/inUnreal/animationImporter.py +206 -0
  32. pyjallib/ue5/inUnreal/baseImporter.py +187 -0
  33. pyjallib/ue5/inUnreal/importerSettings.py +179 -0
  34. pyjallib/ue5/inUnreal/skeletalMeshImporter.py +112 -0
  35. pyjallib/ue5/inUnreal/skeletonImporter.py +105 -0
  36. pyjallib/ue5/logger.py +241 -0
  37. pyjallib/ue5/templateProcessor.py +287 -0
  38. pyjallib/ue5/templates/__init__.py +109 -0
  39. pyjallib/ue5/templates/animImportTemplate.py +22 -0
  40. pyjallib/ue5/templates/batchAnimImportTemplate.py +21 -0
  41. pyjallib/ue5/templates/skeletalMeshImportTemplate.py +22 -0
  42. pyjallib/ue5/templates/skeletonImportTemplate.py +21 -0
  43. {pyjallib-0.1.16.dist-info → pyjallib-0.1.19.dist-info}/METADATA +1 -1
  44. pyjallib-0.1.19.dist-info/RECORD +72 -0
  45. pyjallib-0.1.16.dist-info/RECORD +0 -49
  46. {pyjallib-0.1.16.dist-info → pyjallib-0.1.19.dist-info}/WHEEL +0 -0
pyjallib/perforce.py CHANGED
@@ -9,45 +9,19 @@ P4Python을 사용하는 Perforce 모듈.
9
9
  - 파일 동기화 및 업데이트 확인
10
10
  """
11
11
 
12
- import logging
13
12
  from P4 import P4, P4Exception
14
13
  import os
15
- from pathlib import Path
16
-
17
- # 로깅 설정
18
- logger = logging.getLogger(__name__)
19
-
20
- # 기본 로그 레벨은 ERROR로 설정 (디버그 모드는 생성자에서 설정)
21
- logger.setLevel(logging.ERROR)
22
-
23
- # 사용자 문서 폴더 내 로그 파일 저장
24
- log_path = os.path.join(Path.home() / "Documents", 'Perforce.log')
25
- file_handler = logging.FileHandler(log_path, encoding='utf-8')
26
- file_handler.setLevel(logging.ERROR) # 기본적으로 ERROR 레벨만 기록
27
- file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
28
- logger.addHandler(file_handler)
14
+ from .exceptions import PerforceError, ValidationError
29
15
 
30
16
 
31
17
  class Perforce:
32
18
  """P4Python을 사용하여 Perforce 작업을 수행하는 클래스."""
33
19
 
34
- def __init__(self, debug_mode: bool = False):
35
- """Perforce 인스턴스를 초기화합니다.
36
-
37
- Args:
38
- debug_mode (bool): True로 설정하면 DEBUG 레벨 로그를 활성화합니다.
39
- 기본값은 False (ERROR 레벨만 기록)
40
- """
41
- # 디버그 모드에 따라 로그 레벨 설정
42
- if debug_mode:
43
- logger.setLevel(logging.DEBUG)
44
- file_handler.setLevel(logging.DEBUG)
45
- logger.debug("디버그 모드가 활성화되었습니다.")
46
-
20
+ def __init__(self):
21
+ """Perforce 인스턴스를 초기화합니다."""
47
22
  self.p4 = P4()
48
23
  self.connected = False
49
24
  self.workspaceRoot = r""
50
- logger.info("Perforce 인스턴스 생성됨")
51
25
 
52
26
  def _is_connected(self) -> bool:
53
27
  """Perforce 서버 연결 상태를 확인합니다.
@@ -56,22 +30,18 @@ class Perforce:
56
30
  bool: 연결되어 있으면 True, 아니면 False
57
31
  """
58
32
  if not self.connected:
59
- logger.warning("Perforce 서버에 연결되지 않았습니다.")
60
33
  return False
61
34
  return True
62
-
63
- def _handle_p4_exception(self, e: P4Exception, context_msg: str = "") -> None:
64
- """P4Exception을 처리하고 로깅합니다.
65
-
66
- Args:
67
- e (P4Exception): 발생한 예외
68
- context_msg (str, optional): 예외가 발생한 컨텍스트 설명
35
+
36
+ def _ensure_connected(self) -> None:
37
+ """Perforce 서버 연결 상태를 확인하고 연결되지 않은 경우 예외를 발생시킵니다.
38
+
39
+ Raises:
40
+ PerforceError: 연결되지 않은 상태인 경우
69
41
  """
70
- logger.error(f"{context_msg} P4Exception 발생: {e}")
71
- for err in self.p4.errors:
72
- logger.error(f" P4 Error: {err}")
73
- for warn in self.p4.warnings:
74
- logger.warning(f" P4 Warning: {warn}")
42
+ if not self.connected:
43
+ error_message = "Perforce 서버에 연결되지 않았습니다."
44
+ raise PerforceError(error_message)
75
45
 
76
46
  def connect(self, workspace_name: str) -> bool:
77
47
  """지정된 워크스페이스에 연결합니다.
@@ -82,7 +52,6 @@ class Perforce:
82
52
  Returns:
83
53
  bool: 연결 성공 시 True, 실패 시 False
84
54
  """
85
- logger.info(f"'{workspace_name}' 워크스페이스에 연결 시도 중...")
86
55
  try:
87
56
  self.p4.client = workspace_name
88
57
  self.p4.connect()
@@ -97,17 +66,14 @@ class Perforce:
97
66
  root_path = os.path.normpath(root_path)
98
67
 
99
68
  self.workspaceRoot = root_path
100
- logger.info(f"워크스페이스 루트 절대 경로: {self.workspaceRoot}")
101
- except (IndexError, KeyError) as e:
102
- logger.error(f"워크스페이스 루트 경로 가져오기 실패: {e}")
69
+ except (IndexError, KeyError):
103
70
  self.workspaceRoot = ""
104
71
 
105
- logger.info(f"'{workspace_name}' 워크스페이스에 성공적으로 연결됨 (User: {self.p4.user}, Port: {self.p4.port})")
106
72
  return True
107
73
  except P4Exception as e:
108
74
  self.connected = False
109
- self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결")
110
- return False
75
+ error_message = f"'{workspace_name}' 워크스페이스 연결 실패 중 P4Exception 발생: {e}"
76
+ raise PerforceError(error_message)
111
77
 
112
78
  def get_pending_change_list(self) -> list:
113
79
  """워크스페이스의 Pending된 체인지 리스트를 가져옵니다.
@@ -115,9 +81,7 @@ class Perforce:
115
81
  Returns:
116
82
  list: 체인지 리스트 정보 딕셔너리들의 리스트
117
83
  """
118
- if not self._is_connected():
119
- return []
120
- logger.debug("Pending 체인지 리스트 조회 중...")
84
+ self._ensure_connected()
121
85
  try:
122
86
  pending_changes = self.p4.run_changes("-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
123
87
  change_numbers = [int(cl['change']) for cl in pending_changes]
@@ -129,11 +93,10 @@ class Perforce:
129
93
  if cl_info:
130
94
  change_list_info.append(cl_info)
131
95
 
132
- logger.info(f"Pending 체인지 리스트 {len(change_list_info)}개 조회 완료")
133
96
  return change_list_info
134
97
  except P4Exception as e:
135
- self._handle_p4_exception(e, "Pending 체인지 리스트 조회")
136
- return []
98
+ error_message = f"Pending 체인지 리스트 조회 실패 중 P4Exception 발생: {e}"
99
+ raise PerforceError(error_message)
137
100
 
138
101
  def create_change_list(self, description: str) -> dict:
139
102
  """새로운 체인지 리스트를 생성합니다.
@@ -144,22 +107,19 @@ class Perforce:
144
107
  Returns:
145
108
  dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리
146
109
  """
147
- if not self._is_connected():
148
- return {}
149
- logger.info(f"새 체인지 리스트 생성 시도: '{description}'")
110
+ self._ensure_connected()
150
111
  try:
151
112
  change_spec = self.p4.fetch_change()
152
113
  change_spec["Description"] = description
153
114
  result = self.p4.save_change(change_spec)
154
115
  created_change_number = int(result[0].split()[1])
155
- logger.info(f"체인지 리스트 {created_change_number} 생성 완료: '{description}'")
156
116
  return self.get_change_list_by_number(created_change_number)
157
117
  except P4Exception as e:
158
- self._handle_p4_exception(e, f"체인지 리스트 생성 ('{description}')")
159
- return {}
118
+ error_message = f"체인지 리스트 생성 실패 ('{description}') 중 P4Exception 발생: {e}"
119
+ raise PerforceError(error_message)
160
120
  except (IndexError, ValueError) as e:
161
- logger.error(f"체인지 리스트 번호 파싱 오류: {e}")
162
- return {}
121
+ error_message = f"체인지 리스트 번호 파싱 오류: {e}"
122
+ raise PerforceError(error_message)
163
123
 
164
124
  def get_change_list_by_number(self, change_list_number: int) -> dict:
165
125
  """체인지 리스트 번호로 체인지 리스트를 가져옵니다.
@@ -170,20 +130,17 @@ class Perforce:
170
130
  Returns:
171
131
  dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리
172
132
  """
173
- if not self._is_connected():
174
- return {}
175
- logger.debug(f"체인지 리스트 {change_list_number} 정보 조회 중...")
133
+ self._ensure_connected()
176
134
  try:
177
135
  cl_info = self.p4.fetch_change(change_list_number)
178
136
  if cl_info:
179
- logger.info(f"체인지 리스트 {change_list_number} 정보 조회 완료.")
180
137
  return cl_info
181
138
  else:
182
- logger.warning(f"체인지 리스트 {change_list_number}를 찾을 수 없습니다.")
183
- return {}
139
+ error_message = f"체인지 리스트 {change_list_number}를 찾을 수 없습니다."
140
+ raise PerforceError(error_message)
184
141
  except P4Exception as e:
185
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회")
186
- return {}
142
+ error_message = f"체인지 리스트 {change_list_number} 정보 조회 실패 중 P4Exception 발생: {e}"
143
+ raise PerforceError(error_message)
187
144
 
188
145
  def get_change_list_by_description(self, description: str) -> dict:
189
146
  """체인지 리스트 설명으로 체인지 리스트를 가져옵니다.
@@ -194,21 +151,17 @@ class Perforce:
194
151
  Returns:
195
152
  dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트)
196
153
  """
197
- if not self._is_connected():
198
- return {}
199
- logger.debug(f"설명으로 체인지 리스트 조회 중: '{description}'")
154
+ self._ensure_connected()
200
155
  try:
201
156
  pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
202
157
  for cl in pending_changes:
203
158
  cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip()
204
159
  if cl_desc == description.strip():
205
- logger.info(f"설명 '{description}'에 해당하는 체인지 리스트 {cl['change']} 조회 완료.")
206
160
  return self.get_change_list_by_number(int(cl['change']))
207
- logger.info(f"설명 '{description}'에 해당하는 Pending 체인지 리스트를 찾을 수 없습니다.")
208
161
  return {}
209
162
  except P4Exception as e:
210
- self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')")
211
- return {}
163
+ error_message = f"설명으로 체인지 리스트 조회 실패 ('{description}') 중 P4Exception 발생: {e}"
164
+ raise PerforceError(error_message)
212
165
 
213
166
  def get_change_list_by_description_pattern(self, description_pattern: str, exact_match: bool = False) -> list:
214
167
  """설명 패턴과 일치하는 Pending 체인지 리스트들을 가져옵니다.
@@ -221,12 +174,7 @@ class Perforce:
221
174
  Returns:
222
175
  list: 패턴과 일치하는 체인지 리스트 정보들의 리스트
223
176
  """
224
- if not self._is_connected():
225
- return []
226
-
227
- search_type = "정확히 일치" if exact_match else "패턴 포함"
228
- logger.debug(f"설명 패턴으로 체인지 리스트 조회 중 ({search_type}): '{description_pattern}'")
229
-
177
+ self._ensure_connected()
230
178
  try:
231
179
  pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
232
180
  matching_changes = []
@@ -248,17 +196,25 @@ class Perforce:
248
196
  change_info = self.get_change_list_by_number(change_number)
249
197
  if change_info:
250
198
  matching_changes.append(change_info)
251
- logger.info(f"패턴 '{description_pattern}'에 매칭되는 체인지 리스트 {change_number} 발견: '{cl_desc}'")
252
-
253
- if matching_changes:
254
- logger.info(f"패턴 '{description_pattern}'에 매칭되는 체인지 리스트 {len(matching_changes)}개 조회 완료.")
255
- else:
256
- logger.info(f"패턴 '{description_pattern}'에 매칭되는 Pending 체인지 리스트를 찾을 수 없습니다.")
257
199
 
258
200
  return matching_changes
259
201
  except P4Exception as e:
260
- self._handle_p4_exception(e, f"설명 패턴으로 체인지 리스트 조회 ('{description_pattern}')")
261
- return []
202
+ error_message = f"설명 패턴으로 체인지 리스트 조회 실패 ('{description_pattern}') 중 P4Exception 발생: {e}"
203
+ raise PerforceError(error_message)
204
+
205
+ def disconnect(self):
206
+ """Perforce 서버와의 연결을 해제합니다."""
207
+ if self.connected:
208
+ try:
209
+ self.p4.disconnect()
210
+ self.connected = False
211
+ except P4Exception as e:
212
+ error_message = f"Perforce 서버 연결 해제 중 P4Exception 발생: {e}"
213
+ raise PerforceError(error_message)
214
+
215
+ def __del__(self):
216
+ """객체가 소멸될 때 자동으로 연결을 해제합니다."""
217
+ self.disconnect()
262
218
 
263
219
  def check_files_checked_out(self, file_paths: list) -> dict:
264
220
  """파일들의 체크아웃 상태를 확인합니다.
@@ -278,13 +234,14 @@ class Perforce:
278
234
  }
279
235
  }
280
236
  """
281
- if not self._is_connected():
282
- return {}
237
+ self._ensure_connected()
283
238
  if not file_paths:
284
- logger.debug("체크아웃 상태 확인할 파일 목록이 비어있습니다.")
285
239
  return {}
286
240
 
287
- logger.debug(f"파일 체크아웃 상태 확인 (파일 {len(file_paths)}개)")
241
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
242
+ if not isinstance(file_paths, list):
243
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 is_file_checked_out() 메서드를 사용하세요."
244
+ raise ValidationError(error_msg)
288
245
 
289
246
  result = {}
290
247
  try:
@@ -311,31 +268,20 @@ class Perforce:
311
268
  file_status['user'] = file_info.get('user', '')
312
269
  file_status['workspace'] = file_info.get('client', '')
313
270
 
314
- logger.debug(f"파일 '{file_path}' 체크아웃됨: CL {file_status['change_list']}, "
315
- f"액션: {file_status['action']}, 사용자: {file_status['user']}, "
316
- f"워크스페이스: {file_status['workspace']}")
317
- else:
318
- # 파일이 체크아웃되지 않음
319
- logger.debug(f"파일 '{file_path}' 체크아웃되지 않음")
320
-
321
271
  except P4Exception as e:
322
272
  # 파일이 perforce에 없거나 접근할 수 없는 경우
323
- if any("not opened" in err.lower() or "no such file" in err.lower()
324
- for err in self.p4.errors):
325
- logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (perforce에 없거나 접근 불가)")
326
- else:
327
- self._handle_p4_exception(e, f"파일 '{file_path}' 체크아웃 상태 확인")
273
+ if not any("not opened" in err.lower() or "no such file" in err.lower()
274
+ for err in self.p4.errors):
275
+ error_message = f"파일 '{file_path}' 체크아웃 상태 확인 P4Exception 발생: {e}"
276
+ raise PerforceError(error_message)
328
277
 
329
278
  result[file_path] = file_status
330
279
 
331
- checked_out_count = sum(1 for status in result.values() if status['is_checked_out'])
332
- logger.info(f"파일 체크아웃 상태 확인 완료: 전체 {len(file_paths)}개 중 {checked_out_count}개 체크아웃됨")
333
-
334
280
  return result
335
281
 
336
282
  except P4Exception as e:
337
- self._handle_p4_exception(e, f"파일들 체크아웃 상태 확인 ({file_paths})")
338
- return {}
283
+ error_message = f"파일들 체크아웃 상태 확인 ({file_paths}) 중 P4Exception 발생: {e}"
284
+ raise PerforceError(error_message)
339
285
 
340
286
  def is_file_checked_out(self, file_path: str) -> bool:
341
287
  """단일 파일의 체크아웃 상태를 간단히 확인합니다.
@@ -359,11 +305,7 @@ class Perforce:
359
305
  Returns:
360
306
  bool: 파일이 해당 체인지 리스트에 있으면 True, 아니면 False
361
307
  """
362
- if not self._is_connected():
363
- return False
364
-
365
- logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에 있는지 확인 중...")
366
-
308
+ self._ensure_connected()
367
309
  try:
368
310
  # 해당 체인지 리스트의 파일들 가져오기
369
311
  opened_files = self.p4.run_opened("-c", change_list_number)
@@ -376,16 +318,13 @@ class Perforce:
376
318
  normalized_client_file = os.path.normpath(client_file)
377
319
 
378
320
  if normalized_client_file == normalized_file_path:
379
- logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에서 발견됨 "
380
- f"(액션: {file_info.get('action', '')})")
381
321
  return True
382
322
 
383
- logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에 없음")
384
323
  return False
385
324
 
386
325
  except P4Exception as e:
387
- self._handle_p4_exception(e, f"파일 '{file_path}' 체인지 리스트 {change_list_number} 포함 여부 확인")
388
- return False
326
+ error_message = f"파일 '{file_path}' 체인지 리스트 {change_list_number} 포함 여부 확인 중 P4Exception 발생: {e}"
327
+ raise PerforceError(error_message)
389
328
 
390
329
  def edit_change_list(self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict:
391
330
  """체인지 리스트를 편집합니다.
@@ -399,9 +338,7 @@ class Perforce:
399
338
  Returns:
400
339
  dict: 업데이트된 체인지 리스트 정보
401
340
  """
402
- if not self._is_connected():
403
- return {}
404
- logger.info(f"체인지 리스트 {change_list_number} 편집 시도...")
341
+ self._ensure_connected()
405
342
  try:
406
343
  if description is not None:
407
344
  change_spec = self.p4.fetch_change(change_list_number)
@@ -409,29 +346,28 @@ class Perforce:
409
346
  if current_description != description.strip():
410
347
  change_spec['Description'] = description
411
348
  self.p4.save_change(change_spec)
412
- logger.info(f"체인지 리스트 {change_list_number} 설명 변경 완료: '{description}'")
413
349
 
414
350
  if add_file_paths:
415
351
  for file_path in add_file_paths:
416
352
  try:
417
353
  self.p4.run_reopen("-c", change_list_number, file_path)
418
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}로 이동 완료.")
419
354
  except P4Exception as e_reopen:
420
- self._handle_p4_exception(e_reopen, f"파일 '{file_path}'을 CL {change_list_number}로 이동")
355
+ error_message = f"파일 '{file_path}'을 CL {change_list_number}로 이동 중 P4Exception 발생: {e_reopen}"
356
+ raise PerforceError(error_message)
421
357
 
422
358
  if remove_file_paths:
423
359
  for file_path in remove_file_paths:
424
360
  try:
425
361
  self.p4.run_revert("-c", change_list_number, file_path)
426
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 제거(revert) 완료.")
427
362
  except P4Exception as e_revert:
428
- self._handle_p4_exception(e_revert, f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert)")
363
+ error_message = f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert) 중 P4Exception 발생: {e_revert}"
364
+ raise PerforceError(error_message)
429
365
 
430
366
  return self.get_change_list_by_number(change_list_number)
431
367
 
432
368
  except P4Exception as e:
433
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 편집")
434
- return self.get_change_list_by_number(change_list_number)
369
+ error_message = f"체인지 리스트 {change_list_number} 편집 중 P4Exception 발생: {e}"
370
+ raise PerforceError(error_message)
435
371
 
436
372
  def _file_op(self, command: str, file_path: str, change_list_number: int, op_name: str) -> bool:
437
373
  """파일 작업을 수행하는 내부 헬퍼 함수입니다.
@@ -445,9 +381,7 @@ class Perforce:
445
381
  Returns:
446
382
  bool: 작업 성공 시 True, 실패 시 False
447
383
  """
448
- if not self._is_connected():
449
- return False
450
- logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 시도 (CL: {change_list_number})...")
384
+ self._ensure_connected()
451
385
  try:
452
386
  if command == "edit":
453
387
  self.p4.run_edit("-c", change_list_number, file_path)
@@ -456,13 +390,12 @@ class Perforce:
456
390
  elif command == "delete":
457
391
  self.p4.run_delete("-c", change_list_number, file_path)
458
392
  else:
459
- logger.error(f"지원되지 않는 파일 작업: {command}")
460
- return False
461
- logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 성공 (CL: {change_list_number}).")
393
+ error_message = f"지원되지 않는 파일 작업: {command}"
394
+ raise ValidationError(error_message)
462
395
  return True
463
396
  except P4Exception as e:
464
- self._handle_p4_exception(e, f"파일 '{file_path}' {op_name} (CL: {change_list_number})")
465
- return False
397
+ error_message = f"파일 '{file_path}' {op_name} (CL: {change_list_number}) 중 P4Exception 발생: {e}"
398
+ raise PerforceError(error_message)
466
399
 
467
400
  def checkout_file(self, file_path: str, change_list_number: int) -> bool:
468
401
  """파일을 체크아웃합니다.
@@ -487,23 +420,19 @@ class Perforce:
487
420
  bool: 모든 파일 체크아웃 성공 시 True, 하나라도 실패 시 False
488
421
  """
489
422
  if not file_paths:
490
- logger.debug("체크아웃할 파일 목록이 비어있습니다.")
491
423
  return True
492
-
493
- logger.info(f"체인지 리스트 {change_list_number}에 {len(file_paths)}개 파일 체크아웃 시도...")
494
424
 
425
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
426
+ if not isinstance(file_paths, list):
427
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 checkout_file() 메서드를 사용하세요."
428
+ raise ValidationError(error_msg)
429
+
495
430
  all_success = True
496
431
  for file_path in file_paths:
497
432
  success = self.checkout_file(file_path, change_list_number)
498
433
  if not success:
499
434
  all_success = False
500
- logger.warning(f"파일 '{file_path}' 체크아웃 실패")
501
435
 
502
- if all_success:
503
- logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에 성공적으로 체크아웃했습니다.")
504
- else:
505
- logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에 체크아웃하지 못했습니다.")
506
-
507
436
  return all_success
508
437
 
509
438
  def add_file(self, file_path: str, change_list_number: int) -> bool:
@@ -529,23 +458,19 @@ class Perforce:
529
458
  bool: 모든 파일 추가 성공 시 True, 하나라도 실패 시 False
530
459
  """
531
460
  if not file_paths:
532
- logger.debug("추가할 파일 목록이 비어있습니다.")
533
461
  return True
534
-
535
- logger.info(f"체인지 리스트 {change_list_number}에 {len(file_paths)}개 파일 추가 시도...")
536
462
 
463
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
464
+ if not isinstance(file_paths, list):
465
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 add_file() 메서드를 사용하세요."
466
+ raise ValidationError(error_msg)
467
+
537
468
  all_success = True
538
469
  for file_path in file_paths:
539
470
  success = self.add_file(file_path, change_list_number)
540
471
  if not success:
541
472
  all_success = False
542
- logger.warning(f"파일 '{file_path}' 추가 실패")
543
473
 
544
- if all_success:
545
- logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에 성공적으로 추가했습니다.")
546
- else:
547
- logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에 추가하지 못했습니다.")
548
-
549
474
  return all_success
550
475
 
551
476
  def delete_file(self, file_path: str, change_list_number: int) -> bool:
@@ -571,23 +496,19 @@ class Perforce:
571
496
  bool: 모든 파일 삭제 성공 시 True, 하나라도 실패 시 False
572
497
  """
573
498
  if not file_paths:
574
- logger.debug("삭제할 파일 목록이 비어있습니다.")
575
499
  return True
576
-
577
- logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 삭제 시도...")
578
500
 
501
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
502
+ if not isinstance(file_paths, list):
503
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 delete_file() 메서드를 사용하세요."
504
+ raise ValidationError(error_msg)
505
+
579
506
  all_success = True
580
507
  for file_path in file_paths:
581
508
  success = self.delete_file(file_path, change_list_number)
582
509
  if not success:
583
510
  all_success = False
584
- logger.warning(f"파일 '{file_path}' 삭제 실패")
585
511
 
586
- if all_success:
587
- logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에서 성공적으로 삭제했습니다.")
588
- else:
589
- logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에서 삭제하지 못했습니다.")
590
-
591
512
  return all_success
592
513
 
593
514
  def submit_change_list(self, change_list_number: int, auto_revert_unchanged: bool = True) -> bool:
@@ -601,22 +522,20 @@ class Perforce:
601
522
  Returns:
602
523
  bool: 제출 성공 시 True, 실패 시 False
603
524
  """
604
- if not self._is_connected():
605
- return False
606
- logger.info(f"체인지 리스트 {change_list_number} 제출 시도...")
525
+ self._ensure_connected()
607
526
 
608
527
  submit_success = False
609
528
  try:
610
529
  self.p4.run_submit("-c", change_list_number)
611
- logger.info(f"체인지 리스트 {change_list_number} 제출 성공.")
612
530
  submit_success = True
613
531
  except P4Exception as e:
614
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출")
615
532
  if any("nothing to submit" in err.lower() for err in self.p4.errors):
616
- logger.warning(f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다.")
617
- submit_success = False
533
+ error_message = f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다."
534
+ else:
535
+ error_message = f"체인지 리스트 {change_list_number} 제출 실패 중 P4Exception 발생: {e}"
536
+ raise PerforceError(error_message)
618
537
 
619
- # 제출 성공 여부와 관계없이 후속 작업 실행
538
+ # 제출 성공 후속 작업 실행
620
539
  try:
621
540
  # 제출 후 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트
622
541
  if auto_revert_unchanged:
@@ -626,7 +545,8 @@ class Perforce:
626
545
  # 빈 체인지 리스트 삭제
627
546
  self.delete_empty_change_list(change_list_number)
628
547
  except Exception as e:
629
- logger.error(f"체인지 리스트 {change_list_number} 제출 후 후속 작업 중 오류 발생: {e}")
548
+ error_message = f"체인지 리스트 {change_list_number} 제출 후 후속 작업 중 오류 발생: {e}"
549
+ raise PerforceError(error_message)
630
550
 
631
551
  return submit_success
632
552
 
@@ -636,13 +556,11 @@ class Perforce:
636
556
  Args:
637
557
  change_list_number (int): 체인지 리스트 번호
638
558
  """
639
- logger.debug(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일들 자동 리버트 시도...")
640
559
  try:
641
560
  # 체인지 리스트에서 체크아웃된 파일들 가져오기
642
561
  opened_files = self.p4.run_opened("-c", change_list_number)
643
562
 
644
563
  if not opened_files:
645
- logger.debug(f"체인지 리스트 {change_list_number}에 체크아웃된 파일이 없습니다.")
646
564
  return
647
565
 
648
566
  unchanged_files = []
@@ -659,45 +577,29 @@ class Perforce:
659
577
  # diff 결과가 비어있으면 변경사항이 없음
660
578
  if not diff_result:
661
579
  unchanged_files.append(file_path)
662
- logger.debug(f"파일 '{file_path}'에 변경사항이 없어 리버트 대상으로 추가")
663
- else:
664
- logger.debug(f"파일 '{file_path}'에 변경사항이 있어 리버트하지 않음")
665
580
 
666
- except P4Exception as e:
581
+ except P4Exception:
667
582
  # diff 명령 실패 시에도 리버트 대상으로 추가 (안전하게 처리)
668
583
  unchanged_files.append(file_path)
669
- logger.debug(f"파일 '{file_path}' diff 확인 실패, 리버트 대상으로 추가: {e}")
670
- else:
671
- logger.debug(f"파일 '{file_path}'는 {action} 액션이므로 리버트하지 않음")
672
584
 
673
585
  # 변경사항이 없는 파일들을 리버트
674
586
  if unchanged_files:
675
- logger.info(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 시도...")
676
587
  for file_path in unchanged_files:
677
588
  try:
678
589
  self.p4.run_revert("-c", change_list_number, file_path)
679
- logger.info(f"파일 '{file_path}' 자동 리버트 완료")
680
- except P4Exception as e:
681
- self._handle_p4_exception(e, f"파일 '{file_path}' 자동 리버트")
682
- logger.info(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 완료")
683
- else:
684
- logger.debug(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일이 없습니다.")
685
-
686
- # default change list에서도 변경사항이 없는 파일들 처리
687
- self._auto_revert_unchanged_files_in_default_changelist()
590
+ except P4Exception:
591
+ pass # 개별 파일 리버트 실패는 무시
688
592
 
689
- except P4Exception as e:
690
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 자동 리버트 처리")
593
+ except P4Exception:
594
+ pass # 자동 리버트 실패는 무시
691
595
 
692
596
  def _auto_revert_unchanged_files_in_default_changelist(self) -> None:
693
597
  """default change list에서 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트합니다."""
694
- logger.debug("default change list에서 변경사항이 없는 파일들 자동 리버트 시도...")
695
598
  try:
696
599
  # get_default_change_list를 사용해서 default change list의 파일들 가져오기
697
600
  default_cl_info = self.get_default_change_list()
698
601
 
699
602
  if not default_cl_info or not default_cl_info.get('Files'):
700
- logger.debug("default change list에 체크아웃된 파일이 없습니다.")
701
603
  return
702
604
 
703
605
  files_list = default_cl_info.get('Files', [])
@@ -711,30 +613,21 @@ class Perforce:
711
613
  # diff 결과가 비어있으면 변경사항이 없음
712
614
  if not diff_result:
713
615
  unchanged_files.append(file_path)
714
- logger.debug(f"default change list의 파일 '{file_path}'에 변경사항이 없어 리버트 대상으로 추가")
715
- else:
716
- logger.debug(f"default change list의 파일 '{file_path}'에 변경사항이 있어 리버트하지 않음")
717
616
 
718
- except P4Exception as e:
617
+ except P4Exception:
719
618
  # diff 명령 실패 시에도 리버트 대상으로 추가 (안전하게 처리)
720
619
  unchanged_files.append(file_path)
721
- logger.debug(f"default change list의 파일 '{file_path}' diff 확인 실패, 리버트 대상으로 추가: {e}")
722
620
 
723
621
  # 변경사항이 없는 파일들을 리버트
724
622
  if unchanged_files:
725
- logger.info(f"default change list에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 시도...")
726
623
  for file_path in unchanged_files:
727
624
  try:
728
625
  self.p4.run_revert(file_path)
729
- logger.info(f"default change list의 파일 '{file_path}' 자동 리버트 완료")
730
- except P4Exception as e:
731
- self._handle_p4_exception(e, f"default change list의 파일 '{file_path}' 자동 리버트")
732
- logger.info(f"default change list에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 완료")
733
- else:
734
- logger.debug("default change list에서 변경사항이 없는 파일이 없습니다.")
626
+ except P4Exception:
627
+ pass # 개별 파일 리버트 실패는 무시
735
628
 
736
- except P4Exception as e:
737
- self._handle_p4_exception(e, "default change list 자동 리버트 처리")
629
+ except P4Exception:
630
+ pass # 자동 리버트 실패는 무시
738
631
 
739
632
  def revert_change_list(self, change_list_number: int) -> bool:
740
633
  """체인지 리스트를 되돌리고 삭제합니다.
@@ -747,27 +640,22 @@ class Perforce:
747
640
  Returns:
748
641
  bool: 되돌리기 및 삭제 성공 시 True, 실패 시 False
749
642
  """
750
- if not self._is_connected():
751
- return False
752
- logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 및 삭제 시도...")
643
+ self._ensure_connected()
753
644
  try:
754
645
  # 체인지 리스트의 모든 파일 되돌리기
755
646
  self.p4.run_revert("-c", change_list_number, "//...")
756
- logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 성공.")
757
647
 
758
648
  # 빈 체인지 리스트 삭제
759
649
  try:
760
650
  self.p4.run_change("-d", change_list_number)
761
- logger.info(f"체인지 리스트 {change_list_number} 삭제 완료.")
762
651
  except P4Exception as e_delete:
763
- self._handle_p4_exception(e_delete, f"체인지 리스트 {change_list_number} 삭제")
764
- logger.warning(f"파일 되돌리기는 성공했으나 체인지 리스트 {change_list_number} 삭제에 실패했습니다.")
765
- return False
652
+ error_message = f"체인지 리스트 {change_list_number} 삭제 중 P4Exception 발생: {e_delete}"
653
+ raise PerforceError(error_message)
766
654
 
767
655
  return True
768
656
  except P4Exception as e:
769
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기")
770
- return False
657
+ error_message = f"체인지 리스트 {change_list_number} 전체 되돌리기 실패 중 P4Exception 발생: {e}"
658
+ raise PerforceError(error_message)
771
659
 
772
660
  def delete_empty_change_list(self, change_list_number: int) -> bool:
773
661
  """빈 체인지 리스트를 삭제합니다.
@@ -778,26 +666,22 @@ class Perforce:
778
666
  Returns:
779
667
  bool: 삭제 성공 시 True, 실패 시 False
780
668
  """
781
- if not self._is_connected():
782
- return False
783
-
784
- logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...")
669
+ self._ensure_connected()
785
670
  try:
786
671
  # 체인지 리스트 정보 가져오기
787
672
  change_spec = self.p4.fetch_change(change_list_number)
788
673
 
789
674
  # 파일이 있는지 확인
790
675
  if change_spec and change_spec.get('Files') and len(change_spec['Files']) > 0:
791
- logger.warning(f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다.")
792
- return False
676
+ error_message = f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다."
677
+ raise PerforceError(error_message)
793
678
 
794
679
  # 빈 체인지 리스트 삭제
795
680
  self.p4.run_change("-d", change_list_number)
796
- logger.info(f"빈 체인지 리스트 {change_list_number} 삭제 완료.")
797
681
  return True
798
682
  except P4Exception as e:
799
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제")
800
- return False
683
+ error_message = f"체인지 리스트 {change_list_number} 삭제 실패 중 P4Exception 발생: {e}"
684
+ raise PerforceError(error_message)
801
685
 
802
686
  def revert_file(self, file_path: str, change_list_number: int) -> bool:
803
687
  """체인지 리스트에서 특정 파일을 되돌립니다.
@@ -809,17 +693,13 @@ class Perforce:
809
693
  Returns:
810
694
  bool: 되돌리기 성공 시 True, 실패 시 False
811
695
  """
812
- if not self._is_connected():
813
- return False
814
-
815
- logger.info(f"파일 '{file_path}'을 체인지 리스트 {change_list_number}에서 되돌리기 시도...")
696
+ self._ensure_connected()
816
697
  try:
817
698
  self.p4.run_revert("-c", change_list_number, file_path)
818
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.")
819
699
  return True
820
700
  except P4Exception as e:
821
- self._handle_p4_exception(e, f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기")
822
- return False
701
+ error_message = f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 중 P4Exception 발생: {e}"
702
+ raise PerforceError(error_message)
823
703
 
824
704
  def revert_files(self, change_list_number: int, file_paths: list) -> bool:
825
705
  """체인지 리스트 내의 특정 파일들을 되돌립니다.
@@ -831,26 +711,21 @@ class Perforce:
831
711
  Returns:
832
712
  bool: 모든 파일 되돌리기 성공 시 True, 하나라도 실패 시 False
833
713
  """
834
- if not self._is_connected():
835
- return False
714
+ self._ensure_connected()
836
715
  if not file_paths:
837
- logger.warning("되돌릴 파일 목록이 비어있습니다.")
838
716
  return True
839
-
840
- logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...")
841
717
 
718
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
719
+ if not isinstance(file_paths, list):
720
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 revert_file() 메서드를 사용하세요."
721
+ raise ValidationError(error_msg)
722
+
842
723
  all_success = True
843
724
  for file_path in file_paths:
844
725
  success = self.revert_file(file_path, change_list_number)
845
726
  if not success:
846
727
  all_success = False
847
- logger.warning(f"파일 '{file_path}' 되돌리기 실패")
848
728
 
849
- if all_success:
850
- logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에서 성공적으로 되돌렸습니다.")
851
- else:
852
- logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에서 되돌리지 못했습니다.")
853
-
854
729
  return all_success
855
730
 
856
731
  def check_update_required(self, file_paths: list) -> bool:
@@ -863,23 +738,24 @@ class Perforce:
863
738
  Returns:
864
739
  bool: 업데이트가 필요한 파일이 있으면 True, 없으면 False
865
740
  """
866
- if not self._is_connected():
867
- return False
741
+ self._ensure_connected()
868
742
  if not file_paths:
869
- logger.debug("업데이트 필요 여부 확인할 파일/폴더 목록이 비어있습니다.")
870
743
  return False
871
744
 
745
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
746
+ if not isinstance(file_paths, list):
747
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 경로도 리스트로 감싸서 전달하세요: ['{file_paths}']"
748
+ raise ValidationError(error_msg)
749
+
872
750
  # 폴더 경로에 재귀적 와일드카드 패턴을 추가
873
751
  processed_paths = []
874
752
  for path in file_paths:
875
753
  if os.path.isdir(path):
876
754
  # 폴더 경로에 '...'(재귀) 패턴을 추가
877
755
  processed_paths.append(os.path.join(path, '...'))
878
- logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}")
879
756
  else:
880
757
  processed_paths.append(path)
881
758
 
882
- logger.debug(f"파일/폴더 업데이트 필요 여부 확인 중 (항목 {len(processed_paths)}개): {processed_paths}")
883
759
  try:
884
760
  sync_preview_results = self.p4.run_sync("-n", processed_paths)
885
761
  needs_update = False
@@ -889,26 +765,30 @@ class Perforce:
889
765
  'no such file(s)' not in result.get('depotFile', ''):
890
766
  if result.get('how') and 'syncing' in result.get('how'):
891
767
  needs_update = True
892
- logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요: {result.get('how')}")
893
768
  break
894
769
  elif result.get('action') and result.get('action') not in ['checked', 'exists']:
895
770
  needs_update = True
896
- logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요 (action: {result.get('action')})")
897
771
  break
898
772
  elif isinstance(result, str):
899
773
  if "up-to-date" not in result and "no such file(s)" not in result:
900
774
  needs_update = True
901
- logger.info(f"파일 업데이트 필요 (문자열 결과): {result}")
902
775
  break
903
776
 
904
- if needs_update:
905
- logger.info(f"지정된 파일/폴더 중 업데이트가 필요한 파일이 있습니다.")
906
- else:
907
- logger.info(f"지정된 모든 파일/폴더가 최신 상태입니다.")
908
777
  return needs_update
909
778
  except P4Exception as e:
910
- self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})")
911
- return False
779
+ # "up-to-date" 메시지는 정상적인 응답이므로 에러로 처리하지 않음
780
+ exception_str = str(e)
781
+ error_messages = [str(err) for err in self.p4.errors]
782
+ warning_messages = [str(warn) for warn in self.p4.warnings]
783
+
784
+ # P4Exception 자체 메시지나 에러/경고 메시지에서 "up-to-date" 확인
785
+ if ("up-to-date" in exception_str or
786
+ any("up-to-date" in msg for msg in error_messages) or
787
+ any("up-to-date" in msg for msg in warning_messages)):
788
+ return False
789
+ else:
790
+ error_message = f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths}) 중 P4Exception 발생: {e}"
791
+ raise PerforceError(error_message)
912
792
 
913
793
  def is_file_in_perforce(self, file_path: str) -> bool:
914
794
  """파일이 Perforce에 속하는지 확인합니다.
@@ -919,81 +799,82 @@ class Perforce:
919
799
  Returns:
920
800
  bool: 파일이 Perforce에 속하면 True, 아니면 False
921
801
  """
922
- if not self._is_connected():
923
- return False
924
-
925
- logger.debug(f"파일 '{file_path}'가 Perforce에 속하는지 확인 중...")
802
+ self._ensure_connected()
926
803
  try:
927
804
  # p4 files 명령으로 파일 정보 조회
928
805
  file_info = self.p4.run_files(file_path)
929
806
 
930
807
  # 파일 정보가 있고, 'no such file(s)' 오류가 없는 경우
931
808
  if file_info and not any("no such file(s)" in str(err).lower() for err in self.p4.errors):
932
- logger.info(f"파일 '{file_path}'가 Perforce에 존재합니다.")
933
809
  return True
934
810
  else:
935
- logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
936
811
  return False
937
812
 
938
813
  except P4Exception as e:
939
- # 파일이 존재하지 않는 경우는 일반적인 상황이므로 경고 레벨로 로깅
814
+ # 파일이 존재하지 않는 경우는 일반적인 상황이므로 False 반환
940
815
  if any("no such file(s)" in err.lower() for err in self.p4.errors):
941
- logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
942
816
  return False
943
817
  else:
944
- self._handle_p4_exception(e, f"파일 '{file_path}' Perforce 존재 여부 확인")
945
- return False
818
+ error_message = f"파일 '{file_path}' Perforce 존재 여부 확인 중 P4Exception 발생: {e}"
819
+ raise PerforceError(error_message)
946
820
 
947
821
  def sync_files(self, file_paths: list) -> bool:
948
822
  """파일이나 폴더를 동기화합니다.
949
823
 
950
824
  Args:
951
825
  file_paths (list): 동기화할 파일 또는 폴더 경로 리스트.
952
- 폴더 경로는 자동으로 재귀적으로 처리됩니다.
826
+ 폴더 경로는 자동으로 재귀적으로 처리됩니다.
953
827
 
954
828
  Returns:
955
829
  bool: 동기화 성공 시 True, 실패 시 False
956
830
  """
957
- if not self._is_connected():
958
- return False
831
+ self._ensure_connected()
959
832
  if not file_paths:
960
- logger.debug("싱크할 파일/폴더 목록이 비어있습니다.")
961
833
  return True
962
834
 
835
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
836
+ if not isinstance(file_paths, list):
837
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 경로도 리스트로 감싸서 전달하세요: ['{file_paths}']"
838
+ raise ValidationError(error_msg)
839
+
963
840
  # 폴더 경로에 재귀적 와일드카드 패턴을 추가
964
841
  processed_paths = []
965
842
  for path in file_paths:
966
843
  if os.path.isdir(path):
967
844
  # 폴더 경로에 '...'(재귀) 패턴을 추가
968
845
  processed_paths.append(os.path.join(path, '...'))
969
- logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}")
970
846
  else:
971
847
  processed_paths.append(path)
972
848
 
973
- logger.info(f"파일/폴더 싱크 시도 (항목 {len(processed_paths)}개): {processed_paths}")
974
849
  try:
975
850
  self.p4.run_sync(processed_paths)
976
- logger.info(f"파일/폴더 싱크 완료: {processed_paths}")
977
851
  return True
978
852
  except P4Exception as e:
979
- self._handle_p4_exception(e, f"파일/폴더 싱크 ({processed_paths})")
980
- return False
853
+ error_message = f"파일/폴더 싱크 실패 ({processed_paths}) 중 P4Exception 발생: {e}"
854
+ raise PerforceError(error_message)
981
855
 
982
- def disconnect(self):
983
- """Perforce 서버와의 연결을 해제합니다."""
984
- if self.connected:
985
- try:
986
- self.p4.disconnect()
987
- self.connected = False
988
- logger.info("Perforce 서버 연결 해제 완료.")
989
- except P4Exception as e:
990
- self._handle_p4_exception(e, "Perforce 서버 연결 해제")
991
- else:
992
- logger.debug("Perforce 서버에 이미 연결되지 않은 상태입니다.")
856
+ def get_default_change_list(self) -> dict:
857
+ """default change list의 정보를 가져옵니다.
993
858
 
994
- def __del__(self):
995
- """객체가 소멸될 자동으로 연결을 해제합니다."""
996
- self.disconnect()
859
+ Returns:
860
+ dict: get_change_list_by_number와 동일한 형태의 딕셔너리
861
+ """
862
+ self._ensure_connected()
863
+ try:
864
+ opened_files = self.p4.run_opened("-c", "default")
865
+ files_list = [f.get('clientFile', '') for f in opened_files]
866
+ result = {
867
+ 'Change': 'default',
868
+ 'Description': 'Default change',
869
+ 'User': getattr(self.p4, 'user', ''),
870
+ 'Client': getattr(self.p4, 'client', ''),
871
+ 'Status': 'pending',
872
+ 'Files': files_list
873
+ }
874
+ return result
875
+ except P4Exception as e:
876
+ error_message = f"default change list 정보 조회 실패 중 P4Exception 발생: {e}"
877
+ raise PerforceError(error_message)
997
878
 
998
879
  def check_files_checked_out_all_users(self, file_paths: list) -> dict:
999
880
  """파일들의 체크아웃 상태를 모든 사용자/워크스페이스에서 확인합니다.
@@ -1013,13 +894,14 @@ class Perforce:
1013
894
  }
1014
895
  }
1015
896
  """
1016
- if not self._is_connected():
1017
- return {}
897
+ self._ensure_connected()
1018
898
  if not file_paths:
1019
- logger.debug("체크아웃 상태 확인할 파일 목록이 비어있습니다.")
1020
899
  return {}
1021
900
 
1022
- logger.debug(f"파일 체크아웃 상태 확인 - 모든 사용자 (파일 {len(file_paths)}개)")
901
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
902
+ if not isinstance(file_paths, list):
903
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 get_file_checkout_info_all_users() 메서드를 사용하세요."
904
+ raise ValidationError(error_msg)
1023
905
 
1024
906
  result = {}
1025
907
  try:
@@ -1046,31 +928,20 @@ class Perforce:
1046
928
  file_status['user'] = file_info.get('user', '')
1047
929
  file_status['client'] = file_info.get('client', '')
1048
930
 
1049
- logger.debug(f"파일 '{file_path}' 체크아웃됨: CL {file_status['change_list']}, "
1050
- f"액션: {file_status['action']}, 사용자: {file_status['user']}, "
1051
- f"클라이언트: {file_status['client']}")
1052
- else:
1053
- # 파일이 체크아웃되지 않음
1054
- logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (모든 사용자)")
1055
-
1056
931
  except P4Exception as e:
1057
932
  # 파일이 perforce에 없거나 접근할 수 없는 경우
1058
- if any("not opened" in err.lower() or "no such file" in err.lower()
1059
- for err in self.p4.errors):
1060
- logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (perforce에 없거나 접근 불가)")
1061
- else:
1062
- self._handle_p4_exception(e, f"파일 '{file_path}' 체크아웃 상태 확인 (모든 사용자)")
933
+ if not any("not opened" in err.lower() or "no such file" in err.lower()
934
+ for err in self.p4.errors):
935
+ error_message = f"파일 '{file_path}' 체크아웃 상태 확인 (모든 사용자) P4Exception 발생: {e}"
936
+ raise PerforceError(error_message)
1063
937
 
1064
938
  result[file_path] = file_status
1065
939
 
1066
- checked_out_count = sum(1 for status in result.values() if status['is_checked_out'])
1067
- logger.info(f"파일 체크아웃 상태 확인 완료 (모든 사용자): 전체 {len(file_paths)}개 중 {checked_out_count}개 체크아웃됨")
1068
-
1069
940
  return result
1070
941
 
1071
942
  except P4Exception as e:
1072
- self._handle_p4_exception(e, f"파일들 체크아웃 상태 확인 - 모든 사용자 ({file_paths})")
1073
- return {}
943
+ error_message = f"파일들 체크아웃 상태 확인 - 모든 사용자 ({file_paths}) 중 P4Exception 발생: {e}"
944
+ raise PerforceError(error_message)
1074
945
 
1075
946
  def is_file_checked_out_by_others(self, file_path: str) -> bool:
1076
947
  """단일 파일이 다른 사용자/워크스페이스에 의해 체크아웃되어 있는지 확인합니다.
@@ -1156,6 +1027,11 @@ class Perforce:
1156
1027
  if not file_paths:
1157
1028
  return []
1158
1029
 
1030
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
1031
+ if not isinstance(file_paths, list):
1032
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 is_file_checked_out_by_others() 메서드를 사용하세요."
1033
+ raise ValidationError(error_msg)
1034
+
1159
1035
  result = self.check_files_checked_out_all_users(file_paths)
1160
1036
  files_by_others = []
1161
1037
 
@@ -1177,31 +1053,4 @@ class Perforce:
1177
1053
  'action': status.get('action', '')
1178
1054
  })
1179
1055
 
1180
- logger.info(f"다른 사용자에 의해 체크아웃된 파일: {len(files_by_others)}개")
1181
- return files_by_others
1182
-
1183
- def get_default_change_list(self) -> dict:
1184
- """default change list의 정보를 가져옵니다.
1185
-
1186
- Returns:
1187
- dict: get_change_list_by_number와 동일한 형태의 딕셔너리
1188
- """
1189
- if not self._is_connected():
1190
- return {}
1191
- logger.debug("default change list 정보 조회 중...")
1192
- try:
1193
- opened_files = self.p4.run_opened("-c", "default")
1194
- files_list = [f.get('clientFile', '') for f in opened_files]
1195
- result = {
1196
- 'Change': 'default',
1197
- 'Description': 'Default change',
1198
- 'User': getattr(self.p4, 'user', ''),
1199
- 'Client': getattr(self.p4, 'client', ''),
1200
- 'Status': 'pending',
1201
- 'Files': files_list
1202
- }
1203
- logger.info(f"default change list 정보 조회 완료: {len(files_list)}개 파일")
1204
- return result
1205
- except P4Exception as e:
1206
- self._handle_p4_exception(e, "default change list 정보 조회")
1207
- return {}
1056
+ return files_by_others