Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_7

This commit is contained in:
2026-10-01 23:27:47 +09:00
9 changed files with 269 additions and 4 deletions
+2 -1
View File
@@ -28,7 +28,8 @@ import master_text as mtx # noqa: E402
from M01_MasterData import M01_MasterData_Store_Cache as cache # noqa: E402
from M01_MasterData.M01_MasterData_Store_Shape import dump, keep_shape # noqa: E402
FOLDER: Path = cm.MASTER # 시험은 사본 폴더로 바꿈
_COPY = os.environ.get("AISLO_MASTER_DIR") # 화면 저장 시험 사본 폴더(정함 13) · 없으면 정본
FOLDER: Path = Path(_COPY).resolve() if _COPY else cm.MASTER # 시험은 사본 폴더로 바꿈
DRAFT = "_초안.json" # 저장 안 한 새 로직을 얹는 메모리 자리 — 파일로 쓰지 않음
_CHAPTER = re.compile(r"_(\d\d)장_(.+)\.json$") # 「_03장_토공사.json」
# ponytail: 저장은 한 번에 하나(프로세스 안 잠금) · 서버를 여럿 띄우면 파일 잠금으로
@@ -0,0 +1,37 @@
# 흐름도 본 프로그램 적용 — 정함 (PLAN 36 · 브레인 · 10-01 23시)
사용자 지시 · 결정은 PLAN.md 36장 머리 줄(그대로 인용)이 기준. 아래는 창끼리 맞출 약속.
## 목적
「단가산출 로직」 화면에서 로직을 열면 왼쪽 = 지금 내용(보기) · 오른쪽 = 같은 로직의 흐름도(고치는 곳). 한 로직 · 두 보기 · 고칠 곳은 흐름도 한 곳. 옛 로직은 열 때 흐름도로 자동 변환(파일은 안 바뀜) · 흐름도로 [저장]하면 그 로직은 흐름도가 정본.
## 약속
1. **로직 줄 「흐름」 칸** — 마스터 로직 줄(`resources/master_data/로직_*.json` 의 `줄[]`)에 `흐름`(CfLogic JSON · DataFlow Model 꼴 그대로). 있으면 흐름도가 정본. 쓰는 때 = 화면 [저장](기존 LogicLab `POST /save` 의 줄에 실어 보냄)뿐 · 자동 변환 결과를 파일에 쓰지 않음. 서버는 저장 때 흐름을 검사(Model 검사 꼴)하고 서버 셈으로 다시 셈.
2. **흐름 받기 길** — `GET /api/m01/logic/flow?file=&key=` → `{flow: CfLogic, converted: bool, notes: string[]}` · 줄에 `흐름` 이 있으면 그대로(converted false) · 없으면 변환(converted true · notes = 못 옮긴 곳 쉬운 말). 변환 모듈 = `M02_MasterTemplete/DataFlow/M02_DF_Convert.py`(sub7 · 서버 단독 · 옛 식 파서는 지금 서버가 쓰는 것을 그대로 씀).
3. **변환 규칙** — 입력 → 입력 상자(변수 · 고르기 = choice · 범위) · 중간 식 → 함수 상자(식 하나 = 결과 하나) · 호표 줄 → 요소 알약(단가) + 수량 함수 + 출력 상자(비목별: 노무비 · 재료비 · 경비) · 결과 식 · 덧줄 · 끝수 → 함수 · 끝수 · 옛 식에 나온 이름마다 알약을 칸에 연결하고 식은 칸 주소로(M02 방식) · 만약 → IF · 또는 → OR · 최대/최소 → MAX/MIN · 올림/반올림/버림 → ROUNDUP/ROUND/ROUNDDOWN · 찾기(표, 조건=값).칸 → 표 알약 + 선 규칙 「표에서 찾기」 · 로직(…) → 다른 로직 입력 상자 · 자동 배치(입력 왼쪽 → 함수 가운데 → 출력 오른쪽 · 겹침 없음). 전수 대조 = 1,472 로직마다 옛 셈 값(기본 입력 · 시험입력) = 변환 흐름도 셈 값 · 맞는 비율 · 안 맞는 까닭 목록(읽기만 · 파일 안 씀).
4. **계산 길** — 기존 M01 계산(`/calc` 꼴 · 시험 계산 · 다른 화면이 부르는 셈)이 줄에 `흐름` 이 있으면 DataFlow 서버 셈(`M02_DF_Compute.py`)으로 · 없으면 지금 셈 그대로. 입력 이름 = 흐름의 「설계 때 정할 것」(변수 이름) 그대로.
5. **오른쪽 패널** — `M02_MasterTemplete/DataFlow/M02_DF_Panel.ts`(sub8) `mountFlowPanel(host, opts) → handle` · opts = { logic: CfLogic; readOnly?: boolean; title: string; onChange(lg: CfLogic): void; logics?(key) } · handle = { set(lg), destroy(), expand() } · 다이어그램 · 상세 내용 · 원가 · 검증 칸을 패널 안에(세로로) · 너비 끌어 조절 · 접기 · 「크게 보기」(전체 화면 · 닫으면 패널로) · 저장 단추는 패널에 없음(페이지 [저장] 하나) · 실행 취소는 패널 안.
6. **페이지 저장 하나** — 패널 조작 = 페이지 캐시(drafts · sessionStorage)에 쌓임 · [저장] 한 번에 줄(흐름 포함) 보냄 · [고친 것 버리기] = 흐름도 고친 것도 버림 · 자동저장 없음.
7. **왼쪽 보기(흐름 로직)** — 기본정보(지금대로) · 단가산출 상세 = 흐름도 출력 상자를 비용 종류별 줄(이름 · 단위 · 값) · 소계 · 계 · 텍스트 수식 = 함수 상자마다 「식 보기」 두 줄(이름 식 · 값 식) · 시험 계산 = 계산 길(4) 그대로. 흐름 없는 옛 로직의 왼쪽은 지금 그대로.
8. **식 보기 · 조건 만들기** — `M02_DF_FormulaView.ts`(sub2) `formulaLines(box: CfBox, values) → { name: string; named: string; valued: string; branches?: {when: string; then: string; now: boolean}[] }[]` (칸 주소 → 연결 알약 이름 · ×, ÷ · IF 는 만약 / 아니면 줄) — 상세 내용 함수 판과 왼쪽 텍스트 수식(sub5)이 함께 씀. 「조건 만들기」 = 엑셀 시트 안 기능(`A00_Common/spreadsheet/spreadsheet_condition.ts` 새 파일 · 편집기 우클릭 「조건 만들기」 · 부르는 쪽이 `conditions` 선택을 줄 때만 보임 → M02 화면은 그대로) · 만약 [칸 · 알약] [비교] [값] → [칸 · 값] 줄 · 아니면 만약 · 아니면 → 고른 칸에 IF 식 · 값 목록은 부르는 쪽 `valuesFor(a1)` 이 줌.
9. **선 조건(조건 통과)** — 웹앱 모양 그대로 · CfRule gate = `{ kind: "gate"; conds: { from: CfPort; op; value }[]; join: "그리고" | "또는" }`(옛 한 줄 꼴도 읽음) · Model `valueChoices(lg, port) → (string | number)[] | null`(변수 고르기 · 표 칸 값 · 재료 규격 · 없으면 null = 숫자 칸 + 범위 검사) · 규칙 → 받는 칸 엑셀 식 · 서버 Compute 같은 뜻 · 선 이름표 「지반 = 토사 그리고 높이 ≥ 3 일 때만」.
10. **단추** — 「식으로 새로 만들기」 지움 · 「신규 로직」(빈 흐름도 + 기본정보 칸 이름 · 원문번호 · 결과 단위 · 출처 · 구분 자체 → drafts 새 줄 → [저장] 때 서버가 GX 새 키) · 「본떠 만들기」 → 「로직 복사/생성」(신규 로직 오른쪽 · 흐름도 있으면 함께 복사) · 「시험판 ▾」 과 시험판 단추 둘 지움.
11. **다른 로직 입력** — 고를 대상 = 마스터 로직 전체(기존 로직 목록 찾기) · 내보내는 알약 = 그 로직 출력(흐름이면 출력 상자 · 옛 로직이면 노무비 · 재료비 · 경비 · 계) · 받는 알약 = 그 로직 입력 · 값 = 서버 계산 길(4).
12. **권한** — 흐름도 고치기 · 저장 = 시스템 관리자(공용 로직 포함) · 그 밖은 보기만.
13. **안전** — 서버는 환경 변수 `AISLO_MASTER_DIR` 가 있으면 그 폴더를 마스터 폴더로(없으면 지금대로 · sub4). 화면 저장 시험은 자기 폴더 `tmp/master_copy_36/`(resources/master_data 사본) + 그 변수로 띄운 자기 서버에서만 · 끝나면 git status 로 resources/master_data 안 바뀜 확인. 시험(pytest)은 사본 폴더 · 마스터 내용에 기대지 않음.
14. **정리** — DataFlow · CostFlow 폴더 그대로 · 흐름도 시작 화면 견본 목록 지움 · 시험이 쓰는 견본은 `resources/tester/fixtures_df/` 로 옮기고 그 밖 견본 지움 · 시험판 입구 지움.
## 창
| 창 | 맡음 |
| --- | --- |
| sub7 | 36-0b 선 조건 틀(gate conds · join · valueChoices · Rule 식 · 시험) 먼저 → 36-2 변환(M02_DF_Convert.py · 자동 배치 · 전수 대조) |
| sub2 | 36-0 상세 내용 함수 판 「식 보기」 · 칸 연결 표 · 칸 색 · M02_DF_FormulaView.ts · 엑셀 「조건 만들기」(spreadsheet_condition.ts) · LinkPanel 조건 여러 줄 · 값 목록 고르기 |
| sub6 | 36-1 조사(옛 식 문법 전수 · 파서 위치 · 못 옮기는 꼴 · 계산 길을 부르는 곳) → sub7 에 넘김 → 36-7 정리(견본 · 시험 옮김) |
| sub8 | 36-3 M02_DF_Panel.ts(패널 꼴 · 너비 · 접기 · 크게 보기) · 선 이름표 여러 조건 |
| sub4 | 36-4 서버 — 「흐름」 칸 저장 · 검사 · /api/m01/logic/flow · 계산 길 갈래 · 신규 GX · 복사에 흐름 · 권한 · AISLO_MASTER_DIR · Compute 여러 조건 |
| sub5 | 36-4 왼쪽 보기(흐름 로직의 단가산출 상세 · 텍스트 수식 · 시험 계산) |
| sub3 | 36-6 다른 로직 입력 = 마스터 로직 고르기 · 값 |
| sub1 | 36-5 단추 · 시험판 지움 · LogicLab 두 칸 배치 · 패널 잇기 · drafts 저장 하나 → 36-8 끝 검증 |
+17
View File
@@ -0,0 +1,17 @@
{
"종류": "통합문서",
"판": 1,
"열": "item3",
"서식": [
{}
],
"시트": [
{
"id": "s1",
"이름": "면적산출",
"칸": {}
}
],
"활성": "s1",
"면적번": "SA-000003"
}
@@ -0,0 +1,96 @@
{
"판": 1,
"줄": [
{
"로직": "GC000297",
"입력": "형식",
"자리": "선택"
},
{
"로직": "GC000297",
"입력": "두께",
"자리": "면적",
"알약": "입력|GC000297|두께"
},
{
"로직": "GF000156",
"입력": "구분",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "믹서",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "지역",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "재료배합",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "골재최대치수",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "수중",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "모래상태",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "자갈상태",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "모래단위중량",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "자갈단위중량",
"자리": "선택"
},
{
"로직": "GF000156",
"입력": "자재지역",
"자리": "선택"
},
{
"로직": "GF000162",
"입력": "포장두께",
"자리": "선택"
},
{
"로직": "GF000162",
"입력": "운반",
"자리": "선택"
},
{
"로직": "GF000162",
"입력": "운반감률",
"자리": "선택"
},
{
"로직": "GF000162",
"입력": "공급감률",
"자리": "선택"
},
{
"로직": "GF000162",
"입력": "종단경사",
"자리": "선택"
}
]
}
@@ -0,0 +1,17 @@
{
"종류": "통합문서",
"판": 1,
"열": "item3",
"서식": [
{}
],
"시트": [
{
"id": "s1",
"이름": "재료산출",
"칸": {}
}
],
"활성": "s1",
"재료번": "SM-000003"
}
@@ -0,0 +1,19 @@
{
"양식": "구조물도면",
"종류": "structure",
"판": 1,
"열": "item3",
"도면": {
"format": 6,
"entities": [],
"layers": [
{
"id": "기본",
"name": "기본",
"isVisible": true,
"isLocked": false
}
]
},
"도번": "SD-000003"
}
@@ -53,7 +53,7 @@
"id": "item1",
"단위": "m",
"꼴": "수",
"일위대가": null,
"일위대가": "UA000006",
"자재조합": null,
"경로": [
"기슭막이",
@@ -95,6 +95,25 @@
"ss"
]
},
{
"id": "item3",
"단위": null,
"꼴": "수",
"일위대가": null,
"자재조합": null,
"경로": [
"기슭막이",
"ㄴㄴ"
],
"머리": [
"기슭막이",
"ㄴㄴ",
null,
null,
null,
null
]
},
{
"id": "memo",
"머리": [
@@ -2,7 +2,7 @@
"그룹": "구조물집계표조합",
"판": 1,
"비고": "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · 키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열",
"다음": 54,
"다음": 55,
"지운번호": [
"01010100",
"01010101",
@@ -74,7 +74,7 @@
"분류2": "ss",
"분류3": "aaa",
"단위": "m",
"일위대가": null,
"일위대가": "UA000006",
"자재조합": null,
"도번": "SD-000001",
"면적번": "SA-000001",
@@ -106,6 +106,27 @@
"재료번": "SM-000002",
"바인딩": null,
"펼침": null
},
{
"키": "SS000054",
"열": "item3",
"경로": [
"기슭막이",
"ㄴㄴ"
],
"이름": "기슭막이 ㄴㄴ",
"종류": "기슭막이",
"분류1": "ㄴㄴ",
"분류2": null,
"분류3": null,
"단위": null,
"일위대가": null,
"자재조합": null,
"도번": "SD-000003",
"면적번": "SA-000003",
"재료번": "SM-000003",
"바인딩": null,
"펼침": null
}
]
}
@@ -0,0 +1,38 @@
"""흐름도 적용(PLAN 36-4 · 정함 13) — 마스터 사본 폴더 길 시험.
· 환경 변수 AISLO_MASTER_DIR 가 있으면 M01 저장소 마스터 폴더 = 그 폴더 · 없으면 정본
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PROBE = "from M01_MasterData import M01_MasterData_Store as s; print(s.FOLDER)"
def _folder(env_dir: str | None) -> Path:
env = {k: v for k, v in os.environ.items() if k != "AISLO_MASTER_DIR"}
if env_dir:
env["AISLO_MASTER_DIR"] = env_dir
got = subprocess.run(
[sys.executable, "-c", PROBE],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
timeout=120,
)
assert got.returncode == 0, got.stderr
return Path(got.stdout.strip().splitlines()[-1])
def test_사본_폴더_변수가_있으면_그_폴더(tmp_path: Path) -> None:
assert _folder(str(tmp_path)) == tmp_path.resolve()
def test_변수가_없으면_정본() -> None:
assert _folder(None) == (ROOT / "resources" / "master_data").resolve()