pyjallib 0.1.15__py3-none-any.whl → 0.1.17__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.
pyjallib/perforce.py CHANGED
@@ -9,45 +9,20 @@ 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)
29
14
 
30
15
 
31
16
  class Perforce:
32
17
  """P4Python을 사용하여 Perforce 작업을 수행하는 클래스."""
33
18
 
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
-
19
+ def __init__(self):
20
+ """Perforce 인스턴스를 초기화합니다."""
47
21
  self.p4 = P4()
48
22
  self.connected = False
49
23
  self.workspaceRoot = r""
50
- logger.info("Perforce 인스턴스 생성됨")
24
+ self.last_error = None
25
+ self.last_warnings = []
51
26
 
52
27
  def _is_connected(self) -> bool:
53
28
  """Perforce 서버 연결 상태를 확인합니다.
@@ -56,22 +31,40 @@ class Perforce:
56
31
  bool: 연결되어 있으면 True, 아니면 False
57
32
  """
58
33
  if not self.connected:
59
- logger.warning("Perforce 서버에 연결되지 않았습니다.")
34
+ self.last_error = "Perforce 서버에 연결되지 않았습니다."
60
35
  return False
61
36
  return True
62
37
 
63
38
  def _handle_p4_exception(self, e: P4Exception, context_msg: str = "") -> None:
64
- """P4Exception을 처리하고 로깅합니다.
39
+ """P4Exception을 처리하고 에러 정보를 저장합니다.
65
40
 
66
41
  Args:
67
42
  e (P4Exception): 발생한 예외
68
43
  context_msg (str, optional): 예외가 발생한 컨텍스트 설명
69
44
  """
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}")
45
+ self.last_error = f"{context_msg} 중 P4Exception 발생: {e}"
46
+ self.last_warnings = list(self.p4.warnings) if self.p4.warnings else []
47
+
48
+ def get_last_error(self) -> str:
49
+ """마지막 에러 메시지를 반환합니다.
50
+
51
+ Returns:
52
+ str: 마지막 에러 메시지. 에러가 없으면 None
53
+ """
54
+ return self.last_error
55
+
56
+ def get_last_warnings(self) -> list:
57
+ """마지막 경고 메시지들을 반환합니다.
58
+
59
+ Returns:
60
+ list: 마지막 경고 메시지들의 리스트
61
+ """
62
+ return self.last_warnings
63
+
64
+ def clear_last_error(self) -> None:
65
+ """마지막 에러와 경고 정보를 초기화합니다."""
66
+ self.last_error = None
67
+ self.last_warnings = []
75
68
 
76
69
  def connect(self, workspace_name: str) -> bool:
77
70
  """지정된 워크스페이스에 연결합니다.
@@ -82,7 +75,7 @@ class Perforce:
82
75
  Returns:
83
76
  bool: 연결 성공 시 True, 실패 시 False
84
77
  """
85
- logger.info(f"'{workspace_name}' 워크스페이스에 연결 시도 중...")
78
+ self.clear_last_error()
86
79
  try:
87
80
  self.p4.client = workspace_name
88
81
  self.p4.connect()
@@ -97,16 +90,14 @@ class Perforce:
97
90
  root_path = os.path.normpath(root_path)
98
91
 
99
92
  self.workspaceRoot = root_path
100
- logger.info(f"워크스페이스 루트 절대 경로: {self.workspaceRoot}")
101
93
  except (IndexError, KeyError) as e:
102
- logger.error(f"워크스페이스 루트 경로 가져오기 실패: {e}")
94
+ self.last_error = f"워크스페이스 루트 경로 가져오기 실패: {e}"
103
95
  self.workspaceRoot = ""
104
96
 
105
- logger.info(f"'{workspace_name}' 워크스페이스에 성공적으로 연결됨 (User: {self.p4.user}, Port: {self.p4.port})")
106
97
  return True
107
98
  except P4Exception as e:
108
99
  self.connected = False
109
- self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결")
100
+ self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결 실패")
110
101
  return False
111
102
 
112
103
  def get_pending_change_list(self) -> list:
@@ -117,7 +108,7 @@ class Perforce:
117
108
  """
118
109
  if not self._is_connected():
119
110
  return []
120
- logger.debug("Pending 체인지 리스트 조회 중...")
111
+ self.clear_last_error()
121
112
  try:
122
113
  pending_changes = self.p4.run_changes("-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
123
114
  change_numbers = [int(cl['change']) for cl in pending_changes]
@@ -129,10 +120,9 @@ class Perforce:
129
120
  if cl_info:
130
121
  change_list_info.append(cl_info)
131
122
 
132
- logger.info(f"Pending 체인지 리스트 {len(change_list_info)}개 조회 완료")
133
123
  return change_list_info
134
124
  except P4Exception as e:
135
- self._handle_p4_exception(e, "Pending 체인지 리스트 조회")
125
+ self._handle_p4_exception(e, "Pending 체인지 리스트 조회 실패")
136
126
  return []
137
127
 
138
128
  def create_change_list(self, description: str) -> dict:
@@ -146,19 +136,18 @@ class Perforce:
146
136
  """
147
137
  if not self._is_connected():
148
138
  return {}
149
- logger.info(f"새 체인지 리스트 생성 시도: '{description}'")
139
+ self.clear_last_error()
150
140
  try:
151
141
  change_spec = self.p4.fetch_change()
152
142
  change_spec["Description"] = description
153
143
  result = self.p4.save_change(change_spec)
154
144
  created_change_number = int(result[0].split()[1])
155
- logger.info(f"체인지 리스트 {created_change_number} 생성 완료: '{description}'")
156
145
  return self.get_change_list_by_number(created_change_number)
157
146
  except P4Exception as e:
158
- self._handle_p4_exception(e, f"체인지 리스트 생성 ('{description}')")
147
+ self._handle_p4_exception(e, f"체인지 리스트 생성 실패 ('{description}')")
159
148
  return {}
160
149
  except (IndexError, ValueError) as e:
161
- logger.error(f"체인지 리스트 번호 파싱 오류: {e}")
150
+ self.last_error = f"체인지 리스트 번호 파싱 오류: {e}"
162
151
  return {}
163
152
 
164
153
  def get_change_list_by_number(self, change_list_number: int) -> dict:
@@ -172,17 +161,16 @@ class Perforce:
172
161
  """
173
162
  if not self._is_connected():
174
163
  return {}
175
- logger.debug(f"체인지 리스트 {change_list_number} 정보 조회 중...")
164
+ self.clear_last_error()
176
165
  try:
177
166
  cl_info = self.p4.fetch_change(change_list_number)
178
167
  if cl_info:
179
- logger.info(f"체인지 리스트 {change_list_number} 정보 조회 완료.")
180
168
  return cl_info
181
169
  else:
182
- logger.warning(f"체인지 리스트 {change_list_number}를 찾을 수 없습니다.")
170
+ self.last_error = f"체인지 리스트 {change_list_number}를 찾을 수 없습니다."
183
171
  return {}
184
172
  except P4Exception as e:
185
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회")
173
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회 실패")
186
174
  return {}
187
175
 
188
176
  def get_change_list_by_description(self, description: str) -> dict:
@@ -196,18 +184,16 @@ class Perforce:
196
184
  """
197
185
  if not self._is_connected():
198
186
  return {}
199
- logger.debug(f"설명으로 체인지 리스트 조회 중: '{description}'")
187
+ self.clear_last_error()
200
188
  try:
201
189
  pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
202
190
  for cl in pending_changes:
203
191
  cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip()
204
192
  if cl_desc == description.strip():
205
- logger.info(f"설명 '{description}'에 해당하는 체인지 리스트 {cl['change']} 조회 완료.")
206
193
  return self.get_change_list_by_number(int(cl['change']))
207
- logger.info(f"설명 '{description}'에 해당하는 Pending 체인지 리스트를 찾을 수 없습니다.")
208
194
  return {}
209
195
  except P4Exception as e:
210
- self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')")
196
+ self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 실패 ('{description}')")
211
197
  return {}
212
198
 
213
199
  def get_change_list_by_description_pattern(self, description_pattern: str, exact_match: bool = False) -> list:
@@ -224,9 +210,7 @@ class Perforce:
224
210
  if not self._is_connected():
225
211
  return []
226
212
 
227
- search_type = "정확히 일치" if exact_match else "패턴 포함"
228
- logger.debug(f"설명 패턴으로 체인지 리스트 조회 중 ({search_type}): '{description_pattern}'")
229
-
213
+ self.clear_last_error()
230
214
  try:
231
215
  pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
232
216
  matching_changes = []
@@ -248,18 +232,25 @@ class Perforce:
248
232
  change_info = self.get_change_list_by_number(change_number)
249
233
  if change_info:
250
234
  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
235
 
258
236
  return matching_changes
259
237
  except P4Exception as e:
260
- self._handle_p4_exception(e, f"설명 패턴으로 체인지 리스트 조회 ('{description_pattern}')")
238
+ self._handle_p4_exception(e, f"설명 패턴으로 체인지 리스트 조회 실패 ('{description_pattern}')")
261
239
  return []
262
240
 
241
+ def disconnect(self):
242
+ """Perforce 서버와의 연결을 해제합니다."""
243
+ if self.connected:
244
+ try:
245
+ self.p4.disconnect()
246
+ self.connected = False
247
+ except P4Exception as e:
248
+ self._handle_p4_exception(e, "Perforce 서버 연결 해제")
249
+
250
+ def __del__(self):
251
+ """객체가 소멸될 때 자동으로 연결을 해제합니다."""
252
+ self.disconnect()
253
+
263
254
  def check_files_checked_out(self, file_paths: list) -> dict:
264
255
  """파일들의 체크아웃 상태를 확인합니다.
265
256
 
@@ -281,11 +272,15 @@ class Perforce:
281
272
  if not self._is_connected():
282
273
  return {}
283
274
  if not file_paths:
284
- logger.debug("체크아웃 상태 확인할 파일 목록이 비어있습니다.")
285
275
  return {}
286
276
 
287
- logger.debug(f"파일 체크아웃 상태 확인 (파일 {len(file_paths)}개)")
277
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
278
+ if not isinstance(file_paths, list):
279
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 is_file_checked_out() 메서드를 사용하세요."
280
+ self.last_error = error_msg
281
+ raise TypeError(error_msg)
288
282
 
283
+ self.clear_last_error()
289
284
  result = {}
290
285
  try:
291
286
  # 각 파일의 상태 확인
@@ -311,26 +306,14 @@ class Perforce:
311
306
  file_status['user'] = file_info.get('user', '')
312
307
  file_status['workspace'] = file_info.get('client', '')
313
308
 
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
309
  except P4Exception as e:
322
310
  # 파일이 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:
311
+ if not any("not opened" in err.lower() or "no such file" in err.lower()
312
+ for err in self.p4.errors):
327
313
  self._handle_p4_exception(e, f"파일 '{file_path}' 체크아웃 상태 확인")
328
314
 
329
315
  result[file_path] = file_status
330
316
 
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
317
  return result
335
318
 
336
319
  except P4Exception as e:
@@ -362,8 +345,7 @@ class Perforce:
362
345
  if not self._is_connected():
363
346
  return False
364
347
 
365
- logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에 있는지 확인 중...")
366
-
348
+ self.clear_last_error()
367
349
  try:
368
350
  # 해당 체인지 리스트의 파일들 가져오기
369
351
  opened_files = self.p4.run_opened("-c", change_list_number)
@@ -376,11 +358,8 @@ class Perforce:
376
358
  normalized_client_file = os.path.normpath(client_file)
377
359
 
378
360
  if normalized_client_file == normalized_file_path:
379
- logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에서 발견됨 "
380
- f"(액션: {file_info.get('action', '')})")
381
361
  return True
382
362
 
383
- logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에 없음")
384
363
  return False
385
364
 
386
365
  except P4Exception as e:
@@ -401,7 +380,7 @@ class Perforce:
401
380
  """
402
381
  if not self._is_connected():
403
382
  return {}
404
- logger.info(f"체인지 리스트 {change_list_number} 편집 시도...")
383
+ self.clear_last_error()
405
384
  try:
406
385
  if description is not None:
407
386
  change_spec = self.p4.fetch_change(change_list_number)
@@ -409,13 +388,11 @@ class Perforce:
409
388
  if current_description != description.strip():
410
389
  change_spec['Description'] = description
411
390
  self.p4.save_change(change_spec)
412
- logger.info(f"체인지 리스트 {change_list_number} 설명 변경 완료: '{description}'")
413
391
 
414
392
  if add_file_paths:
415
393
  for file_path in add_file_paths:
416
394
  try:
417
395
  self.p4.run_reopen("-c", change_list_number, file_path)
418
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}로 이동 완료.")
419
396
  except P4Exception as e_reopen:
420
397
  self._handle_p4_exception(e_reopen, f"파일 '{file_path}'을 CL {change_list_number}로 이동")
421
398
 
@@ -423,7 +400,6 @@ class Perforce:
423
400
  for file_path in remove_file_paths:
424
401
  try:
425
402
  self.p4.run_revert("-c", change_list_number, file_path)
426
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 제거(revert) 완료.")
427
403
  except P4Exception as e_revert:
428
404
  self._handle_p4_exception(e_revert, f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert)")
429
405
 
@@ -447,7 +423,7 @@ class Perforce:
447
423
  """
448
424
  if not self._is_connected():
449
425
  return False
450
- logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 시도 (CL: {change_list_number})...")
426
+ self.clear_last_error()
451
427
  try:
452
428
  if command == "edit":
453
429
  self.p4.run_edit("-c", change_list_number, file_path)
@@ -456,9 +432,8 @@ class Perforce:
456
432
  elif command == "delete":
457
433
  self.p4.run_delete("-c", change_list_number, file_path)
458
434
  else:
459
- logger.error(f"지원되지 않는 파일 작업: {command}")
435
+ self.last_error = f"지원되지 않는 파일 작업: {command}"
460
436
  return False
461
- logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 성공 (CL: {change_list_number}).")
462
437
  return True
463
438
  except P4Exception as e:
464
439
  self._handle_p4_exception(e, f"파일 '{file_path}' {op_name} (CL: {change_list_number})")
@@ -487,23 +462,20 @@ class Perforce:
487
462
  bool: 모든 파일 체크아웃 성공 시 True, 하나라도 실패 시 False
488
463
  """
489
464
  if not file_paths:
490
- logger.debug("체크아웃할 파일 목록이 비어있습니다.")
491
465
  return True
492
-
493
- logger.info(f"체인지 리스트 {change_list_number}에 {len(file_paths)}개 파일 체크아웃 시도...")
494
466
 
467
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
468
+ if not isinstance(file_paths, list):
469
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 checkout_file() 메서드를 사용하세요."
470
+ self.last_error = error_msg
471
+ raise TypeError(error_msg)
472
+
495
473
  all_success = True
496
474
  for file_path in file_paths:
497
475
  success = self.checkout_file(file_path, change_list_number)
498
476
  if not success:
499
477
  all_success = False
500
- logger.warning(f"파일 '{file_path}' 체크아웃 실패")
501
478
 
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
479
  return all_success
508
480
 
509
481
  def add_file(self, file_path: str, change_list_number: int) -> bool:
@@ -529,23 +501,20 @@ class Perforce:
529
501
  bool: 모든 파일 추가 성공 시 True, 하나라도 실패 시 False
530
502
  """
531
503
  if not file_paths:
532
- logger.debug("추가할 파일 목록이 비어있습니다.")
533
504
  return True
534
-
535
- logger.info(f"체인지 리스트 {change_list_number}에 {len(file_paths)}개 파일 추가 시도...")
536
505
 
506
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
507
+ if not isinstance(file_paths, list):
508
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 add_file() 메서드를 사용하세요."
509
+ self.last_error = error_msg
510
+ raise TypeError(error_msg)
511
+
537
512
  all_success = True
538
513
  for file_path in file_paths:
539
514
  success = self.add_file(file_path, change_list_number)
540
515
  if not success:
541
516
  all_success = False
542
- logger.warning(f"파일 '{file_path}' 추가 실패")
543
517
 
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
518
  return all_success
550
519
 
551
520
  def delete_file(self, file_path: str, change_list_number: int) -> bool:
@@ -571,23 +540,20 @@ class Perforce:
571
540
  bool: 모든 파일 삭제 성공 시 True, 하나라도 실패 시 False
572
541
  """
573
542
  if not file_paths:
574
- logger.debug("삭제할 파일 목록이 비어있습니다.")
575
543
  return True
576
-
577
- logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 삭제 시도...")
578
544
 
545
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
546
+ if not isinstance(file_paths, list):
547
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 delete_file() 메서드를 사용하세요."
548
+ self.last_error = error_msg
549
+ raise TypeError(error_msg)
550
+
579
551
  all_success = True
580
552
  for file_path in file_paths:
581
553
  success = self.delete_file(file_path, change_list_number)
582
554
  if not success:
583
555
  all_success = False
584
- logger.warning(f"파일 '{file_path}' 삭제 실패")
585
556
 
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
557
  return all_success
592
558
 
593
559
  def submit_change_list(self, change_list_number: int, auto_revert_unchanged: bool = True) -> bool:
@@ -603,21 +569,32 @@ class Perforce:
603
569
  """
604
570
  if not self._is_connected():
605
571
  return False
606
- logger.info(f"체인지 리스트 {change_list_number} 제출 시도...")
572
+ self.clear_last_error()
573
+
574
+ submit_success = False
607
575
  try:
608
576
  self.p4.run_submit("-c", change_list_number)
609
- logger.info(f"체인지 리스트 {change_list_number} 제출 성공.")
610
-
577
+ submit_success = True
578
+ except P4Exception as e:
579
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출 실패")
580
+ if any("nothing to submit" in err.lower() for err in self.p4.errors):
581
+ self.last_error = f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다."
582
+ submit_success = False
583
+
584
+ # 제출 성공 여부와 관계없이 후속 작업 실행
585
+ try:
611
586
  # 제출 후 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트
612
587
  if auto_revert_unchanged:
613
588
  self._auto_revert_unchanged_files(change_list_number)
589
+ self._auto_revert_unchanged_files_in_default_changelist()
614
590
 
615
- return True
616
- except P4Exception as e:
617
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출")
618
- if any("nothing to submit" in err.lower() for err in self.p4.errors):
619
- logger.warning(f"체인지 리스트 {change_list_number} 제출할 파일이 없습니다.")
620
- return False
591
+ # 빈 체인지 리스트 삭제
592
+ self.delete_empty_change_list(change_list_number)
593
+ except Exception as e:
594
+ if not self.last_error: # 기존 에러가 없는 경우만 설정
595
+ self.last_error = f"체인지 리스트 {change_list_number} 제출 후속 작업 중 오류 발생: {e}"
596
+
597
+ return submit_success
621
598
 
622
599
  def _auto_revert_unchanged_files(self, change_list_number: int) -> None:
623
600
  """제출 후 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트합니다.
@@ -625,13 +602,11 @@ class Perforce:
625
602
  Args:
626
603
  change_list_number (int): 체인지 리스트 번호
627
604
  """
628
- logger.debug(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일들 자동 리버트 시도...")
629
605
  try:
630
606
  # 체인지 리스트에서 체크아웃된 파일들 가져오기
631
607
  opened_files = self.p4.run_opened("-c", change_list_number)
632
608
 
633
609
  if not opened_files:
634
- logger.debug(f"체인지 리스트 {change_list_number}에 체크아웃된 파일이 없습니다.")
635
610
  return
636
611
 
637
612
  unchanged_files = []
@@ -648,87 +623,57 @@ class Perforce:
648
623
  # diff 결과가 비어있으면 변경사항이 없음
649
624
  if not diff_result:
650
625
  unchanged_files.append(file_path)
651
- logger.debug(f"파일 '{file_path}'에 변경사항이 없어 리버트 대상으로 추가")
652
- else:
653
- logger.debug(f"파일 '{file_path}'에 변경사항이 있어 리버트하지 않음")
654
626
 
655
- except P4Exception as e:
627
+ except P4Exception:
656
628
  # diff 명령 실패 시에도 리버트 대상으로 추가 (안전하게 처리)
657
629
  unchanged_files.append(file_path)
658
- logger.debug(f"파일 '{file_path}' diff 확인 실패, 리버트 대상으로 추가: {e}")
659
- else:
660
- logger.debug(f"파일 '{file_path}'는 {action} 액션이므로 리버트하지 않음")
661
630
 
662
631
  # 변경사항이 없는 파일들을 리버트
663
632
  if unchanged_files:
664
- logger.info(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 시도...")
665
633
  for file_path in unchanged_files:
666
634
  try:
667
635
  self.p4.run_revert("-c", change_list_number, file_path)
668
- logger.info(f"파일 '{file_path}' 자동 리버트 완료")
669
- except P4Exception as e:
670
- self._handle_p4_exception(e, f"파일 '{file_path}' 자동 리버트")
671
- logger.info(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 완료")
672
- else:
673
- logger.debug(f"체인지 리스트 {change_list_number}에서 변경사항이 없는 파일이 없습니다.")
674
-
675
- # default change list에서도 변경사항이 없는 파일들 처리
676
- self._auto_revert_unchanged_files_in_default_changelist()
636
+ except P4Exception:
637
+ pass # 개별 파일 리버트 실패는 무시
677
638
 
678
- except P4Exception as e:
679
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 자동 리버트 처리")
639
+ except P4Exception:
640
+ pass # 자동 리버트 실패는 무시
680
641
 
681
642
  def _auto_revert_unchanged_files_in_default_changelist(self) -> None:
682
643
  """default change list에서 변경사항이 없는 체크아웃된 파일들을 자동으로 리버트합니다."""
683
- logger.debug("default change list에서 변경사항이 없는 파일들 자동 리버트 시도...")
684
644
  try:
685
- # default change list에서 체크아웃된 파일들 가져오기
686
- opened_files = self.p4.run_opened("-c", "default")
645
+ # get_default_change_list를 사용해서 default change list 파일들 가져오기
646
+ default_cl_info = self.get_default_change_list()
687
647
 
688
- if not opened_files:
689
- logger.debug("default change list에 체크아웃된 파일이 없습니다.")
648
+ if not default_cl_info or not default_cl_info.get('Files'):
690
649
  return
691
650
 
651
+ files_list = default_cl_info.get('Files', [])
692
652
  unchanged_files = []
693
- for file_info in opened_files:
694
- file_path = file_info.get('clientFile', '')
695
- action = file_info.get('action', '')
696
-
697
- # edit 액션의 파일만 확인 (add, delete는 변경사항이 있음)
698
- if action == 'edit':
699
- try:
700
- # p4 diff 명령으로 파일의 변경사항 확인
701
- diff_result = self.p4.run_diff("-sa", file_path)
702
-
703
- # diff 결과가 비어있으면 변경사항이 없음
704
- if not diff_result:
705
- unchanged_files.append(file_path)
706
- logger.debug(f"default change list의 파일 '{file_path}'에 변경사항이 없어 리버트 대상으로 추가")
707
- else:
708
- logger.debug(f"default change list의 파일 '{file_path}'에 변경사항이 있어 리버트하지 않음")
709
-
710
- except P4Exception as e:
711
- # diff 명령 실패 시에도 리버트 대상으로 추가 (안전하게 처리)
653
+
654
+ for file_path in files_list:
655
+ try:
656
+ # p4 diff 명령으로 파일의 변경사항 확인
657
+ diff_result = self.p4.run_diff("-sa", file_path)
658
+
659
+ # diff 결과가 비어있으면 변경사항이 없음
660
+ if not diff_result:
712
661
  unchanged_files.append(file_path)
713
- logger.debug(f"default change list의 파일 '{file_path}' diff 확인 실패, 리버트 대상으로 추가: {e}")
714
- else:
715
- logger.debug(f"default change list의 파일 '{file_path}'는 {action} 액션이므로 리버트하지 않음")
662
+
663
+ except P4Exception:
664
+ # diff 명령 실패 시에도 리버트 대상으로 추가 (안전하게 처리)
665
+ unchanged_files.append(file_path)
716
666
 
717
667
  # 변경사항이 없는 파일들을 리버트
718
668
  if unchanged_files:
719
- logger.info(f"default change list에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 시도...")
720
669
  for file_path in unchanged_files:
721
670
  try:
722
671
  self.p4.run_revert(file_path)
723
- logger.info(f"default change list의 파일 '{file_path}' 자동 리버트 완료")
724
- except P4Exception as e:
725
- self._handle_p4_exception(e, f"default change list의 파일 '{file_path}' 자동 리버트")
726
- logger.info(f"default change list에서 변경사항이 없는 파일 {len(unchanged_files)}개 자동 리버트 완료")
727
- else:
728
- logger.debug("default change list에서 변경사항이 없는 파일이 없습니다.")
672
+ except P4Exception:
673
+ pass # 개별 파일 리버트 실패는 무시
729
674
 
730
- except P4Exception as e:
731
- self._handle_p4_exception(e, "default change list 자동 리버트 처리")
675
+ except P4Exception:
676
+ pass # 자동 리버트 실패는 무시
732
677
 
733
678
  def revert_change_list(self, change_list_number: int) -> bool:
734
679
  """체인지 리스트를 되돌리고 삭제합니다.
@@ -743,24 +688,21 @@ class Perforce:
743
688
  """
744
689
  if not self._is_connected():
745
690
  return False
746
- logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 및 삭제 시도...")
691
+ self.clear_last_error()
747
692
  try:
748
693
  # 체인지 리스트의 모든 파일 되돌리기
749
694
  self.p4.run_revert("-c", change_list_number, "//...")
750
- logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 성공.")
751
695
 
752
696
  # 빈 체인지 리스트 삭제
753
697
  try:
754
698
  self.p4.run_change("-d", change_list_number)
755
- logger.info(f"체인지 리스트 {change_list_number} 삭제 완료.")
756
699
  except P4Exception as e_delete:
757
700
  self._handle_p4_exception(e_delete, f"체인지 리스트 {change_list_number} 삭제")
758
- logger.warning(f"파일 되돌리기는 성공했으나 체인지 리스트 {change_list_number} 삭제에 실패했습니다.")
759
701
  return False
760
702
 
761
703
  return True
762
704
  except P4Exception as e:
763
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기")
705
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기 실패")
764
706
  return False
765
707
 
766
708
  def delete_empty_change_list(self, change_list_number: int) -> bool:
@@ -775,22 +717,21 @@ class Perforce:
775
717
  if not self._is_connected():
776
718
  return False
777
719
 
778
- logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...")
720
+ self.clear_last_error()
779
721
  try:
780
722
  # 체인지 리스트 정보 가져오기
781
723
  change_spec = self.p4.fetch_change(change_list_number)
782
724
 
783
725
  # 파일이 있는지 확인
784
726
  if change_spec and change_spec.get('Files') and len(change_spec['Files']) > 0:
785
- logger.warning(f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다.")
727
+ self.last_error = f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다."
786
728
  return False
787
729
 
788
730
  # 빈 체인지 리스트 삭제
789
731
  self.p4.run_change("-d", change_list_number)
790
- logger.info(f"빈 체인지 리스트 {change_list_number} 삭제 완료.")
791
732
  return True
792
733
  except P4Exception as e:
793
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제")
734
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제 실패")
794
735
  return False
795
736
 
796
737
  def revert_file(self, file_path: str, change_list_number: int) -> bool:
@@ -806,10 +747,9 @@ class Perforce:
806
747
  if not self._is_connected():
807
748
  return False
808
749
 
809
- logger.info(f"파일 '{file_path}'을 체인지 리스트 {change_list_number}에서 되돌리기 시도...")
750
+ self.clear_last_error()
810
751
  try:
811
752
  self.p4.run_revert("-c", change_list_number, file_path)
812
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.")
813
753
  return True
814
754
  except P4Exception as e:
815
755
  self._handle_p4_exception(e, f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기")
@@ -828,23 +768,20 @@ class Perforce:
828
768
  if not self._is_connected():
829
769
  return False
830
770
  if not file_paths:
831
- logger.warning("되돌릴 파일 목록이 비어있습니다.")
832
771
  return True
833
-
834
- logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...")
835
772
 
773
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
774
+ if not isinstance(file_paths, list):
775
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 revert_file() 메서드를 사용하세요."
776
+ self.last_error = error_msg
777
+ raise TypeError(error_msg)
778
+
836
779
  all_success = True
837
780
  for file_path in file_paths:
838
781
  success = self.revert_file(file_path, change_list_number)
839
782
  if not success:
840
783
  all_success = False
841
- logger.warning(f"파일 '{file_path}' 되돌리기 실패")
842
784
 
843
- if all_success:
844
- logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에서 성공적으로 되돌렸습니다.")
845
- else:
846
- logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에서 되돌리지 못했습니다.")
847
-
848
785
  return all_success
849
786
 
850
787
  def check_update_required(self, file_paths: list) -> bool:
@@ -860,20 +797,24 @@ class Perforce:
860
797
  if not self._is_connected():
861
798
  return False
862
799
  if not file_paths:
863
- logger.debug("업데이트 필요 여부 확인할 파일/폴더 목록이 비어있습니다.")
864
800
  return False
865
801
 
802
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
803
+ if not isinstance(file_paths, list):
804
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 경로도 리스트로 감싸서 전달하세요: ['{file_paths}']"
805
+ self.last_error = error_msg
806
+ raise TypeError(error_msg)
807
+
866
808
  # 폴더 경로에 재귀적 와일드카드 패턴을 추가
867
809
  processed_paths = []
868
810
  for path in file_paths:
869
811
  if os.path.isdir(path):
870
812
  # 폴더 경로에 '...'(재귀) 패턴을 추가
871
813
  processed_paths.append(os.path.join(path, '...'))
872
- logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}")
873
814
  else:
874
815
  processed_paths.append(path)
875
816
 
876
- logger.debug(f"파일/폴더 업데이트 필요 여부 확인 중 (항목 {len(processed_paths)}개): {processed_paths}")
817
+ self.clear_last_error()
877
818
  try:
878
819
  sync_preview_results = self.p4.run_sync("-n", processed_paths)
879
820
  needs_update = False
@@ -883,26 +824,30 @@ class Perforce:
883
824
  'no such file(s)' not in result.get('depotFile', ''):
884
825
  if result.get('how') and 'syncing' in result.get('how'):
885
826
  needs_update = True
886
- logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요: {result.get('how')}")
887
827
  break
888
828
  elif result.get('action') and result.get('action') not in ['checked', 'exists']:
889
829
  needs_update = True
890
- logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요 (action: {result.get('action')})")
891
830
  break
892
831
  elif isinstance(result, str):
893
832
  if "up-to-date" not in result and "no such file(s)" not in result:
894
833
  needs_update = True
895
- logger.info(f"파일 업데이트 필요 (문자열 결과): {result}")
896
834
  break
897
835
 
898
- if needs_update:
899
- logger.info(f"지정된 파일/폴더 중 업데이트가 필요한 파일이 있습니다.")
900
- else:
901
- logger.info(f"지정된 모든 파일/폴더가 최신 상태입니다.")
902
836
  return needs_update
903
837
  except P4Exception as e:
904
- self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})")
905
- return False
838
+ # "up-to-date" 메시지는 정상적인 응답이므로 에러로 처리하지 않음
839
+ exception_str = str(e)
840
+ error_messages = [str(err) for err in self.p4.errors]
841
+ warning_messages = [str(warn) for warn in self.p4.warnings]
842
+
843
+ # P4Exception 자체 메시지나 에러/경고 메시지에서 "up-to-date" 확인
844
+ if ("up-to-date" in exception_str or
845
+ any("up-to-date" in msg for msg in error_messages) or
846
+ any("up-to-date" in msg for msg in warning_messages)):
847
+ return False
848
+ else:
849
+ self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})")
850
+ return False
906
851
 
907
852
  def is_file_in_perforce(self, file_path: str) -> bool:
908
853
  """파일이 Perforce에 속하는지 확인합니다.
@@ -916,23 +861,20 @@ class Perforce:
916
861
  if not self._is_connected():
917
862
  return False
918
863
 
919
- logger.debug(f"파일 '{file_path}'가 Perforce에 속하는지 확인 중...")
864
+ self.clear_last_error()
920
865
  try:
921
866
  # p4 files 명령으로 파일 정보 조회
922
867
  file_info = self.p4.run_files(file_path)
923
868
 
924
869
  # 파일 정보가 있고, 'no such file(s)' 오류가 없는 경우
925
870
  if file_info and not any("no such file(s)" in str(err).lower() for err in self.p4.errors):
926
- logger.info(f"파일 '{file_path}'가 Perforce에 존재합니다.")
927
871
  return True
928
872
  else:
929
- logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
930
873
  return False
931
874
 
932
875
  except P4Exception as e:
933
- # 파일이 존재하지 않는 경우는 일반적인 상황이므로 경고 레벨로 로깅
876
+ # 파일이 존재하지 않는 경우는 일반적인 상황이므로 False 반환
934
877
  if any("no such file(s)" in err.lower() for err in self.p4.errors):
935
- logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
936
878
  return False
937
879
  else:
938
880
  self._handle_p4_exception(e, f"파일 '{file_path}' Perforce 존재 여부 확인")
@@ -943,7 +885,7 @@ class Perforce:
943
885
 
944
886
  Args:
945
887
  file_paths (list): 동기화할 파일 또는 폴더 경로 리스트.
946
- 폴더 경로는 자동으로 재귀적으로 처리됩니다.
888
+ 폴더 경로는 자동으로 재귀적으로 처리됩니다.
947
889
 
948
890
  Returns:
949
891
  bool: 동기화 성공 시 True, 실패 시 False
@@ -951,43 +893,55 @@ class Perforce:
951
893
  if not self._is_connected():
952
894
  return False
953
895
  if not file_paths:
954
- logger.debug("싱크할 파일/폴더 목록이 비어있습니다.")
955
896
  return True
956
897
 
898
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
899
+ if not isinstance(file_paths, list):
900
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 경로도 리스트로 감싸서 전달하세요: ['{file_paths}']"
901
+ self.last_error = error_msg
902
+ raise TypeError(error_msg)
903
+
957
904
  # 폴더 경로에 재귀적 와일드카드 패턴을 추가
958
905
  processed_paths = []
959
906
  for path in file_paths:
960
907
  if os.path.isdir(path):
961
908
  # 폴더 경로에 '...'(재귀) 패턴을 추가
962
909
  processed_paths.append(os.path.join(path, '...'))
963
- logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}")
964
910
  else:
965
911
  processed_paths.append(path)
966
912
 
967
- logger.info(f"파일/폴더 싱크 시도 (항목 {len(processed_paths)}개): {processed_paths}")
913
+ self.clear_last_error()
968
914
  try:
969
915
  self.p4.run_sync(processed_paths)
970
- logger.info(f"파일/폴더 싱크 완료: {processed_paths}")
971
916
  return True
972
917
  except P4Exception as e:
973
- self._handle_p4_exception(e, f"파일/폴더 싱크 ({processed_paths})")
918
+ self._handle_p4_exception(e, f"파일/폴더 싱크 실패 ({processed_paths})")
974
919
  return False
975
920
 
976
- def disconnect(self):
977
- """Perforce 서버와의 연결을 해제합니다."""
978
- if self.connected:
979
- try:
980
- self.p4.disconnect()
981
- self.connected = False
982
- logger.info("Perforce 서버 연결 해제 완료.")
983
- except P4Exception as e:
984
- self._handle_p4_exception(e, "Perforce 서버 연결 해제")
985
- else:
986
- logger.debug("Perforce 서버에 이미 연결되지 않은 상태입니다.")
921
+ def get_default_change_list(self) -> dict:
922
+ """default change list의 정보를 가져옵니다.
987
923
 
988
- def __del__(self):
989
- """객체가 소멸될 자동으로 연결을 해제합니다."""
990
- self.disconnect()
924
+ Returns:
925
+ dict: get_change_list_by_number와 동일한 형태의 딕셔너리
926
+ """
927
+ if not self._is_connected():
928
+ return {}
929
+ self.clear_last_error()
930
+ try:
931
+ opened_files = self.p4.run_opened("-c", "default")
932
+ files_list = [f.get('clientFile', '') for f in opened_files]
933
+ result = {
934
+ 'Change': 'default',
935
+ 'Description': 'Default change',
936
+ 'User': getattr(self.p4, 'user', ''),
937
+ 'Client': getattr(self.p4, 'client', ''),
938
+ 'Status': 'pending',
939
+ 'Files': files_list
940
+ }
941
+ return result
942
+ except P4Exception as e:
943
+ self._handle_p4_exception(e, "default change list 정보 조회 실패")
944
+ return {}
991
945
 
992
946
  def check_files_checked_out_all_users(self, file_paths: list) -> dict:
993
947
  """파일들의 체크아웃 상태를 모든 사용자/워크스페이스에서 확인합니다.
@@ -1010,11 +964,15 @@ class Perforce:
1010
964
  if not self._is_connected():
1011
965
  return {}
1012
966
  if not file_paths:
1013
- logger.debug("체크아웃 상태 확인할 파일 목록이 비어있습니다.")
1014
967
  return {}
1015
968
 
1016
- logger.debug(f"파일 체크아웃 상태 확인 - 모든 사용자 (파일 {len(file_paths)}개)")
969
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
970
+ if not isinstance(file_paths, list):
971
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 get_file_checkout_info_all_users() 메서드를 사용하세요."
972
+ self.last_error = error_msg
973
+ raise TypeError(error_msg)
1017
974
 
975
+ self.clear_last_error()
1018
976
  result = {}
1019
977
  try:
1020
978
  # 각 파일의 상태 확인
@@ -1040,26 +998,14 @@ class Perforce:
1040
998
  file_status['user'] = file_info.get('user', '')
1041
999
  file_status['client'] = file_info.get('client', '')
1042
1000
 
1043
- logger.debug(f"파일 '{file_path}' 체크아웃됨: CL {file_status['change_list']}, "
1044
- f"액션: {file_status['action']}, 사용자: {file_status['user']}, "
1045
- f"클라이언트: {file_status['client']}")
1046
- else:
1047
- # 파일이 체크아웃되지 않음
1048
- logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (모든 사용자)")
1049
-
1050
1001
  except P4Exception as e:
1051
1002
  # 파일이 perforce에 없거나 접근할 수 없는 경우
1052
- if any("not opened" in err.lower() or "no such file" in err.lower()
1053
- for err in self.p4.errors):
1054
- logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (perforce에 없거나 접근 불가)")
1055
- else:
1003
+ if not any("not opened" in err.lower() or "no such file" in err.lower()
1004
+ for err in self.p4.errors):
1056
1005
  self._handle_p4_exception(e, f"파일 '{file_path}' 체크아웃 상태 확인 (모든 사용자)")
1057
1006
 
1058
1007
  result[file_path] = file_status
1059
1008
 
1060
- checked_out_count = sum(1 for status in result.values() if status['is_checked_out'])
1061
- logger.info(f"파일 체크아웃 상태 확인 완료 (모든 사용자): 전체 {len(file_paths)}개 중 {checked_out_count}개 체크아웃됨")
1062
-
1063
1009
  return result
1064
1010
 
1065
1011
  except P4Exception as e:
@@ -1150,6 +1096,12 @@ class Perforce:
1150
1096
  if not file_paths:
1151
1097
  return []
1152
1098
 
1099
+ # 타입 검증: 리스트가 아닌 경우 에러 발생
1100
+ if not isinstance(file_paths, list):
1101
+ error_msg = f"file_paths는 리스트여야 합니다. 전달된 타입: {type(file_paths).__name__}. 단일 파일은 is_file_checked_out_by_others() 메서드를 사용하세요."
1102
+ self.last_error = error_msg
1103
+ raise TypeError(error_msg)
1104
+
1153
1105
  result = self.check_files_checked_out_all_users(file_paths)
1154
1106
  files_by_others = []
1155
1107
 
@@ -1171,5 +1123,4 @@ class Perforce:
1171
1123
  'action': status.get('action', '')
1172
1124
  })
1173
1125
 
1174
- logger.info(f"다른 사용자에 의해 체크아웃된 파일: {len(files_by_others)}개")
1175
- return files_by_others
1126
+ return files_by_others