pyjallib 0.1.10__py3-none-any.whl → 0.1.11__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/max/kneeBone.py CHANGED
@@ -16,6 +16,8 @@ from .bone import Bone
16
16
  from .constraint import Constraint
17
17
  from .volumeBone import VolumeBone
18
18
 
19
+ from .boneChain import BoneChain
20
+
19
21
  class KneeBone:
20
22
  """
21
23
  자동 무릎 본(AutoKnee) 관련 기능을 제공하는 클래스.
@@ -310,17 +312,19 @@ class KneeBone:
310
312
  result = self.volumeBone.create_bones(self.calf, self.thigh, inVolumeSize=5.0, inRotAxises=["Z", "Z"], inTransAxises=["PosY", "NegY"], inTransScales=transScales)
311
313
 
312
314
  filteringChar = self.name._get_filtering_char(inCalf.name)
313
- calfName = self.name.get_name_part("RealName", inCalf.name+ filteringChar + "Vol")
315
+ calfName = self.name.get_RealName(inCalf.name)
316
+ calfName = calfName + filteringChar + "Vol"
314
317
  isLower = calfName[0].islower()
315
318
  replaceName = "Knee"
316
319
  if isLower:
317
320
  replaceName = replaceName.lower()
321
+ calfName = calfName.lower()
318
322
 
319
323
  for item in result["Bones"]:
320
- item.name.replace(calfName, replaceName)
324
+ item.name = item.name.replace(calfName, replaceName)
321
325
 
322
- result["rootBone"].name.replace(calfName, replaceName)
323
- result["RotHelper"].name.replace(calfName, replaceName)
326
+ result["RootBone"].name = result["RootBone"].name.replace(calfName, replaceName)
327
+ result["RotHelper"].name = result["RotHelper"].name.replace(calfName, replaceName)
324
328
 
325
329
  # 결과 저장
326
330
  if result and "Bones" in result:
@@ -379,7 +383,7 @@ class KneeBone:
379
383
 
380
384
  liftTwistBone = self.bone.create_nub_bone(liftTwistBoneName, 2)
381
385
  liftTwistBone.name = self.name.remove_name_part("Nub", liftTwistBone.name)
382
- liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", oriThighTwistBones.name))
386
+ liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", item.name))
383
387
 
384
388
  rt.setProperty(liftTwistBone, "transform", item.transform)
385
389
  liftTwistBone.parent = item
@@ -404,7 +408,7 @@ class KneeBone:
404
408
 
405
409
  liftTwistBone = self.bone.create_nub_bone(liftTwistBoneName, 2)
406
410
  liftTwistBone.name = self.name.remove_name_part("Nub", liftTwistBone.name)
407
- liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", oriClafTwistBones.name))
411
+ liftTwistBone.name = self.name.replace_name_part("Index", liftTwistBone.name, self.name.get_name("Index", item.name))
408
412
 
409
413
  rt.setProperty(liftTwistBone, "transform", item.transform)
410
414
  liftTwistBone.parent = item
@@ -420,7 +424,7 @@ class KneeBone:
420
424
  self.calfTwistBones.append(liftTwistBone)
421
425
  self.calfTwistHelpers.append(liftTwistHelper)
422
426
 
423
- def create_bone(self, inThigh, inCalf, inFoot, inLiftScale=0.05, inKneePopScale=1.0, inKneeBackScale=1.0):
427
+ def create_bone(self, inThigh, inCalf, inFoot, inLiftScale=0.05, inKneePopScale=0.1, inKneeBackScale=1.5):
424
428
  """
425
429
  자동 무릎 본 시스템의 모든 요소를 생성하는 주요 메서드입니다.
426
430
 
@@ -441,7 +445,7 @@ class KneeBone:
441
445
  inKneeBackScale: 무릎 뒤쪽 돌출 스케일 (1.0이 기본값)
442
446
 
443
447
  Returns:
444
- bool: 자동 무릎 본 시스템 생성 성공 여부
448
+ BoneChain: 생성된 자동 무릎 본 체인 객체
445
449
  """
446
450
  if not rt.isValidNode(inThigh) or not rt.isValidNode(inCalf) or not rt.isValidNode(inFoot):
447
451
  return False
@@ -454,30 +458,61 @@ class KneeBone:
454
458
  self.create_middle_bone(inThigh, inCalf, inKneePopScale=inKneePopScale, inKneeBackScale=inKneeBackScale)
455
459
  self.create_twist_bones(inThigh, inCalf)
456
460
 
457
- # 결과를 딕셔너리 형태로 준비
461
+ # 모든 생성된 본들 수집
462
+ all_bones = self.thighTwistBones + self.calfTwistBones + self.middleBones
463
+ all_helpers = [self.lookAtHleper, self.thighRotHelper, self.calfRotHelper,
464
+ self.thighRotRootHelper, self.calfRotRootHelper] + self.thighTwistHelpers + self.calfTwistHelpers
465
+
466
+ # 결과를 BoneChain 형태로 준비
458
467
  result = {
459
- "Thigh": inThigh,
460
- "Calf": inCalf,
461
- "Foot": inFoot,
462
- "LookAtHelper": self.lookAtHleper,
463
- "ThighRotHelper": self.thighRotHelper,
464
- "CalfRotHelper": self.calfRotHelper,
465
- "ThighRotRootHelper": self.thighRotRootHelper,
466
- "CalfRotRootHelper": self.calfRotRootHelper,
467
- "ThighTwistBones": self.thighTwistBones,
468
- "CalfTwistBones": self.calfTwistBones,
469
- "ThighTwistHelpers": self.thighTwistHelpers,
470
- "CalfTwistHelpers": self.calfTwistHelpers,
471
- "MiddleBones": self.middleBones,
472
- "LiftScale": inLiftScale,
473
- "KneePopScale": inKneePopScale,
474
- "KneeBackScale": inKneeBackScale
468
+ "Bones": all_bones,
469
+ "Helpers": all_helpers,
470
+ "SourceBones": [inThigh, inCalf, inFoot],
471
+ "Parameters": [inLiftScale, inKneePopScale, inKneeBackScale]
475
472
  }
476
473
 
477
474
  # 메소드 호출 후 데이터 초기화
478
475
  self.reset()
479
476
 
480
- return result
477
+ return BoneChain.from_result(result)
478
+
479
+ def create_bones_from_chain(self, inBoneChain: BoneChain):
480
+ """
481
+ 기존 BoneChain 객체에서 자동 무릎 본을 생성합니다.
482
+ 기존 설정을 복원하거나 저장된 데이터에서 무릎 셋업을 재생성할 때 사용합니다.
483
+
484
+ Args:
485
+ inBoneChain (BoneChain): 자동 무릎 본 정보를 포함한 BoneChain 객체
486
+
487
+ Returns:
488
+ BoneChain: 업데이트된 BoneChain 객체 또는 실패 시 None
489
+ """
490
+ if not inBoneChain or inBoneChain.is_empty():
491
+ return None
492
+
493
+ # 기존 객체 삭제
494
+ inBoneChain.delete()
495
+
496
+ # BoneChain에서 필요한 정보 추출
497
+ sourceBones = inBoneChain.sourceBones
498
+ parameters = inBoneChain.parameters
499
+
500
+ # 필수 소스 본 확인
501
+ if len(sourceBones) < 3 or not rt.isValidNode(sourceBones[0]) or not rt.isValidNode(sourceBones[1]) or not rt.isValidNode(sourceBones[2]):
502
+ return None
503
+
504
+ # 파라미터 가져오기 (또는 기본값 사용)
505
+ liftScale = parameters[0] if len(parameters) > 0 else 0.05
506
+ kneePopScale = parameters[1] if len(parameters) > 1 else 0.1
507
+ kneeBackScale = parameters[2] if len(parameters) > 2 else 1.5
508
+
509
+ # 무릎 본 생성
510
+ inThigh = sourceBones[0]
511
+ inCalf = sourceBones[1]
512
+ inFoot = sourceBones[2]
513
+
514
+ # 새로운 자동 무릎 본 생성
515
+ return self.create_bone(inThigh, inCalf, inFoot, liftScale, kneePopScale, kneeBackScale)
481
516
 
482
517
  def reset(self):
483
518
  """
@@ -273,7 +273,7 @@ def jal_bone_nub_create():
273
273
  for item in non_bone_array:
274
274
  jal.bone.create_nub_bone_on_obj(item)
275
275
  else:
276
- jal.bone.create_nub_bone("Temp Nub", 2)
276
+ jal.bone.create_nub_bone("Temp", 2)
277
277
 
278
278
  # Register macroscripts
279
279
  macroScript_Category = "jalTools"
pyjallib/max/twistBone.py CHANGED
@@ -19,6 +19,8 @@ from .constraint import Constraint
19
19
  from .bip import Bip
20
20
  from .bone import Bone
21
21
 
22
+ from .boneChain import BoneChain
23
+
22
24
 
23
25
  class TwistBone:
24
26
  """
@@ -121,12 +123,7 @@ class TwistBone:
121
123
  twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.
122
124
 
123
125
  Returns:
124
- dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다.
125
- "Bones": 생성된 뼈대 객체들의 배열
126
- "Type": "Upper" (상체 타입)
127
- "Limb": 부모 객체 참조
128
- "Child": 자식 객체 참조
129
- "TwistNum": 생성된 트위스트 뼈대 개수
126
+ BoneChain: 생성된 트위스트 뼈대 BoneChain 객체
130
127
  """
131
128
  limb = inObj
132
129
  distance = rt.distance(limb, inChild)
@@ -134,7 +131,6 @@ class TwistBone:
134
131
  inObjXAxisVec = inObj.objectTransform.row1
135
132
  distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
136
133
  offssetAmount = (distance / twistNum) * distanceDir
137
- weightVal = 100.0 / (twistNum-1)
138
134
 
139
135
  boneChainArray = []
140
136
 
@@ -172,6 +168,8 @@ class TwistBone:
172
168
  lastBone.parent = limb
173
169
  self.anim.move_local(lastBone, offssetAmount*(twistNum-1), 0, 0)
174
170
 
171
+ weightVal = 100.0 / (twistNum-1)
172
+
175
173
  if twistNum > 2:
176
174
  for i in range(1, twistNum-1):
177
175
  twistExtraBone = self.bone.create_nub_bone(boneName, 2)
@@ -197,25 +195,18 @@ class TwistBone:
197
195
 
198
196
  boneChainArray.append(lastBone)
199
197
 
200
- # 결과를 멤버 변수에 저장
201
- self.limb = inObj
202
- self.child = inChild
203
- self.twistNum = twistNum
204
- self.bones = boneChainArray
205
- self.twistType = "Upper"
206
-
207
- returnVal = {
198
+ # 결과를 BoneChain 형태로 준비
199
+ result = {
208
200
  "Bones": boneChainArray,
209
- "Type": "Upper",
210
- "Limb": inObj,
211
- "Child": inChild,
212
- "TwistNum": twistNum
201
+ "Helpers": [],
202
+ "SourceBones": [inObj, inChild],
203
+ "Parameters": [twistNum, "Upper"]
213
204
  }
214
205
 
215
206
  # 메소드 호출 후 데이터 초기화
216
207
  self.reset()
217
208
 
218
- return returnVal
209
+ return BoneChain.from_result(result)
219
210
 
220
211
  def create_lower_limb_bones(self, inObj, inChild, twistNum=4):
221
212
  """
@@ -231,12 +222,7 @@ class TwistBone:
231
222
  twistNum (int, optional): 생성할 트위스트 뼈대의 개수. 기본값은 4입니다.
232
223
 
233
224
  Returns:
234
- dict: 생성된 트위스트 뼈대 정보를 담고 있는 사전 객체입니다.
235
- "Bones": 생성된 뼈대 객체들의 배열
236
- "Type": "Lower" (하체 타입)
237
- "Limb": 부모 객체 참조
238
- "Child": 자식 객체 참조
239
- "TwistNum": 생성된 트위스트 뼈대 개수
225
+ BoneChain: 생성된 트위스트 뼈대 BoneChain 객체
240
226
  """
241
227
  limb = inChild
242
228
  distance = rt.distance(inObj, inChild)
@@ -244,7 +230,6 @@ class TwistBone:
244
230
  inObjXAxisVec = inObj.objectTransform.row1
245
231
  distanceDir = 1.0 if rt.dot(inObjXAxisVec, facingDirVec) > 0 else -1.0
246
232
  offssetAmount = (distance / twistNum) * distanceDir
247
- weightVal = 100.0 / (twistNum-1)
248
233
 
249
234
  boneChainArray = []
250
235
 
@@ -281,6 +266,8 @@ class TwistBone:
281
266
  lastBone.parent = inObj
282
267
  self.anim.move_local(lastBone, 0, 0, 0)
283
268
 
269
+ weightVal = 100.0 / (twistNum-1)
270
+
284
271
  if twistNum > 2:
285
272
  for i in range(1, twistNum-1):
286
273
  twistExtraBone = self.bone.create_nub_bone(boneName, 2)
@@ -306,22 +293,54 @@ class TwistBone:
306
293
 
307
294
  boneChainArray.append(lastBone)
308
295
 
309
- # 결과를 멤버 변수에 저장
310
- self.limb = inObj
311
- self.child = inChild
312
- self.twistNum = twistNum
313
- self.bones = boneChainArray
314
- self.twistType = "Lower"
315
-
316
- returnVal = {
296
+ # 결과를 BoneChain 형태로 준비
297
+ result = {
317
298
  "Bones": boneChainArray,
318
- "Type": "Lower",
319
- "Limb": inObj,
320
- "Child": inChild,
321
- "TwistNum": twistNum
299
+ "Helpers": [],
300
+ "SourceBones": [inObj, inChild],
301
+ "Parameters": [twistNum, "Lower"]
322
302
  }
323
303
 
324
304
  # 메소드 호출 후 데이터 초기화
325
305
  self.reset()
326
306
 
327
- return returnVal
307
+ return BoneChain.from_result(result)
308
+
309
+ def create_bones_from_chain(self, inBoneChain: BoneChain):
310
+ """
311
+ 기존 BoneChain 객체에서 트위스트 본을 생성합니다.
312
+ 기존 설정을 복원하거나 저장된 데이터에서 트위스트 본 셋업을 재생성할 때 사용합니다.
313
+
314
+ Args:
315
+ inBoneChain (BoneChain): 트위스트 본 정보를 포함한 BoneChain 객체
316
+
317
+ Returns:
318
+ BoneChain: 업데이트된 BoneChain 객체 또는 실패 시 None
319
+ """
320
+ if not inBoneChain or inBoneChain.is_empty():
321
+ return None
322
+
323
+ # 기존 객체 삭제
324
+ inBoneChain.delete()
325
+
326
+ # BoneChain에서 필요한 정보 추출
327
+ sourceBones = inBoneChain.sourceBones
328
+ parameters = inBoneChain.parameters
329
+
330
+ # 필수 소스 본 확인
331
+ if len(sourceBones) < 2 or not rt.isValidNode(sourceBones[0]) or not rt.isValidNode(sourceBones[1]):
332
+ return None
333
+
334
+ # 파라미터 가져오기 (또는 기본값 사용)
335
+ twistNum = parameters[0] if len(parameters) > 0 else 4
336
+ twistType = parameters[1] if len(parameters) > 1 else "Upper"
337
+
338
+ # 본 생성
339
+ inObj = sourceBones[0]
340
+ inChild = sourceBones[1]
341
+
342
+ # 타입에 따라 적절한 방식으로 트위스트 본 생성
343
+ if twistType == "Upper":
344
+ return self.create_upper_limb_bones(inObj, inChild, twistNum)
345
+ else:
346
+ return self.create_lower_limb_bones(inObj, inChild, twistNum)
@@ -18,6 +18,8 @@ from .helper import Helper
18
18
  from .bone import Bone
19
19
  from .constraint import Constraint
20
20
 
21
+ from .boneChain import BoneChain
22
+
21
23
 
22
24
  class VolumeBone: # Updated class name to match the new file name
23
25
  """
@@ -69,8 +71,8 @@ class VolumeBone: # Updated class name to match the new file name
69
71
  "\n"
70
72
  "q = localDeltaTm.rotation\n"
71
73
  "\n"
72
- "eulerRot = (quatToEuler q order:6)\n"
73
- "swizzledRot = (eulerAngles eulerRot.z eulerRot.y eulerRot.x)\n"
74
+ "eulerRot = (quatToEuler q order:5)\n"
75
+ "swizzledRot = (eulerAngles eulerRot.y eulerRot.z eulerRot.x)\n"
74
76
  "saturatedTwist = abs ((swizzledRot.x*axis.x + swizzledRot.y*axis.y + swizzledRot.z*axis.z)/180.0)\n"
75
77
  "\n"
76
78
  "trAxis * saturatedTwist * volumeSize * transScale\n"
@@ -213,7 +215,7 @@ class VolumeBone: # Updated class name to match the new file name
213
215
  inTransScales: 변환 비율 리스트
214
216
 
215
217
  Returns:
216
- dict: VolumeBoneChain 생성을 위한 결과 딕셔너리
218
+ BoneChain: 생성된 볼륨 체인 객체
217
219
  """
218
220
  if rt.isValidNode(inObj) == False or rt.isValidNode(inParent) == False:
219
221
  return None
@@ -242,32 +244,81 @@ class VolumeBone: # Updated class name to match the new file name
242
244
  if rt.isValidNode(volBone):
243
245
  bones.append(volBone)
244
246
 
245
- # 클래스 변수에 결과 저장
246
- self.rootBone = rootBone
247
- self.limb = inObj
248
- self.limbParent = inParent
249
- self.bones = bones
250
- self.rotAxises = inRotAxises.copy()
251
- self.transAxises = inTransAxises.copy()
252
- self.transScales = inTransScales.copy()
253
- self.volumeSize = inVolumeSize
254
- self.rotScale = inRotScale
255
-
256
- # VolumeBoneChain이 필요로 하는 형태의 결과 딕셔너리 생성
247
+ # 모든 생성된 본들 모음
248
+ all_bones = [rootBone] + bones
249
+ rotHelper = self.rotHelper
250
+
251
+ # BoneChain에 필요한 형태의 결과 딕셔너리 생성
257
252
  result = {
258
- "RootBone": rootBone,
259
- "RotHelper": self.rotHelper,
260
- "RotScale": inRotScale,
261
- "Limb": inObj,
262
- "LimbParent": inParent,
263
- "Bones": bones,
264
- "RotAxises": inRotAxises,
265
- "TransAxises": inTransAxises,
266
- "TransScales": inTransScales,
267
- "VolumeSize": inVolumeSize
253
+ "Bones": all_bones,
254
+ "Helpers": [rotHelper],
255
+ "SourceBones": [inObj, inParent],
256
+ "Parameters": [inRotScale, inVolumeSize] + inRotAxises + inTransAxises + inTransScales
268
257
  }
269
258
 
270
259
  # 메소드 호출 후 데이터 초기화
271
260
  self.reset()
272
261
 
273
- return result
262
+ return BoneChain.from_result(result)
263
+
264
+ def create_bones_from_chain(self, inBoneChain: BoneChain):
265
+ """
266
+ 기존 BoneChain 객체에서 볼륨 본을 생성합니다.
267
+ 기존 설정을 복원하거나 저장된 데이터에서 볼륨 본 셋업을 재생성할 때 사용합니다.
268
+
269
+ Args:
270
+ inBoneChain (BoneChain): 볼륨 본 정보를 포함한 BoneChain 객체
271
+
272
+ Returns:
273
+ BoneChain: 업데이트된 BoneChain 객체 또는 실패 시 None
274
+ """
275
+ if not inBoneChain or inBoneChain.is_empty():
276
+ return None
277
+
278
+ # 기존 객체 삭제
279
+ inBoneChain.delete()
280
+
281
+ # BoneChain에서 필요한 정보 추출
282
+ sourceBones = inBoneChain.sourceBones
283
+ parameters = inBoneChain.parameters
284
+
285
+ # 필수 소스 본 확인
286
+ if len(sourceBones) < 2 or not rt.isValidNode(sourceBones[0]) or not rt.isValidNode(sourceBones[1]):
287
+ return None
288
+
289
+ # 최소 필요 파라미터 확인
290
+ if len(parameters) < 2:
291
+ return None
292
+
293
+ # 파라미터 가져오기
294
+ inRotScale = parameters[0]
295
+ inVolumeSize = parameters[1]
296
+
297
+ # 회전축, 변환축, 변환비율을 파라미터에서 추출
298
+ # 최소한 하나의 축 세트는 필요
299
+ param_count = len(parameters)
300
+ if param_count <= 2:
301
+ # 기본 값 사용
302
+ inRotAxises = ["Z"]
303
+ inTransAxises = ["PosY"]
304
+ inTransScales = [1.0]
305
+ else:
306
+ # 파라미터 중간을 3등분하여 각 목록 추출
307
+ axis_count = (param_count - 2) // 3
308
+
309
+ inRotAxises = parameters[2:2+axis_count]
310
+ inTransAxises = parameters[2+axis_count:2+axis_count*2]
311
+ inTransScales = parameters[2+axis_count*2:2+axis_count*3]
312
+
313
+ # 리스트 길이가 일치하지 않으면 기본값으로 보완
314
+ if len(inRotAxises) != len(inTransAxises) or len(inRotAxises) != len(inTransScales):
315
+ min_len = min(len(inRotAxises), len(inTransAxises), len(inTransScales))
316
+ inRotAxises = inRotAxises[:min_len] if min_len > 0 else ["Z"]
317
+ inTransAxises = inTransAxises[:min_len] if min_len > 0 else ["PosY"]
318
+ inTransScales = inTransScales[:min_len] if min_len > 0 else [1.0]
319
+
320
+ # 새로운 볼륨 본 생성
321
+ inObj = sourceBones[0]
322
+ inParent = sourceBones[1]
323
+
324
+ return self.create_bones(inObj, inParent, inRotScale, inVolumeSize, inRotAxises, inTransAxises, inTransScales)
pyjallib/naming.py CHANGED
@@ -494,7 +494,7 @@ class Naming:
494
494
  for part in self._nameParts:
495
495
  partName = part.get_name()
496
496
  partType = part.get_type()
497
- if partType != NamePartType.REALNAME:
497
+ if partType.value != NamePartType.REALNAME.value:
498
498
  foundName = self.get_name(partName, inStr)
499
499
  nonRealNameArray.append(foundName)
500
500
 
pyjallib/perforce.py CHANGED
@@ -283,6 +283,36 @@ class Perforce:
283
283
  bool: 체크아웃 성공 시 True, 실패 시 False
284
284
  """
285
285
  return self._file_op("edit", file_path, change_list_number, "체크아웃")
286
+
287
+ def checkout_files(self, file_paths: list, change_list_number: int) -> bool:
288
+ """여러 파일을 한 번에 체크아웃합니다.
289
+
290
+ Args:
291
+ file_paths (list): 체크아웃할 파일 경로 리스트
292
+ change_list_number (int): 체인지 리스트 번호
293
+
294
+ Returns:
295
+ bool: 모든 파일 체크아웃 성공 시 True, 하나라도 실패 시 False
296
+ """
297
+ if not file_paths:
298
+ logger.debug("체크아웃할 파일 목록이 비어있습니다.")
299
+ return True
300
+
301
+ logger.info(f"체인지 리스트 {change_list_number}에 {len(file_paths)}개 파일 체크아웃 시도...")
302
+
303
+ all_success = True
304
+ for file_path in file_paths:
305
+ success = self.checkout_file(file_path, change_list_number)
306
+ if not success:
307
+ all_success = False
308
+ logger.warning(f"파일 '{file_path}' 체크아웃 실패")
309
+
310
+ if all_success:
311
+ logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에 성공적으로 체크아웃했습니다.")
312
+ else:
313
+ logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에 체크아웃하지 못했습니다.")
314
+
315
+ return all_success
286
316
 
287
317
  def add_file(self, file_path: str, change_list_number: int) -> bool:
288
318
  """파일을 추가합니다.
@@ -295,6 +325,36 @@ class Perforce:
295
325
  bool: 추가 성공 시 True, 실패 시 False
296
326
  """
297
327
  return self._file_op("add", file_path, change_list_number, "추가")
328
+
329
+ def add_files(self, file_paths: list, change_list_number: int) -> bool:
330
+ """여러 파일을 한 번에 추가합니다.
331
+
332
+ Args:
333
+ file_paths (list): 추가할 파일 경로 리스트
334
+ change_list_number (int): 체인지 리스트 번호
335
+
336
+ Returns:
337
+ bool: 모든 파일 추가 성공 시 True, 하나라도 실패 시 False
338
+ """
339
+ if not file_paths:
340
+ logger.debug("추가할 파일 목록이 비어있습니다.")
341
+ return True
342
+
343
+ logger.info(f"체인지 리스트 {change_list_number}에 {len(file_paths)}개 파일 추가 시도...")
344
+
345
+ all_success = True
346
+ for file_path in file_paths:
347
+ success = self.add_file(file_path, change_list_number)
348
+ if not success:
349
+ all_success = False
350
+ logger.warning(f"파일 '{file_path}' 추가 실패")
351
+
352
+ if all_success:
353
+ logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에 성공적으로 추가했습니다.")
354
+ else:
355
+ logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에 추가하지 못했습니다.")
356
+
357
+ return all_success
298
358
 
299
359
  def delete_file(self, file_path: str, change_list_number: int) -> bool:
300
360
  """파일을 삭제합니다.
@@ -307,6 +367,36 @@ class Perforce:
307
367
  bool: 삭제 성공 시 True, 실패 시 False
308
368
  """
309
369
  return self._file_op("delete", file_path, change_list_number, "삭제")
370
+
371
+ def delete_files(self, file_paths: list, change_list_number: int) -> bool:
372
+ """여러 파일을 한 번에 삭제합니다.
373
+
374
+ Args:
375
+ file_paths (list): 삭제할 파일 경로 리스트
376
+ change_list_number (int): 체인지 리스트 번호
377
+
378
+ Returns:
379
+ bool: 모든 파일 삭제 성공 시 True, 하나라도 실패 시 False
380
+ """
381
+ if not file_paths:
382
+ logger.debug("삭제할 파일 목록이 비어있습니다.")
383
+ return True
384
+
385
+ logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 삭제 시도...")
386
+
387
+ all_success = True
388
+ for file_path in file_paths:
389
+ success = self.delete_file(file_path, change_list_number)
390
+ if not success:
391
+ all_success = False
392
+ logger.warning(f"파일 '{file_path}' 삭제 실패")
393
+
394
+ if all_success:
395
+ logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에서 성공적으로 삭제했습니다.")
396
+ else:
397
+ logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에서 삭제하지 못했습니다.")
398
+
399
+ return all_success
310
400
 
311
401
  def submit_change_list(self, change_list_number: int) -> bool:
312
402
  """체인지 리스트를 제출합니다.
@@ -393,6 +483,28 @@ class Perforce:
393
483
  self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제")
394
484
  return False
395
485
 
486
+ def revert_file(self, file_path: str, change_list_number: int) -> bool:
487
+ """체인지 리스트에서 특정 파일을 되돌립니다.
488
+
489
+ Args:
490
+ file_path (str): 되돌릴 파일 경로
491
+ change_list_number (int): 체인지 리스트 번호
492
+
493
+ Returns:
494
+ bool: 되돌리기 성공 시 True, 실패 시 False
495
+ """
496
+ if not self._is_connected():
497
+ return False
498
+
499
+ logger.info(f"파일 '{file_path}'을 체인지 리스트 {change_list_number}에서 되돌리기 시도...")
500
+ try:
501
+ self.p4.run_revert("-c", change_list_number, file_path)
502
+ logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.")
503
+ return True
504
+ except P4Exception as e:
505
+ self._handle_p4_exception(e, f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기")
506
+ return False
507
+
396
508
  def revert_files(self, change_list_number: int, file_paths: list) -> bool:
397
509
  """체인지 리스트 내의 특정 파일들을 되돌립니다.
398
510
 
@@ -401,7 +513,7 @@ class Perforce:
401
513
  file_paths (list): 되돌릴 파일 경로 리스트
402
514
 
403
515
  Returns:
404
- bool: 되돌리기 성공 시 True, 실패 시 False
516
+ bool: 모든 파일 되돌리기 성공 시 True, 하나라도 실패 시 False
405
517
  """
406
518
  if not self._is_connected():
407
519
  return False
@@ -410,14 +522,20 @@ class Perforce:
410
522
  return True
411
523
 
412
524
  logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...")
413
- try:
414
- for file_path in file_paths:
415
- self.p4.run_revert("-c", change_list_number, file_path)
416
- logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.")
417
- return True
418
- except P4Exception as e:
419
- self._handle_p4_exception(e, f"체인지 리스트 {change_list_number}에서 파일 되돌리기")
420
- return False
525
+
526
+ all_success = True
527
+ for file_path in file_paths:
528
+ success = self.revert_file(file_path, change_list_number)
529
+ if not success:
530
+ all_success = False
531
+ logger.warning(f"파일 '{file_path}' 되돌리기 실패")
532
+
533
+ if all_success:
534
+ logger.info(f"모든 파일({len(file_paths)}개)을 체인지 리스트 {change_list_number}에서 성공적으로 되돌렸습니다.")
535
+ else:
536
+ logger.warning(f"일부 파일을 체인지 리스트 {change_list_number}에서 되돌리지 못했습니다.")
537
+
538
+ return all_success
421
539
 
422
540
  def check_update_required(self, file_paths: list) -> bool:
423
541
  """파일이나 폴더의 업데이트 필요 여부를 확인합니다.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyjallib
3
- Version: 0.1.10
3
+ Version: 0.1.11
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