pyjallib 0.1.11__py3-none-any.whl → 0.1.13__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.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ ToolManager 모듈 - 3DS Max에서 실행되는 도구들을 관리
6
+ 도구 인스턴스 생성, 닫기 등을 담당
7
+ """
8
+
9
+ from PySide2 import QtWidgets, QtCore
10
+ import gc
11
+
12
+ class ToolManager:
13
+ def __init__(self):
14
+ self.tools = {} # {tool_class_name: [instances]} 형태로 관리
15
+
16
+ def register_tool(self, tool_instance):
17
+ """도구 인스턴스를 등록합니다"""
18
+ class_name = tool_instance.__class__.__name__
19
+
20
+ if class_name not in self.tools:
21
+ self.tools[class_name] = []
22
+
23
+ self.tools[class_name].append(tool_instance)
24
+
25
+ def close_tool_by_type(self, tool_class):
26
+ """특정 유형의 도구를 모두 닫습니다"""
27
+ class_name = tool_class.__name__
28
+
29
+ if class_name not in self.tools:
30
+ return
31
+
32
+ # 해당 클래스의 모든 인스턴스 정리
33
+ for tool in self.tools[class_name]:
34
+ try:
35
+ if hasattr(tool, 'close'):
36
+ tool.close()
37
+ if hasattr(tool, 'deleteLater'):
38
+ tool.deleteLater()
39
+ except (RuntimeError, AttributeError) as e:
40
+ print(f"도구 닫기 오류: {e}")
41
+
42
+ # 목록 비우기
43
+ self.tools[class_name] = []
44
+
45
+ # 추가적으로 QApplication.allWidgets()를 통한 검사
46
+ try:
47
+ window_title = None
48
+ if hasattr(tool_class, 'windowTitle'):
49
+ window_title = tool_class.windowTitle
50
+
51
+ for widget in QtWidgets.QApplication.allWidgets():
52
+ if (isinstance(widget, QtWidgets.QDialog) and
53
+ ((window_title and hasattr(widget, 'windowTitle') and widget.windowTitle() == window_title) or
54
+ widget.__class__.__name__ == class_name)):
55
+ try:
56
+ widget.close()
57
+ widget.deleteLater()
58
+ except:
59
+ pass
60
+ except Exception as e:
61
+ print(f"위젯 검색 오류: {e}")
62
+
63
+ # 가비지 컬렉션 수행
64
+ gc.collect()
65
+
66
+ def show_tool(self, tool_class, **kwargs):
67
+ """
68
+ 도구를 표시합니다. 중복 실행을 방지하고 항상 새 인스턴스를 생성합니다.
69
+
70
+ Args:
71
+ tool_class: 도구 클래스
72
+ **kwargs: 도구 클래스 생성자에 전달할 인자들
73
+
74
+ Returns:
75
+ 새로 생성된 도구 인스턴스
76
+ """
77
+ # 기존 인스턴스 모두 정리
78
+ self.close_tool_by_type(tool_class)
79
+
80
+ # 약간의 지연을 두어 정리 완료를 기다림
81
+ QtCore.QTimer.singleShot(50, lambda: None)
82
+
83
+ # 새 인스턴스 생성
84
+ tool_instance = tool_class(**kwargs)
85
+
86
+ # 도구 등록
87
+ self.register_tool(tool_instance)
88
+
89
+ # 도구 표시
90
+ tool_instance.show()
91
+
92
+ return tool_instance
pyjallib/max/twistBone.py CHANGED
@@ -258,6 +258,9 @@ class TwistBone:
258
258
  twistBoneRotListController.setActive(twistBoneRotListController.count)
259
259
  twistBoneRotListController.weight[0] = 100.0
260
260
 
261
+ # 첫 번째 트위스트 본을 boneChainArray에 추가
262
+ boneChainArray.append(twistBone)
263
+
261
264
  if twistNum > 1:
262
265
  lastBone = self.bone.create_nub_bone(boneName, 2)
263
266
  lastBone.name = self.name.replace_name_part("Index", boneName, str(twistNum))
@@ -320,7 +323,8 @@ class TwistBone:
320
323
  if not inBoneChain or inBoneChain.is_empty():
321
324
  return None
322
325
 
323
- # 기존 객체 삭제
326
+ # 기존 객체 삭제 (delete_all 대신 delete 사용)
327
+ # delete는 bones와 helpers만 삭제하고 sourceBones와 parameters는 유지함
324
328
  inBoneChain.delete()
325
329
 
326
330
  # BoneChain에서 필요한 정보 추출
@@ -275,7 +275,8 @@ class VolumeBone: # Updated class name to match the new file name
275
275
  if not inBoneChain or inBoneChain.is_empty():
276
276
  return None
277
277
 
278
- # 기존 객체 삭제
278
+ # 기존 객체 삭제 (delete_all 대신 delete 사용)
279
+ # delete는 bones와 helpers만 삭제하고 sourceBones와 parameters는 유지함
279
280
  inBoneChain.delete()
280
281
 
281
282
  # BoneChain에서 필요한 정보 추출
pyjallib/naming.py CHANGED
@@ -258,6 +258,18 @@ class Naming:
258
258
  패딩 숫자
259
259
  """
260
260
  return self._paddingNum
261
+
262
+ def get_filtering_char(self, inStr):
263
+ """
264
+ 문자열에서 구분자 문자 가져오기
265
+
266
+ Args:
267
+ inStr: 확인할 문자열
268
+
269
+ Returns:
270
+ 구분자 문자 (' ' 또는 '_' 또는 '')
271
+ """
272
+ return self._get_filtering_char(inStr)
261
273
 
262
274
  def get_name_part(self, inNamePartName):
263
275
  """
@@ -273,6 +285,15 @@ class Naming:
273
285
  if part.get_name() == inNamePartName:
274
286
  return part
275
287
  return None
288
+
289
+ def get_name_parts(self):
290
+ """
291
+ 모든 namePart 객체 가져오기
292
+
293
+ Returns:
294
+ namePart 객체 리스트
295
+ """
296
+ return self._nameParts
276
297
 
277
298
  def get_name_part_index(self, inNamePartName):
278
299
  """
@@ -473,7 +494,11 @@ class Naming:
473
494
 
474
495
  # 배열을 문자열로 결합
475
496
  newName = self._combine(combinedNameArray, inFilChar)
476
- newName = self.set_index_padding_num(newName)
497
+
498
+ # "Index"키가 있을 때만 패딩 적용
499
+ if "Index" in inPartsDict:
500
+ newName = self.set_index_padding_num(newName)
501
+
477
502
  return newName
478
503
 
479
504
  def get_RealName(self, inStr):
pyjallib/namingConfig.py CHANGED
@@ -388,7 +388,7 @@ class NamingConfig:
388
388
 
389
389
  return True
390
390
 
391
- def set_part_value_by_csv(self, part_name: str, csv_file_path: str) -> bool:
391
+ def set_part_value_by_csv(self, part_name: str, csv_file_path: str, encoding: str = "utf-8") -> bool:
392
392
  """
393
393
  특정 NamePart의 사전 정의 값을 CSV 파일로 설정
394
394
  CSV 파일 형식: value,description,koreanDescription (각 줄당)
@@ -396,6 +396,7 @@ class NamingConfig:
396
396
  Args:
397
397
  part_name: NamePart 이름
398
398
  csv_file_path: CSV 파일 경로
399
+ encoding: CSV 파일 인코딩 (기본값: "utf-8", "utf-8-sig" 등 사용 가능)
399
400
 
400
401
  Returns:
401
402
  설정 성공 여부 (True/False)
@@ -415,7 +416,7 @@ class NamingConfig:
415
416
  descriptions = []
416
417
  korean_descriptions = []
417
418
  try:
418
- with open(csv_file_path, 'r', encoding='utf-8', newline='') as f:
419
+ with open(csv_file_path, 'r', encoding=encoding, newline='') as f:
419
420
  reader = csv.reader(f)
420
421
  for row in reader:
421
422
  if len(row) >= 3: # Ensure row has at least 3 columns
pyjallib/perforce.py CHANGED
@@ -195,6 +195,183 @@ class Perforce:
195
195
  self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')")
196
196
  return {}
197
197
 
198
+ def get_change_list_by_description_pattern(self, description_pattern: str, exact_match: bool = False) -> list:
199
+ """설명 패턴과 일치하는 Pending 체인지 리스트들을 가져옵니다.
200
+
201
+ Args:
202
+ description_pattern (str): 검색할 설명 패턴
203
+ exact_match (bool, optional): True면 정확히 일치하는 설명만,
204
+ False면 패턴이 포함된 설명도 포함. 기본값 False
205
+
206
+ Returns:
207
+ list: 패턴과 일치하는 체인지 리스트 정보들의 리스트
208
+ """
209
+ if not self._is_connected():
210
+ return []
211
+
212
+ search_type = "정확히 일치" if exact_match else "패턴 포함"
213
+ logger.debug(f"설명 패턴으로 체인지 리스트 조회 중 ({search_type}): '{description_pattern}'")
214
+
215
+ try:
216
+ pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
217
+ matching_changes = []
218
+
219
+ for cl in pending_changes:
220
+ cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip()
221
+
222
+ # 패턴 매칭 로직
223
+ is_match = False
224
+ if exact_match:
225
+ # 정확한 일치
226
+ is_match = (cl_desc == description_pattern.strip())
227
+ else:
228
+ # 패턴이 포함되어 있는지 확인 (대소문자 구분 없음)
229
+ is_match = (description_pattern.lower().strip() in cl_desc.lower())
230
+
231
+ if is_match:
232
+ change_number = int(cl['change'])
233
+ change_info = self.get_change_list_by_number(change_number)
234
+ if change_info:
235
+ matching_changes.append(change_info)
236
+ logger.info(f"패턴 '{description_pattern}'에 매칭되는 체인지 리스트 {change_number} 발견: '{cl_desc}'")
237
+
238
+ if matching_changes:
239
+ logger.info(f"패턴 '{description_pattern}'에 매칭되는 체인지 리스트 {len(matching_changes)}개 조회 완료.")
240
+ else:
241
+ logger.info(f"패턴 '{description_pattern}'에 매칭되는 Pending 체인지 리스트를 찾을 수 없습니다.")
242
+
243
+ return matching_changes
244
+ except P4Exception as e:
245
+ self._handle_p4_exception(e, f"설명 패턴으로 체인지 리스트 조회 ('{description_pattern}')")
246
+ return []
247
+
248
+ def check_files_checked_out(self, file_paths: list) -> dict:
249
+ """파일들의 체크아웃 상태를 확인합니다.
250
+
251
+ Args:
252
+ file_paths (list): 확인할 파일 경로 리스트
253
+
254
+ Returns:
255
+ dict: 파일별 체크아웃 상태 정보
256
+ {
257
+ 'file_path': {
258
+ 'is_checked_out': bool,
259
+ 'change_list': int or None,
260
+ 'action': str or None,
261
+ 'user': str or None,
262
+ 'workspace': str or None
263
+ }
264
+ }
265
+ """
266
+ if not self._is_connected():
267
+ return {}
268
+ if not file_paths:
269
+ logger.debug("체크아웃 상태 확인할 파일 목록이 비어있습니다.")
270
+ return {}
271
+
272
+ logger.debug(f"파일 체크아웃 상태 확인 중 (파일 {len(file_paths)}개)")
273
+
274
+ result = {}
275
+ try:
276
+ # 각 파일의 상태 확인
277
+ for file_path in file_paths:
278
+ file_status = {
279
+ 'is_checked_out': False,
280
+ 'change_list': None,
281
+ 'action': None,
282
+ 'user': None,
283
+ 'workspace': None
284
+ }
285
+
286
+ try:
287
+ # p4 opened 명령으로 파일이 열려있는지 확인
288
+ opened_files = self.p4.run_opened(file_path)
289
+
290
+ if opened_files:
291
+ # 파일이 체크아웃되어 있음
292
+ file_info = opened_files[0]
293
+ file_status['is_checked_out'] = True
294
+ file_status['change_list'] = int(file_info.get('change', 0))
295
+ file_status['action'] = file_info.get('action', '')
296
+ file_status['user'] = file_info.get('user', '')
297
+ file_status['workspace'] = file_info.get('client', '')
298
+
299
+ logger.debug(f"파일 '{file_path}' 체크아웃됨: CL {file_status['change_list']}, "
300
+ f"액션: {file_status['action']}, 사용자: {file_status['user']}, "
301
+ f"워크스페이스: {file_status['workspace']}")
302
+ else:
303
+ # 파일이 체크아웃되지 않음
304
+ logger.debug(f"파일 '{file_path}' 체크아웃되지 않음")
305
+
306
+ except P4Exception as e:
307
+ # 파일이 perforce에 없거나 접근할 수 없는 경우
308
+ if any("not opened" in err.lower() or "no such file" in err.lower()
309
+ for err in self.p4.errors):
310
+ logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (perforce에 없거나 접근 불가)")
311
+ else:
312
+ self._handle_p4_exception(e, f"파일 '{file_path}' 체크아웃 상태 확인")
313
+
314
+ result[file_path] = file_status
315
+
316
+ checked_out_count = sum(1 for status in result.values() if status['is_checked_out'])
317
+ logger.info(f"파일 체크아웃 상태 확인 완료: 전체 {len(file_paths)}개 중 {checked_out_count}개 체크아웃됨")
318
+
319
+ return result
320
+
321
+ except P4Exception as e:
322
+ self._handle_p4_exception(e, f"파일들 체크아웃 상태 확인 ({file_paths})")
323
+ return {}
324
+
325
+ def is_file_checked_out(self, file_path: str) -> bool:
326
+ """단일 파일의 체크아웃 상태를 간단히 확인합니다.
327
+
328
+ Args:
329
+ file_path (str): 확인할 파일 경로
330
+
331
+ Returns:
332
+ bool: 체크아웃되어 있으면 True, 아니면 False
333
+ """
334
+ result = self.check_files_checked_out([file_path])
335
+ return result.get(file_path, {}).get('is_checked_out', False)
336
+
337
+ def is_file_in_pending_changelist(self, file_path: str, change_list_number: int) -> bool:
338
+ """특정 파일이 지정된 pending 체인지 리스트에 있는지 확인합니다.
339
+
340
+ Args:
341
+ file_path (str): 확인할 파일 경로
342
+ change_list_number (int): 확인할 체인지 리스트 번호
343
+
344
+ Returns:
345
+ bool: 파일이 해당 체인지 리스트에 있으면 True, 아니면 False
346
+ """
347
+ if not self._is_connected():
348
+ return False
349
+
350
+ logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에 있는지 확인 중...")
351
+
352
+ try:
353
+ # 해당 체인지 리스트의 파일들 가져오기
354
+ opened_files = self.p4.run_opened("-c", change_list_number)
355
+
356
+ # 파일 경로 정규화
357
+ normalized_file_path = os.path.normpath(file_path)
358
+
359
+ for file_info in opened_files:
360
+ client_file = file_info.get('clientFile', '')
361
+ normalized_client_file = os.path.normpath(client_file)
362
+
363
+ if normalized_client_file == normalized_file_path:
364
+ logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에서 발견됨 "
365
+ f"(액션: {file_info.get('action', '')})")
366
+ return True
367
+
368
+ logger.debug(f"파일 '{file_path}'가 체인지 리스트 {change_list_number}에 없음")
369
+ return False
370
+
371
+ except P4Exception as e:
372
+ self._handle_p4_exception(e, f"파일 '{file_path}' 체인지 리스트 {change_list_number} 포함 여부 확인")
373
+ return False
374
+
198
375
  def edit_change_list(self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict:
199
376
  """체인지 리스트를 편집합니다.
200
377
 
@@ -594,6 +771,40 @@ class Perforce:
594
771
  self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})")
595
772
  return False
596
773
 
774
+ def is_file_in_perforce(self, file_path: str) -> bool:
775
+ """파일이 Perforce에 속하는지 확인합니다.
776
+
777
+ Args:
778
+ file_path (str): 확인할 파일 경로
779
+
780
+ Returns:
781
+ bool: 파일이 Perforce에 속하면 True, 아니면 False
782
+ """
783
+ if not self._is_connected():
784
+ return False
785
+
786
+ logger.debug(f"파일 '{file_path}'가 Perforce에 속하는지 확인 중...")
787
+ try:
788
+ # p4 files 명령으로 파일 정보 조회
789
+ file_info = self.p4.run_files(file_path)
790
+
791
+ # 파일 정보가 있고, 'no such file(s)' 오류가 없는 경우
792
+ if file_info and not any("no such file(s)" in str(err).lower() for err in self.p4.errors):
793
+ logger.info(f"파일 '{file_path}'가 Perforce에 존재합니다.")
794
+ return True
795
+ else:
796
+ logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
797
+ return False
798
+
799
+ except P4Exception as e:
800
+ # 파일이 존재하지 않는 경우는 일반적인 상황이므로 경고 레벨로 로깅
801
+ if any("no such file(s)" in err.lower() for err in self.p4.errors):
802
+ logger.info(f"파일 '{file_path}'가 Perforce에 존재하지 않습니다.")
803
+ return False
804
+ else:
805
+ self._handle_p4_exception(e, f"파일 '{file_path}' Perforce 존재 여부 확인")
806
+ return False
807
+
597
808
  def sync_files(self, file_paths: list) -> bool:
598
809
  """파일이나 폴더를 동기화합니다.
599
810
 
@@ -644,3 +855,188 @@ class Perforce:
644
855
  def __del__(self):
645
856
  """객체가 소멸될 때 자동으로 연결을 해제합니다."""
646
857
  self.disconnect()
858
+
859
+ def check_files_checked_out_all_users(self, file_paths: list) -> dict:
860
+ """파일들의 체크아웃 상태를 모든 사용자/워크스페이스에서 확인합니다.
861
+
862
+ Args:
863
+ file_paths (list): 확인할 파일 경로 리스트
864
+
865
+ Returns:
866
+ dict: 파일별 체크아웃 상태 정보
867
+ {
868
+ 'file_path': {
869
+ 'is_checked_out': bool,
870
+ 'change_list': int or None,
871
+ 'action': str or None,
872
+ 'user': str or None,
873
+ 'client': str or None
874
+ }
875
+ }
876
+ """
877
+ if not self._is_connected():
878
+ return {}
879
+ if not file_paths:
880
+ logger.debug("체크아웃 상태 확인할 파일 목록이 비어있습니다.")
881
+ return {}
882
+
883
+ logger.debug(f"파일 체크아웃 상태 확인 중 - 모든 사용자 (파일 {len(file_paths)}개)")
884
+
885
+ result = {}
886
+ try:
887
+ # 각 파일의 상태 확인
888
+ for file_path in file_paths:
889
+ file_status = {
890
+ 'is_checked_out': False,
891
+ 'change_list': None,
892
+ 'action': None,
893
+ 'user': None,
894
+ 'client': None
895
+ }
896
+
897
+ try:
898
+ # p4 opened -a 명령으로 모든 사용자의 파일 체크아웃 상태 확인
899
+ opened_files = self.p4.run_opened("-a", file_path)
900
+
901
+ if opened_files:
902
+ # 파일이 체크아웃되어 있음 (첫 번째 결과 사용)
903
+ file_info = opened_files[0]
904
+ file_status['is_checked_out'] = True
905
+ file_status['change_list'] = int(file_info.get('change', 0))
906
+ file_status['action'] = file_info.get('action', '')
907
+ file_status['user'] = file_info.get('user', '')
908
+ file_status['client'] = file_info.get('client', '')
909
+
910
+ logger.debug(f"파일 '{file_path}' 체크아웃됨: CL {file_status['change_list']}, "
911
+ f"액션: {file_status['action']}, 사용자: {file_status['user']}, "
912
+ f"클라이언트: {file_status['client']}")
913
+ else:
914
+ # 파일이 체크아웃되지 않음
915
+ logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (모든 사용자)")
916
+
917
+ except P4Exception as e:
918
+ # 파일이 perforce에 없거나 접근할 수 없는 경우
919
+ if any("not opened" in err.lower() or "no such file" in err.lower()
920
+ for err in self.p4.errors):
921
+ logger.debug(f"파일 '{file_path}' 체크아웃되지 않음 (perforce에 없거나 접근 불가)")
922
+ else:
923
+ self._handle_p4_exception(e, f"파일 '{file_path}' 체크아웃 상태 확인 (모든 사용자)")
924
+
925
+ result[file_path] = file_status
926
+
927
+ checked_out_count = sum(1 for status in result.values() if status['is_checked_out'])
928
+ logger.info(f"파일 체크아웃 상태 확인 완료 (모든 사용자): 전체 {len(file_paths)}개 중 {checked_out_count}개 체크아웃됨")
929
+
930
+ return result
931
+
932
+ except P4Exception as e:
933
+ self._handle_p4_exception(e, f"파일들 체크아웃 상태 확인 - 모든 사용자 ({file_paths})")
934
+ return {}
935
+
936
+ def is_file_checked_out_by_others(self, file_path: str) -> bool:
937
+ """단일 파일이 다른 사용자/워크스페이스에 의해 체크아웃되어 있는지 확인합니다.
938
+
939
+ Args:
940
+ file_path (str): 확인할 파일 경로
941
+
942
+ Returns:
943
+ bool: 다른 사용자에 의해 체크아웃되어 있으면 True, 아니면 False
944
+ """
945
+ result = self.check_files_checked_out_all_users([file_path])
946
+ file_status = result.get(file_path, {})
947
+
948
+ if not file_status.get('is_checked_out', False):
949
+ return False
950
+
951
+ # 현재 사용자와 클라이언트가 아닌 경우 다른 사용자로 간주
952
+ current_user = self.p4.user
953
+ current_client = self.p4.client
954
+
955
+ file_user = file_status.get('user', '')
956
+ file_client = file_status.get('client', '')
957
+
958
+ return (file_user != current_user) or (file_client != current_client)
959
+
960
+ def get_file_checkout_info_all_users(self, file_path: str) -> dict:
961
+ """단일 파일의 상세 체크아웃 정보를 모든 사용자에서 가져옵니다.
962
+
963
+ Args:
964
+ file_path (str): 확인할 파일 경로
965
+
966
+ Returns:
967
+ dict: 체크아웃 정보 또는 빈 딕셔너리
968
+ {
969
+ 'is_checked_out': bool,
970
+ 'change_list': int or None,
971
+ 'action': str or None,
972
+ 'user': str or None,
973
+ 'client': str or None,
974
+ 'is_checked_out_by_current_user': bool,
975
+ 'is_checked_out_by_others': bool
976
+ }
977
+ """
978
+ result = self.check_files_checked_out_all_users([file_path])
979
+ file_status = result.get(file_path, {})
980
+
981
+ if file_status.get('is_checked_out', False):
982
+ # 현재 사용자와 클라이언트인지 확인
983
+ current_user = self.p4.user
984
+ current_client = self.p4.client
985
+
986
+ file_user = file_status.get('user', '')
987
+ file_client = file_status.get('client', '')
988
+
989
+ is_current_user = (file_user == current_user) and (file_client == current_client)
990
+
991
+ file_status['is_checked_out_by_current_user'] = is_current_user
992
+ file_status['is_checked_out_by_others'] = not is_current_user
993
+ else:
994
+ file_status['is_checked_out_by_current_user'] = False
995
+ file_status['is_checked_out_by_others'] = False
996
+
997
+ return file_status
998
+
999
+ def get_files_checked_out_by_others(self, file_paths: list) -> list:
1000
+ """파일 목록에서 다른 사용자/워크스페이스에 의해 체크아웃된 파일들을 찾습니다.
1001
+
1002
+ Args:
1003
+ file_paths (list): 확인할 파일 경로 리스트
1004
+
1005
+ Returns:
1006
+ list: 다른 사용자에 의해 체크아웃된 파일 정보 리스트
1007
+ [
1008
+ {
1009
+ 'file_path': str,
1010
+ 'user': str,
1011
+ 'client': str,
1012
+ 'change_list': int,
1013
+ 'action': str
1014
+ }
1015
+ ]
1016
+ """
1017
+ if not file_paths:
1018
+ return []
1019
+
1020
+ result = self.check_files_checked_out_all_users(file_paths)
1021
+ files_by_others = []
1022
+
1023
+ current_user = self.p4.user
1024
+ current_client = self.p4.client
1025
+
1026
+ for file_path, status in result.items():
1027
+ if status.get('is_checked_out', False):
1028
+ file_user = status.get('user', '')
1029
+ file_client = status.get('client', '')
1030
+
1031
+ # 다른 사용자/클라이언트에 의해 체크아웃된 경우
1032
+ if (file_user != current_user) or (file_client != current_client):
1033
+ files_by_others.append({
1034
+ 'file_path': file_path,
1035
+ 'user': file_user,
1036
+ 'client': file_client,
1037
+ 'change_list': status.get('change_list'),
1038
+ 'action': status.get('action', '')
1039
+ })
1040
+
1041
+ logger.info(f"다른 사용자에 의해 체크아웃된 파일: {len(files_by_others)}개")
1042
+ return files_by_others
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyjallib
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: A utility library for 3D game character development pipelines.
5
5
  Author-email: Dongseok Kim <jalnagakds@gmail.com>
6
6
  Requires-Python: >=3.10