Full dashboard implementation: API, UI, and sharing scripts
This commit is contained in:
5130
PTC/dashboard_preview.html
Normal file
5130
PTC/dashboard_preview.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,9 @@ XLSX_SOURCE_CONFIG_PATH = BASE_DIR / "server" / "ptc_source_path.txt"
|
|||||||
METHOD_XLSX_PATH = BASE_DIR / "PTC공법.xlsx"
|
METHOD_XLSX_PATH = BASE_DIR / "PTC공법.xlsx"
|
||||||
DB_PATH = BASE_DIR / "db" / "ptc_local.sqlite3"
|
DB_PATH = BASE_DIR / "db" / "ptc_local.sqlite3"
|
||||||
FRONTEND_INDEX_PATH = BASE_DIR / "PTC" / "index.html"
|
FRONTEND_INDEX_PATH = BASE_DIR / "PTC" / "index.html"
|
||||||
FRONTEND_CACHE: dict[str, str | int] = {"mtime": -1, "html": ""}
|
FRONTEND_DASHBOARD_PREVIEW_PATH = BASE_DIR / "PTC" / "dashboard_preview.html"
|
||||||
|
FRONTEND_CACHE: dict[str, str | int] = {"mtime_ns": -1, "html": ""}
|
||||||
|
FRONTEND_PREVIEW_CACHE: dict[str, str | int] = {"mtime_ns": -1, "html": ""}
|
||||||
NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
|
||||||
PROJECT_TYPE_OPTIONS = ["관리", "영업", "시공", "설계", "개발", "기술", "교휴", "기타"]
|
PROJECT_TYPE_OPTIONS = ["관리", "영업", "시공", "설계", "개발", "기술", "교휴", "기타"]
|
||||||
METHOD_FAMILY_OPTIONS = ["복합말뚝", "합성형라멘", "강관거더", "가시설"]
|
METHOD_FAMILY_OPTIONS = ["복합말뚝", "합성형라멘", "강관거더", "가시설"]
|
||||||
@@ -187,13 +189,38 @@ def get_frontend_html() -> str:
|
|||||||
if not FRONTEND_INDEX_PATH.exists():
|
if not FRONTEND_INDEX_PATH.exists():
|
||||||
raise FileNotFoundError("PTC frontend not found")
|
raise FileNotFoundError("PTC frontend not found")
|
||||||
|
|
||||||
mtime = int(FRONTEND_INDEX_PATH.stat().st_mtime)
|
mtime_ns = FRONTEND_INDEX_PATH.stat().st_mtime_ns
|
||||||
if FRONTEND_CACHE["mtime"] != mtime:
|
if FRONTEND_CACHE["mtime_ns"] != mtime_ns:
|
||||||
FRONTEND_CACHE["mtime"] = mtime
|
FRONTEND_CACHE["mtime_ns"] = mtime_ns
|
||||||
FRONTEND_CACHE["html"] = FRONTEND_INDEX_PATH.read_text(encoding="utf-8")
|
FRONTEND_CACHE["html"] = FRONTEND_INDEX_PATH.read_text(encoding="utf-8")
|
||||||
return str(FRONTEND_CACHE["html"])
|
return str(FRONTEND_CACHE["html"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_frontend_dashboard_preview_html() -> str:
|
||||||
|
if not FRONTEND_DASHBOARD_PREVIEW_PATH.exists():
|
||||||
|
raise FileNotFoundError("PTC dashboard preview frontend not found")
|
||||||
|
|
||||||
|
mtime_ns = FRONTEND_DASHBOARD_PREVIEW_PATH.stat().st_mtime_ns
|
||||||
|
if FRONTEND_PREVIEW_CACHE["mtime_ns"] != mtime_ns:
|
||||||
|
FRONTEND_PREVIEW_CACHE["mtime_ns"] = mtime_ns
|
||||||
|
FRONTEND_PREVIEW_CACHE["html"] = FRONTEND_DASHBOARD_PREVIEW_PATH.read_text(encoding="utf-8")
|
||||||
|
return str(FRONTEND_PREVIEW_CACHE["html"])
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_dashboard_family(value: str) -> str:
|
||||||
|
text = (value or "").strip()
|
||||||
|
if not text or text.upper() == "NULL":
|
||||||
|
return "기타/미지정"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_dashboard_method(value: str) -> str:
|
||||||
|
text = (value or "").strip()
|
||||||
|
if not text or text.upper() == "NULL":
|
||||||
|
return "공법미지정"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def infer_project_type_from_code(project_code: str) -> str:
|
def infer_project_type_from_code(project_code: str) -> str:
|
||||||
match = re.match(r"\d{2}-(.+?)-\d+", (project_code or "").strip())
|
match = re.match(r"\d{2}-(.+?)-\d+", (project_code or "").strip())
|
||||||
return match.group(1) if match else ""
|
return match.group(1) if match else ""
|
||||||
@@ -1039,6 +1066,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||||
|
self.send_header("Pragma", "no-cache")
|
||||||
|
self.send_header("Expires", "0")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
@@ -1048,6 +1078,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
self._send_html(200, get_frontend_html())
|
self._send_html(200, get_frontend_html())
|
||||||
|
|
||||||
|
def _send_frontend_dashboard_preview(self) -> None:
|
||||||
|
if not FRONTEND_DASHBOARD_PREVIEW_PATH.exists():
|
||||||
|
self._send_html(404, "<h1>PTC dashboard preview frontend not found</h1>")
|
||||||
|
return
|
||||||
|
self._send_html(200, get_frontend_dashboard_preview_html())
|
||||||
|
|
||||||
def _send(self, status: int, payload: dict) -> None:
|
def _send(self, status: int, payload: dict) -> None:
|
||||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
@@ -1435,6 +1471,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
params = parse_qs(parsed.query)
|
params = parse_qs(parsed.query)
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
|
if parsed.path in {"/PTC-lab", "/PTC-lab/", "/PTC/dashboard_preview.html"}:
|
||||||
|
self._send_frontend_dashboard_preview()
|
||||||
|
return
|
||||||
|
|
||||||
if parsed.path in {"/PTC", "/PTC/", "/PTC/index.html"}:
|
if parsed.path in {"/PTC", "/PTC/", "/PTC/index.html"}:
|
||||||
self._send_frontend()
|
self._send_frontend()
|
||||||
return
|
return
|
||||||
@@ -1641,6 +1681,188 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._send(200, summary)
|
self._send(200, summary)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if parsed.path == "/api/dashboard-prototype":
|
||||||
|
project_type = params.get("project_type", ["전체"])[0]
|
||||||
|
clauses = ["t.project_code is not null", "t.project_code <> ''"]
|
||||||
|
values: list[str] = []
|
||||||
|
if project_type and project_type != "전체":
|
||||||
|
clauses.append("coalesce(pm.project_type, t.project_type) = ?")
|
||||||
|
values.append(project_type)
|
||||||
|
where = " where " + " and ".join(clauses)
|
||||||
|
rows = conn.execute(
|
||||||
|
f"""
|
||||||
|
select
|
||||||
|
t.project_code,
|
||||||
|
coalesce(pm.project_name, max(t.project_name)) as project_name,
|
||||||
|
coalesce(pm.project_type, max(t.project_type)) as project_type,
|
||||||
|
coalesce(pm.construction_family, '') as construction_family,
|
||||||
|
coalesce(pm.construction_method, '') as construction_method,
|
||||||
|
coalesce(pp.progress_rate, 0) as progress_rate,
|
||||||
|
coalesce(pp.contract_pile_count, 0) as contract_pile_count,
|
||||||
|
coalesce(pp.constructed_pile_count, 0) as constructed_pile_count,
|
||||||
|
coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) as income_supply,
|
||||||
|
coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0) as expense_supply,
|
||||||
|
count(*) as txn_count
|
||||||
|
from ptc_transactions t
|
||||||
|
left join project_master pm on pm.project_code = t.project_code
|
||||||
|
left join project_progress pp on pp.project_code = t.project_code
|
||||||
|
{where}
|
||||||
|
group by t.project_code, pm.project_name, pm.project_type, pm.construction_family, pm.construction_method, pp.progress_rate, pp.contract_pile_count, pp.constructed_pile_count
|
||||||
|
""",
|
||||||
|
values,
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
amount_buckets = [
|
||||||
|
{"key": "under_5", "label": "5억 미만", "min": 0, "max": 500_000_000},
|
||||||
|
{"key": "5_to_20", "label": "5억~20억", "min": 500_000_000, "max": 2_000_000_000},
|
||||||
|
{"key": "20_to_50", "label": "20억~50억", "min": 2_000_000_000, "max": 5_000_000_000},
|
||||||
|
{"key": "over_50", "label": "50억 이상", "min": 5_000_000_000, "max": None},
|
||||||
|
]
|
||||||
|
status_bands = [
|
||||||
|
{"key": "normal", "label": "정상"},
|
||||||
|
{"key": "upfront", "label": "선투입"},
|
||||||
|
{"key": "delay", "label": "회수지연"},
|
||||||
|
{"key": "risk", "label": "원가위험"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def bucket_amount(value: float) -> str:
|
||||||
|
for bucket in amount_buckets:
|
||||||
|
if bucket["max"] is None and value >= bucket["min"]:
|
||||||
|
return bucket["key"]
|
||||||
|
if bucket["min"] <= value < bucket["max"]:
|
||||||
|
return bucket["key"]
|
||||||
|
return amount_buckets[0]["key"]
|
||||||
|
|
||||||
|
def classify_status(income_supply: float, expense_supply: float, progress_rate: float, contract_pile_count: float, constructed_pile_count: float) -> str:
|
||||||
|
profit_supply = income_supply - expense_supply
|
||||||
|
expense_ratio = (expense_supply / income_supply) if income_supply > 0 else None
|
||||||
|
has_progress_signal = progress_rate > 0 or contract_pile_count > 0 or constructed_pile_count > 0
|
||||||
|
|
||||||
|
if profit_supply >= 0:
|
||||||
|
return "normal"
|
||||||
|
|
||||||
|
if income_supply <= 0 and expense_supply > 0:
|
||||||
|
return "upfront"
|
||||||
|
|
||||||
|
if expense_ratio is not None and expense_ratio >= 1.15:
|
||||||
|
return "risk"
|
||||||
|
|
||||||
|
if expense_supply >= 300_000_000 and profit_supply < -100_000_000:
|
||||||
|
return "risk"
|
||||||
|
|
||||||
|
if income_supply > 0 and income_supply < expense_supply:
|
||||||
|
return "delay" if has_progress_signal else "risk"
|
||||||
|
|
||||||
|
return "normal"
|
||||||
|
|
||||||
|
by_method: dict[str, dict] = {}
|
||||||
|
overview = {
|
||||||
|
"project_count": 0,
|
||||||
|
"income_supply": 0.0,
|
||||||
|
"expense_supply": 0.0,
|
||||||
|
"profit_supply": 0.0,
|
||||||
|
"status_counts": {band["key"]: 0 for band in status_bands},
|
||||||
|
}
|
||||||
|
project_items = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
method = normalize_dashboard_method(row["construction_method"] or "")
|
||||||
|
family = normalize_dashboard_family(row["construction_family"] or "")
|
||||||
|
income_supply = float(row["income_supply"] or 0)
|
||||||
|
expense_supply = float(row["expense_supply"] or 0)
|
||||||
|
profit_supply = income_supply - expense_supply
|
||||||
|
margin_rate = (profit_supply / income_supply * 100) if income_supply > 0 else 0.0
|
||||||
|
progress_rate = float(row["progress_rate"] or 0)
|
||||||
|
contract_pile_count = float(row["contract_pile_count"] or 0)
|
||||||
|
constructed_pile_count = float(row["constructed_pile_count"] or 0)
|
||||||
|
amount_bucket_key = bucket_amount(income_supply)
|
||||||
|
status_key = classify_status(income_supply, expense_supply, progress_rate, contract_pile_count, constructed_pile_count)
|
||||||
|
|
||||||
|
if method not in by_method:
|
||||||
|
by_method[method] = {
|
||||||
|
"method": method,
|
||||||
|
"family": family,
|
||||||
|
"project_count": 0,
|
||||||
|
"income_supply": 0.0,
|
||||||
|
"expense_supply": 0.0,
|
||||||
|
"profit_supply": 0.0,
|
||||||
|
"status_counts": {band["key"]: 0 for band in status_bands},
|
||||||
|
"cells": {
|
||||||
|
bucket["key"]: {
|
||||||
|
"bucket_key": bucket["key"],
|
||||||
|
"bucket_label": bucket["label"],
|
||||||
|
"project_count": 0,
|
||||||
|
"income_supply": 0.0,
|
||||||
|
"expense_supply": 0.0,
|
||||||
|
"profit_supply": 0.0,
|
||||||
|
"status_counts": {band["key"]: 0 for band in status_bands},
|
||||||
|
"projects": [],
|
||||||
|
}
|
||||||
|
for bucket in amount_buckets
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cell = by_method[method]["cells"][amount_bucket_key]
|
||||||
|
cell["project_count"] += 1
|
||||||
|
cell["income_supply"] += income_supply
|
||||||
|
cell["expense_supply"] += expense_supply
|
||||||
|
cell["profit_supply"] += profit_supply
|
||||||
|
cell["status_counts"][status_key] += 1
|
||||||
|
if len(cell["projects"]) < 5:
|
||||||
|
cell["projects"].append({
|
||||||
|
"project_code": row["project_code"],
|
||||||
|
"project_name": row["project_name"],
|
||||||
|
"margin_rate": margin_rate,
|
||||||
|
"income_supply": income_supply,
|
||||||
|
"status_key": status_key,
|
||||||
|
})
|
||||||
|
|
||||||
|
by_method[method]["project_count"] += 1
|
||||||
|
by_method[method]["income_supply"] += income_supply
|
||||||
|
by_method[method]["expense_supply"] += expense_supply
|
||||||
|
by_method[method]["profit_supply"] += profit_supply
|
||||||
|
by_method[method]["status_counts"][status_key] += 1
|
||||||
|
|
||||||
|
overview["project_count"] += 1
|
||||||
|
overview["income_supply"] += income_supply
|
||||||
|
overview["expense_supply"] += expense_supply
|
||||||
|
overview["profit_supply"] += profit_supply
|
||||||
|
overview["status_counts"][status_key] += 1
|
||||||
|
|
||||||
|
project_items.append({
|
||||||
|
"project_code": row["project_code"],
|
||||||
|
"project_name": row["project_name"],
|
||||||
|
"project_type": row["project_type"],
|
||||||
|
"construction_method": method,
|
||||||
|
"construction_family": family,
|
||||||
|
"progress_rate": progress_rate,
|
||||||
|
"income_supply": income_supply,
|
||||||
|
"expense_supply": expense_supply,
|
||||||
|
"profit_supply": profit_supply,
|
||||||
|
"margin_rate": margin_rate,
|
||||||
|
"amount_bucket_key": amount_bucket_key,
|
||||||
|
"status_key": status_key,
|
||||||
|
})
|
||||||
|
|
||||||
|
overview["margin_rate"] = (overview["profit_supply"] / overview["income_supply"] * 100) if overview["income_supply"] > 0 else 0.0
|
||||||
|
method_items = []
|
||||||
|
for method, item in sorted(by_method.items(), key=lambda pair: (-pair[1]["income_supply"], pair[0])):
|
||||||
|
item["margin_rate"] = (item["profit_supply"] / item["income_supply"] * 100) if item["income_supply"] > 0 else 0.0
|
||||||
|
item["cells"] = [item["cells"][bucket["key"]] for bucket in amount_buckets]
|
||||||
|
method_items.append(item)
|
||||||
|
|
||||||
|
self._send(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"overview": overview,
|
||||||
|
"amount_buckets": amount_buckets,
|
||||||
|
"status_bands": status_bands,
|
||||||
|
"methods": method_items,
|
||||||
|
"projects": sorted(project_items, key=lambda item: (-item["income_supply"], item["project_code"])),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if parsed.path == "/api/project-types":
|
if parsed.path == "/api/project-types":
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"select distinct project_type from ptc_transactions where coalesce(project_type,'') <> '' order by project_type"
|
"select distinct project_type from ptc_transactions where coalesce(project_type,'') <> '' order by project_type"
|
||||||
@@ -2092,11 +2314,15 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
master.get("construction_family"),
|
master.get("construction_family"),
|
||||||
)
|
)
|
||||||
summary_dict["construction_method"] = master.get("construction_method") or ""
|
summary_dict["construction_method"] = master.get("construction_method") or ""
|
||||||
|
summary_dict["start_date"] = master.get("start_date") or ""
|
||||||
|
summary_dict["end_date"] = master.get("end_date") or ""
|
||||||
summary_dict["note"] = master.get("note") or ""
|
summary_dict["note"] = master.get("note") or ""
|
||||||
elif summary_dict:
|
elif summary_dict:
|
||||||
summary_dict["project_type"] = resolve_project_type(project_code, summary_dict["project_type"])
|
summary_dict["project_type"] = resolve_project_type(project_code, summary_dict["project_type"])
|
||||||
summary_dict["construction_family"] = resolve_construction_family("")
|
summary_dict["construction_family"] = resolve_construction_family("")
|
||||||
summary_dict["construction_method"] = ""
|
summary_dict["construction_method"] = ""
|
||||||
|
summary_dict["start_date"] = ""
|
||||||
|
summary_dict["end_date"] = ""
|
||||||
summary_dict["note"] = ""
|
summary_dict["note"] = ""
|
||||||
account_issues = get_project_account_issues(
|
account_issues = get_project_account_issues(
|
||||||
conn,
|
conn,
|
||||||
|
|||||||
@@ -1,77 +1,7 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal EnableExtensions
|
setlocal
|
||||||
|
pushd "%~dp0" >nul 2>&1
|
||||||
set "PROJECT_DIR=/home/hyein/project"
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0start_ptc_share.ps1"
|
||||||
set "API_PORT=4000"
|
set "EXIT_CODE=%ERRORLEVEL%"
|
||||||
set "LOCAL_URL=http://localhost:4000/PTC/"
|
popd >nul 2>&1
|
||||||
set "SHARE_URL="
|
exit /b %EXIT_CODE%
|
||||||
set "LAN_IP="
|
|
||||||
set "CURRENT_SOURCE="
|
|
||||||
|
|
||||||
net session >nul 2>&1
|
|
||||||
if not "%errorlevel%"=="0" (
|
|
||||||
echo 관리자 권한으로 다시 실행해 공유 설정까지 적용합니다...
|
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
for /f "usebackq delims=" %%i in (`wsl.exe bash -lc "if [ -f /home/hyein/project/server/ptc_source_path.txt ]; then cat /home/hyein/project/server/ptc_source_path.txt; else echo /home/hyein/project/PTC(2023-2026.02).xlsx; fi"`) do (
|
|
||||||
set "CURRENT_SOURCE=%%i"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo PTC 서버 시작 중...
|
|
||||||
echo 원본 파일: %CURRENT_SOURCE%
|
|
||||||
wsl.exe bash -lc "pkill -f '/home/hyein/project/server/ptc_api_server.py' >/dev/null 2>&1 || true; nohup python3 /home/hyein/project/server/ptc_api_server.py >/tmp/ptc_api.log 2>&1 & sleep 3"
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo WSL에서 서버 시작 명령 실행에 실패했습니다.
|
|
||||||
echo WSL이 실행 가능한지 확인해 주세요.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo 로컬 서버 상태 확인 중...
|
|
||||||
wsl.exe bash -lc "curl -fsS http://127.0.0.1:4000/api/health >/tmp/ptc_api_health.json && curl -fsSI http://127.0.0.1:4000/PTC/ >/tmp/ptc_web_health.txt"
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo 로컬 서버 확인에 실패했습니다.
|
|
||||||
echo 아래 로그를 확인해 주세요.
|
|
||||||
wsl.exe bash -lc "tail -n 80 /tmp/ptc_api.log"
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo 브라우저를 엽니다...
|
|
||||||
start "" "%LOCAL_URL%"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo 로컬 실행 완료
|
|
||||||
echo 메인 화면: %LOCAL_URL%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 사내망 공유용 Windows IP 확인 중...
|
|
||||||
for /f "usebackq delims=" %%i in (`powershell -NoProfile -Command "$ip = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' -and $_.PrefixOrigin -ne 'WellKnown' } | Select-Object -ExpandProperty IPAddress -First 1; if ($ip) { $ip }"`) do (
|
|
||||||
set "LAN_IP=%%i"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo WSL IP 확인 중...
|
|
||||||
for /f "usebackq delims=" %%i in (`wsl.exe bash -lc "hostname -I | awk '{print $1}'"`) do (
|
|
||||||
set "WSL_IP=%%i"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%LAN_IP%"=="" goto :share_done
|
|
||||||
if "%WSL_IP%"=="" goto :share_done
|
|
||||||
|
|
||||||
echo 사내망 공유 설정 중...
|
|
||||||
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=%API_PORT% >nul 2>&1
|
|
||||||
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=%API_PORT% connectaddress=%WSL_IP% connectport=%API_PORT% >nul 2>&1
|
|
||||||
netsh advfirewall firewall delete rule name="PTC 4000" >nul 2>&1
|
|
||||||
netsh advfirewall firewall add rule name="PTC 4000" dir=in action=allow protocol=TCP localport=%API_PORT% >nul 2>&1
|
|
||||||
|
|
||||||
:share_done
|
|
||||||
if not "%LAN_IP%"=="" (
|
|
||||||
set "SHARE_URL=http://%LAN_IP%:%API_PORT%/PTC/"
|
|
||||||
echo 사내망 접속 주소: %SHARE_URL%
|
|
||||||
echo %SHARE_URL%| clip
|
|
||||||
echo 공유 주소를 클립보드에 복사했습니다.
|
|
||||||
)
|
|
||||||
|
|
||||||
pause
|
|
||||||
|
|||||||
91
windows/start_ptc_share.ps1
Normal file
91
windows/start_ptc_share.ps1
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Test-IsAdmin {
|
||||||
|
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = New-Object Security.Principal.WindowsPrincipal($currentIdentity)
|
||||||
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-IsAdmin)) {
|
||||||
|
Start-Process -FilePath "powershell.exe" -Verb RunAs -ArgumentList @(
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-File", "`"$PSCommandPath`""
|
||||||
|
)
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectDir = "/home/hyein/project"
|
||||||
|
$apiPort = 4000
|
||||||
|
$localUrl = "http://localhost:$apiPort/PTC/"
|
||||||
|
$preferredLanIp = "172.16.40.36"
|
||||||
|
$defaultSource = "/home/hyein/project/PTC(2023-2026.02).xlsx"
|
||||||
|
$sourceConfigPath = "/home/hyein/project/server/ptc_source_path.txt"
|
||||||
|
|
||||||
|
function Invoke-WslBash([string]$command) {
|
||||||
|
$output = & wsl.exe bash -lc $command 2>&1
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Output = @($output)
|
||||||
|
ExitCode = $exitCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentSourceResult = Invoke-WslBash "cat '$sourceConfigPath' 2>/dev/null || printf '%s\n' '$defaultSource'"
|
||||||
|
$currentSource = ($currentSourceResult.Output | Select-Object -First 1).Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($currentSource)) {
|
||||||
|
$currentSource = $defaultSource
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Starting PTC server..."
|
||||||
|
Write-Host "Source file: $currentSource"
|
||||||
|
|
||||||
|
$startResult = Invoke-WslBash "pkill -f '/home/hyein/project/server/ptc_api_server.py' >/dev/null 2>&1 || true; nohup python3 /home/hyein/project/server/ptc_api_server.py >/tmp/ptc_api.log 2>&1 & sleep 3"
|
||||||
|
if ($startResult.ExitCode -ne 0) {
|
||||||
|
Write-Host "Failed to start the server in WSL." -ForegroundColor Red
|
||||||
|
Read-Host "Press Enter to exit"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$healthResult = Invoke-WslBash "curl -fsS http://127.0.0.1:$apiPort/api/health >/tmp/ptc_api_health.json && curl -fsSI http://127.0.0.1:$apiPort/PTC/ >/tmp/ptc_web_health.txt"
|
||||||
|
if ($healthResult.ExitCode -ne 0) {
|
||||||
|
Write-Host "Local server check failed. Recent server log:" -ForegroundColor Red
|
||||||
|
$logResult = Invoke-WslBash "tail -n 80 /tmp/ptc_api.log"
|
||||||
|
$logResult.Output | ForEach-Object { Write-Host $_ }
|
||||||
|
Read-Host "Press Enter to exit"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Process $localUrl
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Local URL: $localUrl"
|
||||||
|
|
||||||
|
$lanIp = Get-NetIPAddress -AddressFamily IPv4 |
|
||||||
|
Where-Object {
|
||||||
|
$_.IPAddress -notlike '127.*' -and
|
||||||
|
$_.IPAddress -notlike '169.254.*' -and
|
||||||
|
$_.PrefixOrigin -ne 'WellKnown'
|
||||||
|
} |
|
||||||
|
Select-Object -ExpandProperty IPAddress -First 1
|
||||||
|
|
||||||
|
$wslIpResult = Invoke-WslBash "hostname -I | awk '{print \$1}'"
|
||||||
|
$wslIp = ($wslIpResult.Output | Select-Object -First 1).Trim()
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($lanIp) -and -not [string]::IsNullOrWhiteSpace($wslIp)) {
|
||||||
|
Write-Host "Configuring LAN sharing..."
|
||||||
|
& netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=$apiPort | Out-Null
|
||||||
|
& netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=$apiPort connectaddress=$wslIp connectport=$apiPort | Out-Null
|
||||||
|
& netsh advfirewall firewall delete rule name="PTC 4000" | Out-Null
|
||||||
|
& netsh advfirewall firewall add rule name="PTC 4000" dir=in action=allow protocol=TCP localport=$apiPort | Out-Null
|
||||||
|
|
||||||
|
$shareIp = if ([string]::IsNullOrWhiteSpace($preferredLanIp)) { $lanIp } else { $preferredLanIp }
|
||||||
|
$shareUrl = "http://$shareIp:$apiPort/PTC/"
|
||||||
|
Write-Host "LAN URL: $shareUrl"
|
||||||
|
Set-Clipboard -Value $shareUrl
|
||||||
|
Write-Host "The share URL has been copied to the clipboard."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "LAN sharing was skipped because Windows IP or WSL IP could not be detected."
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Press Enter to close"
|
||||||
Reference in New Issue
Block a user