Merge remote-tracking branch 'origin/main_desktop_1' into sub_laptop_1
This commit is contained in:
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from common_util.common_util_email import send_email
|
from common_util.common_util_email import send_email
|
||||||
from config.config_system import APP_PUBLIC_BASE_URL
|
from config.config_system import ADMIN_EMAIL, APP_PUBLIC_BASE_URL
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -285,3 +285,53 @@ async def send_analysis_error_email(
|
|||||||
)
|
)
|
||||||
logger.info("WF1 오류 이메일 발송 %s: %s", "성공" if success else "실패", to_email)
|
logger.info("WF1 오류 이메일 발송 %s: %s", "성공" if success else "실패", to_email)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
async def send_initial_design_failed_email(
|
||||||
|
*,
|
||||||
|
project_id: UUID,
|
||||||
|
project_name: str,
|
||||||
|
to_email: str,
|
||||||
|
reason: str,
|
||||||
|
) -> bool:
|
||||||
|
"""초기 설계(B05·B06 자동 계산) 실패를 알리고 관리자 문의를 안내한다.
|
||||||
|
|
||||||
|
부분 결과를 초기값으로 삼지 않는다는 방침(2026-09-02 사용자 확정)에 따라, 체인이
|
||||||
|
깨진 프로젝트는 [초기화]가 되돌릴 기준이 없다. 완료 메일 대신 이 메일을 보내
|
||||||
|
사용자가 화면에서 헛작업을 하지 않게 한다.
|
||||||
|
"""
|
||||||
|
contact = ADMIN_EMAIL.strip()
|
||||||
|
contact_line = (
|
||||||
|
f'<p>관리자에게 문의해 주세요 — <a href="mailto:{html.escape(contact)}">'
|
||||||
|
f"{html.escape(contact)}</a></p>"
|
||||||
|
if contact
|
||||||
|
else "<p>관리자에게 문의해 주세요.</p>"
|
||||||
|
)
|
||||||
|
rows = "\n".join(
|
||||||
|
[
|
||||||
|
_summary_row("프로젝트 ID", html.escape(str(project_id))),
|
||||||
|
_summary_row("중단 지점", html.escape(reason or "초기 설계 처리 실패")),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
safe_name = html.escape(project_name)
|
||||||
|
body = f"""
|
||||||
|
<p>프로젝트 <strong>{safe_name}</strong>의 <strong>초기 설계 자동 계산</strong>이
|
||||||
|
완료되지 못했습니다.</p>
|
||||||
|
<div class="box">
|
||||||
|
{rows}
|
||||||
|
</div>
|
||||||
|
<div class="notice">
|
||||||
|
<p>초기 설계가 끝나지 않아 종·횡단 화면의 값이 기준값으로 확정되지 않았습니다.
|
||||||
|
이 상태에서는 [초기화]로 되돌릴 초기값도 없습니다.</p>
|
||||||
|
{contact_line}
|
||||||
|
</div>
|
||||||
|
<p>기술 지원팀이 확인할 수 있도록 서버 로그에도 같은 내용을 기록했습니다.</p>
|
||||||
|
"""
|
||||||
|
subject = f"초기 설계 실패 알림 - {project_name}"
|
||||||
|
success = await send_email(
|
||||||
|
to_email=to_email,
|
||||||
|
subject=subject,
|
||||||
|
html=_email_shell(subject, body, accent="#dc2626"),
|
||||||
|
)
|
||||||
|
logger.info("초기 설계 실패 이메일 발송 %s: %s", "성공" if success else "실패", to_email)
|
||||||
|
return success
|
||||||
|
|||||||
@@ -139,7 +139,9 @@ async def run_auto_design_chain(
|
|||||||
from B05_Profile.B05_Profile_Schema import RoutePoint, RouteSolveRequest
|
from B05_Profile.B05_Profile_Schema import RoutePoint, RouteSolveRequest
|
||||||
from B06_Section.B06_Section_Router_Confirm import confirm_sections
|
from B06_Section.B06_Section_Router_Confirm import confirm_sections
|
||||||
from common_util.common_util_initial_snapshot import (
|
from common_util.common_util_initial_snapshot import (
|
||||||
|
clear_design_failed,
|
||||||
clear_designing,
|
clear_designing,
|
||||||
|
mark_design_failed,
|
||||||
mark_designing,
|
mark_designing,
|
||||||
save_initial_snapshot,
|
save_initial_snapshot,
|
||||||
)
|
)
|
||||||
@@ -167,6 +169,8 @@ async def run_auto_design_chain(
|
|||||||
# 이 체인이 끝나기 전에는 B05·B06에 들어가면 안 된다 — 반쯤 계산된 화면을 만지면
|
# 이 체인이 끝나기 전에는 B05·B06에 들어가면 안 된다 — 반쯤 계산된 화면을 만지면
|
||||||
# 그 편집이 섞인 채 초기값이 찍힌다(2026-08-29 사용자 확정, CLAUDE.md 5장).
|
# 그 편집이 섞인 채 초기값이 찍힌다(2026-08-29 사용자 확정, CLAUDE.md 5장).
|
||||||
mark_designing(project_root)
|
mark_designing(project_root)
|
||||||
|
# 옛 실패 마커는 여기서 지운다 — 이번 체인의 결과로 다시 판정한다.
|
||||||
|
clear_design_failed(project_root)
|
||||||
# WF1이 **실제로 확정한** 선택값(stage 1 스냅샷)을 그대로 쓴다. config 기본값을
|
# WF1이 **실제로 확정한** 선택값(stage 1 스냅샷)을 그대로 쓴다. config 기본값을
|
||||||
# 쓰면 LAS 없이 설계한 프로젝트에서 있지도 않은 모델을 가리켜 404로 체인이
|
# 쓰면 LAS 없이 설계한 프로젝트에서 있지도 않은 모델을 가리켜 404로 체인이
|
||||||
# 끊긴다(2026-08-30 실사고). 노선 트림도 이 지표면을 기준으로 한다.
|
# 끊긴다(2026-08-30 실사고). 노선 트림도 이 지표면을 기준으로 한다.
|
||||||
@@ -176,6 +180,7 @@ async def run_auto_design_chain(
|
|||||||
points = _planned_route_points_in_project_crs(project_root, defaults)
|
points = _planned_route_points_in_project_crs(project_root, defaults)
|
||||||
if not points:
|
if not points:
|
||||||
logger.warning("자동 설계 체인 중단(계획노선 CSV 없음): project_id=%s", project_id)
|
logger.warning("자동 설계 체인 중단(계획노선 CSV 없음): project_id=%s", project_id)
|
||||||
|
mark_design_failed(project_root, "계획노선 CSV가 없어 초기 노선을 세울 수 없습니다.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 3) B05 경로 계산
|
# 3) B05 경로 계산
|
||||||
@@ -198,6 +203,10 @@ async def run_auto_design_chain(
|
|||||||
project_id,
|
project_id,
|
||||||
solve_result.status_code,
|
solve_result.status_code,
|
||||||
)
|
)
|
||||||
|
mark_design_failed(
|
||||||
|
project_root,
|
||||||
|
f"B05 초기 노선 계산 실패 (status={solve_result.status_code}).",
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
route_id = int(solve_result.route_id)
|
route_id = int(solve_result.route_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -216,6 +225,10 @@ async def run_auto_design_chain(
|
|||||||
project_id,
|
project_id,
|
||||||
confirm_result.status_code,
|
confirm_result.status_code,
|
||||||
)
|
)
|
||||||
|
mark_design_failed(
|
||||||
|
project_root,
|
||||||
|
f"B05 초기 노선 확정 실패 (status={confirm_result.status_code}).",
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 4.5) 배수유역 분석 → 관 지점 확정 → 배관 정착 계획선 재산출.
|
# 4.5) 배수유역 분석 → 관 지점 확정 → 배관 정착 계획선 재산출.
|
||||||
@@ -234,6 +247,10 @@ async def run_auto_design_chain(
|
|||||||
route_id,
|
route_id,
|
||||||
sections_result.status_code,
|
sections_result.status_code,
|
||||||
)
|
)
|
||||||
|
mark_design_failed(
|
||||||
|
project_root,
|
||||||
|
f"B06 초기 횡단 확정 실패 (status={sections_result.status_code}).",
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
logger.info(
|
logger.info(
|
||||||
"자동 설계 체인 완료(B05·B06 기본값 확정): project_id=%s route_id=%s",
|
"자동 설계 체인 완료(B05·B06 기본값 확정): project_id=%s route_id=%s",
|
||||||
@@ -242,21 +259,25 @@ async def run_auto_design_chain(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 초기값 스냅샷 — 여기가 [초기화]가 되돌릴 기준선이다(CLAUDE.md 5장).
|
# 초기값 스냅샷 — 여기가 [초기화]가 되돌릴 기준선이다(CLAUDE.md 5장).
|
||||||
# 체인 규약대로 실패는 비치명적이다: 스냅샷이 없으면 [초기화]가 재계산으로 돈다.
|
# 체인 규약대로 실패는 비치명적이다: 계산 결과는 그대로 두고 실패 마커만 남겨
|
||||||
|
# [초기화]가 재계산으로 얼버무리지 않게 한다(2026-09-02 사용자 확정).
|
||||||
try:
|
try:
|
||||||
async with pool.acquire() as connection:
|
async with pool.acquire() as connection:
|
||||||
await save_initial_snapshot(connection, project_root, route_id)
|
await save_initial_snapshot(connection, project_root, route_id)
|
||||||
except Exception: # noqa: BLE001 — 스냅샷 실패가 체인을 막지는 않는다
|
except Exception as exc: # noqa: BLE001 — 스냅샷 실패가 체인을 막지는 않는다
|
||||||
logger.exception("초기값 스냅샷 실패: project_id=%s", project_id)
|
logger.exception("초기값 스냅샷 실패: project_id=%s", project_id)
|
||||||
|
mark_design_failed(project_root, f"초기값 스냅샷 저장 실패: {exc}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"route_id": route_id,
|
"route_id": route_id,
|
||||||
"length_m": float(solve_result.total_length_m or 0.0),
|
"length_m": float(solve_result.total_length_m or 0.0),
|
||||||
"cross_section_count": solve_result.cross_section_count,
|
"cross_section_count": solve_result.cross_section_count,
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
# 체인은 업로드·WF1 흐름의 부가 작업이다 — 어떤 예외도 밖으로 던지지 않는다.
|
# 체인은 업로드·WF1 흐름의 부가 작업이다 — 어떤 예외도 밖으로 던지지 않는다.
|
||||||
logger.exception("자동 설계 체인 실패: project_id=%s", project_id)
|
logger.exception("자동 설계 체인 실패: project_id=%s", project_id)
|
||||||
|
if project_root is not None:
|
||||||
|
mark_design_failed(project_root, f"초기 설계 처리 중 오류: {exc}")
|
||||||
finally:
|
finally:
|
||||||
# 성공·실패·중단 어느 쪽이든 문은 연다 — 마커가 남으면 영영 못 들어간다.
|
# 성공·실패·중단 어느 쪽이든 문은 연다 — 마커가 남으면 영영 못 들어간다.
|
||||||
if project_root is not None:
|
if project_root is not None:
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import aiomysql
|
|||||||
from B03_FileInput.B03_FileInput_Email import (
|
from B03_FileInput.B03_FileInput_Email import (
|
||||||
send_analysis_error_email,
|
send_analysis_error_email,
|
||||||
send_initial_analysis_complete_email,
|
send_initial_analysis_complete_email,
|
||||||
|
send_initial_design_failed_email,
|
||||||
)
|
)
|
||||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||||
|
from common_util.common_util_initial_snapshot import is_design_failed, read_design_failure
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
from common_util.common_util_surface_confirmation import surface_confirmation_defaults
|
from common_util.common_util_surface_confirmation import surface_confirmation_defaults
|
||||||
from common_util.common_util_workflow_state import fail_stage, start_stage
|
from common_util.common_util_workflow_state import fail_stage, start_stage
|
||||||
@@ -210,14 +212,25 @@ async def trigger_wf1_analysis_and_email(
|
|||||||
|
|
||||||
# 알림 메일은 여기 한 통뿐이다 — 업로드 직후와 분석 직후로 나눠 두 통을 보내던 것을
|
# 알림 메일은 여기 한 통뿐이다 — 업로드 직후와 분석 직후로 나눠 두 통을 보내던 것을
|
||||||
# 초기 설계(B04~B06)까지 마친 시점의 통합 메일로 합쳤다(2026-08-08 사용자 지시).
|
# 초기 설계(B04~B06)까지 마친 시점의 통합 메일로 합쳤다(2026-08-08 사용자 지시).
|
||||||
|
# 체인이 깨졌으면 완료 메일 대신 관리자 문의 안내를 보낸다 — 부분 결과는 분석
|
||||||
|
# 안 됨과 다르지 않다(2026-09-02 사용자 확정).
|
||||||
|
design_failure = read_design_failure(project_root) if is_design_failed(project_root) else ""
|
||||||
if SEND_ANALYSIS_COMPLETION_EMAIL and project_info and project_info.get("user_email"):
|
if SEND_ANALYSIS_COMPLETION_EMAIL and project_info and project_info.get("user_email"):
|
||||||
await send_initial_analysis_complete_email(
|
if design_failure:
|
||||||
project_id=project_id,
|
await send_initial_design_failed_email(
|
||||||
project_name=str(project_info.get("project_name") or project_id),
|
project_id=project_id,
|
||||||
to_email=str(project_info["user_email"]),
|
project_name=str(project_info.get("project_name") or project_id),
|
||||||
analysis_result=analysis_result,
|
to_email=str(project_info["user_email"]),
|
||||||
design_summary=design_summary,
|
reason=design_failure,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
await send_initial_analysis_complete_email(
|
||||||
|
project_id=project_id,
|
||||||
|
project_name=str(project_info.get("project_name") or project_id),
|
||||||
|
to_email=str(project_info["user_email"]),
|
||||||
|
analysis_result=analysis_result,
|
||||||
|
design_summary=design_summary,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("WF1 백그라운드 분석 실패: project_id=%s", project_id)
|
logger.exception("WF1 백그라운드 분석 실패: project_id=%s", project_id)
|
||||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||||
|
|||||||
@@ -275,7 +275,9 @@ export interface RouteResetResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** [초기화] — 사용자 편집을 전부 버리고 초기값으로 되돌린다. 초기값 스냅샷이 있으면
|
/** [초기화] — 사용자 편집을 전부 버리고 초기값으로 되돌린다. 초기값 스냅샷이 있으면
|
||||||
* 복원이라 빠르지만, 없는 옛 프로젝트는 재계산으로 폴백하므로 분석용 타임아웃을 쓴다. */
|
* 복원이라 빠르지만, 없는 옛 프로젝트는 재계산으로 폴백하므로 분석용 타임아웃을 쓴다.
|
||||||
|
* 초기 설계가 실패로 끝난 프로젝트는 서버가 409로 거부한다 — 되돌릴 기준이 없어
|
||||||
|
* 재계산으로 얼버무리지 않는다(2026-09-02). 그 안내 문구가 그대로 오류 토스트에 뜬다. */
|
||||||
export async function resetRouteDesign(projectId: string): Promise<RouteResetResponse> {
|
export async function resetRouteDesign(projectId: string): Promise<RouteResetResponse> {
|
||||||
return requestJson<RouteResetResponse>(
|
return requestJson<RouteResetResponse>(
|
||||||
`/projects/${projectId}/route/reset`,
|
`/projects/${projectId}/route/reset`,
|
||||||
|
|||||||
@@ -149,20 +149,26 @@ async def reset_route_design(project_id: UUID) -> JSONResponse:
|
|||||||
`edits/pipe_points.json`이 사용자 편집분인 채로 남아 구조물 측점이 그것에서 다시
|
`edits/pipe_points.json`이 사용자 편집분인 채로 남아 구조물 측점이 그것에서 다시
|
||||||
파생되기 때문이다.
|
파생되기 때문이다.
|
||||||
|
|
||||||
스냅샷이 없는 옛 프로젝트는 종전대로 자동 설계 체인을 다시 돌린다. 어느 경로든
|
초기 설계 체인이 **실패로 끝난 프로젝트**(`initial_design.failed` 마커)는 되돌릴
|
||||||
사용자 편집(제어점 이동·계획선 편집·횡단 설계 지정)은 전부 버려지고, stage 2·3은
|
기준이 없다 — 재계산으로 얼버무리지 않고 409로 관리자 문의를 안내한다. 부분 결과는
|
||||||
IN_PROGRESS(검토 대기)가 된다.
|
분석 안 됨과 다르지 않다(2026-09-02 사용자 확정).
|
||||||
|
|
||||||
|
마커도 스냅샷도 없는 옛 프로젝트는 종전대로 자동 설계 체인을 다시 돌린다. 어느
|
||||||
|
경로든 사용자 편집(제어점 이동·계획선 편집·횡단 설계 지정)은 전부 버려지고, stage
|
||||||
|
2·3은 IN_PROGRESS(검토 대기)가 된다.
|
||||||
"""
|
"""
|
||||||
from B03_FileInput.B03_FileInput_Service_Chain import run_auto_design_chain
|
from B03_FileInput.B03_FileInput_Service_Chain import run_auto_design_chain
|
||||||
from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection
|
from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection
|
||||||
from B05_Profile.B05_Profile_Router_Corridor import prune_corridor_files
|
from B05_Profile.B05_Profile_Router_Corridor import prune_corridor_files
|
||||||
from common_util.common_util_initial_snapshot import (
|
from common_util.common_util_initial_snapshot import (
|
||||||
has_initial_snapshot,
|
has_initial_snapshot,
|
||||||
|
is_design_failed,
|
||||||
|
read_design_failure,
|
||||||
restore_initial_snapshot,
|
restore_initial_snapshot,
|
||||||
restore_snapshot_files,
|
restore_snapshot_files,
|
||||||
wipe_edited_masters,
|
wipe_edited_masters,
|
||||||
)
|
)
|
||||||
from common_util.common_util_surface_confirmation import surface_confirmation_defaults
|
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||||
|
|
||||||
pool = get_db_pool()
|
pool = get_db_pool()
|
||||||
try:
|
try:
|
||||||
@@ -170,12 +176,37 @@ async def reset_route_design(project_id: UUID) -> JSONResponse:
|
|||||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||||
project_root = Path(resolve_stored_project_path(stored_path)) if stored_path else None
|
project_root = Path(resolve_stored_project_path(stored_path)) if stored_path else None
|
||||||
restored = bool(project_root and has_initial_snapshot(project_root))
|
restored = bool(project_root and has_initial_snapshot(project_root))
|
||||||
|
if not restored and project_root and is_design_failed(project_root):
|
||||||
|
# 초기 설계가 깨진 프로젝트 — 되돌릴 기준이 없다. 재계산은 사용자가 본 초기
|
||||||
|
# 화면과 다른 값을 낳으므로 하지 않는다(2026-09-02 사용자 확정).
|
||||||
|
reason = read_design_failure(project_root) or "초기 설계 처리 실패"
|
||||||
|
logger.warning(
|
||||||
|
"B05 초기화 거부(초기 설계 실패 프로젝트): project_id=%s reason=%s",
|
||||||
|
project_id,
|
||||||
|
reason,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=409,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"code": "initial_design_failed",
|
||||||
|
"reason": reason,
|
||||||
|
"message": (
|
||||||
|
"초기 설계가 완료되지 않아 되돌릴 초기값이 없습니다. "
|
||||||
|
"관리자에게 문의해 주세요."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async with pool.acquire() as connection:
|
async with pool.acquire() as connection:
|
||||||
# 확정 지표면 모델을 초기 체인과 같은 기준(config 기본값)으로 다시 찾는다.
|
# 확정 지표면 모델을 초기 체인과 **같은 기준**으로 다시 찾는다 — 체인은 stage 1
|
||||||
|
# 확정 선택값을 쓴다(2026-08-30). config 기본값을 쓰면 그 선택과 어긋난 모델을
|
||||||
|
# 집어 재계산 결과가 초기값과 달라진다(2026-09-02 실측: `classification`/5m
|
||||||
|
# 확정 프로젝트에 `csf`/1m 기본값이 걸림).
|
||||||
try:
|
try:
|
||||||
|
selection = await get_surface_confirmation_params(connection, str(project_id))
|
||||||
surface_model_id: int | None = await find_surface_model_for_selection(
|
surface_model_id: int | None = await find_surface_model_for_selection(
|
||||||
connection, project_id, surface_confirmation_defaults()
|
connection, project_id, selection
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
surface_model_id = None
|
surface_model_id = None
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ SNAPSHOT_DIRNAME = "initial_snapshot"
|
|||||||
_DB_DUMP_NAME = "db.json"
|
_DB_DUMP_NAME = "db.json"
|
||||||
# 초기 설계 체인이 도는 동안만 존재하는 마커(진입 차단 판정용).
|
# 초기 설계 체인이 도는 동안만 존재하는 마커(진입 차단 판정용).
|
||||||
DESIGNING_LOCK_NAME = "initial_design.lock"
|
DESIGNING_LOCK_NAME = "initial_design.lock"
|
||||||
|
# 초기 설계 체인이 실패로 끝났음을 남기는 마커.
|
||||||
|
DESIGN_FAILED_NAME = "initial_design.failed"
|
||||||
|
|
||||||
# 프로젝트 루트 기준 상대 경로 — 정본 파일이 사는 자리 전부.
|
# 프로젝트 루트 기준 상대 경로 — 정본 파일이 사는 자리 전부.
|
||||||
_FILE_TREES = (
|
_FILE_TREES = (
|
||||||
@@ -69,6 +71,45 @@ def clear_designing(project_root: Path) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def design_failed_path(project_root: Path) -> Path:
|
||||||
|
"""초기 설계 체인이 실패로 끝났음을 남기는 마커.
|
||||||
|
|
||||||
|
"스냅샷이 없다"는 사실만으로는 **실패한 프로젝트**와 스냅샷 기능 이전의 **옛
|
||||||
|
프로젝트**를 가를 수 없다. 앞은 [초기화]가 재계산으로 얼버무리면 안 되고(부분 결과는
|
||||||
|
분석 안 됨과 다르지 않다, 2026-09-02 사용자 확정), 뒤는 종전 재계산 폴백이 유일한
|
||||||
|
수단이다. 락과 같은 자리 — 스냅샷 대상 4트리 **밖**이라 복원에 딸려 들어가지 않는다.
|
||||||
|
"""
|
||||||
|
return Path(project_root) / DESIGN_FAILED_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def is_design_failed(project_root: Path) -> bool:
|
||||||
|
return design_failed_path(project_root).is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def read_design_failure(project_root: Path) -> str:
|
||||||
|
"""실패 사유를 읽는다. 마커가 없거나 못 읽으면 빈 문자열."""
|
||||||
|
try:
|
||||||
|
return design_failed_path(project_root).read_text(encoding="utf-8").strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def mark_design_failed(project_root: Path, reason: str) -> None:
|
||||||
|
path = design_failed_path(project_root)
|
||||||
|
try:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(reason, encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
pass # 마커 실패가 체인을 막지는 않는다 — 안내가 덜 정확해질 뿐이다.
|
||||||
|
|
||||||
|
|
||||||
|
def clear_design_failed(project_root: Path) -> None:
|
||||||
|
try:
|
||||||
|
design_failed_path(project_root).unlink(missing_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def discard_initial_snapshot(project_root: Path) -> bool:
|
def discard_initial_snapshot(project_root: Path) -> bool:
|
||||||
"""초기값을 무효화한다 — 지표면·노선이 바뀌어 옛 초기값이 더는 기준이 아닐 때.
|
"""초기값을 무효화한다 — 지표면·노선이 바뀌어 옛 초기값이 더는 기준이 아닐 때.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user