Import S-CANVAS source + iter=1~7 lint cleanup

S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 10:29:08 +09:00
parent 53d8b53c2f
commit b9342f6726
92 changed files with 3413501 additions and 0 deletions

234
filename_classifier.py Normal file
View File

@@ -0,0 +1,234 @@
"""DXF 파일명에서 구조물 종류를 자동 추정.
토목 설계 도면 파일명에는 보통 구조물 종류가 포함됨:
"여수로 수문 설치도.dxf" → spillway_gate
"좌안옹벽 일반도.dxf" → retaining_wall
"신설 취수탑 설비 설치도.dxf" → building
"OO 교량 상세도.dxf" → bridge
"터널 갱구 설치도.dxf" → tunnel_portal
사용법:
from filename_classifier import classify_by_filename
tid = classify_by_filename("좌안옹벽 일반도.dxf")
# → "retaining_wall"
"""
from __future__ import annotations
import re
from pathlib import Path
# ---------------------------------------------------------------------------
# 구조물 유형별 키워드 패턴
# ---------------------------------------------------------------------------
# 우선순위가 높은 것을 먼저 (더 구체적인 패턴 → 일반적인 패턴)
FILENAME_PATTERNS = {
"spillway_gate": [
# 여수로 수문 관련
r"여수로.*수문",
r"수문.*여수로",
r"여수로.*게이트",
r"래디얼.*게이트",
r"테인터.*게이트",
r"tainter.*gate",
r"radial.*gate",
r"spillway.*gate",
r"spillway",
# 수문 단독
r"(?<![\uAC00-\uD7A3])수문(?![\uAC00-\uD7A3])", # 한글 경계
r"gate\b",
],
"retaining_wall": [
r"옹벽",
r"방벽",
r"(?:좌안|우안).*옹벽",
r"retaining.*wall",
r"\bwall\b(?!.*gate)", # wall but not "gate wall" etc.
],
"bridge": [
r"교량",
r"공도교",
r"연륙교",
r"인도교",
r"bridge",
r"(?<![가-힣a-z])교(?:\s|\.|_|$)", # 단독 "교"
r"viaduct",
r"overpass",
],
"tunnel_portal": [
r"터널",
r"갱구",
r"tunnel",
r"portal",
r"굴착.*입구",
],
"intake_tower": [
r"취수탑",
r"intake.*tower",
r"intake.*structure",
],
"valve_chamber": [
r"제수변실",
r"밸브실",
r"변실",
r"valve.*(?:room|chamber)",
r"도수관로",
r"도수.*관",
],
"building": [
# 일반 건축물
r"관리사무",
r"사무소",
r"관리동",
r"수문조작실",
r"변전소",
r"기계실",
r"전기실",
r"건축물",
r"건물",
r"building",
r"office",
r"powerhouse",
r"발전소",
r"펌프장",
r"pump(?:ing)?.*station",
r"조정지",
],
# Generic은 마지막 폴백이므로 패턴 없음
}
# 구조물 종류 힌트가 될 수 있는 추가 키워드 (신뢰도 낮음)
SECONDARY_KEYWORDS = {
"spillway_gate": ["스필웨이", "월류", "유수전환", "가물막이", "cofferdam"],
"retaining_wall": ["절토", "성토사면", "법면"],
"bridge": ["교대", "교각", "상판", "pier", "abutment", "deck"],
"tunnel_portal": ["터파기", "갱내"],
"building": ["사택", "청사", "센터"],
}
# ---------------------------------------------------------------------------
# 분류 함수
# ---------------------------------------------------------------------------
def classify_by_filename(filename: str) -> str | None:
"""파일명에서 구조물 유형을 추정.
Args:
filename: 파일 경로 또는 파일명
Returns:
template_id 문자열 ("spillway_gate", "building" 등) 또는
확실한 매칭이 없으면 None
"""
# 파일명만 추출 (경로/확장자 제거)
name = Path(filename).stem.lower()
# 구두점/숫자 코드 노이즈 제거 (예: "12995740-M40-001")
# 한글/영문 단어만 남김
cleaned = re.sub(r"[\d\-_/\\]+", " ", name)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
# 주 키워드 우선 매칭
for template_id, patterns in FILENAME_PATTERNS.items():
for pat in patterns:
if re.search(pat, cleaned, re.IGNORECASE):
return template_id
# 보조 키워드로 재시도
best_id = None
best_count = 0
for template_id, keywords in SECONDARY_KEYWORDS.items():
count = sum(1 for kw in keywords if kw.lower() in cleaned)
if count > best_count:
best_count = count
best_id = template_id
if best_count >= 1:
return best_id
return None
def classify_by_filenames(filenames: list[str]) -> str | None:
"""여러 파일의 이름을 종합해서 가장 가능성 높은 유형 추정."""
votes = {}
for f in filenames:
tid = classify_by_filename(f)
if tid:
votes[tid] = votes.get(tid, 0) + 1
if not votes:
return None
# 최다 득표
return max(votes.items(), key=lambda x: x[1])[0]
def suggest_with_confidence(filename: str) -> tuple[str | None, float]:
"""추정 결과 + 신뢰도 반환.
Returns:
(template_id, confidence): confidence ∈ [0.0, 1.0]
"""
name = Path(filename).stem.lower()
cleaned = re.sub(r"[\d\-_/\\]+", " ", name)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
# 주 키워드 매칭 개수 및 매칭된 패턴 확인
for template_id, patterns in FILENAME_PATTERNS.items():
matched_patterns = [pat for pat in patterns
if re.search(pat, cleaned, re.IGNORECASE)]
if matched_patterns:
# 매칭 개수에 따라 신뢰도 계산
# 1개 매칭 → 0.75, 2개 → 0.85, 3개+ → 0.95
conf = min(0.95, 0.65 + 0.1 * len(matched_patterns))
return template_id, conf
# 보조 키워드 시도
for template_id, keywords in SECONDARY_KEYWORDS.items():
matched = [kw for kw in keywords if kw.lower() in cleaned]
if matched:
conf = min(0.6, 0.3 + 0.1 * len(matched))
return template_id, conf
return None, 0.0
# ---------------------------------------------------------------------------
# 테스트
# ---------------------------------------------------------------------------
if __name__ == "__main__":
test_cases = [
"12995740-M40-001 여수로 수문 설치도(12).dxf",
"12995740-M40-002 여수로 수문 설치도(22).dxf",
"12996710-M40-001 신설 취수탑 설비 설치도(12).dxf",
"12996710-M40-002 신설 취수탑 설비 설치도(22).dxf",
"12996710-M43-002 신설 제수변실 설비 배치도.dxf",
"1. 좌안옹벽 일반도 작성(2026.0109).dxf",
"사연댐 전체계획 평면도.dxf",
"A-Line 교량 상세도.dxf",
"B-Road 터널 갱구 일반도.dxf",
"관리사무소 평면도.dxf",
"P-Station 펌프장 설치도.dxf",
"random_file.dxf",
]
print("파일명 기반 구조물 유형 추정:")
print("=" * 70)
for f in test_cases:
tid, conf = suggest_with_confidence(f)
if tid:
print(f" [{tid:16}] ({conf:.0%}) {f}")
else:
print(f" [{'' * 16}] (미매칭) {f}")