diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..1a373150 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,212 @@ +#!/bin/bash + +# Pre-commit hook: Unity 에셋 네이밍 컨벤션 검사 +# 검사 대상: Assets/_Game/ 하위에 새로 추가(A)되거나 이름 변경(R)된 파일 +# 제외: .meta 파일, Scripts/ + +ERRORS=() + +STAGED_FILES=$(git diff --cached --name-only --diff-filter=AR \ + | grep "^Assets/_Game/" \ + | grep -v "\.meta$" \ + | grep -v "^Assets/_Game/Scripts/" \ + | grep -v "Human-Custom" \ + | grep -E "\.(asset|prefab|controller|fbx)$") + +for FILE in $STAGED_FILES; do + FILENAME=$(basename "$FILE") + NAME="${FILENAME%.*}" # 확장자 제거 + DIR=$(dirname "$FILE") + + VALID=true + EXPECTED="" + + case "$DIR" in + Assets/_Game/Data/Passives/Nodes*) + EXPECTED="Data_PassiveNode_{대상}_{이름} 예) Data_PassiveNode_Player_Tank_Core" + echo "$NAME" | grep -qE "^Data_PassiveNode_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+$" || VALID=false + ;; + Assets/_Game/Data/Passives/Presets*) + EXPECTED="Data_PassivePreset_{대상}_{이름} 예) Data_PassivePreset_Player_Tank" + echo "$NAME" | grep -qE "^Data_PassivePreset_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+$" || VALID=false + ;; + Assets/_Game/Data/Passives*) + EXPECTED="Data_PassiveTree_{대상}_{이름} / Data_PassiveCatalog_{대상}_{이름} / Data_PassivePrototypeCatalog 예) Data_PassiveTree_Player_Prototype" + echo "$NAME" | grep -qE "^Data_Passive(Tree|Catalog)_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+$|^Data_PassivePrototypeCatalog$" || VALID=false + ;; + Assets/_Game/Data/Skills/Effects*) + EXPECTED="Data_SkillEffect_{대상}_{스킬명}_{순서}_{효과} 예) Data_SkillEffect_Player_베기_0_데미지" + echo "$NAME" | grep -qE "^Data_SkillEffect_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+_[0-9]+_.+$" || VALID=false + ;; + Assets/_Game/Data/Skills*) + EXPECTED="Data_Skill_{대상}_{이름} 예) Data_Skill_Player_근접베기" + echo "$NAME" | grep -qE "^Data_Skill_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+$" || VALID=false + ;; + Assets/_Game/Data/Abnormalities*) + EXPECTED="Data_Abnormality_{대상}_{이름} 예) Data_Abnormality_Test_버프" + echo "$NAME" | grep -qE "^Data_Abnormality_.+_.+$" || VALID=false + ;; + Assets/_Game/Data/Weapons*) + EXPECTED="Data_Weapon_{이름} 예) Data_Weapon_검" + echo "$NAME" | grep -qE "^Data_Weapon_.+$" || VALID=false + ;; + Assets/_Game/Data/Enemies*) + EXPECTED="Data_Enemy_{이름} 예) Data_Enemy_TestBoss" + echo "$NAME" | grep -qE "^Data_Enemy_.+$" || VALID=false + ;; + Assets/_Game/Data/Patterns*) + EXPECTED="Data_Pattern_{대상}_{이름} 예) Data_Pattern_TestBoss_우수2연타" + echo "$NAME" | grep -qE "^Data_Pattern_.+_.+$" || VALID=false + ;; + Assets/_Game/Prefabs/UI*) + EXPECTED="UI_{이름} 예) UI_BossHealthBar" + echo "$NAME" | grep -qE "^UI_.+$" || VALID=false + ;; + Assets/_Game/Prefabs*) + EXPECTED="Prefab_{대상}_{이름} 예) Prefab_Player_Default" + echo "$NAME" | grep -qE "^Prefab_(Player|Boss|Enemy|Weapon|Common|[A-Za-z]+)_.+$" || VALID=false + ;; + Assets/_Game/Animations/Controllers*) + EXPECTED="AC_{대상}_{이름} 예) AC_Player_Default" + echo "$NAME" | grep -qE "^AC_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+$" || VALID=false + ;; + Assets/_Game/Animations*) + if echo "$NAME" | grep -qE "^Anim_Player_"; then + # 플레이어: Anim_Player_{무기타입}_{스킬이름}_{순서} + # 무기타입: 한손, 양손, 활, 방패, 마법, 공용 + # 순서: _{숫자} (선택) + EXPECTED="Anim_Player_{무기타입}_{스킬이름}_{순서} 예) Anim_Player_한손_베기_0, Anim_Player_공용_구르기_0" + echo "$NAME" | grep -qE "^Anim_Player_(한손|양손|활|방패|마법|공용)_.+_[0-9]+$" || VALID=false + else + # 기타: Anim_{소유자}_{이름}_{순서} + # 순서: _{숫자} (선택) + EXPECTED="Anim_{소유자}_{이름}_{순서} 예) Anim_Drog_평타1R_0, Anim_Common_Idle" + echo "$NAME" | grep -qE "^Anim_(Player|Boss|Enemy|Common|[A-Za-z]+)_.+(_[0-9]+)?$" || VALID=false + fi + ;; + Assets/_Game/Models*) + EXPECTED="Model_{대상}_{이름} 예) Model_Player_Base" + echo "$NAME" | grep -qE "^Model_(Player|Boss|Enemy|Weapon|Common|[A-Za-z]+)_.+$" || VALID=false + ;; + Assets/_Game/AI*) + EXPECTED="BT_{이름} 예) BT_TestBoss" + echo "$NAME" | grep -qE "^BT_.+$" || VALID=false + ;; + esac + + if [ "$VALID" = false ]; then + ERRORS+=("$FILE\n 형식: $EXPECTED") + fi +done + +if [ ${#ERRORS[@]} -gt 0 ]; then + echo "" + echo "❌ 네이밍 컨벤션 위반:" + for ERR in "${ERRORS[@]}"; do + echo -e " ✗ $ERR" + done + echo "" + echo "파일 이름을 수정하거나, 의도적으로 건너뛰려면 git commit --no-verify 를 사용하세요." + exit 1 +fi + +# ── Anim_ ↔ Data_Skill_ 매칭 검증 ── +# Anim_{key}_{순서} 클립과 Data_Skill_{key} 애셋의 key가 일치하는지 확인합니다. + +MISMATCH_ERRORS=() + +# 스테이징된 Anim_ 클립에서 key 추출 (Anim_ 접두사 제거 후 마지막 _숫자 제거) +ANIM_KEYS=() +STAGED_ANIMS=$(git diff --cached --name-only --diff-filter=ACDMR \ + | grep "^Assets/_Game/Animations/" \ + | grep "\.anim$" \ + | grep -v "\.meta$") + +for FILE in $STAGED_ANIMS; do + FILENAME=$(basename "$FILE") + NAME="${FILENAME%.*}" + if echo "$NAME" | grep -qE "^Anim_"; then + # Anim_ 제거 → 마지막 _숫자 제거 → key + KEY=$(echo "$NAME" | sed 's/^Anim_//' | sed 's/_[0-9]\+$//') + if [ -n "$KEY" ]; then + ANIM_KEYS+=("$KEY") + fi + fi +done + +# 스테이징된 Data_Skill_ 애셋에서 key 추출 +SKILL_KEYS=() +STAGED_SKILLS=$(git diff --cached --name-only --diff-filter=ACDMR \ + | grep "^Assets/_Game/Data/Skills/Data_Skill_" \ + | grep "\.asset$" \ + | grep -v "\.meta$") + +for FILE in $STAGED_SKILLS; do + FILENAME=$(basename "$FILE") + NAME="${FILENAME%.*}" + KEY=$(echo "$NAME" | sed 's/^Data_Skill_//') + if [ -n "$KEY" ]; then + SKILL_KEYS+=("$KEY") + fi +done + +# 매칭 검증: 스테이징된 Data_Skill_에 대응하는 Anim_ 클립이 있는지 +for SKILL_KEY in "${SKILL_KEYS[@]}"; do + FOUND=false + # 워킹 트리(스테이징 포함)에서 매칭 클립 검색 + MATCHED=$(git diff --cached --name-only --diff-filter=ACDMR \ + | grep "^Assets/_Game/Animations/Anim_${SKILL_KEY}_" \ + | grep "\.anim$" \ + | head -1) + if [ -n "$MATCHED" ]; then + FOUND=true + fi + # 워킹 트리에서 기존 클립도 확인 + if [ "$FOUND" = false ]; then + MATCHED=$(find Assets/_Game/Animations -name "Anim_${SKILL_KEY}_[0-9]*.anim" 2>/dev/null | head -1) + if [ -n "$MATCHED" ]; then + FOUND=true + fi + fi + if [ "$FOUND" = false ]; then + MISMATCH_ERRORS+=("Data_Skill_${SKILL_KEY} 에 대응하는 Anim_${SKILL_KEY}_{순서} 클립이 없습니다") + fi +done + +# 매칭 검증: 스테이징된 Anim_ 클립에 대응하는 Data_Skill_이 있는지 +for ANIM_KEY in "${ANIM_KEYS[@]}"; do + FOUND=false + # 워킹 트리에서 매칭 스킬 검색 + MATCHED=$(find Assets/_Game/Data/Skills -name "Data_Skill_${ANIM_KEY}.asset" 2>/dev/null | head -1) + if [ -n "$MATCHED" ]; then + FOUND=true + fi + # 스테이징된 파일에서도 확인 + if [ "$FOUND" = false ]; then + for FILE in $STAGED_SKILLS; do + FILENAME=$(basename "$FILE") + NAME="${FILENAME%.*}" + CHECK_KEY=$(echo "$NAME" | sed 's/^Data_Skill_//') + if [ "$CHECK_KEY" = "$ANIM_KEY" ]; then + FOUND=true + break + fi + done + fi + if [ "$FOUND" = false ]; then + MISMATCH_ERRORS+=("Anim_${ANIM_KEY} 클립에 대응하는 Data_Skill_${ANIM_KEY} 애셋이 없습니다") + fi +done + +if [ ${#MISMATCH_ERRORS[@]} -gt 0 ]; then + echo "" + echo "❌ Anim_ ↔ Data_Skill_ 매칭 누락:" + for ERR in "${MISMATCH_ERRORS[@]}"; do + echo -e " ✗ $ERR" + done + echo "" + echo "대응하는 애셋을 생성하거나, 의도적으로 건너뛰려면 git commit --no-verify 를 사용하세요." + exit 1 +fi + +exit 0