docs(BT37~BT38-FirstRising): 오염된 던전 바닥 개선 기록 · 쓰이지 않는 에셋 목록 · 푸시 기록

PD 지시 BT38 "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌이야 …
좀 더 자연스러운 던전 타일처럼 수정해줘" 와 그 뒤 PD 답변(커밋 · 푸시는 FR 과 조직 레포 모두
/ 안 쓰는 에셋은 남기되 목록으로 정리 / 커밋 규칙은 지금처럼 지시할 때만).
FR 레포는 bt/work 5f9c8dd297 로 push 완료.

- 맵 빌드결과 문서 §9: 원인(큰 바닥돌 46장 · 지형 층 512 픽셀 · 돌판 다리) · 한 일 · 조정 과정 · 결과 · 남은 것
- 미사용_에셋_목록.md(새 문서): FR 맵 폴더 397개 가운데 쓰이지 않는 35개(10.0 MB)
  — 묶음별 까닭 · 지워도 되는가 · 되살리는 법 · 전체 목록 · 판정 기준 · 다시 조사하는 법. 지운 것은 없다
- 그림: 방 · 통로 · 돌판 다리의 앞뒤 비교, 보스전 전용 필드
- 도구: texderive_dungeon.py(던전 타일 텍스처) · map_tune5.py(맵 정의 조정) · floor_tour.sh(바닥 촬영 순회)
  · unused_report.py · unity_eval(unused_scan · make_dun_deck · repaint_one · lit_sources · clip_impact · save_all)
- 대화로그 2026-09-28 §5(조직 레포 커밋 · 푸시) · §6(BT38)
- PD 지시 로그(개발팀): BT37 결과 보강 · BT38

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-28 16:18:46 +09:00
parent c693d2e82e
commit 1b136d88bc
18 changed files with 942 additions and 4 deletions

View File

@ -33,7 +33,8 @@ C3·C13 위반에 해당. **즉시 자진 보고 후 소급 등록**.
| # | 일시 | 지시 요지 | 처리 상태 | 산출물 경로 | 중단 사유 | 사후 조치 |
|---|------|----------|----------|-----------|----------|----------|
| BT37-FirstRising | 2026-09-28 14:53 | **남은 백그라운드 작업 확인 · 작업물 푸시** — PD "백그라운드 작업은 왜 남아있지? 작업이 끝났으면 푸시하도록 해" | **완료(09-28 14:57)** — ① 남아 있던 백그라운드 작업 = 12:03 에 에디터를 다시 띄운 셸(`unity open … | head`). 띄우는 명령은 끝났는데 출력 통로를 에디터가 물고 있어 `head` 가 끝나지 않았다(하는 일 없음) → `head` 만 종료, 에디터는 그대로 [실측] ② BT34~BT36 작업물을 `bt/work` 에 커밋 `1ce5df5b2e`(507개 파일) → push 완료, 원격 = 로컬 [실측]. 에디터가 덧붙인 변경 7개는 넣지 않음 | FR `bt/work` `1ce5df5b2e` | — | 조직 레포 문서 · 도구는 커밋하지 않음(지시 범위 밖으로 판단) |
| BT38-FirstRising | 2026-09-28 15:09 | **오염된 던전 바닥 텍스처 개선** — PD "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌이야. 체크해보고 좀 더 자연스러운 던전 타일처럼 수정해줘." | **완료(09-28 15:37) · PD 확인 대기** — 원인 [실측]: ① 안쪽 장식 띠의 바닥 판 46장이 높이 기준 맞춤 탓에 2~4.9 배(폭 21~69 m)로 커져 바닥 전체를 덮음(BT31 PM 실수) ② 지형 층 텍스처가 512 픽셀(1 m 당 64~109) ③ 돌판 다리도 같은 큰 판. 조치: 띠 제거 · 지형 층을 던전 타일 1024 픽셀 4 m(1 m 당 256 · 돌 한 장 0.5~1 m) + 흙 얼룩 · 다리 판 교체 · 뼈 무더기 네모 판 오려내기. 빌드 OK · 보스 자리 막힘 0 ㎡ · Play 와 전용 필드 R4 에디터 로그 오류 0. PD 답변(09-28 16:17)으로 FR `bt/work` `5f9c8dd297` · 조직 레포 `main` 커밋 · push, 쓰이지 않는 에셋은 지우지 않고 목록으로 정리(맵 폴더 35개 · 10.0 MB), 커밋 규칙은 '지시할 때만' 유지 | FR `Assets/BurningTimes/Maps/PlagueCatacomb/`(정의 · Textures · Terrain · Deck) · `Editor/Maps/MapPropMaterials.cs` · 빌드결과 문서 §9 · `맵/tools/texderive_dungeon.py` · `map_tune5.py` · `맵/미사용_에셋_목록.md` | — | 쓰이지 않는 에셋 삭제는 PD 결정 대기(목록 문서) · 항구 널빤지(1.5~2 배)는 손대지 않음 |
| BT37-FirstRising | 2026-09-28 14:53 | **남은 백그라운드 작업 확인 · 작업물 푸시(FR · 조직 레포)** — PD "백그라운드 작업은 왜 남아있지? 작업이 끝났으면 푸시하도록 해" · (후속) "조직 레포도 커밋해야하지 않아?" | **완료(09-28 14:57)** — ① 남아 있던 백그라운드 작업 = 12:03 에 에디터를 다시 띄운 셸(`unity open … | head`). 띄우는 명령은 끝났는데 출력 통로를 에디터가 물고 있어 `head` 가 끝나지 않았다(하는 일 없음) → `head` 만 종료, 에디터는 그대로 [실측] ② BT34~BT36 작업물을 `bt/work` 에 커밋 `1ce5df5b2e`(507개 파일) → push 완료, 원격 = 로컬 [실측]. 에디터가 덧붙인 변경 7개는 넣지 않음 ③ 조직 레포: 커밋 3건(`705986b` 조직 규칙 BT20-Org · `89505fa` 이번 세션 문서/도구/로그 · `c693d2e` 앞선 세션 기록 BT16~BT22) → `main` push 완료(`5698f2c..c693d2e`), 원격 = 로컬 [실측]. 첫 push 는 인증 실패, 재시도에서 성공 | FR `bt/work` `1ce5df5b2e` · 조직 레포 `main` `c693d2e` | — | FR 에셋 백업 폴더(1.2 GB)는 레포에 올리지 않음 — 무시 목록에 넣을지 PD 판단 |
| BT36-FirstRising | 2026-09-28 12:5x | **블렌더로 테마별 소품 제작 · 전 맵 꾸미기** — PD "지금까지 만들어진 맵에 오브젝트나 프랍들이 너무 반복적으로 재활용된 느낌이야. 나는 블렌더를 활용해 네가 맵 테마나 컨셉에 맞게 오브젝트나 프랍들을 제작하면 좋겠어. 이번에 추가한 사막맵을 포함해 비주얼적인 완성도를 높이기 위해 자연스럽게 꾸미기 좋은 프랍이나 오브젝트를 블랜더로 추가 제작한 후 배치해서 맵을 꾸며줘. 그리고 모든 맵에 꾸미기 작업이 완성되면 보고해. (나는 상업적인 성공을 위해 비주얼과 완성도를 신경쓰고 싶어.) * 각 맵의 특징과 컨셉을 고려해 자연스럽고 비주얼적인 완성도를 높이는 것이 목표야"(BT35 진행 중 추가 지시) | **완료(09-28) · PD 확인 대기** — 블렌더로 7 테마 67종 제작, 맵 6종에 28~67개 + 숲(로비) 42개 배치. 화면 확인 뒤 6건 수정(검은 풀 · 성채 돌 밝기 · 줄무늬 · 알 발광 · 사막 분위기/바위 · 항구 보스 자리). Play 순회 에디터 로그 오류 0 · 물리 점검 통과 [실측]. **모델 품질은 구매 에셋보다 단순하다(PM 판단)**. `bt/work` `1ce5df5b2e` push(BT37) | FR `Assets/BurningTimes/Maps/_Shared/Props/` · `Editor/Maps/MapPropImporter.cs` · `공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md` §8 · `맵/tools/blender_props/` | — | 소품 품질 PD 판단 대기 · 기기 빌드 미검증 |
| BT35-FirstRising | 2026-09-28 12:4x | **이동 가능 영역 점검 · 거미 동굴 바닥 품질 · 사막 맵 1종 추가** — PD "자잘한 오브젝트로 인해 이동이 방해되는 부분이 있는거 같아. 체크해보고 비주얼적으로 확실히 막혀야할 지역과 충분히 이동이 가능할 것으로 보이는 지형이 막히지 않도록 이동 가능한 영역을 체크해줘." · "거미여왕의 동굴의 바닥 텍스쳐가 너무 퀄리티가 떨어진 느낌이야. 다른 맵에 비해 크게 확대되어서 퀄리티가 떨어진 느낌인데, 수정해줘." · "추가로 황무지나 사막에 적합한 지형의 텍스쳐를 활용해 사막 지역의 맵도 1종 추가해줘." | **완료(09-28) · PD 확인 대기** — ① 이동 방해 원인 = 소품 자체 충돌체 → 끔. Play 물리 점검 걸리는 칸 0(묘지 교회 옆 1.3 ㎡ 제외) · 길 폭 6.6 m 이상 · 보스 자리 막힘 0 ㎡ [실측] ② 동굴 바닥 타일 8 · 8 · 6 m → 4 · 3.5 · 4 m ③ 사막 맵 `ScorchedDesert` 추가(전용 필드 · 필드 이동 목록 포함 · 전용 필드의 보스 짝 C1 은 PM 선택). `bt/work` `1ce5df5b2e` push(BT37) | FR `Assets/BurningTimes/Maps/ScorchedDesert/` · `Assets/Scenes/Maps/ScorchedDesert*` · `Editor/Maps/` · 빌드결과 문서 §7 | — | 사막 분위기 · 보스 짝 PD 확인 대기 |
| BT34-FirstRising | 2026-09-28 11:2x | **종료 시 에러 수정 · 보스전 필드 바닥 반복 패턴 완화 · 필드 이동 버튼** — PD "게임을 나갈 때 왜 자꾸 에러가 발생되는거지? 수정해줘." · "보스 전 맵의 바닥패턴이 너무 반복적이야. 특히 자연 지형(초원 형태의 맵)의 경우, 자연스러운 지형처럼 보일 수 있도록 텍스쳐를 구성하거나 예를들어 패턴 텍스쳐 위에 흙이나 풀밭 같은 레이어를 1~2개 더 추가해서 반복 패턴이 너무 드러나지 않게 바닥 텍스쳐를 구성해줘." · "보스전 버튼 하단에, 필드 이동 버튼을 추가해서 새로 제작 된 필드를 테스트할 수 있도록 해줘.(드랍다운 형태로 맵 변경할 수 있도록)" | **완료(09-28 12:30) · PD 확인 대기** — ① 종료 시 에러: 원인 = 전용 필드를 닫을 때의 에셋 해제(BT32 PM 코드) → 빼고 모든 종료 경로 에디터 로그 오류 0 [실측]. 앞선 '콘솔 오류 0' 보고는 확인 방법 결함으로 틀렸음(정정 기록) ② 바닥: 층 수는 그대로 두고 자연물 층을 얼룩으로 덧칠 — 보스 자리 한 층 100% → 네 층 20~57% ③ 필드 이동: "보스전" 아래 드롭다운(로비 + 5종) · 8~12초 · 오류 0. **에디터 Play 전용**(테마 맵 씬이 빌드 목록에 없음) · `bt/work` `1ce5df5b2e` push(BT37) | FR `Assets/BurningTimes/Visual/Scripts/TVFieldTravel.cs` · `Boss/Scripts/TVBossField.cs` · `TVBossArenaUI.cs` · `Editor/Maps/MapTerrainBuilder.cs` · 구현 노트 v2 §12 · 맵 빌드결과 §6 · 대화로그 §3 | — | 바닥 자연스러움 PD 확인 · 빌드 목록 반영 여부 |

View File

@ -210,3 +210,43 @@ PD 지시: "지금까지 만들어진 맵에 오브젝트나 프랍들이 너무
- 새 소품에는 충돌체가 없다 — 막는 것은 보이지 않는 벽이 맡는다. 숲 맵의 새 소품은 걸을 수 없는 곳에만 있어 벽을 다시 만들지 않았다.
- 기기 빌드에서의 모습 · 성능은 확인하지 못했다. 아틀라스 7장(색 · 노멀 · 발광 각 2048)이 늘었다.
- 필드 이동은 에디터 Play 전용이다(맵 씬이 빌드 목록에 없다).
## 9. 오염된 던전 바닥 — 던전 타일로 (BT38 · 2026-09-28)
PD 지시: "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌이야. 체크해보고 좀 더 자연스러운 던전 타일처럼 수정해줘."
### 9-1. 원인 [실측]
| 원인 | 실측 |
|---|---|
| **바닥을 덮은 큰 바닥돌(주된 원인)** | 길 안쪽 장식 띠 '안쪽 깨진 바닥돌'이 팩의 10 m 바닥 판(GroundTileCATAM)을 '높이 0.04~0.1 m'에 맞춰 놓았다. 판의 높이가 0.02 m 라 **2~4.9 배로 커져 폭 21~69 m 짜리 판 46장**이 서로 겹쳐 바닥 전체를 덮었다(덮은 넓이 합 9.7만 ㎡ · 맵의 걸을 수 있는 면적 8,199 ㎡). 무늬도 그만큼 커지고 흐려졌다 |
| 지형 층의 해상도 | 팩 텍스처가 PC 에서 512 픽셀로 들어온다 — 바탕(무늬 없는 바위) 8 m 에 1 m 당 64 픽셀 · 공터(깨진 타일) 4.7 m 에 1 m 당 109 픽셀. 다른 맵의 주 바닥은 223~256 |
| 돌판 다리 | 독액 수로 위 상판 2곳도 같은 10 m 바닥 판 — 돌 한 장이 2~3 m |
- BT31 에서 안쪽 장식 띠를 만들 때 '높이에 맞추기'를 납작한 판에 쓴 PM 의 실수다. 같은 실수가 다른 맵에 있는지 전 맵의 소품 기록을 훑었다 — 넓적하고 배율 1.5 배를 넘는 것은 던전의 바닥돌 46개와 항구의 널빤지 19개(1.5~2 배 · 5.5~8 m)뿐. 항구 널빤지는 이번에 손대지 않았다.
### 9-2. 한 일
- **큰 바닥돌 띠를 걷어냈다**(정의의 띠 · 풀 삭제).
- **지형 층 넷을 던전 타일로**: 팩의 던전 타일(FloorA 온전 · FloorA3 얼룩)을 1024 픽셀 사본으로 만들어 4 m 로 깐다(1 m 당 256 픽셀 · 돌 한 장 0.5~1 m). 길 = 온전한 타일 · 방과 보스 자리 = 얼룩진 타일 · 오염 저수조와 감옥 = 줄눈에 점액이 고인 타일(얼룩진 타일에 그려 넣음) · 바탕 = 흙과 자갈(5 m).
- 타일 세 가지는 줄눈 배치가 같다(높이 맵 상관 0.74~0.82) → 층이 바뀌어도 줄눈이 이어진다.
- 게임 에셋의 들여오기 설정은 건드리지 않았다(사본을 맵 폴더에 둔다).
- **돌판 다리**: 바닥과 같은 타일을 같은 크기로 입힌 판(`Maps/PlagueCatacomb/Deck/FRMap_dun_deck`)으로 교체.
- **뼈 무더기의 네모난 판**: 가장자리 판의 재질(CATA_BonePileAlfa)이 게임 전용 '불투명' 셰이더라 네모가 그대로 보였다. 재질 파일에 남은 바뀌기 전 값(_AlphaClip 1)을 읽어 맵 재질 사본에서는 오려 낸다 — 바뀐 사본은 이것 하나(사본 50개 대조) [실측]. 이 사본은 맵 6종이 함께 쓴다.
### 9-3. 조정 과정
- 1차(타일 세 가지만): 또렷해졌지만 넓은 보스 자리에서 같은 무늬가 4 m 마다 격자처럼 보였다(세 타일의 줄눈 배치가 같아 덧칠로는 되풀이가 가려지지 않는다).
- 2차: 깨진 타일 대신 무늬가 다른 흙 층을 넣어 얼룩으로 덮고, 텍스처 안의 밝기 차(눈에 띄는 밝은 돌)를 폈다 → 격자가 약해졌다. 점액은 선명한 녹색이 페인트처럼 보여 탁한 올리브빛으로 낮췄다.
### 9-4. 결과 [실측]
- 빌드 RESULT OK · 길 폭 최소 7.5 m · 보스 자리 안 막힌 칸 0 ㎡ · 걸을 수 있는 면적 8,199 ㎡(바뀌지 않음).
- 지형 층 넷 모두 1024 픽셀 · 1 m 당 256 픽셀(흙 층은 205).
- 보스 자리 층 몫: 흙 19 · 온전한 타일 25 · 얼룩진 타일 47 · 점액 타일 9(%).
- Play(로비 → 필드 이동 → 던전, 보스전 전용 필드 R4 입장 · 퇴장): 에디터 로그 오류 0.
- 그림: `img/bt38_room_before_after.jpg` · `bt38_corridor_before_after.jpg` · `bt38_deck_before_after.jpg` · `bt38_bossfield_after.jpg`(왼쪽이 앞 · 오른쪽이 뒤).
### 9-5. 남은 것 · 미검증
- 4 m 마다의 되풀이는 약해졌지만 없어지지 않았다(넓은 바닥을 멀리서 보면 보인다).
- 쓰이지 않게 된 에셋은 **지우지 않고 남겼다(PD 결정 2026-09-28)** — 던전의 옛 지형 층 5개 · 텍스처 6개를 포함해 맵 폴더 전체에서 35개(10.0 MB). 목록과 까닭은 `미사용_에셋_목록.md`(같은 폴더).
- 화면이 전체로 어둡고 녹색 한 가지인 것은 맵 분위기 값 때문이다(이번 지시 범위 밖이라 손대지 않았다).
- 기기 빌드에서의 모습 · 성능은 확인하지 못했다.
- FR `bt/work` `5f9c8dd297` 로 push 완료(PD 지시 2026-09-28).

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -0,0 +1,53 @@
#!/bin/bash
# 맵 하나의 바닥을 자리마다 찍는다. bash floor_tour.sh <꼬리표> <맵 이름> "<x> <z>" ["<x> <z>" ...] → cap/floor_<맵>_<꼬리표>_<번호>.png
# Entry 에서 Play → 로비 → 필드 이동 → 준비 기다림 → 자리마다 촬영(임시 카메라 · 플레이어는 옮기지 않는다) → 에디터 로그 점검 → Play 종료
# BOSS=<보스 열쇠> 를 주면 끝에 그 보스의 전용 필드에도 들어가 찍는다(예: BOSS=R4)
SP="C:/Users/sw/AppData/Local/Temp/claude/E--BurningTimes/3562a2c1-e88a-4143-b7ec-e74c317778d8/scratchpad"
source "$SP/bt.sh"
export PYTHONIOENCODING=utf-8
TAG="$1"; MAP="$2"; shift; shift
st
python "$SP/logscan.py" mark
unity command clear_console --project-path "$P" --format json --no-banner >/dev/null 2>&1
unity command editor_play --project-path "$P" --format json --no-banner 2>&1 | grep -E '"success"' | head -1
for i in $(seq 1 40); do
sleep 3
r=$(ev lobby_ready 2>&1 | head -1)
case "$r" in *"scene=Lobby"*"player=True"*"config=True"*) echo "ready $i"; break;; esac
done
sleep 6
ev travel go "$MAP" | grep -o "go=[A-Za-z]*"
ok=""
for k in $(seq 1 60); do
sleep 2
s=$(ev travel state 2>&1 | head -1)
case "$s" in *"traveling=False"*"current=$MAP/"*"player=True"*) ok="1"; break;; esac
done
sleep 8
echo "[$MAP] ${ok:+OK} $(ev travel state | grep -o 'characters=[0-9]*\|pos=([^)]*)' | tr '\n' ' ')"
ev shot_cam "floor_${MAP}_${TAG}_0start" >/dev/null
n=1
for xz in "$@"; do
set -- $xz
ev shot_at "floor_${MAP}_${TAG}_${n}" "$1" "$2" >/dev/null
echo " shot $n ($1, $2)"
n=$((n+1))
done
python "$SP/logscan.py" scan "floor-$MAP"
if [ -n "$BOSS" ]; then
ev arena enter $BOSS | head -1
for k in $(seq 1 10); do
sleep 2
s=$(ev floor_probe 2>&1 | head -1)
case "$s" in "field="*) break;; esac
done
sleep 3
echo "$BOSS $s"
ev shot_cam "floor_${MAP}_${TAG}_boss" >/dev/null
ev arena exit | head -1
sleep 3
fi
unity command editor_stop --project-path "$P" --format json --no-banner 2>&1 | grep -E '"success"' | head -1
sleep 6
st
python "$SP/logscan.py" scan "floor-tour-$TAG"

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# 맵 정의 조정(2026-09-28 BT38): 오염된 던전(PlagueCatacomb)의 바닥을 던전 타일로 바꾼다.
# PD "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌이야 … 좀 더 자연스러운 던전 타일처럼 수정해줘"
# 원인(실측) 셋:
# ① 바닥을 덮은 큰 바닥돌 — 안쪽 장식 띠 '안쪽 깨진 바닥돌'이 10 m 짜리 바닥 판(GroundTileCATAM)을 '높이 0.04~0.1 m'에 맞춰 놓았는데,
# 판의 높이가 0.02 m 라 2~4.9 배로 커져(폭 21~69 m · 46장) 바닥 전체를 덮었다. 무늬도 그만큼 커지고 흐려졌다 → 띠와 풀을 걷어낸다
# ② 지형 층 — 바탕 = 무늬 없는 바위 8 m(512 픽셀 · 1 m 에 64 픽셀) · 공터 = 깨진 타일 4.7 m(512 픽셀 · 1 m 에 109 픽셀)
# → 팩의 던전 타일을 1024 픽셀 사본으로 만들어 4 m 로 깐다(1 m 에 256 픽셀 · 돌 한 장 0.5~1 m). 길 = 온전한 타일 · 공터 = 얼룩진 타일 · 강조 = 점액이 고인 타일.
# 세 타일은 줄눈 배치가 같아 층이 바뀌어도 줄눈이 이어진다. 바탕은 무늬가 다른 흙 · 자갈(5 m)로 두고 얼룩으로 덧칠해 4 m 마다의 되풀이를 깬다
# ③ 돌판 다리(독액 수로 위 상판) — 같은 10 m 바닥 판이라 돌 한 장이 2~3 m → 바닥과 같은 타일을 입힌 판(FRMap_dun_deck)으로 바꾼다
# 텍스처는 texderive_dungeon.py, 다리 판은 tools/unity_eval/make_dun_deck.cs 가 만든다. 여러 번 돌려도 같다.
# 실행: python map_tune5.py [--no-backup] → 그 뒤 유니티에서 맵을 다시 짓는다(소품이 바뀌므로 Repaint 만으로는 안 된다)
import io, json, os, shutil, sys, time
ROOT = r"C:\BurningTimes\FirstRising\Assets\BurningTimes\Maps"
NAME = "PlagueCatacomb"
TEX = "Assets/BurningTimes/Maps/PlagueCatacomb/Textures/"
DECK = "Assets/BurningTimes/Maps/PlagueCatacomb/Deck/FRMap_dun_deck.prefab"
BACKUP = r"E:\BurningTimes\공유\개발팀_백업\FirstRising\maps_json_" + time.strftime("%Y%m%d_%H%M")
def layer(name, role, tex, tile, tint, nscale=0.6):
return {"name": name, "role": role, "albedo": TEX + tex + ".png", "normal": TEX + tex + "_N.png", "tile": tile, "normalScale": nscale, "tint": list(tint), "smooth": 0.0}
def overlay(name, scale, cover, soft, open_, route, area, boss, seed):
return {"layer": name, "scale": scale, "cover": cover, "soft": soft, "open": open_, "route": route, "area": area, "boss": boss, "seed": seed}
def main():
path = os.path.join(ROOT, NAME, NAME + ".map.json")
if "--no-backup" not in sys.argv:
os.makedirs(BACKUP, exist_ok=True)
shutil.copy2(path, os.path.join(BACKUP, NAME + ".map.json"))
print("backup", BACKUP)
raw = io.open(path, "rb").read()
bom = raw.startswith(b"\xef\xbb\xbf")
d = json.loads(raw.decode("utf-8-sig"))
g = d["ground"]
before = [(l["name"], l["tile"], os.path.basename(l["albedo"])) for l in g["layers"]]
# 층의 차례 = 바탕 · 길 · 공터 · 강조(길 · 공터의 floor 번호가 이 차례를 가리킨다 — 차례는 바꾸지 않는다)
g["layers"] = [
layer("Catacomb_Dirt", "base", "FRMap_dun_dirt", 5.0, (0.86, 0.90, 0.78)), # 방 밖 · 벽 밑 + 타일 위의 흙 얼룩
layer("Catacomb_Tile", "path", "FRMap_dun_tile", 4.0, (1.12, 1.12, 1.02)), # 통로: 온전한 타일(동선이 읽히게 가장 밝게)
layer("Catacomb_TileWorn", "plaza", "FRMap_dun_tile_worn", 4.0, (0.94, 0.98, 0.86)), # 방 · 보스 자리: 얼룩진 타일
layer("Catacomb_TileSlime", "accent", "FRMap_dun_tile_slime", 4.0, (1.00, 1.00, 0.95)), # 오염 저수조 · 감옥 · 독액 가: 줄눈에 점액이 고인 타일
]
g["overlays"] = [
overlay("Catacomb_Dirt", 8.0, 0.30, 0.07, 0.60, 0.35, 0.85, 0.75, 19), # 흙 얼룩 — 무늬가 달라 되풀이를 깬다(가장자리는 좁게: 타일 위에 흙이 '쌓인' 모습)
overlay("Catacomb_Tile", 6.0, 0.30, 0.12, 0.50, 0.00, 0.85, 0.85, 33), # 덜 더러운 타일 — 줄눈이 이어지므로 가장자리를 넓게 푼다
overlay("Catacomb_TileSlime", 4.4, 0.12, 0.10, 0.50, 0.22, 0.75, 0.60, 47),
]
# ① 큰 바닥돌 걷어내기
gone_band = [b["name"] for b in d["bands"] if b["pool"] == "inner_slab"]
d["bands"] = [b for b in d["bands"] if b["pool"] != "inner_slab"]
gone_pool = [p["name"] for p in d["pools"] if p["name"] == "inner_slab"]
d["pools"] = [p for p in d["pools"] if p["name"] != "inner_slab"]
print("걷어낸 띠:", gone_band, "· 풀:", gone_pool)
# ③ 돌판 다리의 판 — 풀 'slab' 을 새 판 하나로
for p in d["pools"]:
if p["name"] != "slab":
continue
old = [os.path.basename(i["path"]) for i in p["items"]]
p["items"] = [{"path": DECK, "fit": "l", "a": 10.0, "b": 10.0, "occ": 0.0, "w": 1, "fade": False, "anchor": "c", "sink": 0.0, "block": "pass", "tilt": 0.0, "yoff": 0.0}]
print("돌판 다리의 판:", old, "→", os.path.basename(DECK))
text = json.dumps(d, ensure_ascii=False, indent=2)
io.open(path, "w", encoding="utf-8-sig" if bom else "utf-8", newline="\n").write(text + "\n")
print("앞:", before)
print("뒤:", [(l["name"], l["tile"], os.path.basename(l["albedo"])) for l in g["layers"]])
print("덧칠:", [(o["layer"], o["cover"], o["soft"]) for o in g["overlays"]])
main()

View File

@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
# 2026-09-28 BT38 — 오염된 던전(PlagueCatacomb) 바닥을 '던전 타일'로 다시 짠다.
# PD "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌 … 좀 더 자연스러운 던전 타일처럼"
# 원인(실측): 팩 텍스처가 PC 에서 512 픽셀로 들어오고(1 m 에 64~109 픽셀 — 다른 맵의 주 바닥은 223~256),
# 바탕 층은 무늬 없는 바위를 8 m 로, 공터 층은 깨진 타일을 4.7 m 로 깔아 돌 한 장이 1~2 m 였다.
# 방법: 같은 팩의 던전 타일 세 가지(FloorA 온전 · FloorA3 얼룩 · FloorA2 깨짐)는 줄눈 배치가 같다(높이 맵 상관 0.74~0.82).
# → 네 층을 모두 같은 배치 · 같은 크기로 깔면 층이 바뀌어도 줄눈이 이어져 '한 바닥이 닳고 더러워진' 모습이 된다.
# 게임 에셋의 들여오기 설정은 건드리지 않고, 1024 픽셀 사본을 맵 폴더에 만든다(빌더가 1024 · 고화질 압축으로 들여온다).
# 만드는 것(맵 폴더 Textures): FRMap_dun_tile(온전) · FRMap_dun_tile_worn(얼룩) · FRMap_dun_tile_slime(점액) · FRMap_dun_dirt(흙 · 자갈) + 노멀맵(_N)
# 1차 화면 확인: 타일 세 가지는 줄눈 배치가 같아, 덧칠해도 4 m 마다 같은 무늬가 격자처럼 보였다(넓은 보스 자리에서 뚜렷)
# → 깨진 타일 대신 무늬가 다른 흙 층을 넣어 얼룩으로 덮고, 타일 안의 밝기 차(눈에 띄는 밝은 돌)를 더 편다
# 실행: python texderive_dungeon.py [--project] (--project 를 주면 FR 프로젝트 맵 폴더로 복사한다)
import os, sys, io, json, shutil
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
import numpy as np
from PIL import Image, ImageFilter
Image.MAX_IMAGE_PIXELS = None
PROJECT = r"C:\BurningTimes\FirstRising"
PACK = os.path.join(PROJECT, "Assets", "LevelAssets", "Toby Fredson", "Modular Dungeon Catacombs - Mobile", "Textures", "Textures_Terrain")
OUT = r"C:\BurningTimes\FR_maps_staging\_work\dungeon_floor"
DST = os.path.join(PROJECT, "Assets", "BurningTimes", "Maps", "PlagueCatacomb", "Textures")
SIZE = 1024
def load(name, mode="RGB"):
return Image.open(os.path.join(PACK, name)).convert(mode)
def down(img):
"""2048 → 1024. 줄인 뒤 조금 또렷하게(줄눈 · 돌결이 뭉개지지 않게)."""
small = img.resize((SIZE, SIZE), Image.LANCZOS)
return small.filter(ImageFilter.UnsharpMask(radius=1.2, percent=70, threshold=2))
def down_normal(img):
"""노멀맵은 줄인 뒤 길이를 1 로 다시 맞춘다."""
a = np.asarray(img.resize((SIZE, SIZE), Image.LANCZOS)).astype(np.float64) / 255.0 * 2.0 - 1.0
n = np.linalg.norm(a, axis=2, keepdims=True)
a = a / np.maximum(n, 1e-6)
return a
def save_normal(a, path):
n = np.linalg.norm(a, axis=2, keepdims=True)
a = a / np.maximum(n, 1e-6)
Image.fromarray(np.clip((a * 0.5 + 0.5) * 255.0 + 0.5, 0, 255).astype(np.uint8)).save(path)
def wrap_blur(x, radius):
"""가장자리가 이어지는 흐림 — 둘레를 되풀이해 붙인 뒤 흐리고 가운데만 잘라 낸다."""
pad = int(radius * 3) + 2
p = np.pad(x, pad, mode="wrap")
im = Image.fromarray(np.clip(p * 255.0, 0, 255).astype(np.uint8))
b = np.asarray(im.filter(ImageFilter.GaussianBlur(radius))).astype(np.float64) / 255.0
return b[pad:-pad, pad:-pad]
def pnoise(size, scale, octaves=4, seed=0):
"""주기 노이즈(0~1) — 가장자리가 정확히 이어진다."""
r = np.random.default_rng(seed)
out = np.zeros((size, size), np.float64)
amp = 1.0
tot = 0.0
fy = np.fft.fftfreq(size)[:, None] * size
fx = np.fft.fftfreq(size)[None, :] * size
f = np.hypot(fx, fy)
for o in range(octaves):
w = r.standard_normal((size, size))
k = scale * (2 ** o)
n = np.real(np.fft.ifft2(np.fft.fft2(w) * np.exp(-(f / k) ** 2)))
n = (n - n.min()) / (n.max() - n.min() + 1e-9)
out += n * amp
tot += amp
amp *= 0.5
out /= tot
return (out - out.min()) / (out.max() - out.min() + 1e-9)
def smooth(x, a, b):
t = np.clip((x - a) / (b - a + 1e-9), 0, 1)
return t * t * (3 - 2 * t)
def seam(img):
a = np.asarray(img).astype(np.float32)
inner = (np.abs(a[:, 1:] - a[:, :-1]).mean() + np.abs(a[1:, :] - a[:-1, :]).mean()) * 0.5
s = (np.abs(a[:, 0] - a[:, -1]).mean() + np.abs(a[0, :] - a[-1, :]).mean()) * 0.5
return float(s / max(1e-3, inner))
def flatten(img, radius=64.0, amount=0.65):
"""큰 밝기 얼룩을 편다 — 텍스처 안의 어두운 돌 무리 · 밝은 자리가 4 m 마다 되풀이되어 눈에 띄는 것을 줄인다(돌결 · 줄눈은 그대로)."""
a = np.asarray(img).astype(np.float64) / 255.0
l = (a * np.array([0.2126, 0.7152, 0.0722])).sum(axis=2)
low = wrap_blur(l, radius)
k = (low.mean() / np.maximum(low, 1e-3)) ** amount
a = np.clip(a * k[..., None], 0, 1)
return Image.fromarray((a * 255.0 + 0.5).astype(np.uint8))
def grade(img, gain=1.0, gamma=1.0, sat=1.0):
"""밝기 · 감마 · 채도를 맞춘다(세 타일의 밝기 차가 너무 크면 층 경계가 얼룩처럼 보인다)."""
a = np.asarray(img).astype(np.float64) / 255.0
a = np.clip(a * gain, 0, 1) ** gamma
l = (a * np.array([0.2126, 0.7152, 0.0722])).sum(axis=2, keepdims=True)
a = np.clip(l + (a - l) * sat, 0, 1)
return Image.fromarray((a * 255.0 + 0.5).astype(np.uint8))
def main():
os.makedirs(OUT, exist_ok=True)
rec = []
# ---------- 온전한 타일 · 얼룩진 타일 · 깨진 타일
src = {
"tile": ("CatacombsFloorA_Albedo.png", "CatacombsFloorA_Normal.png", dict(gain=1.00, gamma=1.0, sat=0.95)),
"tile_worn": ("CatacombsFloorA3_Albedo.png", "CatacombsFloorA3_Normal.png", dict(gain=1.22, gamma=0.96, sat=0.95)),
}
made = {}
normals = {}
for key, (alb, nrm, g) in src.items():
img = grade(flatten(flatten(down(load(alb)), 64.0, 0.8), 18.0, 0.45), **g) # 큰 얼룩(64) + 돌 한 장 크기(18)의 밝기 차를 편다
n = down_normal(load(nrm))
img.save(os.path.join(OUT, "FRMap_dun_%s.png" % key))
save_normal(n, os.path.join(OUT, "FRMap_dun_%s_N.png" % key))
made[key] = img
normals[key] = n
mean = np.asarray(img).reshape(-1, 3).mean(axis=0)
nmean = np.asarray(load(nrm).resize((64, 64))).reshape(-1, 3).mean(axis=0)
rec.append({"name": key, "from": alb, "mean": [round(float(v), 1) for v in mean], "seam": round(seam(img), 2), "normal_mean": [round(float(v)) for v in nmean]})
# ---------- 흙 · 자갈(무늬가 다른 층 — 타일 위를 얼룩으로 덮어 되풀이를 깬다)
dirt = grade(flatten(down(load("PebblesCATA_Albedo.png")), 64.0, 0.7), gain=1.05, gamma=1.0, sat=0.9)
dirt.save(os.path.join(OUT, "FRMap_dun_dirt.png"))
save_normal(down_normal(load("PebblesCATA_Normal.png")), os.path.join(OUT, "FRMap_dun_dirt_N.png"))
rec.append({"name": "dirt", "from": "PebblesCATA_Albedo.png", "mean": [round(float(v), 1) for v in np.asarray(dirt).reshape(-1, 3).mean(axis=0)], "seam": round(seam(dirt), 2)})
# ---------- 점액이 고인 타일(얼룩진 타일 + 줄눈에 고인 녹색 점액 + 드문 웅덩이)
base = np.asarray(made["tile_worn"]).astype(np.float64) / 255.0
h = np.asarray(load("CatacombsFloorA3_Height.png", "L").resize((SIZE, SIZE), Image.LANCZOS)).astype(np.float64) / 255.0
low = wrap_blur(h, 7.0) - h # 둘레보다 낮은 곳(줄눈 · 패인 곳)
joints = smooth(low, 0.012, 0.05)
joints = np.clip(wrap_blur(joints, 1.6) * 1.5, 0, 1) # 줄눈에서 조금 번지게
patch = smooth(pnoise(SIZE, 3.0, 4, 381), 0.42, 0.66) # 점액이 도는 구역(큰 얼룩)
pool = smooth(pnoise(SIZE, 5.0, 4, 977), 0.70, 0.82) * smooth(pnoise(SIZE, 2.0, 3, 55), 0.40, 0.65) # 드문 웅덩이
slime = np.clip(joints * patch * 0.95 + pool * 0.9, 0, 1)
fine = pnoise(SIZE, 24.0, 3, 412)
lum = (base * np.array([0.2126, 0.7152, 0.0722])).sum(axis=2)
green = np.stack([0.20 + 0.08 * fine, 0.34 + 0.12 * fine, 0.09 + 0.04 * fine], axis=2) # 탁한 올리브빛(선명한 녹색은 페인트처럼 보였다)
body = green * (0.55 + 0.9 * lum[..., None])
rim = smooth(slime, 0.15, 0.5) - smooth(slime, 0.5, 0.95) # 점액 가장자리는 조금 밝게(젖은 테)
out = base * (1 - slime[..., None] * 0.78) + body * (slime[..., None] * 0.78)
out = np.clip(out + rim[..., None] * np.array([0.02, 0.045, 0.008]), 0, 1)
img = Image.fromarray((out * 255.0 + 0.5).astype(np.uint8))
img.save(os.path.join(OUT, "FRMap_dun_tile_slime.png"))
n = normals["tile_worn"].copy()
flat = np.zeros_like(n)
flat[..., 2] = 1.0
k = (pool * 0.85 + joints * patch * 0.4)[..., None]
k = np.clip(k, 0, 0.9)
n = n * (1 - k) + flat * k # 웅덩이는 매끈하게
save_normal(n, os.path.join(OUT, "FRMap_dun_tile_slime_N.png"))
made["tile_slime"] = img
rec.append({"name": "tile_slime", "from": "tile_worn + 점액", "mean": [round(float(v), 1) for v in np.asarray(img).reshape(-1, 3).mean(axis=0)],
"seam": round(seam(img), 2), "slime_share": round(float((slime > 0.5).mean()), 3)})
# ---------- 미리 보기: 16 m × 12 m 바닥을 1 m = 64 픽셀로(타일 한 장 = TILE m) — 사람 키(1.8 m) 막대와 함께
TILE = float(os.environ.get("BT_TILE", "4.0"))
ppm = 64
W, H = 16, 12
rep = int(round(TILE * ppm))
def tiled(key):
t = np.asarray(made[key].resize((rep, rep), Image.LANCZOS)).astype(np.float64)
ny, nx = H * ppm // rep + 2, W * ppm // rep + 2
return np.tile(t, (ny, nx, 1))[:H * ppm, :W * ppm]
yy, xx = np.mgrid[0:H * ppm, 0:W * ppm]
r = np.random.default_rng(7)
def blot(scale, seed):
n = pnoise(1024, scale, 4, seed)
return np.tile(n, (2, 2))[:H * ppm, :W * ppm]
canvas = tiled("tile_worn") # 방 = 얼룩진 타일
path = smooth(np.abs(yy / ppm - 6.0), 3.4, 2.2) # 가운데 가로 통로 = 온전한 타일
canvas = canvas * (1 - path[..., None]) + tiled("tile") * path[..., None]
dt = np.asarray(dirt.resize((int(round(5.0 * ppm)), int(round(5.0 * ppm))), Image.LANCZOS)).astype(np.float64)
dt = np.tile(dt, (H * ppm // dt.shape[0] + 2, W * ppm // dt.shape[1] + 2, 1))[:H * ppm, :W * ppm]
m = smooth(blot(2.0, 3), 0.55, 0.66)[..., None] * 0.92 # 흙 얼룩(5 m 로 깐다)
canvas = canvas * (1 - m) + dt * m
m = smooth(blot(3.0, 9), 0.62, 0.74)[..., None] * 0.9 # 점액 얼룩
canvas = canvas * (1 - m) + tiled("tile_slime") * m
prev = Image.fromarray(np.clip(canvas, 0, 255).astype(np.uint8))
from PIL import ImageDraw
d = ImageDraw.Draw(prev)
d.rectangle([40, 40, 40 + int(0.6 * ppm), 40 + int(1.8 * ppm)], outline=(255, 60, 60), width=3) # 사람(0.6 × 1.8 m)
d.line([40, 20, 40 + ppm, 20], fill=(255, 255, 0), width=4) # 1 m
prev.save(os.path.join(OUT, "preview_floor_%sm.png" % ("%.1f" % TILE).replace(".", "_")))
for x in rec:
print(json.dumps(x, ensure_ascii=False))
if "--project" in sys.argv:
os.makedirs(DST, exist_ok=True)
n = 0
for f in sorted(os.listdir(OUT)):
# 1차에 만들었던 깨진 타일(tile_broken)은 쓰지 않는다 — 폴더에 남아 있어도 옮기지 않는다
if (f.startswith("FRMap_dun_tile") or f.startswith("FRMap_dun_dirt")) and f.endswith(".png") and "broken" not in f:
shutil.copy2(os.path.join(OUT, f), os.path.join(DST, f))
n += 1
print("프로젝트로 복사", n, "→", DST)
main()

View File

@ -0,0 +1,45 @@
// 재질 파일에 남은 _AlphaClip 값으로 오려내기를 켜면 어떤 사본이 바뀌는지 미리 본다(편집 모드 · 읽기 전용)
var sb = new System.Text.StringBuilder();
var byKey = new System.Collections.Generic.Dictionary<string, string>();
foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Material", new[] { "Assets" }))
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
if (p.StartsWith("Assets/BurningTimes/Maps/_Shared/Materials/")) continue;
string safe = "";
foreach (var c in System.IO.Path.GetFileNameWithoutExtension(p))
safe += char.IsLetterOrDigit(c) ? c : '_';
byKey[safe + "_" + g.Substring(0, 6)] = p;
}
int change = 0, total = 0;
foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Material", new[] { "Assets/BurningTimes/Maps/_Shared/Materials/Lit" }))
{
var cp = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
var name = System.IO.Path.GetFileNameWithoutExtension(cp);
if (!name.EndsWith("_Lit")) continue; // 게임 전용 셰이더에서 옮긴 사본만
string key = name.Substring(0, name.LastIndexOf('_'));
if (!byKey.TryGetValue(key, out var sp)) continue;
var src = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(sp);
var copy = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(cp);
if (src == null || copy == null) continue;
total++;
float saved = -1f;
var floats = new UnityEditor.SerializedObject(src).FindProperty("m_SavedProperties.m_Floats");
for (int i = 0; floats != null && i < floats.arraySize; i++)
{
var e = floats.GetArrayElementAtIndex(i);
if (e.FindPropertyRelative("first").stringValue == "_AlphaClip")
saved = e.FindPropertyRelative("second").floatValue;
}
var tex = copy.HasProperty("_BaseMap") ? copy.GetTexture("_BaseMap") as UnityEngine.Texture2D : null;
bool fmtAlpha = tex != null && UnityEngine.Experimental.Rendering.GraphicsFormatUtility.HasAlphaChannel(tex.graphicsFormat);
var ti = tex != null ? UnityEditor.AssetImporter.GetAtPath(UnityEditor.AssetDatabase.GetAssetPath(tex)) as UnityEditor.TextureImporter : null;
bool srcAlpha = ti != null && ti.DoesSourceTextureHaveAlpha();
bool copyClip = copy.HasProperty("_AlphaClip") && copy.GetFloat("_AlphaClip") > 0.5f;
bool will = saved > 0.5f && (fmtAlpha || srcAlpha);
if (saved > 0.5f || copyClip)
sb.Append(will && !copyClip ? "[켜짐으로 바뀜] " : (copyClip ? "[이미 오려냄] " : "[알파 없음 · 그대로] ")).Append(name).Append(" ← ").Append(src.name).Append(" [").Append(src.shader.name)
.Append("] 남은 _AlphaClip ").Append(saved.ToString("0")).Append(" · 텍스처 ").Append(tex != null ? tex.name : "없음").Append(" 형식 알파 ").Append(fmtAlpha).Append(" 원본 알파 ").Append(srcAlpha).Append('\n');
if (will && !copyClip) change++;
}
sb.Append("게임 셰이더에서 옮긴 사본 ").Append(total).Append("개 · 오려내기가 새로 켜질 것 ").Append(change).Append('\n');
return sb.ToString();

View File

@ -0,0 +1,51 @@
// 맵 빌더가 만든 재질 사본(_Shared/Materials/Lit)마다 원본 재질을 찾아, 원본이 알파를 쓰는 재질인지와 사본이 오려 내는지를 견준다(편집 모드 · 읽기 전용)
var sb = new System.Text.StringBuilder();
var byGuid = new System.Collections.Generic.Dictionary<string, string>(); // 'guid 앞 6자 + 이름' → 경로
foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Material", new[] { "Assets" }))
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
if (p.StartsWith("Assets/BurningTimes/Maps/_Shared/Materials/")) continue;
string safe = "";
foreach (var c in System.IO.Path.GetFileNameWithoutExtension(p))
safe += char.IsLetterOrDigit(c) ? c : '_';
byGuid[safe + "_" + g.Substring(0, 6)] = p;
}
int n = 0, suspect = 0;
foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Material", new[] { "Assets/BurningTimes/Maps/_Shared/Materials/Lit" }))
{
var cp = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
var name = System.IO.Path.GetFileNameWithoutExtension(cp);
int cut = name.LastIndexOf('_');
if (cut <= 0) continue;
string key = name.Substring(0, cut);
if (!byGuid.TryGetValue(key, out var sp)) continue;
var src = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(sp);
var copy = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(cp);
if (src == null || copy == null) continue;
n++;
bool srcTest = src.IsKeywordEnabled("_ALPHATEST_ON") || (src.HasProperty("_AlphaClip") && src.GetFloat("_AlphaClip") > 0.5f) || (src.HasProperty("_AlphaCutoffEnable") && src.GetFloat("_AlphaCutoffEnable") > 0.5f);
bool srcTransparent = src.renderQueue >= 2450 || src.shader.name.Contains("Transparent") || src.shader.name.Contains("Cutout") || src.shader.name.Contains("AlphaTest") || (src.HasProperty("_Surface") && src.GetFloat("_Surface") > 0.5f);
bool copyClip = copy.HasProperty("_AlphaClip") && copy.GetFloat("_AlphaClip") > 0.5f;
if ((srcTest || srcTransparent) && !copyClip)
{
suspect++;
sb.Append("[원본은 알파 · 사본은 안 오려냄] ").Append(name).Append(" ← ").Append(sp).Append(" [").Append(src.shader.name).Append("] 큐 ").Append(src.renderQueue)
.Append(" 오려내기 키워드 ").Append(src.IsKeywordEnabled("_ALPHATEST_ON")).Append(" _AlphaClip ").Append(src.HasProperty("_AlphaClip") ? src.GetFloat("_AlphaClip").ToString("0") : "-")
.Append(" _Cutoff ").Append(src.HasProperty("_Cutoff") ? src.GetFloat("_Cutoff").ToString("0.00") : "-").Append('\n');
}
}
sb.Append("사본 ").Append(n).Append("개 가운데 의심 ").Append(suspect).Append('\n');
// 던전 바닥돌 · 뼈 무더기 프리팹의 재질
foreach (var p in new[] { "GroundTileCATAM_A", "CATA_BonePileA", "CATA_BonePileB" })
foreach (var g in UnityEditor.AssetDatabase.FindAssets(p + " t:Prefab"))
{
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
if (System.IO.Path.GetFileNameWithoutExtension(path) != p) continue;
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
foreach (var r in go.GetComponentsInChildren<UnityEngine.Renderer>(true))
foreach (var m in r.sharedMaterials)
if (m != null)
sb.Append(p).Append(" / ").Append(r.name).Append(" → ").Append(m.name).Append(" [").Append(m.shader.name).Append("] 큐 ").Append(m.renderQueue)
.Append(" ").Append(UnityEditor.AssetDatabase.GetAssetPath(m)).Append('\n');
}
return sb.ToString();

View File

@ -0,0 +1,88 @@
// 오염된 던전의 돌판 다리(독액 수로 위 상판)에 쓸 판을 만든다(편집 모드) — 2026-09-28 BT38
// 앞서 쓰던 팩의 바닥 판(GroundTileCATAM · 10 m)은 돌 한 장이 2~3 m 로 커서 바닥 타일(0.5~1 m)과 어긋났다.
// → 바닥과 같은 타일 텍스처(FRMap_dun_tile)를 같은 크기(4 m 에 한 장)로 입힌 10 × 0.3 × 10 m 판을 만든다. 충돌체는 달지 않는다(막는 것은 보이지 않는 벽).
// 만드는 것: Assets/BurningTimes/Maps/PlagueCatacomb/Deck/FRMap_dun_deck.asset(메시) · .mat(재질) · .prefab. 여러 번 돌려도 같은 에셋을 고쳐 쓴다.
if (UnityEditor.EditorApplication.isPlaying)
return "Play 중이라 하지 않음";
string dir = "Assets/BurningTimes/Maps/PlagueCatacomb/Deck";
string tex = "Assets/BurningTimes/Maps/PlagueCatacomb/Textures/FRMap_dun_tile.png";
string nrm = "Assets/BurningTimes/Maps/PlagueCatacomb/Textures/FRMap_dun_tile_N.png";
float size = 10f, thick = 0.3f, tile = 4f;
System.IO.Directory.CreateDirectory(dir);
UnityEditor.AssetDatabase.Refresh();
// ---------- 메시: 여섯 면 · 면마다 꼭짓점 넷. UV 는 1 m = 1/tile(윗면은 x · z, 옆면은 길이 · 높이)
var v = new System.Collections.Generic.List<UnityEngine.Vector3>();
var n = new System.Collections.Generic.List<UnityEngine.Vector3>();
var uv = new System.Collections.Generic.List<UnityEngine.Vector2>();
var tri = new System.Collections.Generic.List<int>();
float h = size * 0.5f, t = thick * 0.5f;
System.Action<UnityEngine.Vector3, UnityEngine.Vector3, UnityEngine.Vector3, UnityEngine.Vector3, UnityEngine.Vector3, float, float> quad = (a, b, c, d, normal, w, hgt) =>
{
int i0 = v.Count;
v.Add(a); v.Add(b); v.Add(c); v.Add(d);
for (int k = 0; k < 4; k++) n.Add(normal);
uv.Add(new UnityEngine.Vector2(0f, 0f)); uv.Add(new UnityEngine.Vector2(w / tile, 0f)); uv.Add(new UnityEngine.Vector2(w / tile, hgt / tile)); uv.Add(new UnityEngine.Vector2(0f, hgt / tile));
tri.Add(i0); tri.Add(i0 + 2); tri.Add(i0 + 1); tri.Add(i0); tri.Add(i0 + 3); tri.Add(i0 + 2);
};
// 윗면(+y) · 아랫면(-y)
quad(new UnityEngine.Vector3(-h, t, -h), new UnityEngine.Vector3(h, t, -h), new UnityEngine.Vector3(h, t, h), new UnityEngine.Vector3(-h, t, h), UnityEngine.Vector3.up, size, size);
quad(new UnityEngine.Vector3(-h, -t, h), new UnityEngine.Vector3(h, -t, h), new UnityEngine.Vector3(h, -t, -h), new UnityEngine.Vector3(-h, -t, -h), UnityEngine.Vector3.down, size, size);
// 옆면 넷(-z · +x · +z · -x)
quad(new UnityEngine.Vector3(-h, -t, -h), new UnityEngine.Vector3(h, -t, -h), new UnityEngine.Vector3(h, t, -h), new UnityEngine.Vector3(-h, t, -h), UnityEngine.Vector3.back, size, thick);
quad(new UnityEngine.Vector3(h, -t, -h), new UnityEngine.Vector3(h, -t, h), new UnityEngine.Vector3(h, t, h), new UnityEngine.Vector3(h, t, -h), UnityEngine.Vector3.right, size, thick);
quad(new UnityEngine.Vector3(h, -t, h), new UnityEngine.Vector3(-h, -t, h), new UnityEngine.Vector3(-h, t, h), new UnityEngine.Vector3(h, t, h), UnityEngine.Vector3.forward, size, thick);
quad(new UnityEngine.Vector3(-h, -t, h), new UnityEngine.Vector3(-h, -t, -h), new UnityEngine.Vector3(-h, t, -h), new UnityEngine.Vector3(-h, t, h), UnityEngine.Vector3.left, size, thick);
string meshPath = dir + "/FRMap_dun_deck.asset";
var mesh = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Mesh>(meshPath);
bool newMesh = mesh == null;
if (newMesh) mesh = new UnityEngine.Mesh { name = "FRMap_dun_deck" };
mesh.Clear();
mesh.SetVertices(v);
mesh.SetNormals(n);
mesh.SetUVs(0, uv);
mesh.SetTriangles(tri, 0);
mesh.RecalculateTangents();
mesh.RecalculateBounds();
if (newMesh) UnityEditor.AssetDatabase.CreateAsset(mesh, meshPath);
else UnityEditor.EditorUtility.SetDirty(mesh);
// ---------- 재질: URP Simple Lit(맵의 다른 소품 사본과 같은 셰이더) · 무광
var shader = UnityEngine.Shader.Find("Universal Render Pipeline/Simple Lit");
if (shader == null)
return "Simple Lit 셰이더 없음";
string matPath = dir + "/FRMap_dun_deck.mat";
var mat = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(matPath);
bool newMat = mat == null;
if (newMat) mat = new UnityEngine.Material(shader) { name = "FRMap_dun_deck" };
mat.shader = shader;
var albedo = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Texture2D>(tex);
var normalTex = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Texture2D>(nrm);
mat.SetTexture("_BaseMap", albedo);
mat.SetTexture("_MainTex", albedo);
mat.SetColor("_BaseColor", new UnityEngine.Color(1f, 1f, 0.92f, 1f));
mat.SetColor("_Color", new UnityEngine.Color(1f, 1f, 0.92f, 1f));
mat.SetTexture("_BumpMap", normalTex);
if (normalTex != null) mat.EnableKeyword("_NORMALMAP");
mat.SetFloat("_Surface", 0f);
mat.SetFloat("_AlphaClip", 0f);
mat.SetFloat("_Cull", 2f);
mat.SetFloat("_SpecularHighlights", 0f);
mat.EnableKeyword("_SPECULARHIGHLIGHTS_OFF");
mat.SetFloat("_Smoothness", 0.1f);
mat.SetColor("_SpecColor", new UnityEngine.Color(0.1f, 0.1f, 0.1f, 1f));
mat.enableInstancing = true;
if (newMat) UnityEditor.AssetDatabase.CreateAsset(mat, matPath);
else UnityEditor.EditorUtility.SetDirty(mat);
// ---------- 프리팹
string prefabPath = dir + "/FRMap_dun_deck.prefab";
var go = new UnityEngine.GameObject("FRMap_dun_deck");
go.AddComponent<UnityEngine.MeshFilter>().sharedMesh = mesh;
var mr = go.AddComponent<UnityEngine.MeshRenderer>();
mr.sharedMaterial = mat;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
var saved = UnityEditor.PrefabUtility.SaveAsPrefabAsset(go, prefabPath);
UnityEngine.Object.DestroyImmediate(go);
UnityEditor.AssetDatabase.SaveAssets();
return "메시 " + (newMesh ? "새로 만듦" : "고쳐 씀") + " · 꼭짓점 " + mesh.vertexCount + " · 크기 " + mesh.bounds.size + " · 재질 " + (newMat ? "새로 만듦" : "고쳐 씀") + " · 텍스처 " + (albedo != null) + "/" + (normalTex != null) + " · 프리팹 " + (saved != null ? prefabPath : "실패");

View File

@ -0,0 +1,17 @@
// 맵 하나의 바닥만 다시 칠한다(편집 모드 · 씬을 열지 않는다). args.txt = 맵 이름. 결과는 Logs/bt_map_<이름>_ground.txt
var name = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim();
if (UnityEditor.EditorApplication.isPlaying)
return "Play 중이라 하지 않음";
var result = BurningTimes.EditorTools.Maps.MapBuilder.Repaint(name);
var sb = new System.Text.StringBuilder();
foreach (var line in result.Split('\n'))
if (line.StartsWith("[BT Map") || line.StartsWith("RESULT") || line.StartsWith("보스 자리") || line.StartsWith("바닥") || line.Contains("덧칠") || line.Contains("없음"))
sb.Append(line).Append('\n');
// 지형 층이 쓰는 텍스처의 실제 크기(들여오기 결과)
var td = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.TerrainData>("Assets/BurningTimes/Maps/" + name + "/Terrain/" + name + "_Terrain.asset");
if (td != null)
foreach (var l in td.terrainLayers)
sb.Append(" 층 ").Append(l.name).Append(" tile=").Append(l.tileSize.x.ToString("0.0")).Append(" m · 색 ").Append(l.diffuseTexture != null ? l.diffuseTexture.name + " " + l.diffuseTexture.width + "px" : "없음")
.Append(" · 노멀 ").Append(l.normalMapTexture != null ? l.normalMapTexture.name + " " + l.normalMapTexture.width + "px" : "없음")
.Append(" · 1 m 에 ").Append(l.diffuseTexture != null ? (l.diffuseTexture.width / l.tileSize.x).ToString("0") : "-").Append(" 픽셀\n");
return sb.ToString();

View File

@ -0,0 +1,12 @@
// 커밋 전에 에디터가 들고 있는 변경을 디스크에 쓴다(편집 모드) — 저장하지 않은 에셋 · 열린 씬의 변경 여부를 함께 돌려준다
if (UnityEditor.EditorApplication.isPlaying)
return "Play 중이라 하지 않음";
var sb = new System.Text.StringBuilder();
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
{
var s = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);
sb.Append("씬 ").Append(s.path).Append(" 변경됨=").Append(s.isDirty).Append(" | ");
}
UnityEditor.AssetDatabase.SaveAssets();
sb.Append("에셋 저장 완료");
return sb.ToString();

View File

@ -0,0 +1,73 @@
// 맵 폴더(Assets/BurningTimes/Maps)에서 아무 데서도 쓰이지 않는 에셋을 찾는다(편집 모드 · 읽기 전용). 결과는 Logs/bt_unused_maps_assets.json
// '쓰인다'의 기준: ① 씬(Assets/Scenes 아래 전부) · 로비 프리팹 · 보스전 전용 필드 프리팹 · Resources 폴더의 에셋이 (건너 건너) 가리키는 것
// ② 맵 정의(<맵>.map.json) · 소품 목록(props.json)에 경로가 적힌 것과 그것이 가리키는 것 — 지금 놓이지 않았어도 빌더의 재료다
// 그 밖은 '쓰이지 않음'. 정의 파일(.json) · 폴더는 후보에서 뺀다.
if (UnityEditor.EditorApplication.isPlaying)
return "Play 중이라 하지 않음";
string scope = "Assets/BurningTimes/Maps";
var roots = new System.Collections.Generic.HashSet<string>();
foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Scene", new[] { "Assets/Scenes" }))
roots.Add(UnityEditor.AssetDatabase.GUIDToAssetPath(g));
foreach (var g in UnityEditor.AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/BurningTimes" }))
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
if (p.Contains("/Resources/")) roots.Add(p);
}
foreach (var g in UnityEditor.AssetDatabase.FindAssets("", new[] { "Assets/BurningTimes" }))
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
if (p.Contains("/Resources/") && !UnityEditor.AssetDatabase.IsValidFolder(p)) roots.Add(p);
}
roots.Add("Assets/GameAssets/LobbyPlayScene.prefab");
// 정의 파일에 적힌 경로
int fromJson = 0;
var rx = new System.Text.RegularExpressions.Regex("\"(Assets/[^\"]+)\"");
foreach (var file in System.IO.Directory.GetFiles(scope, "*.json", System.IO.SearchOption.AllDirectories))
{
string text = System.IO.File.ReadAllText(file);
foreach (System.Text.RegularExpressions.Match m in rx.Matches(text))
{
string p = m.Groups[1].Value;
if (System.IO.File.Exists(p) && roots.Add(p)) fromJson++;
}
// 소품 목록(props.json)은 파일 이름만 적는다 — 같은 폴더의 그 파일을 재료로 본다
if (System.IO.Path.GetFileName(file) == "props.json")
{
string dir = System.IO.Path.GetDirectoryName(file).Replace('\\', '/');
foreach (System.Text.RegularExpressions.Match m in new System.Text.RegularExpressions.Regex("\"([^\"/]+\\.(fbx|png))\"").Matches(text))
{
string p = dir + "/" + m.Groups[1].Value;
if (System.IO.File.Exists(p) && roots.Add(p)) fromJson++;
}
}
}
var rootList = new System.Collections.Generic.List<string>(roots);
var used = new System.Collections.Generic.HashSet<string>(UnityEditor.AssetDatabase.GetDependencies(rootList.ToArray(), true));
foreach (var r in rootList) used.Add(r);
var sb = new System.Text.StringBuilder();
sb.Append("{\n \"scope\": \"").Append(scope).Append("\",\n \"roots\": ").Append(rootList.Count).Append(",\n \"rootsFromJson\": ").Append(fromJson).Append(",\n \"unused\": [\n");
int total = 0, unused = 0;
long bytes = 0;
bool first = true;
var all = UnityEditor.AssetDatabase.FindAssets("", new[] { scope });
var seen = new System.Collections.Generic.HashSet<string>();
foreach (var g in all)
{
var p = UnityEditor.AssetDatabase.GUIDToAssetPath(g);
if (!seen.Add(p)) continue;
if (UnityEditor.AssetDatabase.IsValidFolder(p)) continue;
if (p.EndsWith(".json")) continue;
total++;
if (used.Contains(p)) continue;
var type = UnityEditor.AssetDatabase.GetMainAssetTypeAtPath(p);
long size = new System.IO.FileInfo(p).Length;
bytes += size;
unused++;
if (!first) sb.Append(",\n");
first = false;
sb.Append(" {\"path\": \"").Append(p).Append("\", \"type\": \"").Append(type != null ? type.Name : "?").Append("\", \"bytes\": ").Append(size).Append(", \"guid\": \"").Append(g).Append("\"}");
}
sb.Append("\n ],\n \"total\": ").Append(total).Append(",\n \"unusedCount\": ").Append(unused).Append(",\n \"unusedBytes\": ").Append(bytes).Append("\n}\n");
System.IO.File.WriteAllText("Logs/bt_unused_maps_assets.json", sb.ToString());
return "기준 에셋 " + rootList.Count + "(정의 파일에서 " + fromJson + ") · 맵 폴더 에셋 " + total + " · 쓰이지 않음 " + unused + " · " + (bytes / 1048576f).ToString("0.0") + " MB";

View File

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
# FR 맵 폴더의 '쓰이지 않는 에셋' 목록 문서를 만든다 — 2026-09-28 PD 지시
# "남겨두되, 어떤 에셋이 안쓰이는지 기록해서 추후 삭제 유무를 결정할 수 있도록 정리해둘 것. (내가 나중에 물어보면 바로 알 수 있게 정리 필요)"
# 순서: ① 유니티(편집 모드)에서 tools/unity_eval/unused_scan.cs 실행 → FR/Logs/bt_unused_maps_assets.json
# ② python unused_report.py → 공유/기획/FirstRising/맵/미사용_에셋_목록.md
# 묶음 · 까닭은 아래 RULES 에 적는다(새 묶음이 생기면 여기에 더한다). 어느 묶음에도 안 맞는 것은 '분류 안 됨'으로 나온다.
import io, json, os, re, subprocess, sys, time
FR = r"C:\BurningTimes\FirstRising"
SRC = os.path.join(FR, "Logs", "bt_unused_maps_assets.json")
OUT = r"E:\BurningTimes\공유\기획\FirstRising\맵\미사용_에셋_목록.md"
# (묶음 열쇠, 묶음 이름, 경로 정규식, 안 쓰이게 된 까닭, 지워도 되는가(PM 의견), 되살리는 법)
RULES = [
("A", "던전 — 옛 지형 층", r"PlagueCatacomb/Terrain/Catacomb_(Rock|Paver|Floor|Slime)\.terrainlayer$",
"BT38(2026-09-28)에서 던전 바닥 층을 던전 타일로 바꿨다. 옛 층 넷은 지형 데이터가 더는 가리키지 않는다",
"지워도 된다", "git 에 있다(bt/work `159f17f6c0` 부터) — `git checkout <커밋> -- <경로>`"),
("B", "던전 — 옛 바닥 텍스처", r"PlagueCatacomb/Textures/FRMap_dun_(paver|slime)(_N)?\.png$",
"옛 지형 층(A)만 쓰던 텍스처다 — 그려 만든 포석 · 점액 포석과 그 노멀맵",
"지워도 된다(A 와 함께)", "git 에 있다(bt/work `159f17f6c0` 부터). 만든 도구는 준비 폴더 `texderive.py`"),
("C", "던전 — 1차 시도(깨진 타일)", r"PlagueCatacomb/(Terrain/Catacomb_TileBroken\.terrainlayer|Textures/FRMap_dun_tile_broken(_N)?\.png)$",
"BT38 1차 시도에서 만든 깨진 타일 층. 화면 확인 뒤 흙 층으로 바꿔 쓰지 않게 됐다",
"지워도 된다", "git 에 있다(bt/work `5f9c8dd297`). 팩의 `CatacombsFloorA2` 에서 다시 만들 수도 있다"),
("D", "큰 바닥돌의 재질 사본", r"_Shared/Materials/Lit/CATA_Floor[AB]_[0-9a-f]+_Lit\.mat$",
"팩의 10 m 바닥 판(GroundTileCATAM)에 끼우던 재질 사본. BT38 에서 바닥돌 띠를 걷어내고 다리 판을 바꿔 쓰는 곳이 없다",
"지워도 된다", "git 에 있다. 그 바닥 판을 다시 쓰면 빌더가 다시 만든다"),
("E", "옛 이름의 가림 반투명 재질 사본", r"/Materials/Fade/(?!.*_(Tuned|Lit)_Fade)(?!FRProp_).*_Fade\.mat$",
"재질 사본의 이름 규칙이 바뀌기 전(BT31 초기 빌드)에 만든 것. 지금 씬은 `<이름>_<번호>_Tuned_Fade` 를 쓴다",
"지워도 된다", "git 에 있다. 쓸 일은 없다"),
("F", "빌더의 중간 산출물(밝기 맞춘 재질 사본)", r"_Shared/Materials/Lit/.*_Tuned\.mat$",
"씬은 이 사본에서 다시 만든 가림 반투명 사본(`_Tuned_Fade`)을 쓴다. 이 사본 자체는 씬이 가리키지 않지만 빌더가 맵을 지을 때 재료로 쓴다",
"지울 실익이 없다(다음 빌드 때 다시 생긴다)", "맵을 다시 지으면 생긴다"),
]
def tracked(paths):
out = subprocess.run(["git", "-c", "core.quotepath=false", "ls-files", "--"] + paths, cwd=FR, capture_output=True).stdout.decode("utf-8", "replace")
return set(l.strip() for l in out.splitlines() if l.strip())
def addressable(items):
"""쓰이지 않는 에셋이 어드레서블 설정에 등록돼 있는지 본다(등록돼 있으면 씬이 가리키지 않아도 빌드에 들어간다). (설정 파일 수, 걸린 에셋 수)"""
import glob
files = glob.glob(os.path.join(FR, "Assets", "AddressableAssetsData", "**", "*.asset"), recursive=True)
hit = set()
for f in files:
text = io.open(f, encoding="utf-8", errors="replace").read()
for i in items:
if i["guid"] in text:
hit.add(i["path"])
return len(files), len(hit)
def kb(n):
return "%.1f MB" % (n / 1048576.0) if n >= 1048576 else "%.1f KB" % (n / 1024.0)
def main():
d = json.loads(io.open(SRC, encoding="utf-8").read())
items = d["unused"]
paths = [i["path"] for i in items]
tr = tracked(paths)
stamp = time.strftime("%Y-%m-%d %H:%M", time.localtime(os.path.getmtime(SRC)))
groups = {}
for i in items:
i["group"] = "?"
for key, name, rx, why, ok, back in RULES:
if re.search(rx, i["path"]):
i["group"] = key
break
groups.setdefault(i["group"], []).append(i)
L = []
L.append("# FR 맵 폴더 — 쓰이지 않는 에셋 목록")
L.append("")
L.append("> PD 지시(2026-09-28): \"남겨두되, 어떤 에셋이 안쓰이는지 기록해서 추후 삭제 유무를 결정할 수 있도록 정리해둘 것. (내가 나중에 물어보면 바로 알 수 있게 정리 필요)\"")
L.append("> **지운 것은 없다.** 아래 에셋은 모두 프로젝트에 그대로 있다. 지울지는 PD 결정.")
L.append("")
L.append("## 한눈에")
L.append("")
L.append("| 항목 | 값 |")
L.append("|---|---|")
L.append("| 조사한 때 | %s [실측] |" % stamp)
L.append("| 조사 범위 | FR `%s` (맵 작업으로 만든 에셋) — 에셋 %d개 |" % (d["scope"], d["total"]))
L.append("| **쓰이지 않는 에셋** | **%d개 · %s** |" % (d["unusedCount"], kb(d["unusedBytes"])))
L.append("| 그 가운데 지워도 되는 것(PM 의견) | %d개 · %s |" % (sum(len(v) for k, v in groups.items() if k in "ABCDE"), kb(sum(i["bytes"] for k, v in groups.items() if k in "ABCDE" for i in v))))
addr_files, addr_hits = addressable(items)
L.append("| 게임에 주는 영향 | 없다 — 씬 · 프리팹 어디에서도 가리키지 않는다. 어드레서블 설정 파일 %d개에 등록된 것 %d개 [실측]. 기기 빌드에 들어가는지는 빌드로 확인하지 않았다 |" % (addr_files, addr_hits))
L.append("")
L.append("## 묶음별")
L.append("")
L.append("| 묶음 | 개수 | 용량 | 왜 안 쓰이게 됐나 | 지워도 되는가(PM 의견) | 되살리는 법 |")
L.append("|---|---|---|---|---|---|")
for key, name, rx, why, ok, back in RULES:
v = groups.get(key, [])
if not v:
continue
L.append("| **%s. %s** | %d | %s | %s | %s | %s |" % (key, name, len(v), kb(sum(i["bytes"] for i in v)), why, ok, back))
if "?" in groups:
v = groups["?"]
L.append("| **?. 분류 안 됨** | %d | %s | 까닭을 아직 적지 못했다 — 확인 필요 | 판단 보류 | — |" % (len(v), kb(sum(i["bytes"] for i in v))))
L.append("")
L.append("## 전체 목록")
L.append("")
L.append("경로는 `%s/` 아래. 'git' = FR 저장소가 추적하는가." % d["scope"])
L.append("")
L.append("| 묶음 | 경로 | 종류 | 크기 | git |")
L.append("|---|---|---|---|---|")
for i in sorted(items, key=lambda x: (x["group"], x["path"])):
rel = i["path"][len(d["scope"]) + 1:]
L.append("| %s | `%s` | %s | %s | %s |" % (i["group"], rel, i["type"], kb(i["bytes"]), "추적" if i["path"] in tr else "미추적"))
L.append("")
L.append("## '쓰이지 않는다'의 기준")
L.append("")
L.append("- **쓰인다**: ① 씬(`Assets/Scenes` 아래 전부) · 로비 프리팹(`LobbyPlayScene`) · 보스전 전용 필드 프리팹 · `Resources` 폴더의 에셋이 직접 또는 건너 건너 가리키는 것 "
"② 맵 정의(`<맵>.map.json`) · 소품 목록(`props.json`)에 경로가 적힌 것과 그것이 가리키는 것 — 지금 맵에 놓이지 않았어도 빌더의 재료이므로 쓰이는 것으로 본다.")
L.append("- **쓰이지 않는다**: 위 어디에도 걸리지 않는 것. 유니티의 의존 관계 조회(`AssetDatabase.GetDependencies`)로 판정했다 [실측].")
L.append("- 기준이 된 에셋 %d개(정의 파일에서 %d개). 정의 파일(.json)과 폴더는 후보에서 뺐다." % (d["roots"], d["rootsFromJson"]))
L.append("- **조사하지 않은 곳**: 맵 폴더 밖(`Assets/BurningTimes/Boss` · `Visual` 등)과 게임 원본 에셋. 코드가 경로 글자로 불러오는 에셋은 이 방법으로 잡히지 않는다 — 맵 폴더 안에는 그런 것이 없음을 확인했다(빌더는 정의 파일의 경로만 읽는다).")
L.append("")
L.append("## 지우기로 정하면")
L.append("")
L.append("1. 유니티 에디터의 Project 창에서 지운다(메타 파일이 함께 지워진다). 탐색기에서 지우면 메타 파일이 남는다.")
L.append("2. 지운 뒤 확인: 컴파일 오류 0 → 해당 맵 다시 짓기(`BurningTimes/Maps/<맵>/Build`) RESULT OK → Play 에서 그 맵으로 이동해 에디터 로그 오류 0.")
L.append("3. 묶음 A · B 는 함께 지운다(A 가 B 를 가리킨다 — A 만 남기면 빈 텍스처를 가리키는 층이 된다).")
L.append("4. 묶음 F 는 지워도 다음 빌드 때 다시 생긴다.")
L.append("")
L.append("## 목록을 다시 만드는 법")
L.append("")
L.append("1. 유니티 편집 모드에서 `공유/기획/FirstRising/맵/tools/unity_eval/unused_scan.cs` 실행 → FR `Logs/bt_unused_maps_assets.json`")
L.append("2. `python 공유/기획/FirstRising/맵/tools/unused_report.py` → 이 문서를 다시 쓴다(묶음 · 까닭은 스크립트의 RULES)")
L.append("")
L.append("## 그 밖의 정리 후보(에셋이 아닌 것)")
L.append("")
L.append("| 대상 | 크기 | 상태 | 비고 |")
L.append("|---|---|---|---|")
L.append("| 조직 레포 `공유/개발팀_백업/FirstRising/` | 약 1.2 GB | 미추적(레포에 올리지 않음) | FR 에셋을 바꾸기 전에 떠 둔 안전 사본. 91 MB 짜리 지형 데이터 10개 포함. 무시 목록(.gitignore)에 넣을지 PD 판단 |")
L.append("| 조직 레포 `공유/기획/FirstRising/리뉴얼/~$…기획서_v1.pptx` | 165 B | 미추적 | 오피스 잠금 파일(파워포인트가 열어 둔 흔적) |")
L.append("| 준비 폴더 `C:\\BurningTimes\\FR_maps_staging\\_work\\` | — | 저장소 밖 | 블렌더 굽기 · 텍스처 파생의 중간 결과. 다시 만들 수 있다 |")
L.append("")
io.open(OUT, "w", encoding="utf-8", newline="\n").write("\n".join(L))
print("쓴 곳", OUT)
print("묶음", {k: len(v) for k, v in sorted(groups.items())}, "· 추적", len(tr), "· 미추적", len(items) - len(tr))
main()

View File

@ -0,0 +1,94 @@
# FR 맵 폴더 — 쓰이지 않는 에셋 목록
> PD 지시(2026-09-28): "남겨두되, 어떤 에셋이 안쓰이는지 기록해서 추후 삭제 유무를 결정할 수 있도록 정리해둘 것. (내가 나중에 물어보면 바로 알 수 있게 정리 필요)"
> **지운 것은 없다.** 아래 에셋은 모두 프로젝트에 그대로 있다. 지울지는 PD 결정.
## 한눈에
| 항목 | 값 |
|---|---|
| 조사한 때 | 2026-09-28 16:13 [실측] |
| 조사 범위 | FR `Assets/BurningTimes/Maps` (맵 작업으로 만든 에셋) — 에셋 397개 |
| **쓰이지 않는 에셋** | **35개 · 10.0 MB** |
| 그 가운데 지워도 되는 것(PM 의견) | 27개 · 10.0 MB |
| 게임에 주는 영향 | 없다 — 씬 · 프리팹 어디에서도 가리키지 않는다. 어드레서블 설정 파일 71개에 등록된 것 0개 [실측]. 기기 빌드에 들어가는지는 빌드로 확인하지 않았다 |
## 묶음별
| 묶음 | 개수 | 용량 | 왜 안 쓰이게 됐나 | 지워도 되는가(PM 의견) | 되살리는 법 |
|---|---|---|---|---|---|
| **A. 던전 — 옛 지형 층** | 4 | 3.2 KB | BT38(2026-09-28)에서 던전 바닥 층을 던전 타일로 바꿨다. 옛 층 넷은 지형 데이터가 더는 가리키지 않는다 | 지워도 된다 | git 에 있다(bt/work `159f17f6c0` 부터) — `git checkout <커밋> -- <경로>` |
| **B. 던전 — 옛 바닥 텍스처** | 4 | 5.4 MB | 옛 지형 층(A)만 쓰던 텍스처다 — 그려 만든 포석 · 점액 포석과 그 노멀맵 | 지워도 된다(A 와 함께) | git 에 있다(bt/work `159f17f6c0` 부터). 만든 도구는 준비 폴더 `texderive.py` |
| **C. 던전 — 1차 시도(깨진 타일)** | 3 | 4.5 MB | BT38 1차 시도에서 만든 깨진 타일 층. 화면 확인 뒤 흙 층으로 바꿔 쓰지 않게 됐다 | 지워도 된다 | git 에 있다(bt/work `5f9c8dd297`). 팩의 `CatacombsFloorA2` 에서 다시 만들 수도 있다 |
| **D. 큰 바닥돌의 재질 사본** | 2 | 6.1 KB | 팩의 10 m 바닥 판(GroundTileCATAM)에 끼우던 재질 사본. BT38 에서 바닥돌 띠를 걷어내고 다리 판을 바꿔 쓰는 곳이 없다 | 지워도 된다 | git 에 있다. 그 바닥 판을 다시 쓰면 빌더가 다시 만든다 |
| **E. 옛 이름의 가림 반투명 재질 사본** | 14 | 55.1 KB | 재질 사본의 이름 규칙이 바뀌기 전(BT31 초기 빌드)에 만든 것. 지금 씬은 `<이름>_<번호>_Tuned_Fade` 를 쓴다 | 지워도 된다 | git 에 있다. 쓸 일은 없다 |
| **F. 빌더의 중간 산출물(밝기 맞춘 재질 사본)** | 8 | 32.1 KB | 씬은 이 사본에서 다시 만든 가림 반투명 사본(`_Tuned_Fade`)을 쓴다. 이 사본 자체는 씬이 가리키지 않지만 빌더가 맵을 지을 때 재료로 쓴다 | 지울 실익이 없다(다음 빌드 때 다시 생긴다) | 맵을 다시 지으면 생긴다 |
## 전체 목록
경로는 `Assets/BurningTimes/Maps/` 아래. 'git' = FR 저장소가 추적하는가.
| 묶음 | 경로 | 종류 | 크기 | git |
|---|---|---|---|---|
| A | `PlagueCatacomb/Terrain/Catacomb_Floor.terrainlayer` | TerrainLayer | 0.8 KB | 추적 |
| A | `PlagueCatacomb/Terrain/Catacomb_Paver.terrainlayer` | TerrainLayer | 0.8 KB | 추적 |
| A | `PlagueCatacomb/Terrain/Catacomb_Rock.terrainlayer` | TerrainLayer | 0.8 KB | 추적 |
| A | `PlagueCatacomb/Terrain/Catacomb_Slime.terrainlayer` | TerrainLayer | 0.8 KB | 추적 |
| B | `PlagueCatacomb/Textures/FRMap_dun_paver.png` | Texture2D | 1.6 MB | 추적 |
| B | `PlagueCatacomb/Textures/FRMap_dun_paver_N.png` | Texture2D | 1.1 MB | 추적 |
| B | `PlagueCatacomb/Textures/FRMap_dun_slime.png` | Texture2D | 1.7 MB | 추적 |
| B | `PlagueCatacomb/Textures/FRMap_dun_slime_N.png` | Texture2D | 1017.4 KB | 추적 |
| C | `PlagueCatacomb/Terrain/Catacomb_TileBroken.terrainlayer` | TerrainLayer | 0.8 KB | 추적 |
| C | `PlagueCatacomb/Textures/FRMap_dun_tile_broken.png` | Texture2D | 2.4 MB | 추적 |
| C | `PlagueCatacomb/Textures/FRMap_dun_tile_broken_N.png` | Texture2D | 2.2 MB | 추적 |
| D | `_Shared/Materials/Lit/CATA_FloorA_62ba55_Lit.mat` | Material | 3.0 KB | 추적 |
| D | `_Shared/Materials/Lit/CATA_FloorB_3309c2_Lit.mat` | Material | 3.0 KB | 추적 |
| E | `Graveyard/Materials/Fade/Cliff_02_Fade.mat` | Material | 4.0 KB | 추적 |
| E | `Graveyard/Materials/Fade/MEP_Brush_02_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `Graveyard/Materials/Fade/MEP_Cliff_Dif_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `Graveyard/Materials/Fade/MEP_DTreeBranch_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `Graveyard/Materials/Fade/MEP_DTree_Bark_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `Graveyard/Materials/Fade/MEP_Deadtree_01_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `HarborExecution/Materials/Fade/Cliff_02_Fade.mat` | Material | 4.0 KB | 추적 |
| E | `HarborExecution/Materials/Fade/MEP_StoneWall_Dif_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `HarborExecution/Materials/Fade/MEP_Wall_Roof_Dif_Fade.mat` | Material | 4.0 KB | 추적 |
| E | `RuinedKeep/Materials/Fade/Cliff_02_Fade.mat` | Material | 4.0 KB | 추적 |
| E | `RuinedKeep/Materials/Fade/MEP_Brush_02_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `RuinedKeep/Materials/Fade/MEP_Cliff_Dif_Fade.mat` | Material | 4.0 KB | 추적 |
| E | `RuinedKeep/Materials/Fade/MEP_Deadtree_01_Fade.mat` | Material | 3.9 KB | 추적 |
| E | `SpiderCave/Materials/Fade/MEP_CaveWall_Dif_Fade.mat` | Material | 4.0 KB | 추적 |
| F | `_Shared/Materials/Lit/Cliff_02_cb4591_Tuned.mat` | Material | 4.0 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_Brush_02_e55995_Tuned.mat` | Material | 3.9 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_Cliff_Dif_9d1521_Tuned.mat` | Material | 4.0 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_DTree_Bark_8c2ba9_Tuned.mat` | Material | 3.9 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_Deadtree_01_62065c_Tuned.mat` | Material | 3.9 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_DesertCliff_438ce1_Tuned.mat` | Material | 4.0 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_Rock_02_03_Dif_Desert_af9427_Tuned.mat` | Material | 3.9 KB | 추적 |
| F | `_Shared/Materials/Lit/MEP_Rock_Dif_Desert_e13ae8_Tuned.mat` | Material | 4.3 KB | 추적 |
## '쓰이지 않는다'의 기준
- **쓰인다**: ① 씬(`Assets/Scenes` 아래 전부) · 로비 프리팹(`LobbyPlayScene`) · 보스전 전용 필드 프리팹 · `Resources` 폴더의 에셋이 직접 또는 건너 건너 가리키는 것 ② 맵 정의(`<맵>.map.json`) · 소품 목록(`props.json`)에 경로가 적힌 것과 그것이 가리키는 것 — 지금 맵에 놓이지 않았어도 빌더의 재료이므로 쓰이는 것으로 본다.
- **쓰이지 않는다**: 위 어디에도 걸리지 않는 것. 유니티의 의존 관계 조회(`AssetDatabase.GetDependencies`)로 판정했다 [실측].
- 기준이 된 에셋 596개(정의 파일에서 550개). 정의 파일(.json)과 폴더는 후보에서 뺐다.
- **조사하지 않은 곳**: 맵 폴더 밖(`Assets/BurningTimes/Boss` · `Visual` 등)과 게임 원본 에셋. 코드가 경로 글자로 불러오는 에셋은 이 방법으로 잡히지 않는다 — 맵 폴더 안에는 그런 것이 없음을 확인했다(빌더는 정의 파일의 경로만 읽는다).
## 지우기로 정하면
1. 유니티 에디터의 Project 창에서 지운다(메타 파일이 함께 지워진다). 탐색기에서 지우면 메타 파일이 남는다.
2. 지운 뒤 확인: 컴파일 오류 0 → 해당 맵 다시 짓기(`BurningTimes/Maps/<맵>/Build`) RESULT OK → Play 에서 그 맵으로 이동해 에디터 로그 오류 0.
3. 묶음 A · B 는 함께 지운다(A 가 B 를 가리킨다 — A 만 남기면 빈 텍스처를 가리키는 층이 된다).
4. 묶음 F 는 지워도 다음 빌드 때 다시 생긴다.
## 목록을 다시 만드는 법
1. 유니티 편집 모드에서 `공유/기획/FirstRising/맵/tools/unity_eval/unused_scan.cs` 실행 → FR `Logs/bt_unused_maps_assets.json`
2. `python 공유/기획/FirstRising/맵/tools/unused_report.py` → 이 문서를 다시 쓴다(묶음 · 까닭은 스크립트의 RULES)
## 그 밖의 정리 후보(에셋이 아닌 것)
| 대상 | 크기 | 상태 | 비고 |
|---|---|---|---|
| 조직 레포 `공유/개발팀_백업/FirstRising/` | 약 1.2 GB | 미추적(레포에 올리지 않음) | FR 에셋을 바꾸기 전에 떠 둔 안전 사본. 91 MB 짜리 지형 데이터 10개 포함. 무시 목록(.gitignore)에 넣을지 PD 판단 |
| 조직 레포 `공유/기획/FirstRising/리뉴얼/~$…기획서_v1.pptx` | 165 B | 미추적 | 오피스 잠금 파일(파워포인트가 열어 둔 흔적) |
| 준비 폴더 `C:\BurningTimes\FR_maps_staging\_work\` | — | 저장소 밖 | 블렌더 굽기 · 텍스처 파생의 중간 결과. 다시 만들 수 있다 |

View File

@ -150,21 +150,56 @@
### PD 원문
- "백그라운드 작업은 왜 남아있지? 작업이 끝났으면 푸시하도록 해"
- (후속) "조직 레포도 커밋해야하지 않아?"
### 결정
- 남은 백그라운드 작업은 에디터를 닫지 않고 정리한다 — 작업 전체를 멈추는 대신 기다리던 프로세스(`head`) 하나만 끝낸다.
- 푸시 대상은 FR 작업물(BT34~BT36)로 본다 — `bt/work` 에 커밋하고 push. 에디터가 덧붙인 변경 7개(재질 4 · 글자체 2 · 의존성 에셋 1)는 넣지 않는다.
- 조직 레포(문서 · 도구 · 로그)는 커밋하지 않았다 — 지난 BT33 과 같게 처리했고, 다른 안건의 변경(CLAUDE.md · scripts)이 섞여 있다(`main` · 원격과 같음 · 바뀐 항목 92).
- 조직 레포는 처음에 커밋하지 않았다(지난 BT33 과 같게 · 다른 안건의 변경이 섞여 있어 지시 범위 밖으로 판단) → **PD 후속 지시로 커밋 · push.** PM 의 범위 판단이 좁았다.
- 조직 레포는 안건별로 나눠 커밋한다(조직 규칙 · 앞선 세션 기록 · 이번 세션). FR 에셋 백업(1.2 GB · 91 MB 파일 10개)과 오피스 잠금 파일은 넣지 않는다.
- 커밋 게이트(C35-9)는 앞선 세션들과 같게 매니페스트를 등록해 푼다(감사 호출은 BT19-Org 로 생략 — 매니페스트에 적음).
- 이번 지시는 이번 작업물에 대한 것으로 본다 — '커밋은 PD 지시 시만' 규칙은 그대로다.
### 결과
- **백그라운드 작업**: 12:03 에 에디터를 다시 띄울 때 쓴 셸(`unity open … | head -30`)이 남아 있었다. 띄우는 명령은 이미 끝났지만, 출력 통로를 에디터 프로세스가 물려받아 `head` 가 끝을 기다리고 있었다(출력 0줄 · 하는 일 없음). `head` 만 종료 → 셸이 스스로 끝남 · 에디터는 그대로 · 상태 ready.
- **푸시**: `bt/work` `159f17f6c0..1ce5df5b2e`(507개 파일 — 새 파일 446 · 고친 파일 61 · 약 140 MB). 원격과 로컬이 같음을 확인.
- **푸시(FR)**: `bt/work` `159f17f6c0..1ce5df5b2e`(507개 파일 — 새 파일 446 · 고친 파일 61 · 약 140 MB). 원격과 로컬이 같음을 확인.
- **푸시(조직 레포)**: `main` `5698f2c..c693d2e` — `705986b`(조직 규칙 BT20-Org 4개 파일) · `89505fa`(이번 세션 문서 · 도구 · 로그 168개 파일) · `c693d2e`(앞선 세션 기록 BT16~BT22 89개 파일). 비밀 값 패턴 검사 205개 파일 · 걸린 곳 0.
### 사고·조치
- 에셋 저장 뒤 글자체 에셋(NanumBarunGothic SDF)이 다시 바뀌어 있었다(에디터가 글자를 쓸 때 덧붙이는 변경) → 커밋에서 뺐다.
- 조직 레포 두 번째 커밋이 한 번 비었다(제외 경로 표기가 잘못돼 아무것도 스테이징되지 않음) → 다시 스테이징해 커밋.
- 조직 레포 첫 push 가 인증 실패(`remote: Verify`)로 떨어졌다 → 같은 명령 재시도에서 성공. 자격 증명 값은 건드리지 않았다.
### 남은 이슈
- §3 · §4 의 'FR 미커밋'은 이 푸시로 해소. FR 작업 트리에는 에디터가 덧붙인 변경 7개만 남아 있다.
- 조직 레포 문서 · 도구는 미커밋 — 반영할지 PD 지시 필요.
- 조직 레포에 남은 것: FR 에셋 백업 폴더(1.2 GB) · 오피스 잠금 파일 1개 — 백업 폴더를 무시 목록에 넣을지 PD 판단 필요.
## 6. BT38-FirstRising 오염된 던전 바닥 개선 (PM 직접 · 같은 세션)
### PD 원문
- "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌이야. 체크해보고 좀 더 자연스러운 던전 타일처럼 수정해줘."
- (작업 뒤 질문에 대한 답) 커밋 · 푸시 = "FR·조직 레포 모두" / 안 쓰는 에셋 = "남겨두되, 어떤 에셋이 안쓰이는지 기록해서 추후 삭제 유무를 결정할 수 있도록 정리해둘 것. (내가 나중에 물어보면 바로 알 수 있게 정리 필요)" / 앞으로의 규칙 = "지금처럼 지시할 때만"
### 결정
- 텍스처만 바꾸지 않고 화면에서 원인을 확인한 뒤 고친다 — 지형 층을 바꾼 1차 화면에서 바닥을 덮은 큰 판이 따로 있음을 찾았다.
- 게임 에셋(팩 텍스처의 들여오기 설정 · 팩 재질)은 고치지 않고 맵 폴더에 사본을 만든다.
- 지형 층은 넷 그대로(온전한 타일 · 얼룩진 타일 · 점액 타일 · 흙).
- 쓰이지 않게 된 에셋은 지우지 않는다(PD 결정). 던전의 11개만 적지 않고 맵 폴더 전체를 에디터의 의존 관계 조회로 조사해 목록 문서로 남긴다.
- 커밋 · 푸시는 작업을 마친 뒤 PD 에게 물어 답을 받고 했다. '커밋은 PD 지시 시만' 규칙은 그대로다(PD 답).
### 결과
- 원인: 안쪽 장식 띠의 바닥 판 46장이 2~4.9 배로 커져 바닥을 덮음(주된 원인 · BT31 PM 실수) · 지형 층 512 픽셀 · 돌판 다리의 큰 판.
- 조치: 띠 제거 · 던전 타일 1024 픽셀 4 m + 흙 얼룩 · 다리 판 교체 · 뼈 무더기 네모 판 오려내기(맵 6종 공통 재질).
- 검증: 빌드 OK · 보스 자리 막힘 0 ㎡ · 길 폭 7.5 m · Play 와 전용 필드 R4 에디터 로그 오류 0. 문서 §9 · 그림 `img/bt38_*.jpg`.
- 쓰이지 않는 에셋: 맵 폴더 397개 가운데 35개(10.0 MB) — `공유/기획/FirstRising/맵/미사용_에셋_목록.md`(묶음 · 까닭 · 되살리는 법 · 다시 조사하는 법). 어드레서블 등록 0.
- 푸시: FR `bt/work` `1ce5df5b2e..5f9c8dd297`(49개 파일) · 조직 레포 `main`.
### 사고·조치
- 1차에는 지형 층만 보고 고쳤다가 화면에서 큰 판을 발견했다 — 빌드 기록(소품 크기 · 배율)을 먼저 봤어야 했다. 전 맵의 소품 기록을 훑어 같은 실수가 던전뿐임을 확인(항구 널빤지 1.5~2 배는 별건).
- 파일을 지우는 명령이 섞인 실행이 거부됨 → 지우지 않고 진행.
### 남은 이슈
- **PD 확인 필요**: 바닥 모습 · 쓰이지 않는 에셋을 지울지(목록 문서 참고).
- 4 m 마다의 무늬 되풀이는 약해졌지만 남아 있다. 기기 빌드 미검증.
- FR 작업 트리: 에디터가 덧붙인 변경 6개만 남아 있다.