This commit is contained in:
2026-07-18 21:55:14 +09:00
parent 5c3d9a28f8
commit 173d88be7f
7 changed files with 1022 additions and 238 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ SERVER_PORT = int(os.getenv("SERVER_PORT", "8000"))
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
# 정적 파일 서빙 (프론트엔드 빌드 결과)
STATIC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "node_modules", ".build")
STATIC_DIR = os.path.join(os.path.dirname(__file__), "node_modules", ".build")
STATIC_URL = "/static"
# ─────────────────────────────────────────────────────────────────────────
File diff suppressed because it is too large Load Diff
+18 -5
View File
@@ -59,6 +59,16 @@ logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────────────
def _frontend_process_env(root_dir: Path) -> dict[str, str]:
"""config/node_modules를 사용하는 프론트엔드 프로세스 환경을 만든다."""
env = os.environ.copy()
node_modules = root_dir / "config" / "node_modules"
bin_dir = node_modules / ".bin"
env["PATH"] = f"{bin_dir}{os.pathsep}{env.get('PATH', '')}"
env["NODE_PATH"] = str(node_modules)
return env
def build_frontend() -> bool:
"""프론트엔드 빌드 (npm run build from project root)"""
root_dir = Path(__file__).resolve().parent
@@ -69,6 +79,7 @@ def build_frontend() -> bool:
"npm run build",
shell=True,
cwd=str(root_dir),
env=_frontend_process_env(root_dir),
capture_output=True,
text=True,
encoding="utf-8",
@@ -103,6 +114,7 @@ def serve_frontend_dev() -> None:
"npm run dev",
shell=True,
cwd=str(root_dir),
env=_frontend_process_env(root_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
@@ -203,11 +215,12 @@ app.add_middleware(
# ─────────────────────────────────────────────────────────────────────────
# 정적 파일 서빙 (프론트엔드)
# ─────────────────────────────────────────────────────────────────────────
if os.path.isdir(STATIC_DIR):
app.mount(STATIC_URL, StaticFiles(directory=STATIC_DIR), name="static")
logger.info(f"✓ 정적 파일 서빙: {STATIC_URL}{STATIC_DIR}")
else:
logger.warning(f"⚠ 정적 파일 디렉토리 없음: {STATIC_DIR}")
app.mount(
STATIC_URL,
StaticFiles(directory=STATIC_DIR, check_dir=False),
name="static",
)
logger.info(f"✓ 정적 파일 서빙 경로 등록: {STATIC_URL}{STATIC_DIR}")
# ─────────────────────────────────────────────────────────────────────────
# 기본 엔드포인트
+5 -5
View File
@@ -4,11 +4,11 @@
"description": "임도 설계 및 견적 자동화 프로그램 (프론트엔드)",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"format": "prettier --write \"../**/*.{ts,css,html}\""
"dev": "node ./config/node_modules/vite/bin/vite.js --configLoader runner",
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner",
"preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner",
"typecheck": "node ./config/node_modules/typescript/bin/tsc --noEmit",
"format": "node ./config/node_modules/prettier/bin/prettier.cjs --write \"../**/*.{ts,css,html}\""
},
"dependencies": {
"maplibre-gl": "^5.24.0",
+107 -144
View File
@@ -1,157 +1,120 @@
import os
import re
from pathlib import Path
# 위키 루트 경로 설정
wiki_root = Path(r"D:\02_Software_Prog\임도설계 및 견적자동화 프로그램 개발\docs\wiki")
raw_root = Path(r"D:\02_Software_Prog\임도설계 및 견적자동화 프로그램 개발\docs\raw")
WIKI_DIR = r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발\docs\wiki"
PAGES_DIR = os.path.join(WIKI_DIR, "pages")
CONCEPTS_DIR = os.path.join(WIKI_DIR, "concepts")
INDEX_PATH = os.path.join(WIKI_DIR, "index.md")
def load_frontmatter(file_path):
content = file_path.read_text(encoding="utf-8")
# Frontmatter regex
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if match:
fm_text = match.group(1)
fm = {}
# Simple YAML key-value parser for basic string/list properties
for line in fm_text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line:
key, val = line.split(":", 1)
key = key.strip()
val = val.strip().strip("'\"")
fm[key] = val
return fm, content[match.end():]
return None, content
errors = []
warnings = []
all_wiki_files = {} # relative_path -> absolute_path
def extract_wiki_links(text):
# Matches [[link]] or [[link|alias]] or [[link#section]] or [[link#section|alias]]
links = re.findall(r"\[\[(.*?)\]\]", text)
cleaned_links = []
for l in links:
# Split alias
if "|" in l:
l = l.split("|")[0]
# Split section
if "#" in l:
l = l.split("#")[0]
l = l.strip()
if l:
cleaned_links.append(l)
return cleaned_links
# Collect all markdown files in wiki/ (ignoring graphify-out and memory directories)
for root, dirs, files in os.walk(WIKI_DIR):
# skip graphify-out or memory
if "graphify-out" in root or "memory" in root:
continue
for file in files:
if file.endswith(".md"):
abs_path = os.path.join(root, file)
rel_path = os.path.relpath(abs_path, WIKI_DIR).replace("\\", "/")
all_wiki_files[rel_path] = abs_path
simple_name = os.path.splitext(rel_path)[0]
all_wiki_files[simple_name] = abs_path
def run_lint():
print("=== Start Wiki Linting (No PyYAML, Filtered) ===")
all_files = list(wiki_root.glob("**/*.md"))
index_file = wiki_root / "index.md"
log_file = wiki_root / "log.md"
# 1. 파일 목록화 및 Frontmatter 체크
pages = {}
concepts = {}
errors = []
warnings = []
for f in all_files:
rel_path = f.relative_to(wiki_root).as_posix()
# Skip output files generated by graphify in graphify-out
if rel_path.startswith("graphify-out/") or rel_path in ["index.md", "log.md", "AGENTS.md", "CLAUDE.md"]:
continue
fm, body = load_frontmatter(f)
# Read index.md content
with open(INDEX_PATH, "r", encoding="utf-8") as f:
index_content = f.read()
# Pattern for wikilinks: [[link]] or [[link|label]]
wikilink_pat = re.compile(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]")
for rel_path, abs_path in list(all_wiki_files.items()):
if "/" not in rel_path and rel_path != "index" and rel_path != "log":
continue
if rel_path in ["index", "log", "index.md", "log.md"]:
continue
# 기본 규칙 검사
if not fm:
errors.append(f"Missing Frontmatter: {rel_path}")
continue
# status 값 검사
status = fm.get("status")
with open(abs_path, "r", encoding="utf-8") as f:
lines = f.readlines()
line_count = len(lines)
content = "".join(lines)
# Rule 14: Max 100 lines
if line_count > 100:
warnings.append(f"[Line Count] `{rel_path}.md` exceeds 100 lines ({line_count} lines).")
# Check YAML Frontmatter via simple regex
frontmatter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if not frontmatter_match:
errors.append(f"[Frontmatter] `{rel_path}.md` has no valid YAML frontmatter.")
continue
fm_text = frontmatter_match.group(1)
# Simple regex parsing for status & page_id
status_match = re.search(r"^status:\s*(\w+)", fm_text, re.MULTILINE)
page_id_match = re.search(r"^page_id:\s*([^\n\r]+)", fm_text, re.MULTILINE)
if not status_match:
errors.append(f"[Status] `{rel_path}.md` has no status field in frontmatter.")
else:
status = status_match.group(1).strip()
if status not in ["draft", "stable", "stale"]:
errors.append(f"Invalid status '{status}' in {rel_path}. Must be draft, stable, or stale.")
errors.append(f"[Status] `{rel_path}.md` has invalid status '{status}'. Must be draft, stable, or stale.")
elif status == "stale":
warnings.append(f"[Stale Page] `{rel_path}.md` is marked as stale and needs updates from raw inputs.")
# 100줄 제한 검사 (10% 즉 110줄 허용)
lines_count = len(f.read_text(encoding="utf-8").splitlines())
if lines_count > 110:
warnings.append(f"File exceeds 110 lines ({lines_count} lines): {rel_path}")
# Rule 5: page_id required for pages/
if "pages/" in rel_path:
if not page_id_match:
errors.append(f"[page_id] `{rel_path}.md` is in pages/ but lacks a 'page_id' field.")
# 페이지와 컨셉 분류
if "pages/" in rel_path:
pages[rel_path] = {
"fm": fm,
"body": body,
"path": f,
"links": extract_wiki_links(body)
}
# 파일명 형식 규칙 5 검사: {page_id}_{기능명}.md
filename = f.name
page_id = fm.get("page_id")
if not page_id:
errors.append(f"Missing page_id in page: {rel_path}")
else:
expected_prefix = page_id.split("_")[0] # e.g. A01
if not filename.startswith(expected_prefix):
warnings.append(f"Filename does not match page_id format: {rel_path} (page_id: {page_id})")
else:
concepts[rel_path] = {
"fm": fm,
"body": body,
"path": f,
"links": extract_wiki_links(body)
}
# 2. 위키 링크 정합성 검증 (Broken Links)
# 개념/페이지 맵 구성
available_links = {}
for p_rel in pages:
name_no_ext = Path(p_rel).with_suffix("").as_posix()
available_links[name_no_ext] = p_rel
# Also map short form if distinct
short_name = Path(p_rel).name[:-3]
available_links[short_name] = p_rel
# Check for broken wikilinks in content
links = wikilink_pat.findall(content)
for link in links:
link_clean = link.strip().replace("\\", "/").split("#")[0] # ignore anchor for file existence check
if not link_clean:
continue
found = False
for c_rel in concepts:
name_no_ext = Path(c_rel).with_suffix("").as_posix()
available_links[name_no_ext] = c_rel
# Short form
short_name = Path(c_rel).name[:-3]
available_links[short_name] = c_rel
# Subdirectories for concepts like db_schema/*
if "concepts/" in name_no_ext:
available_links[name_no_ext.replace("concepts/", "")] = c_rel
# 1. Absolute link from vault root (e.g. concepts/storage_paths)
if link_clean in all_wiki_files:
found = True
# 2. Simple name match (e.g. storage_paths, B01_frontend)
elif link_clean.split("/")[-1] in all_wiki_files:
found = True
# 3. Handle subpages like [[B01_Dashboard/B01_frontend]]
elif f"pages/{link_clean}" in all_wiki_files:
found = True
elif f"concepts/{link_clean}" in all_wiki_files:
found = True
# 4. Handle nested directory patterns
elif link_clean.startswith("../"):
# Resolve relative link
curr_dir = os.path.dirname(rel_path)
resolved = os.path.normpath(os.path.join(curr_dir, link_clean)).replace("\\", "/")
if resolved in all_wiki_files or resolved.replace("pages/", "") in all_wiki_files:
found = True
if not found:
errors.append(f"[Broken Link] `{rel_path}.md` contains broken wikilink: [[{link}]]")
# 링크 검사
for rel_path, info in {**pages, **concepts}.items():
for link in info["links"]:
if link.startswith("http://") or link.startswith("https://") or link.startswith("file:///"):
continue
normalized_link = link.replace("\\", "/")
if normalized_link not in available_links and f"concepts/{normalized_link}" not in available_links and f"pages/{normalized_link}" not in available_links:
warnings.append(f"Broken Link in {rel_path}: [[{link}]]")
# Check if this file is registered in index.md (avoid orphans)
file_base = os.path.basename(abs_path)
file_name_no_ext = os.path.splitext(file_base)[0]
if file_name_no_ext not in index_content:
# check if relative path is in index
rel_path_no_ext = rel_path.replace(".md", "")
if rel_path_no_ext not in index_content and rel_path_no_ext.split("/")[-1] not in index_content:
warnings.append(f"[Orphan Page] `{rel_path}.md` is not linked or mentioned in index.md.")
# 3. index.md 등록 상태 확인
index_content = index_file.read_text(encoding="utf-8") if index_file.exists() else ""
for rel_path in pages:
short_name = Path(rel_path).name[:-3]
dir_name = Path(rel_path).parent.name
expected_ref_dir = f"{dir_name}/{short_name}"
if expected_ref_dir not in index_content and f"[[{short_name}]]" not in index_content and short_name not in index_content:
warnings.append(f"Page not indexed in index.md: {rel_path} (Expected link to [[{expected_ref_dir}]] or [[{short_name}]])")
# 4. 결과 출력
print(f"\nScanning completed: {len(all_files)} total markdown files.")
print(f"Detected {len(pages)} pages and {len(concepts)} concept documents.")
print(f"\n--- Errors ({len(errors)}) ---")
for e in errors:
print(f"[ERROR] {e}")
print(f"\n--- Warnings ({len(warnings)}) ---")
for w in warnings:
print(f"[WARN] {w}")
if __name__ == "__main__":
run_lint()
print("=== LINT ERRORS ===")
for e in sorted(list(set(errors))):
print(e)
print("\n=== LINT WARNINGS ===")
for w in sorted(list(set(warnings))):
print(w)
print("\nLint completed.")
+7 -11
View File
@@ -12,7 +12,8 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"],
"types": ["node", "vite/client"],
"typeRoots": ["./config/node_modules/@types", "./config/node_modules"],
"ignoreDeprecations": "6.0",
"baseUrl": ".",
@@ -25,17 +26,12 @@
"paths": {
"@ui/*": ["ui_template/*"],
"@config/*": ["config/*"],
"@util/*": ["common_util/*"]
"@util/*": ["common_util/*"],
"three": ["config/node_modules/@types/three"],
"three/*": ["config/node_modules/@types/three/*"],
"maplibre-gl": ["config/node_modules/maplibre-gl"]
}
},
"include": [
"A00_Common",
"A0*",
"B0*",
"B1*",
"ui_template",
"config/**/*",
"common_util"
],
"include": ["A00_Common", "A0*", "B0*", "B1*", "ui_template", "config/**/*", "common_util"],
"exclude": ["0_old", "node_modules", "venv", "dist"]
}
+6 -4
View File
@@ -1,4 +1,3 @@
import { defineConfig } from "vite";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
@@ -11,14 +10,17 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
* - 페이지 폴더(A01_Home 등)는 ../를 통해 접근
* - 0_old(구형 코드), venv(파이썬)는 빌드에서 제외
*/
export default defineConfig({
export default {
root: "A00_Common",
publicDir: false,
cacheDir: "config/node_modules/.vite",
resolve: {
alias: {
"@ui": resolve(__dirname, "./ui_template"),
"@config": resolve(__dirname, "./config"),
"@util": resolve(__dirname, "./common_util"),
three: resolve(__dirname, "./config/node_modules/three"),
"maplibre-gl": resolve(__dirname, "./config/node_modules/maplibre-gl"),
},
},
server: {
@@ -33,11 +35,11 @@ export default defineConfig({
},
},
build: {
outDir: "../node_modules/.build",
outDir: "../config/node_modules/.build",
emptyOutDir: true,
target: "es2022",
},
optimizeDeps: {
exclude: ["0_old"],
},
});
};